new Deps
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Validation;
|
||||
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
use Nuwave\Lighthouse\Exceptions\DefinitionException;
|
||||
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
|
||||
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgManipulator;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgumentValidation;
|
||||
|
||||
abstract class BaseRulesDirective extends BaseDirective implements ArgumentValidation, ArgManipulator
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = $this->directiveArgValue('apply');
|
||||
|
||||
// Custom rules may be referenced through their fully qualified class name.
|
||||
// The Laravel validator expects a class instance to be passed, so we
|
||||
// resolve any given rule where a corresponding class exists.
|
||||
foreach ($rules as $key => $rule) {
|
||||
if (class_exists($rule)) {
|
||||
$rules[$key] = app($rule);
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
$messages = $this->directiveArgValue('messages');
|
||||
if ($messages === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isset($messages[0])) {
|
||||
/** @var array<string, string> $flattened */
|
||||
$flattened = [];
|
||||
|
||||
/**
|
||||
* We know this holds true, because it has been validated before.
|
||||
*
|
||||
* @var array{rule: string, message: string} $messageMap
|
||||
*/
|
||||
foreach ($messages as $messageMap) {
|
||||
$flattened[$messageMap['rule']] = $messageMap['message'];
|
||||
}
|
||||
|
||||
return $flattened;
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
public function attribute(): ?string
|
||||
{
|
||||
return $this->directiveArgValue('attribute');
|
||||
}
|
||||
|
||||
public function manipulateArgDefinition(
|
||||
DocumentAST &$documentAST,
|
||||
InputValueDefinitionNode &$argDefinition,
|
||||
FieldDefinitionNode &$parentField,
|
||||
ObjectTypeDefinitionNode &$parentType
|
||||
) {
|
||||
$this->validateRulesArg();
|
||||
$this->validateMessageArg();
|
||||
}
|
||||
|
||||
protected function validateRulesArg(): void
|
||||
{
|
||||
$rules = $this->directiveArgValue('apply');
|
||||
|
||||
if (! is_array($rules)) {
|
||||
$this->invalidApplyArgument($rules);
|
||||
}
|
||||
|
||||
if (count($rules) === 0) {
|
||||
$this->invalidApplyArgument($rules);
|
||||
}
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
if (! is_string($rule)) {
|
||||
$this->invalidApplyArgument($rules);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateMessageArg(): void
|
||||
{
|
||||
$messages = $this->directiveArgValue('messages');
|
||||
if ($messages === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_array($messages)) {
|
||||
$this->invalidMessageArgument($messages);
|
||||
}
|
||||
|
||||
if (isset($messages[0])) {
|
||||
foreach ($messages as $messageMap) {
|
||||
if (! is_array($messageMap)) {
|
||||
$this->invalidMessageArgument($messages);
|
||||
}
|
||||
|
||||
$rule = $messageMap['rule'] ?? null;
|
||||
if (! is_string($rule)) {
|
||||
$this->invalidMessageArgument($messages);
|
||||
}
|
||||
|
||||
$message = $messageMap['message'] ?? null;
|
||||
if (! is_string($message)) {
|
||||
$this->invalidMessageArgument($messages);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($messages as $rule => $message) {
|
||||
if (! is_string($rule)) {
|
||||
$this->invalidMessageArgument($messages);
|
||||
}
|
||||
|
||||
if (! is_string($message)) {
|
||||
$this->invalidMessageArgument($messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $messages Whatever faulty value was given for messages
|
||||
* @throws DefinitionException
|
||||
*/
|
||||
protected function invalidMessageArgument($messages): void
|
||||
{
|
||||
$encoded = \Safe\json_encode($messages);
|
||||
throw new DefinitionException(
|
||||
"The `messages` argument of @`{$this->name()}` on `{$this->nodeName()} must be a list of input values with the string keys `rule` and `message`, got: {$encoded}"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $apply Any invalid value
|
||||
*
|
||||
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
|
||||
*/
|
||||
protected function invalidApplyArgument($apply): void
|
||||
{
|
||||
$encoded = \Safe\json_encode($apply);
|
||||
throw new DefinitionException(
|
||||
"The `apply` argument of @`{$this->name()}` on `{$this->nodeName()}` has to be a list of strings, got: {$encoded}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Validation;
|
||||
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgDirective;
|
||||
|
||||
class RulesDirective extends BaseRulesDirective implements ArgDirective
|
||||
{
|
||||
public static function definition(): string
|
||||
{
|
||||
return /** @lang GraphQL */ <<<'GRAPHQL'
|
||||
"""
|
||||
Validate an argument using [Laravel validation](https://laravel.com/docs/validation).
|
||||
"""
|
||||
directive @rules(
|
||||
"""
|
||||
Specify the validation rules to apply to the field.
|
||||
This can either be a reference to [Laravel's built-in validation rules](https://laravel.com/docs/validation#available-validation-rules),
|
||||
or the fully qualified class name of a custom validation rule.
|
||||
|
||||
Rules that mutate the incoming arguments, such as `exclude_if`, are not supported
|
||||
by Lighthouse. Use ArgTransformerDirectives or FieldMiddlewareDirectives instead.
|
||||
"""
|
||||
apply: [String!]!
|
||||
|
||||
"""
|
||||
Specify a custom attribute name to use in your validation message.
|
||||
"""
|
||||
attribute: String
|
||||
|
||||
"""
|
||||
Specify the messages to return if the validators fail.
|
||||
"""
|
||||
messages: [RulesMessage!]
|
||||
) repeatable on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
||||
|
||||
"""
|
||||
Input for the `messages` argument of `@rules`.
|
||||
"""
|
||||
input RulesMessage {
|
||||
"""
|
||||
Name of the rule, e.g. `"email"`.
|
||||
"""
|
||||
rule: String!
|
||||
|
||||
"""
|
||||
Message to display if the rule fails, e.g. `"Must be a valid email"`.
|
||||
"""
|
||||
message: String!
|
||||
}
|
||||
GRAPHQL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Validation;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgDirectiveForArray;
|
||||
|
||||
class RulesForArrayDirective extends BaseRulesDirective implements ArgDirectiveForArray
|
||||
{
|
||||
public static function definition(): string
|
||||
{
|
||||
return /** @lang GraphQL */ <<<'GRAPHQL'
|
||||
"""
|
||||
Run validation on an array itself, using [Laravel built-in validation](https://laravel.com/docs/validation).
|
||||
"""
|
||||
directive @rulesForArray(
|
||||
"""
|
||||
Specify the validation rules to apply to the field.
|
||||
This can either be a reference to any of Laravel's built-in validation rules: https://laravel.com/docs/validation#available-validation-rules,
|
||||
or the fully qualified class name of a custom validation rule.
|
||||
"""
|
||||
apply: [String!]!
|
||||
|
||||
"""
|
||||
Specify a custom attribute name to use in your validation message.
|
||||
"""
|
||||
attribute: String
|
||||
|
||||
"""
|
||||
Specify the messages to return if the validators fail.
|
||||
"""
|
||||
messages: [RulesForArrayMessage!]
|
||||
) repeatable on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
||||
|
||||
"""
|
||||
Input for the `messages` argument of `@rulesForArray`.
|
||||
"""
|
||||
input RulesForArrayMessage {
|
||||
"""
|
||||
Name of the rule, e.g. `"email"`.
|
||||
"""
|
||||
rule: String!
|
||||
|
||||
"""
|
||||
Message to display if the rule fails, e.g. `"Must be a valid email"`.
|
||||
"""
|
||||
message: String!
|
||||
}
|
||||
GRAPHQL;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = parent::rules();
|
||||
|
||||
if (! in_array('array', $rules)) {
|
||||
$rules = Arr::prepend($rules, 'array');
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Validation;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Validation\ValidationRuleParser;
|
||||
use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet;
|
||||
use Nuwave\Lighthouse\Execution\Arguments\ListType;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgDirective;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgDirectiveForArray;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgumentSetValidation;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgumentValidation;
|
||||
use Nuwave\Lighthouse\Support\Traits\HasArgumentValue;
|
||||
use Nuwave\Lighthouse\Support\Utils;
|
||||
|
||||
class RulesGatherer
|
||||
{
|
||||
/**
|
||||
* The gathered rules.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
public $rules = [];
|
||||
|
||||
/**
|
||||
* The gathered messages.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
public $messages = [];
|
||||
|
||||
/**
|
||||
* The gathered attributes.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $attributes = [];
|
||||
|
||||
public function __construct(ArgumentSet $argumentSet)
|
||||
{
|
||||
$this->gatherRulesRecursively($argumentSet, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string> $argumentPath
|
||||
*/
|
||||
public function gatherRulesRecursively(ArgumentSet $argumentSet, array $argumentPath): void
|
||||
{
|
||||
$this->gatherRulesForArgumentSet($argumentSet, $argumentSet->directives, $argumentPath);
|
||||
|
||||
$argumentsWithUndefined = $argumentSet->argumentsWithUndefined();
|
||||
foreach ($argumentsWithUndefined as $name => $argument) {
|
||||
$nestedPath = array_merge($argumentPath, [$name]);
|
||||
|
||||
$directivesForArray = $argument->directives->filter(
|
||||
Utils::instanceofMatcher(ArgDirectiveForArray::class)
|
||||
);
|
||||
$this->gatherRulesForArgument($argument, $directivesForArray, $nestedPath);
|
||||
|
||||
$directivesForArgument = $argument->directives->filter(
|
||||
Utils::instanceofMatcher(ArgDirective::class)
|
||||
);
|
||||
|
||||
if (
|
||||
$argument->type instanceof ListType
|
||||
&& is_array($argument->value)
|
||||
) {
|
||||
foreach ($argument->value as $index => $value) {
|
||||
$this->handleArgumentValue($value, $directivesForArgument, array_merge($nestedPath, [$index]));
|
||||
}
|
||||
} else {
|
||||
$this->handleArgumentValue($argument->value, $directivesForArgument, $nestedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<\Nuwave\Lighthouse\Support\Contracts\Directive> $directives
|
||||
* @param array<int|string> $path
|
||||
*/
|
||||
public function gatherRulesForArgumentSet(ArgumentSet $argumentSet, Collection $directives, array $path): void
|
||||
{
|
||||
foreach ($directives as $directive) {
|
||||
if ($directive instanceof ArgumentSetValidation) {
|
||||
if (Utils::classUsesTrait($directive, HasArgumentValue::class)) {
|
||||
/**
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
* @var \Nuwave\Lighthouse\Support\Contracts\Directive&\Nuwave\Lighthouse\Support\Contracts\ArgumentSetValidation&\Nuwave\Lighthouse\Support\Traits\HasArgumentValue $directive
|
||||
*/
|
||||
// @phpstan-ignore-next-line using trait in typehint
|
||||
$directive->setArgumentValue($argumentSet);
|
||||
}
|
||||
|
||||
$this->extractValidationForArgumentSet($directive, $path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value Any argument value is possible
|
||||
* @param \Illuminate\Support\Collection<\Nuwave\Lighthouse\Support\Contracts\Directive> $directives
|
||||
* @param array<int|string> $path
|
||||
*/
|
||||
public function gatherRulesForArgument($value, Collection $directives, array $path): void
|
||||
{
|
||||
foreach ($directives as $directive) {
|
||||
if ($directive instanceof ArgumentValidation) {
|
||||
if (Utils::classUsesTrait($directive, HasArgumentValue::class)) {
|
||||
/**
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
* @var \Nuwave\Lighthouse\Support\Contracts\Directive&\Nuwave\Lighthouse\Support\Contracts\ArgumentValidation&\Nuwave\Lighthouse\Support\Traits\HasArgumentValue $directive
|
||||
*/
|
||||
// @phpstan-ignore-next-line using trait in typehint
|
||||
$directive->setArgumentValue($value);
|
||||
}
|
||||
|
||||
$this->extractValidationForArgument($directive, $path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Nuwave\Lighthouse\Execution\Arguments\Argument|\Nuwave\Lighthouse\Execution\Arguments\ArgumentSet|mixed $value
|
||||
* @param \Illuminate\Support\Collection<\Nuwave\Lighthouse\Support\Contracts\Directive> $directives
|
||||
* @param array<int|string> $path
|
||||
*/
|
||||
protected function handleArgumentValue($value, Collection $directives, array $path): void
|
||||
{
|
||||
$this->gatherRulesForArgument($value, $directives, $path);
|
||||
|
||||
if ($value instanceof ArgumentSet) {
|
||||
$this->gatherRulesRecursively($value, $path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string> $argumentPath
|
||||
*/
|
||||
public function extractValidationForArgumentSet(ArgumentSetValidation $directive, array $argumentPath): void
|
||||
{
|
||||
$qualifiedRulesMap = array_map(
|
||||
function (array $rules) use ($argumentPath): array {
|
||||
return $this->qualifyArgumentReferences($rules, $argumentPath);
|
||||
},
|
||||
$directive->rules()
|
||||
);
|
||||
|
||||
$this->rules = array_merge_recursive(
|
||||
$this->rules,
|
||||
$this->wrap($qualifiedRulesMap, $argumentPath)
|
||||
);
|
||||
|
||||
$this->messages += $this->wrap($directive->messages(), $argumentPath);
|
||||
|
||||
$this->attributes = array_merge(
|
||||
$this->attributes,
|
||||
$this->wrap($directive->attributes(), $argumentPath)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string> $argumentPath
|
||||
*/
|
||||
public function extractValidationForArgument(ArgumentValidation $directive, array $argumentPath): void
|
||||
{
|
||||
$qualifiedRules = $this->qualifyArgumentReferences(
|
||||
$directive->rules(),
|
||||
// The last element is the name of the argument the rule is defined upon.
|
||||
// We want the qualified path to start from the parent level.
|
||||
array_slice($argumentPath, 0, -1)
|
||||
);
|
||||
|
||||
$this->rules = array_merge_recursive(
|
||||
$this->rules,
|
||||
[implode('.', $argumentPath) => $qualifiedRules]
|
||||
);
|
||||
|
||||
$this->messages += $this->wrap($directive->messages(), $argumentPath);
|
||||
|
||||
$attribute = $directive->attribute();
|
||||
if (null !== $attribute) {
|
||||
$this->attributes = array_merge(
|
||||
$this->attributes,
|
||||
[implode('.', $argumentPath) => $attribute]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $rulesOrMessages
|
||||
* @param array<int|string> $path
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function wrap(array $rulesOrMessages, array $path): array
|
||||
{
|
||||
$withPath = [];
|
||||
|
||||
foreach ($rulesOrMessages as $key => $value) {
|
||||
$combinedPath = implode('.', array_merge($path, [$key]));
|
||||
|
||||
$withPath[$combinedPath] = $value;
|
||||
}
|
||||
|
||||
return $withPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend rule arguments that refer to other arguments with the full path.
|
||||
*
|
||||
* This may be necessary to allow certain rules to be reusable when placed
|
||||
* upon input arguments. For example, `required_with:foo` may be defined
|
||||
* on an input value that is nested within the arguments under `input.0`.
|
||||
* It is thus changed to the full reference `required_with:input.0.foo`.
|
||||
*
|
||||
* @param array<int, mixed> $rules
|
||||
* @param array<int|string> $argumentPath
|
||||
* @return array<int, array<int, mixed>|object>
|
||||
*/
|
||||
protected function qualifyArgumentReferences(array $rules, array $argumentPath): array
|
||||
{
|
||||
return array_map(
|
||||
static function ($rule) use ($argumentPath) {
|
||||
if (is_object($rule)) {
|
||||
return $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array{
|
||||
* 0: string,
|
||||
* 1: array<int, mixed>,
|
||||
* } $parsed
|
||||
*/
|
||||
$parsed = ValidationRuleParser::parse($rule);
|
||||
|
||||
$name = $parsed[0];
|
||||
$args = $parsed[1];
|
||||
|
||||
// Those rule lists are a subset of https://github.com/illuminate/validation/blob/8079fd53dee983e7c52d1819ae3b98c71a64fbc0/Validator.php#L206-L236
|
||||
// using the docs to know which ones reference other fields: https://laravel.com/docs/8.x/validation#available-validation-rules
|
||||
// We do not handle the Exclude* rules, those mutate the input and are not supported.
|
||||
|
||||
// Rules where the first argument is a field reference
|
||||
if (in_array($name, [
|
||||
'Different',
|
||||
'Gt',
|
||||
'Gte',
|
||||
'Lt',
|
||||
'Lte',
|
||||
'RequiredIf',
|
||||
'RequiredUnless',
|
||||
'ProhibitedIf',
|
||||
'ProhibitedUnless',
|
||||
'Same',
|
||||
])) {
|
||||
$args[0] = implode('.', array_merge($argumentPath, [$args[0]]));
|
||||
}
|
||||
|
||||
// Rules where all arguments are field references
|
||||
if (in_array($name, [
|
||||
'RequiredWith',
|
||||
'RequiredWithAll',
|
||||
'RequiredWithout',
|
||||
'RequiredWithoutAll',
|
||||
])) {
|
||||
$args = array_map(
|
||||
static function (string $field) use ($argumentPath): string {
|
||||
return implode('.', array_merge($argumentPath, [$field]));
|
||||
},
|
||||
$args
|
||||
);
|
||||
}
|
||||
|
||||
// Laravel expects the rule to be a flat array of name, arg1, arg2, ...
|
||||
return array_merge([$name], $args);
|
||||
},
|
||||
$rules
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Validation;
|
||||
|
||||
use Closure;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||
use Nuwave\Lighthouse\Exceptions\ValidationException;
|
||||
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
|
||||
use Nuwave\Lighthouse\Schema\Values\FieldValue;
|
||||
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
|
||||
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
|
||||
|
||||
class ValidateDirective extends BaseDirective implements FieldMiddleware
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Validation\Factory
|
||||
*/
|
||||
protected $validationFactory;
|
||||
|
||||
public function __construct(ValidationFactory $validationFactory)
|
||||
{
|
||||
$this->validationFactory = $validationFactory;
|
||||
}
|
||||
|
||||
public static function definition(): string
|
||||
{
|
||||
return /** @lang GraphQL */ <<<'GRAPHQL'
|
||||
"""
|
||||
Run validation on a field.
|
||||
"""
|
||||
directive @validate on FIELD_DEFINITION
|
||||
GRAPHQL;
|
||||
}
|
||||
|
||||
public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
|
||||
{
|
||||
$resolver = $fieldValue->getResolver();
|
||||
|
||||
return $next(
|
||||
$fieldValue->setResolver(
|
||||
function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($resolver) {
|
||||
$argumentSet = $resolveInfo->argumentSet;
|
||||
$rulesGatherer = new RulesGatherer($argumentSet);
|
||||
|
||||
$validator = $this->validationFactory
|
||||
->make(
|
||||
$args,
|
||||
$rulesGatherer->rules,
|
||||
$rulesGatherer->messages,
|
||||
$rulesGatherer->attributes
|
||||
);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$path = implode('.', $resolveInfo->path);
|
||||
|
||||
throw new ValidationException("Validation failed for the field [$path].", $validator);
|
||||
}
|
||||
|
||||
return $resolver($root, $args, $context, $resolveInfo);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Validation;
|
||||
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;
|
||||
|
||||
class ValidationServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(Dispatcher $dispatcher): void
|
||||
{
|
||||
$dispatcher->listen(
|
||||
RegisterDirectiveNamespaces::class,
|
||||
static function (): string {
|
||||
return __NAMESPACE__;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Validation;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Nuwave\Lighthouse\Execution\Arguments\ArgumentSet;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgumentSetValidation;
|
||||
|
||||
abstract class Validator implements ArgumentSetValidation
|
||||
{
|
||||
/**
|
||||
* The slice of incoming arguments to validate.
|
||||
*
|
||||
* @var \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet
|
||||
*/
|
||||
protected $args;
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the slice of args to validate.
|
||||
*/
|
||||
public function setArgs(ArgumentSet $args): void
|
||||
{
|
||||
$this->args = $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value of an argument.
|
||||
*
|
||||
* @param string $key The key of the argument, may use dot notation to get nested values.
|
||||
* @param mixed|null $default Returned in case the argument is not present.
|
||||
* @return mixed The value of the argument or the default.
|
||||
*/
|
||||
protected function arg(string $key, $default = null)
|
||||
{
|
||||
return Arr::get(
|
||||
$this->args->toArray(),
|
||||
$key,
|
||||
$default
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Nuwave\Lighthouse\Validation;
|
||||
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use Nuwave\Lighthouse\Exceptions\DefinitionException;
|
||||
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
|
||||
use Nuwave\Lighthouse\Schema\AST\DocumentAST;
|
||||
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgDirective;
|
||||
use Nuwave\Lighthouse\Support\Contracts\ArgumentSetValidation;
|
||||
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
|
||||
use Nuwave\Lighthouse\Support\Contracts\TypeManipulator;
|
||||
use Nuwave\Lighthouse\Support\Traits\HasArgumentValue;
|
||||
|
||||
class ValidatorDirective extends BaseDirective implements ArgDirective, ArgumentSetValidation, TypeManipulator, FieldManipulator
|
||||
{
|
||||
use HasArgumentValue;
|
||||
|
||||
/**
|
||||
* @var \Nuwave\Lighthouse\Validation\Validator|null
|
||||
*/
|
||||
protected $validator;
|
||||
|
||||
public static function definition(): string
|
||||
{
|
||||
return /** @lang GraphQL */ <<<'GRAPHQL'
|
||||
"""
|
||||
Provide validation rules through a PHP class.
|
||||
"""
|
||||
directive @validator(
|
||||
"""
|
||||
The name of the class to use.
|
||||
|
||||
If defined on an input, this defaults to a class called `{$inputName}Validator` in the
|
||||
default validator namespace. For fields, it uses the name of the parent type
|
||||
and the field name: `{$parent}{$field}Validator`.
|
||||
"""
|
||||
class: String
|
||||
) repeatable on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION | INPUT_OBJECT
|
||||
GRAPHQL;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return $this->validator()->rules();
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return $this->validator()->messages();
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return $this->validator()->attributes();
|
||||
}
|
||||
|
||||
protected function validator(): Validator
|
||||
{
|
||||
if ($this->validator === null) {
|
||||
/** @var \Nuwave\Lighthouse\Validation\Validator $validator */
|
||||
$validator = app(
|
||||
// We precomputed and validated the full class name at schema build time
|
||||
$this->directiveArgValue('class')
|
||||
);
|
||||
// @phpstan-ignore-next-line Since this directive can only be defined on a field or input, this must be ArgumentSet
|
||||
$validator->setArgs($this->argumentValue);
|
||||
|
||||
return $this->validator = $validator;
|
||||
}
|
||||
|
||||
return $this->validator;
|
||||
}
|
||||
|
||||
public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition)
|
||||
{
|
||||
if (! $typeDefinition instanceof InputObjectTypeDefinitionNode) {
|
||||
throw new DefinitionException(
|
||||
"Can not use @validator on non input type {$typeDefinition->name->value}."
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->directiveHasArgument('class')) {
|
||||
$classCandidate = $this->directiveArgValue('class');
|
||||
} else {
|
||||
$classCandidate = $typeDefinition->name->value.'Validator';
|
||||
}
|
||||
|
||||
$this->setFullClassnameOnDirective($typeDefinition, $classCandidate);
|
||||
}
|
||||
|
||||
public function manipulateFieldDefinition(
|
||||
DocumentAST &$documentAST,
|
||||
FieldDefinitionNode &$fieldDefinition,
|
||||
ObjectTypeDefinitionNode &$parentType
|
||||
) {
|
||||
if ($this->directiveHasArgument('class')) {
|
||||
$classCandidate = $this->directiveArgValue('class');
|
||||
} else {
|
||||
$classCandidate = $parentType->name->value
|
||||
.'\\'
|
||||
.ucfirst($fieldDefinition->name->value)
|
||||
.'Validator';
|
||||
}
|
||||
|
||||
$this->setFullClassnameOnDirective($fieldDefinition, $classCandidate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the full classname of the validator class on the directive.
|
||||
*
|
||||
* This allows accessing it straight away when resolving the query.
|
||||
*
|
||||
* @param (\GraphQL\Language\AST\TypeDefinitionNode&\GraphQL\Language\AST\Node)|\GraphQL\Language\AST\FieldDefinitionNode $definition
|
||||
*/
|
||||
protected function setFullClassnameOnDirective(Node &$definition, string $classCandidate): void
|
||||
{
|
||||
$validatorClass = $this->namespaceValidatorClass($classCandidate);
|
||||
|
||||
// @phpstan-ignore-next-line The passed in Node types all have the property $directives
|
||||
foreach ($definition->directives as $directive) {
|
||||
if ($directive->name->value === $this->name()) {
|
||||
$directive->arguments = ASTHelper::mergeUniqueNodeList(
|
||||
$directive->arguments,
|
||||
[Parser::argument('class: "'.addslashes($validatorClass).'"')],
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<\Nuwave\Lighthouse\Validation\Validator>
|
||||
*/
|
||||
protected function namespaceValidatorClass(string $classCandidate): string
|
||||
{
|
||||
/**
|
||||
* @var class-string<\Nuwave\Lighthouse\Validation\Validator> $validatorClassName We know this because of the callback
|
||||
*/
|
||||
$validatorClassName = $this->namespaceClassName(
|
||||
$classCandidate,
|
||||
(array) config('lighthouse.namespaces.validators'),
|
||||
function (string $classCandidate): bool {
|
||||
return is_subclass_of($classCandidate, Validator::class);
|
||||
}
|
||||
);
|
||||
|
||||
return $validatorClassName;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user