From af9fd3bec94740fee6beb384e9e6db28d8a5fe6c Mon Sep 17 00:00:00 2001 From: murtukov Date: Sun, 8 Nov 2020 06:02:39 +0100 Subject: [PATCH 1/3] Refactor validation feature * Update docblocks * Remove dead code and annotations * Fix tests * Fix service definitions * Increase coverage * Update GraphQLServices * Refactor methods in TypeBuilder related to validation * Refactor InputValidator * Throw in InputValidatorFactory if no validator defined * Add extra checks in tests related to validator * Update isListOfType method * Refactor class ValidatorFactory into InputValidatorFactory * Move InputValidator instantiation into the InputValidatorFactory service --- src/Definition/GraphQLServices.php | 9 + .../OverblogGraphQLExtension.php | 24 +- src/Generator/TypeBuilder.php | 303 ++++++------------ src/Resources/config/services.yaml | 8 + src/Validator/InputValidator.php | 182 +++++++---- src/Validator/InputValidatorFactory.php | 44 +++ src/Validator/ValidatorFactory.php | 39 --- tests/ExpressionLanguage/TestCase.php | 3 +- .../mapping/Mutation.types.yml | 2 +- .../config/public/mapping/public.types.yml | 1 + .../connectionWithComplexity.types.yml | 5 + .../validator/mapping/Mutation.types.yml | 9 + .../Generator/TypeGeneratorTest.php | 6 +- .../Functional/Validator/StaticValidator.php | 5 + tests/Validator/InputValidatorTest.php | 10 +- 15 files changed, 307 insertions(+), 343 deletions(-) create mode 100644 src/Validator/InputValidatorFactory.php delete mode 100644 src/Validator/ValidatorFactory.php diff --git a/src/Definition/GraphQLServices.php b/src/Definition/GraphQLServices.php index 6abfd0694..0bcc911b9 100644 --- a/src/Definition/GraphQLServices.php +++ b/src/Definition/GraphQLServices.php @@ -9,6 +9,7 @@ use Overblog\GraphQLBundle\Resolver\MutationResolver; use Overblog\GraphQLBundle\Resolver\QueryResolver; use Overblog\GraphQLBundle\Resolver\TypeResolver; +use Overblog\GraphQLBundle\Validator\InputValidator; /** * Container for special services to be passed to all generated types. @@ -81,4 +82,12 @@ public function getType(string $typeName): ?Type { return $this->types->resolve($typeName); } + + /** + * Creates an instance of InputValidator + */ + public function createInputValidator(array $resolverArgs): InputValidator + { + return $this->services['input_validator_factory']->create($resolverArgs); + } } diff --git a/src/DependencyInjection/OverblogGraphQLExtension.php b/src/DependencyInjection/OverblogGraphQLExtension.php index 1d391e814..40de83011 100644 --- a/src/DependencyInjection/OverblogGraphQLExtension.php +++ b/src/DependencyInjection/OverblogGraphQLExtension.php @@ -23,7 +23,6 @@ use Overblog\GraphQLBundle\EventListener\ErrorHandlerListener; use Overblog\GraphQLBundle\EventListener\ErrorLoggerListener; use Overblog\GraphQLBundle\Request\Executor; -use Overblog\GraphQLBundle\Validator\ValidatorFactory; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -31,9 +30,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\Validator\Validation; use function array_fill_keys; -use function class_exists; use function realpath; use function sprintf; @@ -59,7 +56,6 @@ public function load(array $configs, ContainerBuilder $container): void $this->setCompilerCacheWarmer($config, $container); $this->registerForAutoconfiguration($container); $this->setDefaultFieldResolver($config, $container); - $this->registerValidatorFactory($container); $container->setParameter($this->getAlias().'.config', $config); $container->setParameter($this->getAlias().'.resources_dir', realpath(__DIR__.'/../Resources')); @@ -104,24 +100,6 @@ private function registerForAutoconfiguration(ContainerBuilder $container): void ->addTag('overblog_graphql.type'); } - private function registerValidatorFactory(ContainerBuilder $container): void - { - if (class_exists(Validation::class)) { - $container->register(ValidatorFactory::class) - ->setArguments([ - new Reference('validator.validator_factory'), - new Reference('translator.default', $container::NULL_ON_INVALID_REFERENCE), - ]) - ->addTag( - 'overblog_graphql.service', - [ - 'alias' => 'validatorFactory', - 'public' => false, - ] - ); - } - } - private function setDefaultFieldResolver(array $config, ContainerBuilder $container): void { $container->setAlias($this->getAlias().'.default_field_resolver', $config['definitions']['default_field_resolver']); @@ -295,7 +273,7 @@ private function setServicesAliases(array $config, ContainerBuilder $container): /** * Returns a list of custom exceptions mapped to error/warning classes. * - * @param array $exceptionConfig + * @param array> $exceptionConfig * * @return array Custom exception map, [exception => UserError/UserWarning] */ diff --git a/src/Generator/TypeBuilder.php b/src/Generator/TypeBuilder.php index 2f3e24b5f..efbefed0d 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -16,7 +16,6 @@ use Murtukov\PHPCodeGenerator\Closure; use Murtukov\PHPCodeGenerator\Config; use Murtukov\PHPCodeGenerator\ConverterInterface; -use Murtukov\PHPCodeGenerator\Exception\UnrecognizedValueTypeException; use Murtukov\PHPCodeGenerator\GeneratorInterface; use Murtukov\PHPCodeGenerator\Instance; use Murtukov\PHPCodeGenerator\Literal; @@ -31,10 +30,7 @@ use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter; use Overblog\GraphQLBundle\Generator\Exception\GeneratorException; use Overblog\GraphQLBundle\Validator\InputValidator; -use function array_filter; -use function array_intersect; use function array_map; -use function array_replace_recursive; use function class_exists; use function count; use function explode; @@ -46,12 +42,10 @@ use function ltrim; use function reset; use function rtrim; -use function str_split; use function strpos; use function strrchr; use function strtolower; use function substr; -use function trim; /** * Service that exposes a single method `build` called for each GraphQL @@ -84,6 +78,7 @@ class TypeBuilder protected string $namespace; protected array $config; protected string $type; + protected string $currentField; protected string $gqlServices = '$'.TypeGenerator::GRAPHQL_SERVICES; public function __construct(ExpressionConverter $expressionConverter, string $namespace) @@ -112,7 +107,6 @@ public function __construct(ExpressionConverter $expressionConverter, string $na * } $config * * @throws GeneratorException - * @throws UnrecognizedValueTypeException */ public function build(array $config, string $type): PhpFile { @@ -290,7 +284,6 @@ protected function wrapTypeRecursive($typeNode, bool &$isReference) * ] * * @throws GeneratorException - * @throws UnrecognizedValueTypeException */ protected function buildConfig(array $config): Collection { @@ -358,7 +351,7 @@ protected function buildConfig(array $config): Collection } } - return $configLoader; // @phpstan-ignore-line + return $configLoader; } /** @@ -422,8 +415,8 @@ protected function buildScalarCallback($callback, string $fieldName) * Render example (with validation): * * function ($value, $args, $context, $info) use ($services) { - * $validator = {@see buildValidatorInstance} - * return $services->mutation("create_post", $validator); + * $validator = $services->createInputValidator(func_get_args()); + * return $services->mutation("create_post", $validator]); * } * * Render example (with validation, but errors are injected into the user-defined resolver): @@ -431,7 +424,7 @@ protected function buildScalarCallback($callback, string $fieldName) * * function ($value, $args, $context, $info) use ($services) { * $errors = new ResolveErrors(); - * $validator = {@see buildValidatorInstance} + * $validator = $services->createInputValidator(func_get_args()); * * $errors->setValidationErrors($validator->validate(null, false)) * @@ -441,36 +434,36 @@ protected function buildScalarCallback($callback, string $fieldName) * @param mixed $resolve * * @throws GeneratorException - * @throws UnrecognizedValueTypeException * * @return GeneratorInterface|string */ - protected function buildResolve($resolve, ?array $validationConfig = null) + protected function buildResolve($resolve, ?array $groups = null) { if (is_callable($resolve) && is_array($resolve)) { return Collection::numeric($resolve); } + // TODO: before creating an input validator, check if any validation rules are defined if (EL::isStringWithTrigger($resolve)) { $closure = Closure::new() ->addArguments('value', 'args', 'context', 'info') ->bindVar(TypeGenerator::GRAPHQL_SERVICES); - $injectErrors = EL::expressionContainsVar('errors', $resolve); + $injectValidator = EL::expressionContainsVar('validator', $resolve); - if ($injectErrors) { - $closure->append('$errors = ', Instance::new(ResolveErrors::class)); - } + if ($this->configContainsValidation()) { + $injectErrors = EL::expressionContainsVar('errors', $resolve); - $injectValidator = EL::expressionContainsVar('validator', $resolve); + if ($injectErrors) { + $closure->append('$errors = ', Instance::new(ResolveErrors::class)); + } - if (null !== $validationConfig) { - $closure->append('$validator = ', $this->buildValidatorInstance($validationConfig)); + $closure->append('$validator = ', "$this->gqlServices->createInputValidator(func_get_args())"); // If auto-validation on or errors are injected if (!$injectValidator || $injectErrors) { - if (!empty($validationConfig['validationGroups'])) { - $validationGroups = Collection::numeric($validationConfig['validationGroups']); + if (!empty($groups)) { + $validationGroups = Collection::numeric($groups); } else { $validationGroups = 'null'; } @@ -485,12 +478,8 @@ protected function buildResolve($resolve, ?array $validationConfig = null) $closure->emptyLine(); } - } elseif (true === $injectValidator) { - throw new GeneratorException( - 'Unable to inject an instance of the InputValidator. No validation constraints provided. '. - 'Please remove the "validator" argument from the list of dependencies of your resolver '. - 'or provide validation configs.' - ); + } elseif ($injectValidator) { + throw new GeneratorException('Unable to inject an instance of the InputValidator. No validation constraints provided. Please remove the "validator" argument from the list of dependencies of your resolver or provide validation configs.'); } $closure->append('return ', $this->expressionConverter->convert($resolve)); @@ -502,35 +491,23 @@ protected function buildResolve($resolve, ?array $validationConfig = null) } /** - * Render example: - * - * new InputValidator( - * \func_get_args(), - * $services->get('container')->get('validator'), - * $services->get('validatorFactory'), - * {@see buildProperties}, - * {@see buildValidationRules}, - * ) - * - * @throws GeneratorException + * Checks if given config contains any validation rules. */ - protected function buildValidatorInstance(array $mapping): Instance + private function configContainsValidation(): bool { - $validator = Instance::new(InputValidator::class) - ->setMultiline() - ->addArgument(new Literal('\\func_get_args()')) - ->addArgument("$this->gqlServices->get('container')->get('validator')") - ->addArgument("$this->gqlServices->get('validatorFactory')"); - - if (!empty($mapping['properties'])) { - $validator->addArgument($this->buildProperties($mapping['properties'])); + $fieldConfig = $this->config['fields'][$this->currentField]; + + if (!empty($fieldConfig['validation'])) { + return true; } - if (!empty($mapping['class'])) { - $validator->addArgument($this->buildValidationRules($mapping['class'])); + foreach ($fieldConfig['args'] ?? [] as $argConfig) { + if (!empty($argConfig['validation'])) { + return true; + } } - return $validator; + return false; } /** @@ -538,7 +515,9 @@ protected function buildValidatorInstance(array $mapping): Instance * * [ * 'link' => {@see normalizeLink} - * 'cascade' => {@see buildCascade}, + * 'cascade' => [ + * 'groups' => ['my_group'], + * ], * 'constraints' => {@see buildConstraints} * ] * @@ -552,7 +531,7 @@ protected function buildValidatorInstance(array $mapping): Instance * * @throws GeneratorException */ - protected function buildValidationRules(array $config): Collection + protected function buildValidationRules(array $config): GeneratorInterface { // Convert to object for better readability $c = (object) $config; @@ -569,26 +548,32 @@ protected function buildValidationRules(array $config): Collection } } - if (!empty($c->cascade)) { - $array->addItem('cascade', $this->buildCascade($c->cascade)); + if (isset($c->cascade)) { + // If there are only constarainst, use short syntax + if (empty($c->cascade['groups'])) { + $this->file->addUse(InputValidator::class); + + return Literal::new('InputValidator::CASCADE'); + } + $array->addItem('cascade', $c->cascade['groups']); } if (!empty($c->constraints)) { - // If there are only constarainst, dont use additional nesting + // If there are only constarainst, use short syntax if (0 === $array->count()) { return $this->buildConstraints($c->constraints); } $array->addItem('constraints', $this->buildConstraints($c->constraints)); } - return $array; // @phpstan-ignore-line + return $array; } /** - * Builds a numeric multiline array with Symfony Constraint instances. - * The array is used by {@see InputValidator} during requests. + * Builds a closure or a numeric multiline array with Symfony Constraint + * instances. The array is used by {@see InputValidator} during requests. * - * Render example: + * Render example (array): * * [ * new NotNull(), @@ -599,9 +584,22 @@ protected function buildValidationRules(array $config): Collection * ... * ] * + * Render example (in a closure): + * + * fn() => [ + * new NotNull(), + * new Length([ + * 'min' => 5, + * 'max' => 10 + * ]), + * ... + * ] + * * @throws GeneratorException + * + * @return ArrowFunction|Collection */ - protected function buildConstraints(array $constraints = []): Collection + protected function buildConstraints(array $constraints = [], bool $inClosure = true) { $result = Collection::numeric()->setMultiline(); @@ -628,8 +626,8 @@ protected function buildConstraints(array $constraints = []): Collection if (is_array($args)) { if (isset($args[0]) && is_array($args[0])) { - // Another instance? - $instance->addArgument($this->buildConstraints($args)); + // Nested instance + $instance->addArgument($this->buildConstraints($args, false)); } else { // Numeric or Assoc array? $instance->addArgument(isset($args[0]) ? $args : Collection::assoc($args)); @@ -641,79 +639,11 @@ protected function buildConstraints(array $constraints = []): Collection $result->push($instance); } - return $result; // @phpstan-ignore-line - } - - /** - * Builds an assoc multiline array with a predefined shape. The array - * is used by {@see InputValidator} during requests. - * - * Possible keys are: 'groups', 'isCollection' and 'referenceType'. - * - * Render example: - * - * [ - * 'groups' => ['my_group'], - * 'isCollection' => true, - * 'referenceType' => $services->getType('Article') - * ] - * - * @param array{ - * referenceType: string, - * groups: array, - * isCollection: bool - * } $cascade - * - * @throws GeneratorException - */ - protected function buildCascade(array $cascade): Collection - { - $c = (object) $cascade; - - /** - * todo: remove this type-hint after fixing return type in the php generator - * - * @var Collection $array - */ - $array = Collection::assoc()->addIfNotEmpty('groups', $c->groups); - - if (isset($c->isCollection)) { - $array->addItem('isCollection', $c->isCollection); - } - - if (isset($c->referenceType)) { - $type = trim($c->referenceType, '[]!'); - - if (in_array($type, static::BUILT_IN_TYPES)) { - throw new GeneratorException('Cascade validation cannot be applied to built-in types.'); - } - - $array->addItem('referenceType', "$this->gqlServices->getType('$c->referenceType')"); - } - - return $array; // @phpstan-ignore-line - } - - /** - * Render example: - * - * [ - * 'firstName' => {@see buildValidationRules}, - * 'lastName' => {@see buildValidationRules}, - * ... - * ] - * - * @throws GeneratorException - */ - protected function buildProperties(array $properties): Collection - { - $array = Collection::assoc(); - - foreach ($properties as $name => $props) { - $array->addItem($name, $this->buildValidationRules($props)); + if ($inClosure) { + return ArrowFunction::new($result); } - return $array; // @phpstan-ignore-line + return $result; // @phpstan-ignore-line } /** @@ -745,12 +675,13 @@ protected function buildProperties(array $properties): Collection * @internal * * @throws GeneratorException - * @throws UnrecognizedValueTypeException * * @return GeneratorInterface|Collection|string */ - public function buildField(array $fieldConfig /*, $fieldname */) + public function buildField(array $fieldConfig, string $fieldname) { + $this->currentField = $fieldname; + // Convert to object for better readability $c = (object) $fieldConfig; @@ -764,8 +695,10 @@ public function buildField(array $fieldConfig /*, $fieldname */) // only for object types if (isset($c->resolve)) { - $validationConfig = $this->restructureObjectValidationConfig($fieldConfig); - $field->addItem('resolve', $this->buildResolve($c->resolve, $validationConfig)); + if (isset($c->validation)) { + $field->addItem('validation', $this->buildValidationRules($c->validation)); + } + $field->addItem('resolve', $this->buildResolve($c->resolve, $fieldConfig['validationGroups'] ?? null)); } if (isset($c->deprecationReason)) { @@ -797,12 +730,6 @@ public function buildField(array $fieldConfig /*, $fieldname */) } if ('input-object' === $this->type && isset($c->validation)) { - // restructure validation config - if (!empty($c->validation['cascade'])) { - $c->validation['cascade']['isCollection'] = $this->isCollectionType($c->type); - $c->validation['cascade']['referenceType'] = trim($c->type, '[]!'); - } - $field->addItem('validation', $this->buildValidationRules($c->validation)); } @@ -811,21 +738,24 @@ public function buildField(array $fieldConfig /*, $fieldname */) /** * Render example: - * - * [ - * 'name' => 'username', - * 'type' => {@see buildType}, - * 'description' => 'Some fancy description.', - * 'defaultValue' => 'admin', - * ] - * - * @internal + * + * [ + * 'name' => 'username', + * 'type' => {@see buildType}, + * 'description' => 'Some fancy description.', + * 'defaultValue' => 'admin', + * ] + * * * @param array{ * type: string, * description?: string, * defaultValue?: string * } $argConfig + * + * @internal + * + * @throws GeneratorException */ public function buildArg(array $argConfig, string $argName): Collection { @@ -844,7 +774,15 @@ public function buildArg(array $argConfig, string $argName): Collection $arg->addIfNotEmpty('defaultValue', $c->defaultValue); } - return $arg; // @phpstan-ignore-line + if (!empty($c->validation)) { + if (in_array($c->type, self::BUILT_IN_TYPES) && isset($c->validation['cascade'])) { + throw new GeneratorException('Cascade validation cannot be applied to built-in types.'); + } + + $arg->addIfNotEmpty('validation', $this->buildValidationRules($c->validation)); + } + + return $arg; } /** @@ -975,61 +913,6 @@ protected function buildResolveType($resolveType) return $resolveType; } - // TODO (murtukov): rework this method to use builders - protected function restructureObjectValidationConfig(array $fieldConfig): ?array - { - $properties = []; - - foreach ($fieldConfig['args'] ?? [] as $name => $arg) { - if (empty($arg['validation'])) { - continue; - } - - $properties[$name] = $arg['validation']; - - if (empty($arg['validation']['cascade'])) { - continue; - } - - $properties[$name]['cascade']['isCollection'] = $this->isCollectionType($arg['type']); - $properties[$name]['cascade']['referenceType'] = trim($arg['type'], '[]!'); - } - - // Merge class and field constraints - $classValidation = $this->config['validation'] ?? []; - - if (!empty($fieldConfig['validation'])) { - $classValidation = array_replace_recursive($classValidation, $fieldConfig['validation']); - } - - $mapping = []; - - if (!empty($properties)) { - $mapping['properties'] = $properties; - } - - // class - if (!empty($classValidation)) { - $mapping['class'] = $classValidation; - } - - // validationGroups - if (!empty($fieldConfig['validationGroups'])) { - $mapping['validationGroups'] = $fieldConfig['validationGroups']; - } - - if (empty($classValidation) && !array_filter($properties)) { - return null; - } else { - return $mapping; - } - } - - protected function isCollectionType(string $type): bool - { - return 2 === count(array_intersect(['[', ']'], str_split($type))); - } - /** * Creates and array from a formatted string. * diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index db17309c1..a91aaf436 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -108,3 +108,11 @@ services: arguments: - '@Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter' - '%overblog_graphql.class_namespace%' + + Overblog\GraphQLBundle\Validator\InputValidatorFactory: + arguments: + - '@?validator.validator_factory' + - '@?validator' + - '@?translator.default' + tags: + - { name: overblog_graphql.service, alias: input_validator_factory, public: false } diff --git a/src/Validator/InputValidator.php b/src/Validator/InputValidator.php index b5f7afe64..6d9eb0c73 100644 --- a/src/Validator/InputValidator.php +++ b/src/Validator/InputValidator.php @@ -4,63 +4,58 @@ namespace Overblog\GraphQLBundle\Validator; +use Closure; use GraphQL\Type\Definition\InputObjectType; +use GraphQL\Type\Definition\ListOfType; +use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; +use GraphQL\Type\Definition\Type; use Overblog\GraphQLBundle\Definition\ArgumentInterface; +use Overblog\GraphQLBundle\Definition\Type\GeneratedTypeInterface; use Overblog\GraphQLBundle\Validator\Exception\ArgumentsValidationException; use Overblog\GraphQLBundle\Validator\Mapping\MetadataFactory; use Overblog\GraphQLBundle\Validator\Mapping\ObjectMetadata; -use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\GetterMetadata; use Symfony\Component\Validator\Mapping\PropertyMetadata; +use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; use function in_array; class InputValidator { private const TYPE_PROPERTY = 'property'; private const TYPE_GETTER = 'getter'; + public const CASCADE = 'cascade'; private array $resolverArgs; - private array $propertiesMapping; - private array $classMapping; - private ValidatorInterface $validator; + private ValidatorInterface $defaultValidator; private MetadataFactory $metadataFactory; private ResolveInfo $info; - private ValidatorFactory $validatorFactory; + private ConstraintValidatorFactoryInterface $constraintValidatorFactory; + private ?TranslatorInterface $defaultTranslator; /** @var ClassMetadataInterface[] */ private array $cachedMetadata = []; - /** - * InputValidator constructor. - */ public function __construct( array $resolverArgs, - ?ValidatorInterface $validator, - ValidatorFactory $factory, - array $propertiesMapping = [], - array $classMapping = [] + ValidatorInterface $validator, + ConstraintValidatorFactoryInterface $constraintValidatorFactory, + ?TranslatorInterface $translator ) { - if (null === $validator) { - throw new ServiceNotFoundException( - "The 'validator' service is not found. To use the 'InputValidator' you need to install the - Symfony Validator Component first. See: 'https://symfony.com/doc/current/validation.html'" - ); - } - $this->resolverArgs = $this->mapResolverArgs(...$resolverArgs); $this->info = $this->resolverArgs['info']; - $this->propertiesMapping = $propertiesMapping; - $this->classMapping = $classMapping; - $this->validator = $validator; - $this->validatorFactory = $factory; + $this->defaultValidator = $validator; + $this->constraintValidatorFactory = $constraintValidatorFactory; + $this->defaultTranslator = $translator; $this->metadataFactory = new MetadataFactory(); } @@ -89,14 +84,16 @@ public function validate($groups = null, bool $throw = true): ?ConstraintViolati { $rootObject = new ValidationNode($this->info->parentType, $this->info->fieldName, null, $this->resolverArgs); + $classMapping = $this->mergeClassValidation(); + $this->buildValidationTree( $rootObject, - $this->propertiesMapping, - $this->classMapping, + $this->info->fieldDefinition->config['args'], + $classMapping, $this->resolverArgs['args']->getArrayCopy() ); - $validator = $this->validatorFactory->createValidator($this->metadataFactory); + $validator = $this->createValidator($this->metadataFactory); $errors = $validator->validate($rootObject, null, $groups); @@ -107,51 +104,85 @@ public function validate($groups = null, bool $throw = true): ?ConstraintViolati } } + private function mergeClassValidation(): array + { + $common = static::normalizeConfig($this->info->parentType->config['validation'] ?? []); + $specific = static::normalizeConfig($this->info->fieldDefinition->config['validation'] ?? []); + + return array_filter([ + 'link' => $specific['link'] ?? $common['link'] ?? null, + 'constraints' => [ + ...($common['constraints'] ?? []), + ...($specific['constraints'] ?? []), + ], + ]); + } + + private function createValidator(MetadataFactory $metadataFactory): ValidatorInterface + { + $builder = Validation::createValidatorBuilder() + ->setMetadataFactory($metadataFactory) + ->setConstraintValidatorFactory($this->constraintValidatorFactory); + + if (null !== $this->defaultTranslator) { + // @phpstan-ignore-next-line (only for Symfony 4.4) + $builder + ->setTranslator($this->defaultTranslator) + ->setTranslationDomain('validators'); + } + + return $builder->getValidator(); + } + /** * Creates a composition of ValidationNode objects from args * and simultaneously applies to them validation constraints. */ - protected function buildValidationTree(ValidationNode $rootObject, array $propertiesMapping, array $classMapping, array $args): ValidationNode + protected function buildValidationTree(ValidationNode $rootObject, array $fields, array $classValidation, array $inputData): ValidationNode { $metadata = new ObjectMetadata($rootObject); - if (!empty($classMapping)) { - $this->applyClassConstraints($metadata, $classMapping); + if (!empty($classValidation)) { + $this->applyClassValidation($metadata, $classValidation); } - foreach ($propertiesMapping as $property => $params) { - if (!empty($params['cascade']) && isset($args[$property])) { - $options = $params['cascade']; + foreach ($fields as $name => $arg) { + $property = $arg['name'] ?? $name; + $validation = static::normalizeConfig($arg['validation'] ?? []); + + if (isset($validation['cascade']) && isset($inputData[$property])) { + $groups = $validation['cascade']; + $argType = $this->unclosure($arg['type']); /** @var ObjectType|InputObjectType $type */ - $type = $options['referenceType']; + $type = Type::getNamedType($argType); - if ($options['isCollection']) { - $rootObject->$property = $this->createCollectionNode($args[$property], $type, $rootObject); + if (static::isListOfType($argType)) { + $rootObject->$property = $this->createCollectionNode($inputData[$property], $type, $rootObject); } else { - $rootObject->$property = $this->createObjectNode($args[$property], $type, $rootObject); + $rootObject->$property = $this->createObjectNode($inputData[$property], $type, $rootObject); } $valid = new Valid(); - if (!empty($options['groups'])) { - $valid->groups = $options['groups']; + if (!empty($groups)) { + $valid->groups = $groups; } $metadata->addPropertyConstraint($property, $valid); } else { - $rootObject->$property = $args[$property] ?? null; + $rootObject->$property = $inputData[$property] ?? null; } - $this->restructureShortForm($params); + $validation = static::normalizeConfig($validation); - foreach ($params ?? [] as $key => $value) { + foreach ($validation as $key => $value) { switch ($key) { case 'link': [$fqcn, $property, $type] = $value; if (!in_array($fqcn, $this->cachedMetadata)) { - $this->cachedMetadata[$fqcn] = $this->validator->getMetadataFor($fqcn); + $this->cachedMetadata[$fqcn] = $this->defaultValidator->getMetadataFor($fqcn); } // Get metadata from the property and it's getters @@ -187,6 +218,18 @@ protected function buildValidationTree(ValidationNode $rootObject, array $proper return $rootObject; } + /** + * @param GeneratedTypeInterface|ListOfType|NonNull $type + */ + private static function isListOfType($type): bool + { + if ($type instanceof ListOfType || ($type instanceof NonNull && $type->getOfType() instanceof ListOfType)) { + return true; + } + + return false; + } + /** * @param ObjectType|InputObjectType $type */ @@ -206,33 +249,28 @@ private function createCollectionNode(array $values, $type, ValidationNode $pare */ private function createObjectNode(array $value, $type, ValidationNode $parent): ValidationNode { - $classMapping = $type->config['validation'] ?? []; - $propertiesMapping = []; - - foreach ($type->getFields() as $fieldName => $inputField) { - $propertiesMapping[$fieldName] = $inputField->config['validation'] ?? []; - } + $classValidation = static::normalizeConfig($type->config['validation'] ?? []); return $this->buildValidationTree( new ValidationNode($type, null, $parent, $this->resolverArgs), - $propertiesMapping, - $classMapping, + $type->config['fields'](), + $classValidation, $value ); } - private function applyClassConstraints(ObjectMetadata $metadata, array $rules): void + private function applyClassValidation(ObjectMetadata $metadata, array $rules): void { - $this->restructureShortForm($rules); + $rules = static::normalizeConfig($rules); foreach ($rules as $key => $value) { switch ($key) { case 'link': - $linkedMetadata = $this->validator->getMetadataFor($value); + $linkedMetadata = $this->defaultValidator->getMetadataFor($value); $metadata->addConstraints($linkedMetadata->getConstraints()); break; case 'constraints': - foreach ($value as $constraint) { + foreach ($this->unclosure($value) as $constraint) { if ($constraint instanceof Constraint) { $metadata->addConstraint($constraint); } elseif ($constraint instanceof GroupSequence) { @@ -244,11 +282,41 @@ private function applyClassConstraints(ObjectMetadata $metadata, array $rules): } } - private function restructureShortForm(array &$rules): void + /** + * Restructures short forms into the full form array and + * unwraps constraints in closures. + * + * @param mixed $config + */ + public static function normalizeConfig($config): array { - if (isset($rules[0])) { - $rules = ['constraints' => $rules]; + if ($config instanceof Closure) { + return ['constraints' => $config()]; + } + + if (self::CASCADE === $config) { + return ['cascade' => []]; + } + + if (isset($config['constraints']) && $config['constraints'] instanceof Closure) { + $config['constraints'] = $config['constraints'](); } + + return $config; + } + + /** + * @param mixed $value + * + * @return mixed + */ + private function unclosure($value) + { + if ($value instanceof Closure) { + return $value(); + } + + return $value; } /** diff --git a/src/Validator/InputValidatorFactory.php b/src/Validator/InputValidatorFactory.php new file mode 100644 index 000000000..5a8c1a1a6 --- /dev/null +++ b/src/Validator/InputValidatorFactory.php @@ -0,0 +1,44 @@ +defaultValidator = $validator; + $this->defaultTranslator = $translator; + $this->constraintValidatorFactory = $constraintValidatorFactory; + } + + public function create(array $resolverArgs): InputValidator + { + if (null === $this->defaultValidator) { + throw new ServiceNotFoundException("The 'validator' service is not found. To use the 'InputValidator' you need to install the Symfony Validator Component first. See: 'https://symfony.com/doc/current/validation.html'"); + } + + return new InputValidator( + $resolverArgs, + $this->defaultValidator, + $this->constraintValidatorFactory, + $this->defaultTranslator + ); + } +} diff --git a/src/Validator/ValidatorFactory.php b/src/Validator/ValidatorFactory.php deleted file mode 100644 index 3b483c541..000000000 --- a/src/Validator/ValidatorFactory.php +++ /dev/null @@ -1,39 +0,0 @@ -defaultTranslator = $translator; - $this->constraintValidatorFactory = $constraintValidatorFactory; - } - - public function createValidator(MetadataFactory $metadataFactory): ValidatorInterface - { - $builder = Validation::createValidatorBuilder() - ->setMetadataFactory($metadataFactory) - ->setConstraintValidatorFactory($this->constraintValidatorFactory); - - if (null !== $this->defaultTranslator) { - // @phpstan-ignore-next-line (only for Symfony 4.4) - $builder - ->setTranslator($this->defaultTranslator) - ->setTranslationDomain('validators'); - } - - return $builder->getValidator(); - } -} diff --git a/tests/ExpressionLanguage/TestCase.php b/tests/ExpressionLanguage/TestCase.php index 1014fecbd..40658f237 100644 --- a/tests/ExpressionLanguage/TestCase.php +++ b/tests/ExpressionLanguage/TestCase.php @@ -101,8 +101,7 @@ private function getCoreSecurityMock(): CoreSecurity return $this->getMockBuilder(CoreSecurity::class) ->disableOriginalConstructor() ->setMethods(['isGranted']) - ->getMock() - ; + ->getMock(); } protected function createGraphQLServices(array $services = []): GraphQLServices diff --git a/tests/Functional/App/config/cascadeOnScalars/mapping/Mutation.types.yml b/tests/Functional/App/config/cascadeOnScalars/mapping/Mutation.types.yml index d60b2364e..7001457ad 100644 --- a/tests/Functional/App/config/cascadeOnScalars/mapping/Mutation.types.yml +++ b/tests/Functional/App/config/cascadeOnScalars/mapping/Mutation.types.yml @@ -4,7 +4,7 @@ Mutation: fields: cascadeOnScalar: type: Boolean - resolve: '@=mut("mutation_mock", [args, validator])' + resolve: '@=mut("mutation_mock", args, validator)' args: test: type: "String" diff --git a/tests/Functional/App/config/public/mapping/public.types.yml b/tests/Functional/App/config/public/mapping/public.types.yml index d2cd16abe..2eca0507f 100644 --- a/tests/Functional/App/config/public/mapping/public.types.yml +++ b/tests/Functional/App/config/public/mapping/public.types.yml @@ -15,6 +15,7 @@ ObjectWithPrivateField: type: String other: type: String + public: "@=typeName == 'nonsense'" privateData: type: String public: "@=service('security.authorization_checker').isGranted('ROLE_ADMIN')" diff --git a/tests/Functional/App/config/queryComplexity/mapping/connectionWithComplexity.types.yml b/tests/Functional/App/config/queryComplexity/mapping/connectionWithComplexity.types.yml index 097165010..8607fa723 100644 --- a/tests/Functional/App/config/queryComplexity/mapping/connectionWithComplexity.types.yml +++ b/tests/Functional/App/config/queryComplexity/mapping/connectionWithComplexity.types.yml @@ -17,6 +17,11 @@ User: type: friendConnection argsBuilder: "Relay::Connection" resolve: '@=query("friends", value, args)' + noFriend: + complexity: "@=15 + 20 + childrenComplexity" + type: friendConnection + argsBuilder: "Relay::Connection" + resolve: '@=query("friends", value, args)' friendConnection: type: relay-connection diff --git a/tests/Functional/App/config/validator/mapping/Mutation.types.yml b/tests/Functional/App/config/validator/mapping/Mutation.types.yml index bff02536f..988965c36 100644 --- a/tests/Functional/App/config/validator/mapping/Mutation.types.yml +++ b/tests/Functional/App/config/validator/mapping/Mutation.types.yml @@ -1,6 +1,11 @@ Mutation: type: object config: + validation: # Applied to all fields + - Callback: [Overblog\GraphQLBundle\Tests\Functional\Validator\StaticValidator, alwaysTrue] + - Expression: + expression: this.getFieldName() == this.getFieldName() + message: "parent" fields: noValidation: complexity: 1 @@ -14,6 +19,10 @@ Mutation: validation: ~ simpleValidation: + validation: # Applied to all fields + - Expression: + expression: this.getFieldName() == this.getFieldName() + message: "child" type: Boolean resolve: "@=mut('mutation_mock', args, validator)" args: diff --git a/tests/Functional/Generator/TypeGeneratorTest.php b/tests/Functional/Generator/TypeGeneratorTest.php index 3039c9dcf..39665ca48 100644 --- a/tests/Functional/Generator/TypeGeneratorTest.php +++ b/tests/Functional/Generator/TypeGeneratorTest.php @@ -89,11 +89,7 @@ public function testNonExistentConstraintThrowsException(): void public function testInjectValidatorWithoutConstraintsThrowsException(): void { $this->expectException(GeneratorException::class); - $this->expectExceptionMessage( - 'Unable to inject an instance of the InputValidator. No validation constraints provided. '. - 'Please remove the "validator" argument from the list of dependencies of your resolver '. - 'or provide validation configs.' - ); + $this->expectExceptionMessage('Unable to inject an instance of the InputValidator. No validation constraints provided. Please remove the "validator" argument from the list of dependencies of your resolver or provide validation configs.'); parent::setUp(); static::bootKernel(['test_case' => 'validatorWithoutConstraints']); diff --git a/tests/Functional/Validator/StaticValidator.php b/tests/Functional/Validator/StaticValidator.php index f4a91614b..7b224a99f 100644 --- a/tests/Functional/Validator/StaticValidator.php +++ b/tests/Functional/Validator/StaticValidator.php @@ -36,4 +36,9 @@ public static function validateClass($object, ExecutionContextInterface $context $context->buildViolation('Class is invalid'); } } + + public static function alwaysTrue(): bool + { + return true; + } } diff --git a/tests/Validator/InputValidatorTest.php b/tests/Validator/InputValidatorTest.php index 7869df3e8..0d4d99612 100644 --- a/tests/Validator/InputValidatorTest.php +++ b/tests/Validator/InputValidatorTest.php @@ -4,11 +4,9 @@ namespace Overblog\GraphQLBundle\Tests\Validator; -use Overblog\GraphQLBundle\Validator\InputValidator; -use Overblog\GraphQLBundle\Validator\ValidatorFactory; +use Overblog\GraphQLBundle\Validator\InputValidatorFactory; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; -use Symfony\Component\Validator\ConstraintValidatorFactory; use Symfony\Component\Validator\Validation; use function class_exists; @@ -24,10 +22,10 @@ public function setUp(): void public function testNoDefaultValidatorException(): void { - $factory = new ValidatorFactory(new ConstraintValidatorFactory(), null); - $this->expectException(ServiceNotFoundException::class); - new InputValidator([], null, $factory, []); + $factory = new InputValidatorFactory(null, null, null); + + $factory->create([]); } } From 69d2d380ac64ac13762599415578e67444e60ae7 Mon Sep 17 00:00:00 2001 From: murtukov Date: Wed, 27 Jan 2021 19:48:27 +0100 Subject: [PATCH 2/3] Fix new PHPStan errors --- src/DependencyInjection/Compiler/ConfigParserPass.php | 5 ++++- .../Compiler/ResolverMapTaggedServiceMappingPass.php | 4 +++- src/DependencyInjection/OverblogGraphQLExtension.php | 6 ++++-- tests/Functional/App/IsolatedResolver/EchoQuery.php | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/DependencyInjection/Compiler/ConfigParserPass.php b/src/DependencyInjection/Compiler/ConfigParserPass.php index 6579a71cd..2750b5baa 100644 --- a/src/DependencyInjection/Compiler/ConfigParserPass.php +++ b/src/DependencyInjection/Compiler/ConfigParserPass.php @@ -177,6 +177,7 @@ private function mappingConfig(array $config, ContainerBuilder $container): arra // app only config files (yml or xml or graphql) if ($mappingConfig['auto_discover']['root_dir'] && $container->hasParameter('kernel.root_dir')) { + // @phpstan-ignore-next-line $typesMappings[] = ['dir' => $container->getParameter('kernel.root_dir').'/config/graphql', 'types' => null]; } if ($mappingConfig['auto_discover']['bundles']) { @@ -212,10 +213,12 @@ function (array $typeMapping) use ($container) { private function mappingFromBundles(ContainerBuilder $container): array { $typesMappings = []; + + /** @var array $bundles */ $bundles = $container->getParameter('kernel.bundles'); // auto detect from bundle - foreach ($bundles as $name => $class) { + foreach ($bundles as $class) { // skip this bundle if (OverblogGraphQLBundle::class === $class) { continue; diff --git a/src/DependencyInjection/Compiler/ResolverMapTaggedServiceMappingPass.php b/src/DependencyInjection/Compiler/ResolverMapTaggedServiceMappingPass.php index 44ffcfa7c..49423b042 100644 --- a/src/DependencyInjection/Compiler/ResolverMapTaggedServiceMappingPass.php +++ b/src/DependencyInjection/Compiler/ResolverMapTaggedServiceMappingPass.php @@ -25,9 +25,11 @@ final class ResolverMapTaggedServiceMappingPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { $resolverMapsSortedBySchema = []; - $resolverMapsBySchemas = $container->getParameter('overblog_graphql.resolver_maps'); $typeDecoratorListenerDefinition = $container->getDefinition(TypeDecoratorListener::class); + /** @var array $resolverMapsBySchemas */ + $resolverMapsBySchemas = $container->getParameter('overblog_graphql.resolver_maps'); + foreach ($container->findTaggedServiceIds(self::SERVICE_TAG, true) as $serviceId => $tags) { foreach ($tags as $tag) { if (!isset($tag['schema'])) { diff --git a/src/DependencyInjection/OverblogGraphQLExtension.php b/src/DependencyInjection/OverblogGraphQLExtension.php index 40de83011..29b64a35b 100644 --- a/src/DependencyInjection/OverblogGraphQLExtension.php +++ b/src/DependencyInjection/OverblogGraphQLExtension.php @@ -61,15 +61,17 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter($this->getAlias().'.resources_dir', realpath(__DIR__.'/../Resources')); } - public function getAlias() + public function getAlias(): string { return Configuration::NAME; } - public function getConfiguration(array $config, ContainerBuilder $container) + public function getConfiguration(array $config, ContainerBuilder $container): Configuration { return new Configuration( + // @phpstan-ignore-next-line $container->getParameter('kernel.debug'), + // @phpstan-ignore-next-line $container->hasParameter('kernel.cache_dir') ? $container->getParameter('kernel.cache_dir') : null ); } diff --git a/tests/Functional/App/IsolatedResolver/EchoQuery.php b/tests/Functional/App/IsolatedResolver/EchoQuery.php index 250ca43ec..6a7b1d6fc 100644 --- a/tests/Functional/App/IsolatedResolver/EchoQuery.php +++ b/tests/Functional/App/IsolatedResolver/EchoQuery.php @@ -14,6 +14,7 @@ final class EchoQuery implements QueryInterface, ContainerAwareInterface public function display(string $message): string { + // @phpstan-ignore-next-line return $this->container->getParameter('echo.prefix').$message; } } From 779caeac05faa4c539e68826f88ad91b7664c652 Mon Sep 17 00:00:00 2001 From: Timur Murtukov Date: Sun, 31 Jan 2021 02:21:17 +0100 Subject: [PATCH 3/3] Create ResolverArgs class --- src/Definition/GraphQLServices.php | 10 ++++- src/Definition/ResolverArgs.php | 29 ++++++++++++++ src/Generator/TypeBuilder.php | 6 +-- src/Validator/InputValidator.php | 51 ++++++++++--------------- src/Validator/InputValidatorFactory.php | 5 ++- src/Validator/ValidationNode.php | 15 +++++--- tests/Validator/InputValidatorTest.php | 11 +++++- tests/Validator/ValidationNodeTest.php | 15 ++++---- 8 files changed, 91 insertions(+), 51 deletions(-) create mode 100644 src/Definition/ResolverArgs.php diff --git a/src/Definition/GraphQLServices.php b/src/Definition/GraphQLServices.php index 0bcc911b9..85aa194e9 100644 --- a/src/Definition/GraphQLServices.php +++ b/src/Definition/GraphQLServices.php @@ -4,6 +4,7 @@ namespace Overblog\GraphQLBundle\Definition; +use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use LogicException; use Overblog\GraphQLBundle\Resolver\MutationResolver; @@ -85,9 +86,14 @@ public function getType(string $typeName): ?Type /** * Creates an instance of InputValidator + * + * @param mixed $value + * @param mixed $context */ - public function createInputValidator(array $resolverArgs): InputValidator + public function createInputValidator($value, ArgumentInterface $args, $context, ResolveInfo $info): InputValidator { - return $this->services['input_validator_factory']->create($resolverArgs); + return $this->services['input_validator_factory']->create( + new ResolverArgs($value, $args, $context, $info) + ); } } diff --git a/src/Definition/ResolverArgs.php b/src/Definition/ResolverArgs.php new file mode 100644 index 000000000..6cb4b9687 --- /dev/null +++ b/src/Definition/ResolverArgs.php @@ -0,0 +1,29 @@ +value = $value; + $this->args = $args; + $this->context = $context; + $this->info = $info; + } +} diff --git a/src/Generator/TypeBuilder.php b/src/Generator/TypeBuilder.php index efbefed0d..3704a0556 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -415,7 +415,7 @@ protected function buildScalarCallback($callback, string $fieldName) * Render example (with validation): * * function ($value, $args, $context, $info) use ($services) { - * $validator = $services->createInputValidator(func_get_args()); + * $validator = $services->createInputValidator(...func_get_args()); * return $services->mutation("create_post", $validator]); * } * @@ -424,7 +424,7 @@ protected function buildScalarCallback($callback, string $fieldName) * * function ($value, $args, $context, $info) use ($services) { * $errors = new ResolveErrors(); - * $validator = $services->createInputValidator(func_get_args()); + * $validator = $services->createInputValidator(...func_get_args()); * * $errors->setValidationErrors($validator->validate(null, false)) * @@ -458,7 +458,7 @@ protected function buildResolve($resolve, ?array $groups = null) $closure->append('$errors = ', Instance::new(ResolveErrors::class)); } - $closure->append('$validator = ', "$this->gqlServices->createInputValidator(func_get_args())"); + $closure->append('$validator = ', "$this->gqlServices->createInputValidator(...func_get_args())"); // If auto-validation on or errors are injected if (!$injectValidator || $injectErrors) { diff --git a/src/Validator/InputValidator.php b/src/Validator/InputValidator.php index 6d9eb0c73..1508d61f9 100644 --- a/src/Validator/InputValidator.php +++ b/src/Validator/InputValidator.php @@ -11,7 +11,7 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; -use Overblog\GraphQLBundle\Definition\ArgumentInterface; +use Overblog\GraphQLBundle\Definition\ResolverArgs; use Overblog\GraphQLBundle\Definition\Type\GeneratedTypeInterface; use Overblog\GraphQLBundle\Validator\Exception\ArgumentsValidationException; use Overblog\GraphQLBundle\Validator\Mapping\MetadataFactory; @@ -35,7 +35,7 @@ class InputValidator private const TYPE_GETTER = 'getter'; public const CASCADE = 'cascade'; - private array $resolverArgs; + private ResolverArgs $resolverArgs; private ValidatorInterface $defaultValidator; private MetadataFactory $metadataFactory; private ResolveInfo $info; @@ -46,35 +46,19 @@ class InputValidator private array $cachedMetadata = []; public function __construct( - array $resolverArgs, + ResolverArgs $resolverArgs, ValidatorInterface $validator, ConstraintValidatorFactoryInterface $constraintValidatorFactory, ?TranslatorInterface $translator ) { - $this->resolverArgs = $this->mapResolverArgs(...$resolverArgs); - $this->info = $this->resolverArgs['info']; + $this->resolverArgs = $resolverArgs; + $this->info = $this->resolverArgs->info; $this->defaultValidator = $validator; $this->constraintValidatorFactory = $constraintValidatorFactory; $this->defaultTranslator = $translator; $this->metadataFactory = new MetadataFactory(); } - /** - * Converts a numeric array of resolver args to an associative one. - * - * @param mixed $value - * @param mixed $context - */ - private function mapResolverArgs($value, ArgumentInterface $args, $context, ResolveInfo $info): array - { - return [ - 'value' => $value, - 'args' => $args, - 'context' => $context, - 'info' => $info, - ]; - } - /** * @param string|array|null $groups * @@ -82,20 +66,25 @@ private function mapResolverArgs($value, ArgumentInterface $args, $context, Reso */ public function validate($groups = null, bool $throw = true): ?ConstraintViolationListInterface { - $rootObject = new ValidationNode($this->info->parentType, $this->info->fieldName, null, $this->resolverArgs); + $rootNode = new ValidationNode( + $this->info->parentType, + $this->info->fieldName, + null, + $this->resolverArgs + ); $classMapping = $this->mergeClassValidation(); $this->buildValidationTree( - $rootObject, + $rootNode, $this->info->fieldDefinition->config['args'], $classMapping, - $this->resolverArgs['args']->getArrayCopy() + $this->resolverArgs->args->getArrayCopy() ); $validator = $this->createValidator($this->metadataFactory); - $errors = $validator->validate($rootObject, null, $groups); + $errors = $validator->validate($rootNode, null, $groups); if ($throw && $errors->count() > 0) { throw new ArgumentsValidationException($errors); @@ -148,10 +137,10 @@ protected function buildValidationTree(ValidationNode $rootObject, array $fields foreach ($fields as $name => $arg) { $property = $arg['name'] ?? $name; - $validation = static::normalizeConfig($arg['validation'] ?? []); + $config = static::normalizeConfig($arg['validation'] ?? []); - if (isset($validation['cascade']) && isset($inputData[$property])) { - $groups = $validation['cascade']; + if (isset($config['cascade']) && isset($inputData[$property])) { + $groups = $config['cascade']; $argType = $this->unclosure($arg['type']); /** @var ObjectType|InputObjectType $type */ @@ -174,9 +163,9 @@ protected function buildValidationTree(ValidationNode $rootObject, array $fields $rootObject->$property = $inputData[$property] ?? null; } - $validation = static::normalizeConfig($validation); + $config = static::normalizeConfig($config); - foreach ($validation as $key => $value) { + foreach ($config as $key => $value) { switch ($key) { case 'link': [$fqcn, $property, $type] = $value; @@ -253,7 +242,7 @@ private function createObjectNode(array $value, $type, ValidationNode $parent): return $this->buildValidationTree( new ValidationNode($type, null, $parent, $this->resolverArgs), - $type->config['fields'](), + self::unclosure($type->config['fields']), $classValidation, $value ); diff --git a/src/Validator/InputValidatorFactory.php b/src/Validator/InputValidatorFactory.php index 5a8c1a1a6..30afaffe0 100644 --- a/src/Validator/InputValidatorFactory.php +++ b/src/Validator/InputValidatorFactory.php @@ -4,6 +4,7 @@ namespace Overblog\GraphQLBundle\Validator; +use Overblog\GraphQLBundle\Definition\ResolverArgs; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -28,14 +29,14 @@ public function __construct( $this->constraintValidatorFactory = $constraintValidatorFactory; } - public function create(array $resolverArgs): InputValidator + public function create(ResolverArgs $args): InputValidator { if (null === $this->defaultValidator) { throw new ServiceNotFoundException("The 'validator' service is not found. To use the 'InputValidator' you need to install the Symfony Validator Component first. See: 'https://symfony.com/doc/current/validation.html'"); } return new InputValidator( - $resolverArgs, + $args, $this->defaultValidator, $this->constraintValidatorFactory, $this->defaultTranslator diff --git a/src/Validator/ValidationNode.php b/src/Validator/ValidationNode.php index c1e7f1f53..4925b65de 100644 --- a/src/Validator/ValidationNode.php +++ b/src/Validator/ValidationNode.php @@ -9,6 +9,7 @@ use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use Overblog\GraphQLBundle\Definition\Argument; +use Overblog\GraphQLBundle\Definition\ResolverArgs; use function in_array; /** @@ -41,10 +42,14 @@ class ValidationNode /** * Arguments of the resolver, where the current validation is being executed. */ - private array $__resolverArgs; - - public function __construct(Type $type, string $field = null, ?ValidationNode $parent = null, array $resolverArgs = []) - { + private ?ResolverArgs $__resolverArgs; + + public function __construct( + Type $type, + string $field = null, + ?ValidationNode $parent = null, + ?ResolverArgs $resolverArgs = null + ) { $this->__type = $type; $this->__fieldName = $field; $this->__resolverArgs = $resolverArgs; @@ -121,7 +126,7 @@ public function findParent(string $name): ?ValidationNode public function getResolverArg(string $name) { if (in_array($name, self::KNOWN_VAR_NAMES)) { - return $this->__resolverArgs[$name]; + return $this->__resolverArgs->$name; } return null; diff --git a/tests/Validator/InputValidatorTest.php b/tests/Validator/InputValidatorTest.php index 0d4d99612..cc1654ed5 100644 --- a/tests/Validator/InputValidatorTest.php +++ b/tests/Validator/InputValidatorTest.php @@ -4,6 +4,10 @@ namespace Overblog\GraphQLBundle\Tests\Validator; +use ArrayObject; +use GraphQL\Type\Definition\ResolveInfo; +use Overblog\GraphQLBundle\Definition\Argument; +use Overblog\GraphQLBundle\Definition\ResolverArgs; use Overblog\GraphQLBundle\Validator\InputValidatorFactory; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; @@ -26,6 +30,11 @@ public function testNoDefaultValidatorException(): void $factory = new InputValidatorFactory(null, null, null); - $factory->create([]); + $factory->create(new ResolverArgs( + true, + new Argument(), + new ArrayObject(), + $this->getMockBuilder(ResolveInfo::class)->disableOriginalConstructor()->getMock(), + )); } } diff --git a/tests/Validator/ValidationNodeTest.php b/tests/Validator/ValidationNodeTest.php index 04459522c..337d1dd3a 100644 --- a/tests/Validator/ValidationNodeTest.php +++ b/tests/Validator/ValidationNodeTest.php @@ -8,6 +8,7 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use Overblog\GraphQLBundle\Definition\Argument; +use Overblog\GraphQLBundle\Definition\ResolverArgs; use Overblog\GraphQLBundle\Validator\ValidationNode; use PHPUnit\Framework\TestCase; @@ -39,13 +40,13 @@ public function testValidationNode(): void $this->assertSame($childType, $childNode->getType()); } - private function createResolveArgs(): array + private function createResolveArgs(): ResolverArgs { - return [ - 'value' => true, - 'args' => new Argument(), - 'context' => new ArrayObject(), - 'info' => $this->getMockBuilder(ResolveInfo::class)->disableOriginalConstructor()->getMock(), - ]; + return new ResolverArgs( + true, + new Argument(), + new ArrayObject(), + $this->getMockBuilder(ResolveInfo::class)->disableOriginalConstructor()->getMock(), + ); } }