From 0cfbf7d6fe176d356bbb5df8ad5b7290127527ab Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <jeremiah@overblog.com> Date: Sat, 15 May 2021 12:26:19 +0200 Subject: [PATCH 01/14] wip --- src/Config/InterfaceTypeDefinition.php | 3 +- src/Config/ObjectTypeDefinition.php | 3 +- src/Config/TypeDefinition.php | 88 ++++- src/Config/TypeWithOutputFieldsDefinition.php | 5 +- src/Config/UnionTypeDefinition.php | 3 +- src/Generator/Config/AbstractConfig.php | 48 +++ src/Generator/Config/Arg.php | 25 ++ src/Generator/Config/Callback.php | 12 + src/Generator/Config/Config.php | 50 +++ src/Generator/Config/Field.php | 50 +++ src/Generator/Config/Validation.php | 12 + .../Converter/ExpressionConverter.php | 2 +- src/Generator/TypeBuilder.php | 326 ++++++++---------- .../Compiler/ConfigParserPassTest.php | 12 +- 14 files changed, 446 insertions(+), 193 deletions(-) create mode 100644 src/Generator/Config/AbstractConfig.php create mode 100644 src/Generator/Config/Arg.php create mode 100644 src/Generator/Config/Callback.php create mode 100644 src/Generator/Config/Config.php create mode 100644 src/Generator/Config/Field.php create mode 100644 src/Generator/Config/Validation.php diff --git a/src/Config/InterfaceTypeDefinition.php b/src/Config/InterfaceTypeDefinition.php index 42e6dffb1..42d5133f6 100644 --- a/src/Config/InterfaceTypeDefinition.php +++ b/src/Config/InterfaceTypeDefinition.php @@ -12,13 +12,14 @@ public function getDefinition(): ArrayNodeDefinition { /** @var ArrayNodeDefinition $node */ $node = self::createNode('_interface_config'); + $this->resolverNormalization($node, 'typeResolver', 'resolveType'); /** @phpstan-ignore-next-line */ $node ->children() ->append($this->nameSection()) ->append($this->outputFieldsSection()) - ->append($this->resolveTypeSection()) + ->append($this->resolverSection('typeResolver', 'GraphQL type resolver')) ->append($this->descriptionSection()) ->arrayNode('interfaces') ->prototype('scalar')->info('One of internal or custom interface types.')->end() diff --git a/src/Config/ObjectTypeDefinition.php b/src/Config/ObjectTypeDefinition.php index 8b4067bdc..41d06e469 100644 --- a/src/Config/ObjectTypeDefinition.php +++ b/src/Config/ObjectTypeDefinition.php @@ -16,6 +16,7 @@ public function getDefinition(): ArrayNodeDefinition /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); + $this->resolverNormalization($node, 'fieldResolver', 'resolveField'); /** @phpstan-ignore-next-line */ $node @@ -29,7 +30,7 @@ public function getDefinition(): ArrayNodeDefinition ->prototype('scalar')->info('One of internal or custom interface types.')->end() ->end() ->variableNode('isTypeOf')->end() - ->variableNode('resolveField')->end() + ->append($this->resolverSection('fieldResolver', 'GraphQL field value resolver')) ->variableNode('fieldsDefaultAccess') ->info('Default access control to fields (expression language can be use here)') ->end() diff --git a/src/Config/TypeDefinition.php b/src/Config/TypeDefinition.php index f02588750..86df0c0b5 100644 --- a/src/Config/TypeDefinition.php +++ b/src/Config/TypeDefinition.php @@ -4,7 +4,9 @@ namespace Overblog\GraphQLBundle\Config; +use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\Builder\VariableNodeDefinition; @@ -32,11 +34,6 @@ public static function create(): self return new static(); } - protected function resolveTypeSection(): VariableNodeDefinition - { - return self::createNode('resolveType', 'variable'); - } - protected function nameSection(): ScalarNodeDefinition { /** @var ScalarNodeDefinition $node */ @@ -156,6 +153,87 @@ protected function typeSection(bool $isRequired = false): ScalarNodeDefinition return $node; } + protected function resolverNormalization(NodeDefinition $node, string $new, string $old): void + { + $node + ->beforeNormalization() + ->ifTrue(fn ($options) => !empty($options[$old]) && empty($options[$new])) + ->then(function ($options) use ($old, $new) { + if (is_callable($options[$old])) { + if (is_array($options[$old])) { + $options[$new]['method'] = implode('::', $options[$old]); + } else { + $options[$new]['method'] = $options[$old]; + } + } elseif (is_string($options[$old])) { + $options[$new]['expression'] = ExpressionLanguage::stringHasTrigger($options[$old]) ? + ExpressionLanguage::unprefixExpression($options[$old]) : + json_encode($options[$old]); + } else { + $options[$new]['expression'] = json_encode($options[$old]); + } + + return $options; + }) + ->end() + ->beforeNormalization() + ->ifTrue(fn ($options) => is_array($options) && array_key_exists($old, $options)) + ->then(function ($options) use ($old) { + unset($options[$old]); + + return $options; + }) + ->end() + ->validate() + ->ifTrue(fn (array $v) => !empty($v[$new]) && !empty($v[$old])) + ->thenInvalid(sprintf( + '"%s" and "%s" should not be use together in "%%s".', + $new, + $old, + )) + ->end() + ; + } + + protected function resolverSection(string $name, string $info): ArrayNodeDefinition + { + /** @var ArrayNodeDefinition $node */ + $node = self::createNode($name); + /** @phpstan-ignore-next-line */ + $node + ->info($info) + ->validate() + ->ifTrue(fn (array $v) => !empty($v['method']) && !empty($v['expression'])) + ->thenInvalid('"method" and "expression" should not be use together.') + ->end() + ->beforeNormalization() + // Allow short syntax + ->ifTrue(fn ($options) => is_string($options) && ExpressionLanguage::stringHasTrigger($options)) + ->then(fn ($options) => ['expression' => ExpressionLanguage::unprefixExpression($options)]) + ->end() + ->beforeNormalization() + ->ifTrue(fn ($options) => is_string($options) && !ExpressionLanguage::stringHasTrigger($options)) + ->then(fn ($options) => ['method' => $options]) + ->end() + ->beforeNormalization() + // clean expression + ->ifTrue(fn ($options) => isset($options['expression']) && is_string($options['expression']) && ExpressionLanguage::stringHasTrigger($options['expression'])) + ->then(function ($options) { + $options['expression'] = ExpressionLanguage::unprefixExpression($options['expression']); + + return $options; + }) + ->end() + ->children() + ->scalarNode('method')->end() + ->scalarNode('expression')->end() + ->scalarNode('id')->end() + ->end() + ; + + return $node; + } + /** * @return mixed * diff --git a/src/Config/TypeWithOutputFieldsDefinition.php b/src/Config/TypeWithOutputFieldsDefinition.php index 438f8da3f..bb7ae544d 100644 --- a/src/Config/TypeWithOutputFieldsDefinition.php +++ b/src/Config/TypeWithOutputFieldsDefinition.php @@ -18,6 +18,7 @@ protected function outputFieldsSection(): NodeDefinition $node->isRequired()->requiresAtLeastOneElement(); $prototype = $node->useAttributeAsKey('name', false)->prototype('array'); + $this->resolverNormalization($prototype, 'resolver', 'resolve'); /** @phpstan-ignore-next-line */ $prototype @@ -68,9 +69,7 @@ protected function outputFieldsSection(): NodeDefinition ->end() ->end() ->end() - ->variableNode('resolve') - ->info('Value resolver (expression language can be used here)') - ->end() + ->append($this->resolverSection('resolver', 'GraphQL value resolver')) ->append($this->descriptionSection()) ->append($this->deprecationReasonSection()) ->variableNode('access') diff --git a/src/Config/UnionTypeDefinition.php b/src/Config/UnionTypeDefinition.php index 47c9d32da..fa48a922e 100644 --- a/src/Config/UnionTypeDefinition.php +++ b/src/Config/UnionTypeDefinition.php @@ -12,6 +12,7 @@ public function getDefinition(): ArrayNodeDefinition { /** @var ArrayNodeDefinition $node */ $node = self::createNode('_union_config'); + $this->resolverNormalization($node, 'typeResolver', 'resolveType'); /** @phpstan-ignore-next-line */ $node @@ -24,7 +25,7 @@ public function getDefinition(): ArrayNodeDefinition ->isRequired() ->requiresAtLeastOneElement() ->end() - ->append($this->resolveTypeSection()) + ->append($this->resolverSection('typeResolver', 'GraphQL type resolver')) ->append($this->descriptionSection()) ->end(); diff --git a/src/Generator/Config/AbstractConfig.php b/src/Generator/Config/AbstractConfig.php new file mode 100644 index 000000000..e8e6ef92a --- /dev/null +++ b/src/Generator/Config/AbstractConfig.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +namespace Overblog\GraphQLBundle\Generator\Config; + +use InvalidArgumentException; + +/** + * @internal + */ +abstract class AbstractConfig +{ + protected const NORMALIZERS = []; + + public function __construct(array $config) + { + $this->populate($config); + } + + protected function populate(array $config): void + { + foreach ($config as $key => $value) { + $property = lcfirst(str_replace('_', '', ucwords($key, '_'))); + $normalizer = static::NORMALIZERS[$property] ?? 'normalize'.ucfirst($property); + if (method_exists($this, $normalizer)) { + $this->$property = $this->$normalizer($value); + } elseif (property_exists($this, $property)) { + $this->$property = $value; + } else { + throw new InvalidArgumentException(sprintf('Unknown config "%s".', $property)); + } + } + } + + /** + * @param array|mixed $value + */ + protected function normalizeCallback($value): Callback + { + return new Callback($value); + } + + protected function normalizeValidation(array $config): Validation + { + return new Validation($config); + } +} diff --git a/src/Generator/Config/Arg.php b/src/Generator/Config/Arg.php new file mode 100644 index 000000000..adb15d837 --- /dev/null +++ b/src/Generator/Config/Arg.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace Overblog\GraphQLBundle\Generator\Config; + +final class Arg extends AbstractConfig +{ + public string $type; + public ?string $description = null; + public ?Validation $validation = null; + + /** + * @var mixed + */ + public $defaultValue; + + public bool $hasDefaultValue; + + public function __construct(array $config) + { + parent::__construct($config); + $this->hasDefaultValue = array_key_exists('defaultValue', $config); + } +} diff --git a/src/Generator/Config/Callback.php b/src/Generator/Config/Callback.php new file mode 100644 index 000000000..a032b8224 --- /dev/null +++ b/src/Generator/Config/Callback.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace Overblog\GraphQLBundle\Generator\Config; + +final class Callback extends AbstractConfig +{ + public ?string $method = null; + public ?string $expression = null; + public ?string $id = null; +} diff --git a/src/Generator/Config/Config.php b/src/Generator/Config/Config.php new file mode 100644 index 000000000..41f693a05 --- /dev/null +++ b/src/Generator/Config/Config.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +namespace Overblog\GraphQLBundle\Generator\Config; + +/** + * @internal + */ +final class Config extends AbstractConfig +{ + protected const NORMALIZERS = [ + 'fieldResolver' => 'normalizeCallback', + 'typeResolver' => 'normalizeCallback', +// 'fieldsDefaultAccess' => 'normalizeCallback', +// 'isTypeOf' => 'normalizeCallback', +// 'fieldsDefaultPublic' => 'normalizeCallback', + ]; + + public string $name; + public ?string $description = null; + public string $className; + /** @var Field[]|null */ + public ?array $fields = null; + public ?array $interfaces = null; + public ?Callback $fieldResolver = null; + public ?Callback $typeResolver = null; + public ?Validation $validation = null; + public ?array $builders = null; + public ?array $types = null; + public ?array $values = null; +/** @var mixed|null */ + /*?Callback*/ public $fieldsDefaultAccess = null; +/** @var mixed|null */ + /*?Callback*/ public $isTypeOf = null; +/** @var mixed|null */ + /*?Callback*/ public $fieldsDefaultPublic = null; + public ?string $scalarType = null; + /** @var callable|null */ + public $serialize = null; + /** @var callable|null */ + public $parseValue = null; + /** @var callable|null */ + public $parseLiteral = null; + + protected function normalizeFields(array $fields): array + { + return array_map(fn (array $field) => new Field($field), $fields); + } +} diff --git a/src/Generator/Config/Field.php b/src/Generator/Config/Field.php new file mode 100644 index 000000000..f8b3703eb --- /dev/null +++ b/src/Generator/Config/Field.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +namespace Overblog\GraphQLBundle\Generator\Config; + +final class Field extends AbstractConfig +{ + protected const NORMALIZERS = [ + 'resolver' => 'normalizeCallback', +// 'access' => 'normalizeCallback', +// 'public' => 'normalizeCallback', +// 'complexity' => 'normalizeCallback', + ]; + + public string $type; + public ?string $description = null; + /** @var Arg[]|null */ + public ?array $args = null; + public ?Callback $resolver = null; +/** @var mixed|null */ + /*?Callback*/ public $access = null; +/** @var mixed|null */ + /*?Callback*/ public $public = null; +/** @var mixed|null */ + /*?Callback*/ public $complexity = null; + public ?Validation $validation = null; + public ?array $validationGroups = null; + public ?string $deprecationReason = null; + + /** + * @var mixed + */ + public $defaultValue; + + public bool $hasDefaultValue; + public bool $hasOnlyType; + + public function __construct(array $config) + { + parent::__construct($config); + $this->hasOnlyType = 1 === count($config) && isset($config['type']); + $this->hasDefaultValue = array_key_exists('defaultValue', $config); + } + + protected function normalizeArgs(array $args): array + { + return array_map(fn (array $arg) => new Arg($arg), $args); + } +} diff --git a/src/Generator/Config/Validation.php b/src/Generator/Config/Validation.php new file mode 100644 index 000000000..fd2dc92de --- /dev/null +++ b/src/Generator/Config/Validation.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace Overblog\GraphQLBundle\Generator\Config; + +final class Validation extends AbstractConfig +{ + public ?array $constraints = null; + public ?string $link = null; + public ?array $cascade = null; +} diff --git a/src/Generator/Converter/ExpressionConverter.php b/src/Generator/Converter/ExpressionConverter.php index 801558f1f..0b26f31d0 100644 --- a/src/Generator/Converter/ExpressionConverter.php +++ b/src/Generator/Converter/ExpressionConverter.php @@ -24,7 +24,7 @@ public function __construct(ExpressionLanguage $expressionLanguage) public function convert($value) { return $this->expressionLanguage->compile( - ExpressionLanguage::unprefixExpression($value), + ExpressionLanguage::stringHasTrigger($value) ? ExpressionLanguage::unprefixExpression($value) : $value, ExpressionLanguage::KNOWN_NAMES ); } diff --git a/src/Generator/TypeBuilder.php b/src/Generator/TypeBuilder.php index d4ea8cf51..8434f30b2 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -28,12 +28,15 @@ use Overblog\GraphQLBundle\Definition\Type\GeneratedTypeInterface; use Overblog\GraphQLBundle\Error\ResolveErrors; use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage as EL; +use Overblog\GraphQLBundle\Generator\Config\Arg; +use Overblog\GraphQLBundle\Generator\Config\Callback; +use Overblog\GraphQLBundle\Generator\Config\Field; +use Overblog\GraphQLBundle\Generator\Config\Validation; use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter; use Overblog\GraphQLBundle\Generator\Exception\GeneratorException; use Overblog\GraphQLBundle\Validator\InputValidator; use function array_map; use function class_exists; -use function count; use function explode; use function in_array; use function is_array; @@ -75,7 +78,7 @@ final class TypeBuilder private ExpressionConverter $expressionConverter; private PhpFile $file; private string $namespace; - private array $config; + private Config\Config $config; private string $type; private string $currentField; private string $gqlServices = '$'.TypeGenerator::GRAPHQL_SERVICES; @@ -110,12 +113,12 @@ public function __construct(ExpressionConverter $expressionConverter, string $na public function build(array $config, string $type): PhpFile { // This values should be accessible from every method - $this->config = $config; + $this->config = new Config\Config($config); $this->type = $type; $this->file = PhpFile::new()->setNamespace($this->namespace); - $class = $this->file->createClass($config['class_name']) + $class = $this->file->createClass($this->config->className) ->setFinal() ->setExtends(static::EXTENDS[$type]) ->addImplements(GeneratedTypeInterface::class, AliasedInterface::class) @@ -127,7 +130,7 @@ public function build(array $config, string $type): PhpFile $class->createConstructor() ->addArgument('configProcessor', ConfigProcessor::class) ->addArgument(TypeGenerator::GRAPHQL_SERVICES, GraphQLServices::class) - ->append('$config = ', $this->buildConfig($config)) + ->append('$config = ', $this->buildConfig()) ->emptyLine() ->append('parent::__construct($configProcessor->process($config))'); @@ -224,7 +227,7 @@ private function wrapTypeRecursive($typeNode, bool &$isReference) * $services->getType('PostInterface'), * ... * ], - * 'resolveField' => {@see buildResolveField}, + * 'resolveField' => {@see buildResolver}, * ] * * Render example (input-object): @@ -248,7 +251,7 @@ private function wrapTypeRecursive($typeNode, bool &$isReference) * {@see buildField}, * ... * ], - * 'resolveType' => {@see buildResolveType}, + * 'resolveType' => {@see buildTypeResolver}, * ] * * Render example (union): @@ -260,7 +263,7 @@ private function wrapTypeRecursive($typeNode, bool &$isReference) * $services->getType('Photo'), * ... * ], - * 'resolveType' => {@see buildResolveType}, + * 'resolveType' => {@see buildTypeResolver}, * ] * * Render example (custom-scalar): @@ -290,69 +293,66 @@ private function wrapTypeRecursive($typeNode, bool &$isReference) * * @throws GeneratorException */ - private function buildConfig(array $config): Collection + private function buildConfig(): Collection { - // Convert to an object for a better readability - $c = (object) $config; - $configLoader = Collection::assoc(); $configLoader->addItem('name', new Literal('self::NAME')); - if (isset($c->description)) { - $configLoader->addItem('description', $c->description); + if ($this->config->description) { + $configLoader->addItem('description', $this->config->description); } // only by input-object types (for class level validation) - if (isset($c->validation)) { - $configLoader->addItem('validation', $this->buildValidationRules($c->validation)); + if (null !== $this->config->validation) { + $configLoader->addItem('validation', $this->buildValidationRules($this->config->validation)); } // only by object, input-object and interface types - if (!empty($c->fields)) { + if (!empty($this->config->fields)) { $configLoader->addItem('fields', ArrowFunction::new( - Collection::map($c->fields, [$this, 'buildField']) + Collection::map($this->config->fields, [$this, 'buildField']) )); } - if (!empty($c->interfaces)) { - $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $c->interfaces); + if (!empty($this->config->interfaces)) { + $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $this->config->interfaces); $configLoader->addItem('interfaces', ArrowFunction::new(Collection::numeric($items, true))); } - if (!empty($c->types)) { - $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $c->types); + if (!empty($this->config->types)) { + $items = array_map(fn ($type) => "$this->gqlServices->getType('$type')", $this->config->types); $configLoader->addItem('types', ArrowFunction::new(Collection::numeric($items, true))); } - if (isset($c->resolveType)) { - $configLoader->addItem('resolveType', $this->buildResolveType($c->resolveType)); + if (null !== $this->config->typeResolver) { + $configLoader->addItem('resolveType', $this->buildTypeResolver($this->config->typeResolver)); } - if (isset($c->resolveField)) { - $configLoader->addItem('resolveField', $this->buildResolve($c->resolveField)); + if (null !== $this->config->fieldResolver) { + $configLoader->addItem('resolveField', $this->buildResolver($this->config->fieldResolver)); } // only by enum types - if (isset($c->values)) { - $configLoader->addItem('values', Collection::assoc($c->values)); + if (null !== $this->config->values) { + $configLoader->addItem('values', Collection::assoc($this->config->values)); } // only by custom-scalar types if ('custom-scalar' === $this->type) { - if (isset($c->scalarType)) { - $configLoader->addItem('scalarType', $c->scalarType); + if ($this->config->scalarType) { + $configLoader->addItem('scalarType', $this->config->scalarType); } - if (isset($c->serialize)) { - $configLoader->addItem('serialize', $this->buildScalarCallback($c->serialize, 'serialize')); + if (null !== $this->config->serialize) { + $configLoader->addItem('serialize', $this->buildScalarCallback($this->config->serialize, 'serialize')); } - if (isset($c->parseValue)) { - $configLoader->addItem('parseValue', $this->buildScalarCallback($c->parseValue, 'parseValue')); + if (null !== $this->config->parseValue) { + $configLoader->addItem('parseValue', $this->buildScalarCallback($this->config->parseValue, 'parseValue')); } - if (isset($c->parseLiteral)) { - $configLoader->addItem('parseLiteral', $this->buildScalarCallback($c->parseLiteral, 'parseLiteral')); + if (null !== $this->config->parseLiteral) { + $configLoader->addItem('parseLiteral', $this->buildScalarCallback($this->config->parseLiteral, 'parseLiteral')); } } @@ -388,7 +388,7 @@ private function buildScalarCallback($callback, string $fieldName) $className = Utils::resolveQualifier($class); - if ($className === $this->config['class_name']) { + if ($className === $this->config->className) { // Create an alias if name of serializer is same as type name $className = 'Base'.$className; $this->file->addUse($class, $className); @@ -436,61 +436,57 @@ private function buildScalarCallback($callback, string $fieldName) * return $services->mutation("create_post", $errors); * } * - * @param mixed $resolve - * * @throws GeneratorException */ - private function buildResolve($resolve, ?array $groups = null): GeneratorInterface + private function buildResolver(Callback $resolver, ?array $groups = null): ?GeneratorInterface { - 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); + return $this->buildCallback( + $resolver, + ['value', 'args', 'context', 'info'], + function (string $expression) use ($groups) { + $closure = Closure::new() + ->addArguments('value', 'args', 'context', 'info') + ->bindVar(TypeGenerator::GRAPHQL_SERVICES); - $injectValidator = EL::expressionContainsVar('validator', $resolve); + $injectValidator = EL::expressionContainsVar('validator', $expression); - if ($this->configContainsValidation()) { - $injectErrors = EL::expressionContainsVar('errors', $resolve); + if ($this->configContainsValidation()) { + $injectErrors = EL::expressionContainsVar('errors', $expression); - if ($injectErrors) { - $closure->append('$errors = ', Instance::new(ResolveErrors::class)); - } + if ($injectErrors) { + $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) { - if (!empty($groups)) { - $validationGroups = Collection::numeric($groups); - } else { - $validationGroups = 'null'; - } + // If auto-validation on or errors are injected + if (!$injectValidator || $injectErrors) { + if (!empty($groups)) { + $validationGroups = Collection::numeric($groups); + } else { + $validationGroups = 'null'; + } - $closure->emptyLine(); + $closure->emptyLine(); - if ($injectErrors) { - $closure->append('$errors->setValidationErrors($validator->validate(', $validationGroups, ', false))'); - } else { - $closure->append('$validator->validate(', $validationGroups, ')'); - } + if ($injectErrors) { + $closure->append('$errors->setValidationErrors($validator->validate(', $validationGroups, ', false))'); + } else { + $closure->append('$validator->validate(', $validationGroups, ')'); + } - $closure->emptyLine(); + $closure->emptyLine(); + } + } 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.'); } - } 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)); - return $closure; - } + $closure->append('return ', $this->expressionConverter->convert($expression)); - return ArrowFunction::new($resolve); + return $closure; + } + ); } /** @@ -498,14 +494,14 @@ private function buildResolve($resolve, ?array $groups = null): GeneratorInterfa */ private function configContainsValidation(): bool { - $fieldConfig = $this->config['fields'][$this->currentField]; + $fieldConfig = $this->config->fields[$this->currentField]; - if (!empty($fieldConfig['validation'])) { + if (!empty($fieldConfig->validation)) { return true; } - foreach ($fieldConfig['args'] ?? [] as $argConfig) { - if (!empty($argConfig['validation'])) { + foreach ($fieldConfig->args ?? [] as $argConfig) { + if (!empty($argConfig->validation)) { return true; } } @@ -526,47 +522,38 @@ private function configContainsValidation(): bool * * If only constraints provided, uses {@see buildConstraints} directly. * - * @param array{ - * constraints: array, - * link: string, - * cascade: array - * } $config - * * @throws GeneratorException */ - private function buildValidationRules(array $config): GeneratorInterface + private function buildValidationRules(Validation $validationConfig): GeneratorInterface { - // Convert to object for better readability - $c = (object) $config; - $array = Collection::assoc(); - if (!empty($c->link)) { - if (!str_contains($c->link, '::')) { + if (null !== $validationConfig->link) { + if (str_contains($validationConfig->link, '::')) { // e.g. App\Entity\Droid - $array->addItem('link', $c->link); + $array->addItem('link', $validationConfig->link); } else { // e.g. App\Entity\Droid::$id - $array->addItem('link', Collection::numeric($this->normalizeLink($c->link))); + $array->addItem('link', Collection::numeric($this->normalizeLink($validationConfig->link))); } } - if (isset($c->cascade)) { - // If there are only constarainst, use short syntax - if (empty($c->cascade['groups'])) { + if (null !== $validationConfig->cascade) { + // If there are only constraints, use short syntax + if (empty($validationConfig->cascade['groups'])) { $this->file->addUse(InputValidator::class); return Literal::new('InputValidator::CASCADE'); } - $array->addItem('cascade', $c->cascade['groups']); + $array->addItem('cascade', $validationConfig->cascade['groups']); } - if (!empty($c->constraints)) { - // If there are only constarainst, use short syntax + if (!empty($validationConfig->constraints)) { + // If there are only constraints, use short syntax if (0 === $array->count()) { - return $this->buildConstraints($c->constraints); + return $this->buildConstraints($validationConfig->constraints); } - $array->addItem('constraints', $this->buildConstraints($c->constraints)); + $array->addItem('constraints', $this->buildConstraints($validationConfig->constraints)); } return $array; @@ -659,84 +646,71 @@ private function buildConstraints(array $constraints = [], bool $inClosure = tru * {@see buildArg}, * ... * ], - * 'resolve' => {@see buildResolve}, + * 'resolve' => {@see buildResolver}, * 'complexity' => {@see buildComplexity}, * ] * - * @param array{ - * type: string, - * resolve?: string, - * description?: string, - * args?: array, - * complexity?: string, - * deprecatedReason?: string, - * validation?: array, - * } $fieldConfig - * - * @internal + * @return GeneratorInterface|Collection|string * * @throws GeneratorException * - * @return GeneratorInterface|Collection|string + * @internal */ - public function buildField(array $fieldConfig, string $fieldname) + public function buildField(Field $fieldConfig, string $fieldName) { - $this->currentField = $fieldname; - - // Convert to object for better readability - $c = (object) $fieldConfig; + $this->currentField = $fieldName; // If there is only 'type', use shorthand - if (1 === count($fieldConfig) && isset($c->type)) { - return $this->buildType($c->type); + if ($fieldConfig->hasOnlyType) { + return $this->buildType($fieldConfig->type); } $field = Collection::assoc() - ->addItem('type', $this->buildType($c->type)); + ->addItem('type', $this->buildType($fieldConfig->type)); // only for object types - if (isset($c->resolve)) { - if (isset($c->validation)) { - $field->addItem('validation', $this->buildValidationRules($c->validation)); + if (!empty($fieldConfig->resolver)) { + if ($fieldConfig->validation) { + $field->addItem('validation', $this->buildValidationRules($fieldConfig->validation)); } - $field->addItem('resolve', $this->buildResolve($c->resolve, $fieldConfig['validationGroups'] ?? null)); + $field->addItem('resolve', $this->buildResolver($fieldConfig->resolver, $fieldConfig->validationGroups ?? null)); } - if (isset($c->deprecationReason)) { - $field->addItem('deprecationReason', $c->deprecationReason); + if (null !== $fieldConfig->deprecationReason) { + $field->addItem('deprecationReason', $fieldConfig->deprecationReason); } - if (isset($c->description)) { - $field->addItem('description', $c->description); + if (null !== $fieldConfig->description) { + $field->addItem('description', $fieldConfig->description); } - if (!empty($c->args)) { - $field->addItem('args', Collection::map($c->args, [$this, 'buildArg'], false)); + if (!empty($fieldConfig->args)) { + $field->addItem('args', Collection::map($fieldConfig->args, [$this, 'buildArg'], false)); } - if (isset($c->complexity)) { - $field->addItem('complexity', $this->buildComplexity($c->complexity)); + if (null !== $fieldConfig->complexity) { + $field->addItem('complexity', $this->buildComplexity($fieldConfig->complexity)); } - if (isset($c->public)) { - $field->addItem('public', $this->buildPublic($c->public)); + if (null !== $fieldConfig->public) { + $field->addItem('public', $this->buildPublic($fieldConfig->public)); } - if (isset($c->access)) { - $field->addItem('access', $this->buildAccess($c->access)); + if (null !== $fieldConfig->access) { + $field->addItem('access', $this->buildAccess($fieldConfig->access)); } - if (!empty($c->access) && is_string($c->access) && EL::expressionContainsVar('object', $c->access)) { + if (!empty($fieldConfig->access) && is_string($fieldConfig->access) && EL::expressionContainsVar('object', $fieldConfig->access)) { $field->addItem('useStrictAccess', false); } if ('input-object' === $this->type) { - if (property_exists($c, 'defaultValue')) { - $field->addItem('defaultValue', $c->defaultValue); + if ($fieldConfig->hasDefaultValue) { + $field->addItem('defaultValue', $fieldConfig->defaultValue); } - if (isset($c->validation)) { - $field->addItem('validation', $this->buildValidationRules($c->validation)); + if (null !== $fieldConfig->validation) { + $field->addItem('validation', $this->buildValidationRules($fieldConfig->validation)); } } @@ -754,39 +728,30 @@ public function buildField(array $fieldConfig, string $fieldname) * ] * </code> * - * @param array{ - * type: string, - * description?: string, - * defaultValue?: string - * } $argConfig - * * @internal * * @throws GeneratorException */ - public function buildArg(array $argConfig, string $argName): Collection + public function buildArg(Arg $argConfig, string $argName): Collection { - // Convert to object for better readability - $c = (object) $argConfig; - $arg = Collection::assoc() ->addItem('name', $argName) - ->addItem('type', $this->buildType($c->type)); + ->addItem('type', $this->buildType($argConfig->type)); - if (isset($c->description)) { - $arg->addIfNotEmpty('description', $c->description); + if (null !== $argConfig->description) { + $arg->addIfNotEmpty('description', $argConfig->description); } - if (property_exists($c, 'defaultValue')) { - $arg->addItem('defaultValue', $c->defaultValue); + if ($argConfig->hasDefaultValue) { + $arg->addItem('defaultValue', $argConfig->defaultValue); } - if (!empty($c->validation)) { - if (in_array($c->type, self::BUILT_IN_TYPES) && isset($c->validation['cascade'])) { + if (!empty($argConfig->validation)) { + if (in_array($argConfig->type, self::BUILT_IN_TYPES) && null !== $argConfig->validation->cascade) { throw new GeneratorException('Cascade validation cannot be applied to built-in types.'); } - $arg->addIfNotEmpty('validation', $this->buildValidationRules($c->validation)); + $arg->addIfNotEmpty('validation', $this->buildValidationRules($argConfig->validation)); } return $arg; @@ -902,22 +867,33 @@ private function buildAccess($access) * Render example: * * fn($value, $context, $info) => $services->getType($value) - * - * @param mixed $resolveType - * - * @return mixed|ArrowFunction */ - private function buildResolveType($resolveType) + private function buildTypeResolver(Callback $typeResolver): GeneratorInterface { - if (EL::isStringWithTrigger($resolveType)) { - $expression = $this->expressionConverter->convert($resolveType); + return $this->buildCallback($typeResolver, ['value', 'context', 'info']); + } - return ArrowFunction::new() - ->addArguments('value', 'context', 'info') - ->setExpression(Literal::new($expression)); + protected function buildCallback(Callback $callback, array $argNames, ?callable $expressionBuilder = null): GeneratorInterface + { + if (null !== $callback->expression) { + if (null === $expressionBuilder) { + return ArrowFunction::new() + ->addArguments(...$argNames) + ->setExpression(Literal::new($this->expressionConverter->convert($callback->expression))) + ; + } else { + return $expressionBuilder($callback->expression); + } + } elseif (null !== $callback->id) { + $fn = "$this->gqlServices->get('callbacks')->get('$callback->id')"; + if ($callback->method) { + return Collection::numeric([$fn, $callback->method]); + } else { + return Literal::new($fn); + } + } else { + return Literal::new("'$callback->method'"); } - - return $resolveType; } /** diff --git a/tests/DependencyInjection/Compiler/ConfigParserPassTest.php b/tests/DependencyInjection/Compiler/ConfigParserPassTest.php index f5e6a78c2..8f9332955 100644 --- a/tests/DependencyInjection/Compiler/ConfigParserPassTest.php +++ b/tests/DependencyInjection/Compiler/ConfigParserPassTest.php @@ -243,22 +243,22 @@ public function testCustomBuilders(): void 'createdAt' => [ 'description' => 'The creation date of the object', 'type' => 'Int!', - 'resolve' => '@=value.createdAt', + 'resolver' => ['expression' => 'value.createdAt'], ], 'updatedAt' => [ 'description' => 'The update date of the object', 'type' => 'Int!', - 'resolve' => '@=value.updatedAt', + 'resolver' => ['expression' => 'value.updatedAt'], ], 'rawIDWithDescriptionOverride' => [ 'description' => 'rawIDWithDescriptionOverride description', 'type' => 'Int!', - 'resolve' => '@=value.id', + 'resolver' => ['expression' => 'value.id'], ], 'rawID' => [ 'description' => 'The raw ID of an object', 'type' => 'Int!', - 'resolve' => '@=value.id', + 'resolver' => ['expression' => 'value.id'], ], 'rawIDs' => [ 'type' => '[RawID!]!', @@ -329,10 +329,10 @@ public function testCustomBuilders(): void 'fields' => [ 'foo' => [ 'type' => 'FooPayload!', - 'resolve' => '@=mutation("Mutation.foo", args.input)', 'args' => [ 'input' => ['type' => 'FooInput!'], ], + 'resolver' => ['expression' => 'mutation("Mutation.foo", args.input)'], ], ], 'name' => 'Mutation', @@ -389,8 +389,8 @@ public function testCustomBuilders(): void 'decorator' => false, 'config' => [ 'types' => ['FooSuccessPayload', 'FooFailurePayload'], - 'resolveType' => '@=query("PayloadTypeResolver", value, "FooSuccessPayload", "FooFailurePayload")', 'name' => 'FooPayload', + 'typeResolver' => ['expression' => 'query("PayloadTypeResolver", value, "FooSuccessPayload", "FooFailurePayload")'], ], ], 'FooSuccessPayload' => [ From a543004ce1bb8d64f072f28028c2b3ddba3b056d Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <jeremiah@overblog.com> Date: Sat, 15 May 2021 20:26:17 +0200 Subject: [PATCH 02/14] wip --- docs/definitions/expression-language.md | 2 +- src/Definition/GraphQLServices.php | 12 ++++++--- .../Compiler/GraphQLServicesPass.php | 27 ++++++++++--------- .../DependencyInjection/Parameter.php | 4 +-- .../DependencyInjection/Service.php | 4 +-- .../ExpressionFunction/GraphQL/Arguments.php | 4 +-- .../ExpressionFunction/Security/GetUser.php | 5 ++-- .../Security/HasAnyPermission.php | 5 ++-- .../Security/HasAnyRole.php | 5 ++-- .../Security/HasPermission.php | 5 ++-- .../ExpressionFunction/Security/HasRole.php | 5 ++-- .../Security/IsAnonymous.php | 5 ++-- .../Security/IsAuthenticated.php | 5 ++-- .../Security/IsFullyAuthenticated.php | 5 ++-- .../ExpressionFunction/Security/IsGranted.php | 5 ++-- .../Security/IsRememberMe.php | 5 ++-- src/ExpressionLanguage/ExpressionLanguage.php | 22 ++++++++++++--- src/Generator/TypeBuilder.php | 3 ++- .../Constraints/ExpressionValidator.php | 8 +++--- .../DependencyInjection/ParameterTest.php | 5 ++-- .../DependencyInjection/ServiceTest.php | 5 ++-- .../GraphQL/ArgumentsTest.php | 2 +- .../Security/GetUserTest.php | 11 +++----- .../Security/HasAnyPermissionTest.php | 3 ++- .../Security/HasAnyRoleTest.php | 3 ++- .../Security/HasPermissionTest.php | 3 ++- .../Security/HasRoleTest.php | 3 ++- .../Security/IsAnonymousTest.php | 3 ++- .../Security/IsAuthenticatedTest.php | 3 ++- .../Security/IsFullyAuthenticatedTest.php | 3 ++- .../Security/IsGrantedTest.php | 3 ++- .../Security/IsRememberMeTest.php | 3 ++- tests/ExpressionLanguage/TestCase.php | 10 +++---- 33 files changed, 116 insertions(+), 80 deletions(-) diff --git a/docs/definitions/expression-language.md b/docs/definitions/expression-language.md index 8cbf79ae9..b4af60a58 100644 --- a/docs/definitions/expression-language.md +++ b/docs/definitions/expression-language.md @@ -386,7 +386,7 @@ MyType: fields: name: type: String! - resolve: "@=service('my_private_service').formatName(value)" + resolve: "@=my_private_service.formatName(value)" ``` To use a vendor private services: diff --git a/src/Definition/GraphQLServices.php b/src/Definition/GraphQLServices.php index 8e458a9aa..e3ff379cf 100644 --- a/src/Definition/GraphQLServices.php +++ b/src/Definition/GraphQLServices.php @@ -6,7 +6,11 @@ use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; +use Overblog\GraphQLBundle\Resolver\MutationResolver; +use Overblog\GraphQLBundle\Resolver\QueryResolver; +use Overblog\GraphQLBundle\Resolver\TypeResolver; use Overblog\GraphQLBundle\Validator\InputValidator; +use Overblog\GraphQLBundle\Validator\InputValidatorFactory; use Symfony\Component\DependencyInjection\ServiceLocator; /** @@ -21,7 +25,7 @@ final class GraphQLServices extends ServiceLocator */ public function query(string $alias, ...$args) { - return $this->get('queryResolver')->resolve([$alias, $args]); + return $this->get(QueryResolver::class)->resolve([$alias, $args]); } /** @@ -31,7 +35,7 @@ public function query(string $alias, ...$args) */ public function mutation(string $alias, ...$args) { - return $this->get('mutationResolver')->resolve([$alias, $args]); + return $this->get(MutationResolver::class)->resolve([$alias, $args]); } /** @@ -41,7 +45,7 @@ public function mutation(string $alias, ...$args) */ public function getType(string $typeName): ?Type { - return $this->get('typeResolver')->resolve($typeName); + return $this->get(TypeResolver::class)->resolve($typeName); } /** @@ -52,7 +56,7 @@ public function getType(string $typeName): ?Type */ public function createInputValidator($value, ArgumentInterface $args, $context, ResolveInfo $info): InputValidator { - return $this->get('input_validator_factory')->create( + return $this->get(InputValidatorFactory::class)->create( new ResolverArgs($value, $args, $context, $info) ); } diff --git a/src/DependencyInjection/Compiler/GraphQLServicesPass.php b/src/DependencyInjection/Compiler/GraphQLServicesPass.php index 4b88c48fa..56d63d706 100644 --- a/src/DependencyInjection/Compiler/GraphQLServicesPass.php +++ b/src/DependencyInjection/Compiler/GraphQLServicesPass.php @@ -6,7 +6,6 @@ use InvalidArgumentException; use Overblog\GraphQLBundle\Definition\GraphQLServices; -use Overblog\GraphQLBundle\Generator\TypeGenerator; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -27,26 +26,30 @@ public function process(ContainerBuilder $container): void foreach ($taggedServices as $id => $tags) { foreach ($tags as $attributes) { - if (empty($attributes['alias']) || !is_string($attributes['alias'])) { - throw new InvalidArgumentException( - sprintf('Service "%s" tagged "overblog_graphql.service" should have a valid "alias" attribute.', $id) - ); - } - $locateableServices[$attributes['alias']] = new Reference($id); + $locateableServices[] = new Reference($id); + + if (array_key_exists('alias', $attributes)) { + if (empty($attributes['alias']) || !is_string($attributes['alias'])) { + throw new InvalidArgumentException( + sprintf('Service "%s" tagged "overblog_graphql.service" should have a valid "alias" attribute.', $id) + ); + } - $isPublic = !isset($attributes['public']) || $attributes['public']; - if ($isPublic) { $expressionLanguageDefinition->addMethodCall( - 'addGlobalName', + 'addExpressionVariableNameServiceId', [ - sprintf(TypeGenerator::GRAPHQL_SERVICES.'->get(\'%s\')', $attributes['alias']), $attributes['alias'], + $id, ] ); } } } - $locateableServices['container'] = new Reference('service_container'); + $locateableServices[] = new Reference('service_container'); + $expressionLanguageDefinition->addMethodCall( + 'addExpressionVariableNameServiceId', + ['container', 'service_container'] + ); $container->findDefinition(GraphQLServices::class)->addArgument($locateableServices); } diff --git a/src/ExpressionLanguage/ExpressionFunction/DependencyInjection/Parameter.php b/src/ExpressionLanguage/ExpressionFunction/DependencyInjection/Parameter.php index ee5b5ea32..4da745ce3 100644 --- a/src/ExpressionLanguage/ExpressionFunction/DependencyInjection/Parameter.php +++ b/src/ExpressionLanguage/ExpressionFunction/DependencyInjection/Parameter.php @@ -13,8 +13,8 @@ public function __construct($name = 'parameter') { parent::__construct( $name, - fn (string $value) => "$this->gqlServices->get('container')->getParameter($value)", - static fn (array $arguments, $paramName) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('container')->getParameter($paramName) + fn (string $value) => "$this->gqlServices->get('service_container')->getParameter($value)", + static fn (array $arguments, $paramName) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('service_container')->getParameter($paramName) ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/DependencyInjection/Service.php b/src/ExpressionLanguage/ExpressionFunction/DependencyInjection/Service.php index d1679d424..2f03c8a34 100644 --- a/src/ExpressionLanguage/ExpressionFunction/DependencyInjection/Service.php +++ b/src/ExpressionLanguage/ExpressionFunction/DependencyInjection/Service.php @@ -13,8 +13,8 @@ public function __construct($name = 'service') { parent::__construct( $name, - fn (string $serviceId) => "$this->gqlServices->get('container')->get($serviceId)", - static fn (array $arguments, $serviceId) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('container')->get($serviceId) + fn (string $serviceId) => "$this->gqlServices->get('service_container')->get($serviceId)", + static fn (array $arguments, $serviceId) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('service_container')->get($serviceId) ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/GraphQL/Arguments.php b/src/ExpressionLanguage/ExpressionFunction/GraphQL/Arguments.php index 18366d36a..4edc33e9d 100644 --- a/src/ExpressionLanguage/ExpressionFunction/GraphQL/Arguments.php +++ b/src/ExpressionLanguage/ExpressionFunction/GraphQL/Arguments.php @@ -13,8 +13,8 @@ public function __construct() { parent::__construct( 'arguments', - fn ($mapping, $data) => "$this->gqlServices->get('container')->get('overblog_graphql.arguments_transformer')->getArguments($mapping, $data, \$info)", - static fn (array $arguments, $mapping, $data) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('container')->get('overblog_graphql.arguments_transformer')->getArguments($mapping, $data, $arguments['info']) + fn ($mapping, $data) => "$this->gqlServices->get('service_container')->get('overblog_graphql.arguments_transformer')->getArguments($mapping, $data, \$info)", + static fn (array $arguments, $mapping, $data) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('service_container')->get('overblog_graphql.arguments_transformer')->getArguments($mapping, $data, $arguments['info']) ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/GetUser.php b/src/ExpressionLanguage/ExpressionFunction/Security/GetUser.php index 2d773a65b..951580dda 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/GetUser.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/GetUser.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; final class GetUser extends ExpressionFunction { @@ -13,8 +14,8 @@ public function __construct() { parent::__construct( 'getUser', - fn () => "$this->gqlServices->get('security')->getUser()", - static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('security')->getUser() + fn () => "$this->gqlServices->get('".Security::class.'\')->getUser()', + static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->getUser() ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/HasAnyPermission.php b/src/ExpressionLanguage/ExpressionFunction/Security/HasAnyPermission.php index d947ec423..69b9023f2 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/HasAnyPermission.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/HasAnyPermission.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; final class HasAnyPermission extends ExpressionFunction { @@ -13,8 +14,8 @@ public function __construct() { parent::__construct( 'hasAnyPermission', - fn ($object, $permissions) => "$this->gqlServices->get('security')->hasAnyPermission($object, $permissions)", - static fn (array $arguments, $object, $permissions) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('security')->hasAnyPermission($object, $permissions) + fn ($object, $permissions) => "$this->gqlServices->get('".Security::class."')->hasAnyPermission($object, $permissions)", + static fn (array $arguments, $object, $permissions) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->hasAnyPermission($object, $permissions) ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/HasAnyRole.php b/src/ExpressionLanguage/ExpressionFunction/Security/HasAnyRole.php index 79d4ada77..242a1bb9b 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/HasAnyRole.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/HasAnyRole.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; final class HasAnyRole extends ExpressionFunction { @@ -13,8 +14,8 @@ public function __construct() { parent::__construct( 'hasAnyRole', - fn ($roles) => "$this->gqlServices->get('security')->hasAnyRole($roles)", - static fn (array $arguments, $roles) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('security')->hasAnyRole($roles) + fn ($roles) => "$this->gqlServices->get('".Security::class."')->hasAnyRole($roles)", + static fn (array $arguments, $roles) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->hasAnyRole($roles) ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/HasPermission.php b/src/ExpressionLanguage/ExpressionFunction/Security/HasPermission.php index 2a76085a5..d617bbe44 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/HasPermission.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/HasPermission.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; final class HasPermission extends ExpressionFunction { @@ -13,8 +14,8 @@ public function __construct() { parent::__construct( 'hasPermission', - fn ($object, $permission) => "$this->gqlServices->get('security')->hasPermission($object, $permission)", - static fn (array $arguments, $object, $permission) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('security')->hasPermission($object, $permission) + fn ($object, $permission) => "$this->gqlServices->get('".Security::class."')->hasPermission($object, $permission)", + static fn (array $arguments, $object, $permission) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->hasPermission($object, $permission) ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/HasRole.php b/src/ExpressionLanguage/ExpressionFunction/Security/HasRole.php index 2bfb5aa34..14cadd75f 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/HasRole.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/HasRole.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; final class HasRole extends ExpressionFunction { @@ -13,8 +14,8 @@ public function __construct() { parent::__construct( 'hasRole', - fn ($role) => "$this->gqlServices->get('security')->hasRole($role)", - static fn (array $arguments, $role) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('security')->hasRole($role) + fn ($role) => "$this->gqlServices->get('".Security::class."')->hasRole($role)", + static fn (array $arguments, $role) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->hasRole($role) ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/IsAnonymous.php b/src/ExpressionLanguage/ExpressionFunction/Security/IsAnonymous.php index 042374615..0a7620237 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/IsAnonymous.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/IsAnonymous.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; final class IsAnonymous extends ExpressionFunction { @@ -13,8 +14,8 @@ public function __construct() { parent::__construct( 'isAnonymous', - fn () => "$this->gqlServices->get('security')->isAnonymous()", - static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('security')->isAnonymous() + fn () => "$this->gqlServices->get('".Security::class.'\')->isAnonymous()', + static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->isAnonymous() ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/IsAuthenticated.php b/src/ExpressionLanguage/ExpressionFunction/Security/IsAuthenticated.php index 78589758e..ee306b1c0 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/IsAuthenticated.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/IsAuthenticated.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; final class IsAuthenticated extends ExpressionFunction { @@ -13,8 +14,8 @@ public function __construct() { parent::__construct( 'isAuthenticated', - fn () => "$this->gqlServices->get('security')->isAuthenticated()", - static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('security')->isAuthenticated() + fn () => "$this->gqlServices->get('".Security::class.'\')->isAuthenticated()', + static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->isAuthenticated() ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/IsFullyAuthenticated.php b/src/ExpressionLanguage/ExpressionFunction/Security/IsFullyAuthenticated.php index c6d4ce9c7..5a4af74df 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/IsFullyAuthenticated.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/IsFullyAuthenticated.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; final class IsFullyAuthenticated extends ExpressionFunction { @@ -13,8 +14,8 @@ public function __construct() { parent::__construct( 'isFullyAuthenticated', - fn () => "$this->gqlServices->get('security')->isFullyAuthenticated()", - fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('security')->isFullyAuthenticated() + fn () => "$this->gqlServices->get('".Security::class.'\')->isFullyAuthenticated()', + fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->isFullyAuthenticated() ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/IsGranted.php b/src/ExpressionLanguage/ExpressionFunction/Security/IsGranted.php index ede9c6043..5c1a3e5d5 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/IsGranted.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/IsGranted.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; final class IsGranted extends ExpressionFunction { @@ -13,8 +14,8 @@ public function __construct() { parent::__construct( 'isGranted', - fn ($attributes, $object = 'null') => "$this->gqlServices->get('security')->isGranted($attributes, $object)", - static fn (array $arguments, $attributes, $object = null) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('security')->isGranted($attributes, $object) + fn ($attributes, $object = 'null') => "$this->gqlServices->get('".Security::class."')->isGranted($attributes, $object)", + static fn (array $arguments, $attributes, $object = null) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->isGranted($attributes, $object) ); } } diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/IsRememberMe.php b/src/ExpressionLanguage/ExpressionFunction/Security/IsRememberMe.php index 16290da38..ae22f7526 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/IsRememberMe.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/IsRememberMe.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; final class IsRememberMe extends ExpressionFunction { @@ -13,8 +14,8 @@ public function __construct() { parent::__construct( 'isRememberMe', - fn () => "$this->gqlServices->get('security')->isRememberMe()", - static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get('security')->isRememberMe() + fn () => "$this->gqlServices->get('".Security::class.'\')->isRememberMe()', + static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->isRememberMe() ); } } diff --git a/src/ExpressionLanguage/ExpressionLanguage.php b/src/ExpressionLanguage/ExpressionLanguage.php index d6709bce4..c7754b6e0 100644 --- a/src/ExpressionLanguage/ExpressionLanguage.php +++ b/src/ExpressionLanguage/ExpressionLanguage.php @@ -4,6 +4,7 @@ namespace Overblog\GraphQLBundle\ExpressionLanguage; +use Overblog\GraphQLBundle\Generator\TypeGenerator; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage; use Symfony\Component\ExpressionLanguage\Lexer; @@ -19,16 +20,29 @@ final class ExpressionLanguage extends BaseExpressionLanguage public const KNOWN_NAMES = ['value', 'args', 'context', 'info', 'object', 'validator', 'errors', 'childrenComplexity', 'typeName', 'fieldName']; public const EXPRESSION_TRIGGER = '@='; + /** @var array<string, string> */ public array $globalNames = []; - public function addGlobalName(string $index, string $name): void + /** @var array<string, string> */ + public array $expressionVariableServiceIds = []; + + public function addExpressionVariableNameServiceId(string $expressionVarName, string $serviceId): void + { + $this->expressionVariableServiceIds[$expressionVarName] = $serviceId; + $this->addGlobalName(sprintf(TypeGenerator::GRAPHQL_SERVICES.'->get(\'%s\')', $serviceId), $expressionVarName); + } + + /** + * @return array<string, string> + */ + public function getExpressionVariableServiceIds(): array { - $this->globalNames[$index] = $name; + return $this->expressionVariableServiceIds; } - public function getGlobalNames(): array + public function addGlobalName(string $code, string $expressionVarName): void { - return array_values($this->globalNames); + $this->globalNames[$code] = $expressionVarName; } /** diff --git a/src/Generator/TypeBuilder.php b/src/Generator/TypeBuilder.php index 8434f30b2..c4fdfb404 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -21,6 +21,7 @@ use Murtukov\PHPCodeGenerator\Literal; use Murtukov\PHPCodeGenerator\PhpFile; use Murtukov\PHPCodeGenerator\Utils; +use Overblog\GraphQLBundle\Definition\ArgumentFactory; use Overblog\GraphQLBundle\Definition\ConfigProcessor; use Overblog\GraphQLBundle\Definition\GraphQLServices; use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface; @@ -785,7 +786,7 @@ private function buildComplexity($complexity) ->addArgument('childrenComplexity') ->addArgument('arguments', '', []) ->bindVar(TypeGenerator::GRAPHQL_SERVICES) - ->append('$args = ', "$this->gqlServices->get('argumentFactory')->create(\$arguments)") + ->append('$args = ', "$this->gqlServices->get('".ArgumentFactory::class."')->create(\$arguments)") ->append('return ', $expression) ; } diff --git a/src/Validator/Constraints/ExpressionValidator.php b/src/Validator/Constraints/ExpressionValidator.php index 2aff26b8f..4675c3928 100644 --- a/src/Validator/Constraints/ExpressionValidator.php +++ b/src/Validator/Constraints/ExpressionValidator.php @@ -66,17 +66,17 @@ public function validate($value, Constraint $constraint): void */ private function addGlobalVariables($expression, array &$variables): void { - $globalVariables = $this->expressionLanguage->getGlobalNames(); + $expressionVariableServiceIds = $this->expressionLanguage->getExpressionVariableServiceIds(); foreach (ExpressionLanguage::extractExpressionVarNames($expression) as $extractExpressionVarName) { + $serviceIds = $expressionVariableServiceIds[$extractExpressionVarName] ?? null; if ( isset($variables[$extractExpressionVarName]) - || !$this->graphQLServices->has($extractExpressionVarName) - || !in_array($extractExpressionVarName, $globalVariables) + || !$this->graphQLServices->has($serviceIds) ) { continue; } - $variables[$extractExpressionVarName] = $this->graphQLServices->get($extractExpressionVarName); + $variables[$extractExpressionVarName] = $this->graphQLServices->get($serviceIds); } } } diff --git a/tests/ExpressionLanguage/ExpressionFunction/DependencyInjection/ParameterTest.php b/tests/ExpressionLanguage/ExpressionFunction/DependencyInjection/ParameterTest.php index 5f0e7cd1c..22d19381d 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/DependencyInjection/ParameterTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/DependencyInjection/ParameterTest.php @@ -25,9 +25,8 @@ protected function getFunctions() public function testParameterCompilation($name): void { ${TypeGenerator::GRAPHQL_SERVICES} = $this->createGraphQLServices( - ['container' => $this->getDIContainerMock([], ['test' => 5])] + ['service_container' => $this->getDIContainerMock([], ['test' => 5])] ); - ${TypeGenerator::GRAPHQL_SERVICES}->get('container'); $this->assertSame(5, eval('return '.$this->expressionLanguage->compile($name.'("test")').';')); } @@ -37,7 +36,7 @@ public function testParameterCompilation($name): void */ public function testParameterEvaluation($name): void { - $services = $this->createGraphQLServices(['container' => $this->getDIContainerMock([], ['test' => 5])]); + $services = $this->createGraphQLServices(['service_container' => $this->getDIContainerMock([], ['test' => 5])]); $this->assertSame( 5, $this->expressionLanguage->evaluate($name.'("test")', [TypeGenerator::GRAPHQL_SERVICES => $services]) diff --git a/tests/ExpressionLanguage/ExpressionFunction/DependencyInjection/ServiceTest.php b/tests/ExpressionLanguage/ExpressionFunction/DependencyInjection/ServiceTest.php index f264c1dfb..110852792 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/DependencyInjection/ServiceTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/DependencyInjection/ServiceTest.php @@ -26,8 +26,7 @@ public function testServiceCompilation(string $name): void { $object = new stdClass(); - ${TypeGenerator::GRAPHQL_SERVICES} = $this->createGraphQLServices(['container' => $this->getDIContainerMock(['toto' => $object])]); - ${TypeGenerator::GRAPHQL_SERVICES}->get('container'); + ${TypeGenerator::GRAPHQL_SERVICES} = $this->createGraphQLServices(['service_container' => $this->getDIContainerMock(['toto' => $object])]); $this->assertSame($object, eval('return '.$this->expressionLanguage->compile($name.'("toto")').';')); } @@ -37,7 +36,7 @@ public function testServiceCompilation(string $name): void public function testServiceEvaluation(string $name): void { $object = new stdClass(); - $services = $this->createGraphQLServices(['container' => $this->getDIContainerMock(['toto' => $object])]); + $services = $this->createGraphQLServices(['service_container' => $this->getDIContainerMock(['toto' => $object])]); $this->assertSame( $object, diff --git a/tests/ExpressionLanguage/ExpressionFunction/GraphQL/ArgumentsTest.php b/tests/ExpressionLanguage/ExpressionFunction/GraphQL/ArgumentsTest.php index cdb5647cd..571a21314 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/GraphQL/ArgumentsTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/GraphQL/ArgumentsTest.php @@ -83,7 +83,7 @@ public function testEvaluator(): void $services = $this->createGraphQLServices( [ - 'container' => $this->getDIContainerMock(['overblog_graphql.arguments_transformer' => $transformer]), + 'service_container' => $this->getDIContainerMock(['overblog_graphql.arguments_transformer' => $transformer]), ] ); diff --git a/tests/ExpressionLanguage/ExpressionFunction/Security/GetUserTest.php b/tests/ExpressionLanguage/ExpressionFunction/Security/GetUserTest.php index ef21537ef..670842c8f 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/Security/GetUserTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/Security/GetUserTest.php @@ -31,7 +31,7 @@ public function testEvaluator(): void } $coreSecurity = $this->createMock(CoreSecurity::class); $coreSecurity->method('getUser')->willReturn($testUser); - $services = $this->createGraphQLServices(['security' => new Security($coreSecurity)]); + $services = $this->createGraphQLServices([Security::class => new Security($coreSecurity)]); $user = $this->expressionLanguage->evaluate('getUser()', [TypeGenerator::GRAPHQL_SERVICES => $services]); $this->assertInstanceOf(UserInterface::class, $user); @@ -40,9 +40,8 @@ public function testEvaluator(): void public function testGetUserNoTokenStorage(): void { ${TypeGenerator::GRAPHQL_SERVICES} = $this->createGraphQLServices( - ['security' => new Security($this->createMock(CoreSecurity::class))] + [Security::class => new Security($this->createMock(CoreSecurity::class))] ); - ${TypeGenerator::GRAPHQL_SERVICES}->get('security'); $this->assertNull(eval($this->getCompileCode())); } @@ -51,14 +50,13 @@ public function testGetUserNoToken(): void $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); ${TypeGenerator::GRAPHQL_SERVICES} = $this->createGraphQLServices( [ - 'security' => new Security( + Security::class => new Security( new CoreSecurity( $this->getDIContainerMock(['security.token_storage' => $tokenStorage]) ) ), ] ); - ${TypeGenerator::GRAPHQL_SERVICES}->get('security'); $this->getDIContainerMock(['security.token_storage' => $tokenStorage]); $this->assertNull(eval($this->getCompileCode())); @@ -77,14 +75,13 @@ public function testGetUser($user, $expectedUser): void ${TypeGenerator::GRAPHQL_SERVICES} = $this->createGraphQLServices( [ - 'security' => new Security( + Security::class => new Security( new CoreSecurity( $this->getDIContainerMock(['security.token_storage' => $tokenStorage]) ) ), ] ); - ${TypeGenerator::GRAPHQL_SERVICES}->get('security'); $token ->expects($this->once()) diff --git a/tests/ExpressionLanguage/ExpressionFunction/Security/HasAnyPermissionTest.php b/tests/ExpressionLanguage/ExpressionFunction/Security/HasAnyPermissionTest.php index a2f9399a8..225079e3a 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/Security/HasAnyPermissionTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/Security/HasAnyPermissionTest.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction\Security\HasAnyPermission; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; use Overblog\GraphQLBundle\Tests\ExpressionLanguage\TestCase; use stdClass; @@ -28,7 +29,7 @@ public function testEvaluator(): void ], $this->any() ); - $services = $this->createGraphQLServices(['security' => $security]); + $services = $this->createGraphQLServices([Security::class => $security]); $hasPermission = $this->expressionLanguage->evaluate( $this->testedExpression, diff --git a/tests/ExpressionLanguage/ExpressionFunction/Security/HasAnyRoleTest.php b/tests/ExpressionLanguage/ExpressionFunction/Security/HasAnyRoleTest.php index 5d728050c..ca859b4b1 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/Security/HasAnyRoleTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/Security/HasAnyRoleTest.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction\Security\HasAnyRole; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; use Overblog\GraphQLBundle\Tests\ExpressionLanguage\TestCase; final class HasAnyRoleTest extends TestCase @@ -18,7 +19,7 @@ protected function getFunctions() public function testEvaluator(): void { $security = $this->getSecurityIsGrantedWithExpectation('ROLE_ADMIN', $this->any()); - $services = $this->createGraphQLServices(['security' => $security]); + $services = $this->createGraphQLServices([Security::class => $security]); $hasRole = $this->expressionLanguage->evaluate( 'hasAnyRole(["ROLE_ADMIN", "ROLE_USER"])', diff --git a/tests/ExpressionLanguage/ExpressionFunction/Security/HasPermissionTest.php b/tests/ExpressionLanguage/ExpressionFunction/Security/HasPermissionTest.php index a14921e6f..f5b3d2b84 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/Security/HasPermissionTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/Security/HasPermissionTest.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction\Security\HasPermission; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; use Overblog\GraphQLBundle\Tests\ExpressionLanguage\TestCase; use stdClass; @@ -28,7 +29,7 @@ public function testEvaluator(): void ], $this->any() ); - $services = $this->createGraphQLServices(['security' => $security]); + $services = $this->createGraphQLServices([Security::class => $security]); $hasPermission = $this->expressionLanguage->evaluate( $this->testedExpression, diff --git a/tests/ExpressionLanguage/ExpressionFunction/Security/HasRoleTest.php b/tests/ExpressionLanguage/ExpressionFunction/Security/HasRoleTest.php index d1f803077..c64b8eb3b 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/Security/HasRoleTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/Security/HasRoleTest.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction\Security\HasRole; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; use Overblog\GraphQLBundle\Tests\ExpressionLanguage\TestCase; final class HasRoleTest extends TestCase @@ -21,7 +22,7 @@ public function testEvaluator(): void 'ROLE_USER', $this->any() ); - $services = $this->createGraphQLServices(['security' => $security]); + $services = $this->createGraphQLServices([Security::class => $security]); $hasRole = $this->expressionLanguage->evaluate('hasRole("ROLE_USER")', [TypeGenerator::GRAPHQL_SERVICES => $services]); $this->assertTrue($hasRole); diff --git a/tests/ExpressionLanguage/ExpressionFunction/Security/IsAnonymousTest.php b/tests/ExpressionLanguage/ExpressionFunction/Security/IsAnonymousTest.php index d1e0ae2f5..88e0dd2e9 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/Security/IsAnonymousTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/Security/IsAnonymousTest.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction\Security\IsAnonymous; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; use Overblog\GraphQLBundle\Tests\ExpressionLanguage\TestCase; final class IsAnonymousTest extends TestCase @@ -21,7 +22,7 @@ public function testEvaluator(): void 'IS_AUTHENTICATED_ANONYMOUSLY', $this->any() ); - $services = $this->createGraphQLServices(['security' => $security]); + $services = $this->createGraphQLServices([Security::class => $security]); $isAnonymous = $this->expressionLanguage->evaluate('isAnonymous()', [TypeGenerator::GRAPHQL_SERVICES => $services]); $this->assertTrue($isAnonymous); diff --git a/tests/ExpressionLanguage/ExpressionFunction/Security/IsAuthenticatedTest.php b/tests/ExpressionLanguage/ExpressionFunction/Security/IsAuthenticatedTest.php index 1a5864671..993d6bfe6 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/Security/IsAuthenticatedTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/Security/IsAuthenticatedTest.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction\Security\IsAuthenticated; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; use Overblog\GraphQLBundle\Tests\ExpressionLanguage\TestCase; final class IsAuthenticatedTest extends TestCase @@ -21,7 +22,7 @@ public function testEvaluator(): void $this->matchesRegularExpression('/^IS_AUTHENTICATED_(REMEMBERED|FULLY)$/'), $this->any() ); - $gqlServices = $this->createGraphQLServices(['security' => $security]); + $gqlServices = $this->createGraphQLServices([Security::class => $security]); $isAuthenticated = $this->expressionLanguage->evaluate( 'isAuthenticated()', diff --git a/tests/ExpressionLanguage/ExpressionFunction/Security/IsFullyAuthenticatedTest.php b/tests/ExpressionLanguage/ExpressionFunction/Security/IsFullyAuthenticatedTest.php index 70f5a099d..3f5535c2a 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/Security/IsFullyAuthenticatedTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/Security/IsFullyAuthenticatedTest.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction\Security\IsFullyAuthenticated; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; use Overblog\GraphQLBundle\Tests\ExpressionLanguage\TestCase; final class IsFullyAuthenticatedTest extends TestCase @@ -21,7 +22,7 @@ public function testEvaluator(): void 'IS_AUTHENTICATED_FULLY', $this->any() ); - $gqlServices = $this->createGraphQLServices(['security' => $security]); + $gqlServices = $this->createGraphQLServices([Security::class => $security]); $isFullyAuthenticated = $this->expressionLanguage->evaluate( 'isFullyAuthenticated()', diff --git a/tests/ExpressionLanguage/ExpressionFunction/Security/IsGrantedTest.php b/tests/ExpressionLanguage/ExpressionFunction/Security/IsGrantedTest.php index 4091ba8f0..8b8761f32 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/Security/IsGrantedTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/Security/IsGrantedTest.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction\Security\IsGranted; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; use Overblog\GraphQLBundle\Tests\ExpressionLanguage\TestCase; final class IsGrantedTest extends TestCase @@ -21,7 +22,7 @@ public function testEvaluator(): void $this->matchesRegularExpression('/^ROLE_(USER|ADMIN)$/'), $this->any() ); - $gqlServices = $this->createGraphQLServices(['security' => $security]); + $gqlServices = $this->createGraphQLServices([Security::class => $security]); $this->assertTrue( $this->expressionLanguage->evaluate('isGranted("ROLE_USER")', [TypeGenerator::GRAPHQL_SERVICES => $gqlServices]) diff --git a/tests/ExpressionLanguage/ExpressionFunction/Security/IsRememberMeTest.php b/tests/ExpressionLanguage/ExpressionFunction/Security/IsRememberMeTest.php index 277ae1a8c..1eb9d695c 100644 --- a/tests/ExpressionLanguage/ExpressionFunction/Security/IsRememberMeTest.php +++ b/tests/ExpressionLanguage/ExpressionFunction/Security/IsRememberMeTest.php @@ -6,6 +6,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionFunction\Security\IsRememberMe; use Overblog\GraphQLBundle\Generator\TypeGenerator; +use Overblog\GraphQLBundle\Security\Security; use Overblog\GraphQLBundle\Tests\ExpressionLanguage\TestCase; final class IsRememberMeTest extends TestCase @@ -21,7 +22,7 @@ public function testEvaluator(): void 'IS_AUTHENTICATED_REMEMBERED', $this->any() ); - $gqlServices = $this->createGraphQLServices(['security' => $security]); + $gqlServices = $this->createGraphQLServices([Security::class => $security]); $isRememberMe = $this->expressionLanguage->evaluate('isRememberMe()', [TypeGenerator::GRAPHQL_SERVICES => $gqlServices]); $this->assertTrue($isRememberMe); diff --git a/tests/ExpressionLanguage/TestCase.php b/tests/ExpressionLanguage/TestCase.php index 0cd96f6f6..0bec6f948 100644 --- a/tests/ExpressionLanguage/TestCase.php +++ b/tests/ExpressionLanguage/TestCase.php @@ -8,6 +8,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage; use Overblog\GraphQLBundle\Generator\TypeGenerator; use Overblog\GraphQLBundle\Resolver\MutationResolver; +use Overblog\GraphQLBundle\Resolver\QueryResolver; use Overblog\GraphQLBundle\Resolver\TypeResolver; use Overblog\GraphQLBundle\Security\Security; use Overblog\GraphQLBundle\Tests\DIContainerMockTrait; @@ -57,9 +58,8 @@ protected function assertExpressionCompile( ): void { $code = $this->expressionLanguage->compile($expression, array_keys($vars)); ${TypeGenerator::GRAPHQL_SERVICES} = $this->createGraphQLServices([ - 'security' => $this->getSecurityIsGrantedWithExpectation($with, $expects, $return), + Security::class => $this->getSecurityIsGrantedWithExpectation($with, $expects, $return), ]); - ${TypeGenerator::GRAPHQL_SERVICES}->get('security'); extract($vars); $this->$assertMethod(eval('return '.$code.';')); @@ -106,9 +106,9 @@ private function getCoreSecurityMock(): CoreSecurity protected function createGraphQLServices(array $services = []): GraphQLServices { $locateableServices = [ - 'typeResolver' => fn () => $this->createMock(TypeResolver::class), - 'queryResolver' => fn () => $this->createMock(TypeResolver::class), - 'mutationResolver' => fn () => $$this->createMock(MutationResolver::class), + TypeResolver::class => fn () => $this->createMock(TypeResolver::class), + QueryResolver::class => fn () => $this->createMock(QueryResolver::class), + MutationResolver::class => fn () => $$this->createMock(MutationResolver::class), ]; foreach ($services as $id => $service) { From 387c0ac57344020d9f560ce42c8154aaf7c57d32 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <jeremiah@overblog.com> Date: Sun, 16 May 2021 00:15:59 +0200 Subject: [PATCH 03/14] wip --- .../Compiler/GraphQLServicesPass.php | 85 ++++++++++++++++++- src/Generator/TypeBuilder.php | 2 +- .../connection/mapping/connection.types.yaml | 8 +- .../App/config/connection/services.yml | 5 +- 4 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/DependencyInjection/Compiler/GraphQLServicesPass.php b/src/DependencyInjection/Compiler/GraphQLServicesPass.php index 56d63d706..c91fff80c 100644 --- a/src/DependencyInjection/Compiler/GraphQLServicesPass.php +++ b/src/DependencyInjection/Compiler/GraphQLServicesPass.php @@ -4,10 +4,11 @@ namespace Overblog\GraphQLBundle\DependencyInjection\Compiler; -use InvalidArgumentException; use Overblog\GraphQLBundle\Definition\GraphQLServices; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Reference; use function is_string; use function sprintf; @@ -19,6 +20,8 @@ final class GraphQLServicesPass implements CompilerPassInterface */ public function process(ContainerBuilder $container): void { + $this->tagAllNotTaggedGraphQLServices($container); + $taggedServices = $container->findTaggedServiceIds('overblog_graphql.service', true); $locateableServices = []; @@ -51,6 +54,84 @@ public function process(ContainerBuilder $container): void ['container', 'service_container'] ); - $container->findDefinition(GraphQLServices::class)->addArgument($locateableServices); + $container->findDefinition(GraphQLServices::class)->addArgument(array_unique($locateableServices)); + } + + private function tagAllNotTaggedGraphQLServices(ContainerBuilder $container): void + { + if (!$container->hasParameter('overblog_graphql_types.config')) { + return; + } + /** @var array $configs */ + $configs = $container->getParameter('overblog_graphql_types.config'); + foreach ($configs as &$typeConfig) { + switch ($typeConfig['type']) { + case 'object': + if (isset($typeConfig['config']['fieldResolver'])) { + $this->resolveServiceIdAndMethod($container, $typeConfig['config']['fieldResolver']); + } + + foreach ($typeConfig['config']['fields'] as &$field) { + if (isset($field['resolver'])) { + $this->resolveServiceIdAndMethod($container, $field['resolver']); + } + } + break; + + case 'interface': + case 'union': + if (isset($typeConfig['config']['typeResolver'])) { + $this->resolveServiceIdAndMethod($container, $typeConfig['config']['typeResolver']); + } + break; + } + } + $container->setParameter('overblog_graphql_types.config', $configs); + } + + private function resolveServiceIdAndMethod(ContainerBuilder $container, ?array &$resolver): void + { + if (!isset($resolver['id']) && !isset($resolver['method'])) { + return; + } + $originalId = $resolver['id'] ?? null; + $originalMethod = $resolver['method'] ?? null; + + if (null === $originalId) { + [$id, $method] = explode('::', $originalMethod, 2) + [null, null]; + $throw = false; + } else { + $id = $originalId; + $method = $originalMethod; + $throw = true; + } + + try { + $definition = $container->getDefinition($id); + } catch (ServiceNotFoundException $e) { + // get Alias real service ID + try { + $alias = $container->getAlias($id); + $id = (string) $alias; + $definition = $container->getDefinition($id); + } catch (ServiceNotFoundException | InvalidArgumentException $e) { + if ($throw) { + throw $e; + } + $resolver['id'] = null; + $resolver['method'] = $originalMethod; + + return; + } + } + if ( + !$definition->hasTag('overblog_graphql.service') + && !$definition->hasTag('overblog_graphql.global_variable') + ) { + $definition->addTag('overblog_graphql.service'); + } + + $resolver['id'] = $id; + $resolver['method'] = $method; } } diff --git a/src/Generator/TypeBuilder.php b/src/Generator/TypeBuilder.php index c4fdfb404..7b683988d 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -886,7 +886,7 @@ protected function buildCallback(Callback $callback, array $argNames, ?callable return $expressionBuilder($callback->expression); } } elseif (null !== $callback->id) { - $fn = "$this->gqlServices->get('callbacks')->get('$callback->id')"; + $fn = "$this->gqlServices->get('$callback->id')"; if ($callback->method) { return Collection::numeric([$fn, $callback->method]); } else { diff --git a/tests/Functional/App/config/connection/mapping/connection.types.yaml b/tests/Functional/App/config/connection/mapping/connection.types.yaml index ebb1b1db8..b45b9b4ce 100644 --- a/tests/Functional/App/config/connection/mapping/connection.types.yaml +++ b/tests/Functional/App/config/connection/mapping/connection.types.yaml @@ -4,7 +4,7 @@ Query: fields: user: type: User - resolve: '@=query("query")' + resolver: 'Overblog\GraphQLBundle\Tests\Functional\App\Resolver\ConnectionResolver::resolveQuery' User: type: object @@ -16,15 +16,15 @@ User: friends: type: friendConnection argsBuilder: "Relay::Connection" - resolve: '@=query("friends", value, args)' + resolver: 'overblog_graphql.test.resolver.node::friendsResolver' friendsForward: type: userConnection argsBuilder: "Relay::ForwardConnection" - resolve: '@=query("friends", value, args)' + resolver: 'overblog_graphql.test.resolver.node::friendsResolver' friendsBackward: type: userConnection argsBuilder: "Relay::BackwardConnection" - resolve: '@=query("friends", value, args)' + resolver: 'overblog_graphql.test.resolver.node::friendsResolver' friendConnection: type: relay-connection diff --git a/tests/Functional/App/config/connection/services.yml b/tests/Functional/App/config/connection/services.yml index e0498c639..62fc74216 100644 --- a/tests/Functional/App/config/connection/services.yml +++ b/tests/Functional/App/config/connection/services.yml @@ -1,6 +1,5 @@ services: - overblog_graphql.test.resolver.node: - class: Overblog\GraphQLBundle\Tests\Functional\App\Resolver\ConnectionResolver + Overblog\GraphQLBundle\Tests\Functional\App\Resolver\ConnectionResolver: arguments: - "@overblog_graphql.promise_adapter" tags: @@ -9,3 +8,5 @@ services: - { name: "overblog_graphql.query", alias: "query", method: "resolveQuery" } - { name: "overblog_graphql.query", alias: "connection", method: "resolveConnection" } - { name: "overblog_graphql.query", alias: "promise", method: "resolvePromiseFullFilled" } + + overblog_graphql.test.resolver.node: '@Overblog\GraphQLBundle\Tests\Functional\App\Resolver\ConnectionResolver' From 7cbbcd30cfe3d2b40e2c5110acd76d27ff828b9a Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <jeremiah@overblog.com> Date: Mon, 17 May 2021 22:19:19 +0200 Subject: [PATCH 04/14] wip --- src/Config/InterfaceTypeDefinition.php | 4 +- src/Config/ObjectTypeDefinition.php | 4 +- src/Config/TypeDefinition.php | 4 +- src/Config/TypeWithOutputFieldsDefinition.php | 4 +- src/Config/UnionTypeDefinition.php | 4 +- .../Compiler/GraphQLServicesPass.php | 81 ---------------- .../IdentifyCallbackServiceIdsPass.php | 94 +++++++++++++++++++ src/OverblogGraphQLBundle.php | 2 + src/Relay/Node/NodeDefinition.php | 5 +- .../connection/mapping/connection.types.yaml | 9 +- 10 files changed, 114 insertions(+), 97 deletions(-) create mode 100644 src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php diff --git a/src/Config/InterfaceTypeDefinition.php b/src/Config/InterfaceTypeDefinition.php index 42d5133f6..fb2e5b83c 100644 --- a/src/Config/InterfaceTypeDefinition.php +++ b/src/Config/InterfaceTypeDefinition.php @@ -12,14 +12,14 @@ public function getDefinition(): ArrayNodeDefinition { /** @var ArrayNodeDefinition $node */ $node = self::createNode('_interface_config'); - $this->resolverNormalization($node, 'typeResolver', 'resolveType'); + $this->callbackNormalization($node, 'typeResolver', 'resolveType'); /** @phpstan-ignore-next-line */ $node ->children() ->append($this->nameSection()) ->append($this->outputFieldsSection()) - ->append($this->resolverSection('typeResolver', 'GraphQL type resolver')) + ->append($this->callbackSection('typeResolver', 'GraphQL type resolver')) ->append($this->descriptionSection()) ->arrayNode('interfaces') ->prototype('scalar')->info('One of internal or custom interface types.')->end() diff --git a/src/Config/ObjectTypeDefinition.php b/src/Config/ObjectTypeDefinition.php index 41d06e469..8b83fcd5e 100644 --- a/src/Config/ObjectTypeDefinition.php +++ b/src/Config/ObjectTypeDefinition.php @@ -16,7 +16,7 @@ public function getDefinition(): ArrayNodeDefinition /** @var ArrayNodeDefinition $node */ $node = $builder->getRootNode(); - $this->resolverNormalization($node, 'fieldResolver', 'resolveField'); + $this->callbackNormalization($node, 'fieldResolver', 'resolveField'); /** @phpstan-ignore-next-line */ $node @@ -30,7 +30,7 @@ public function getDefinition(): ArrayNodeDefinition ->prototype('scalar')->info('One of internal or custom interface types.')->end() ->end() ->variableNode('isTypeOf')->end() - ->append($this->resolverSection('fieldResolver', 'GraphQL field value resolver')) + ->append($this->callbackSection('fieldResolver', 'GraphQL field value resolver')) ->variableNode('fieldsDefaultAccess') ->info('Default access control to fields (expression language can be use here)') ->end() diff --git a/src/Config/TypeDefinition.php b/src/Config/TypeDefinition.php index 86df0c0b5..549ff46a2 100644 --- a/src/Config/TypeDefinition.php +++ b/src/Config/TypeDefinition.php @@ -153,7 +153,7 @@ protected function typeSection(bool $isRequired = false): ScalarNodeDefinition return $node; } - protected function resolverNormalization(NodeDefinition $node, string $new, string $old): void + protected function callbackNormalization(NodeDefinition $node, string $new, string $old): void { $node ->beforeNormalization() @@ -195,7 +195,7 @@ protected function resolverNormalization(NodeDefinition $node, string $new, stri ; } - protected function resolverSection(string $name, string $info): ArrayNodeDefinition + protected function callbackSection(string $name, string $info): ArrayNodeDefinition { /** @var ArrayNodeDefinition $node */ $node = self::createNode($name); diff --git a/src/Config/TypeWithOutputFieldsDefinition.php b/src/Config/TypeWithOutputFieldsDefinition.php index bb7ae544d..d9dcd523e 100644 --- a/src/Config/TypeWithOutputFieldsDefinition.php +++ b/src/Config/TypeWithOutputFieldsDefinition.php @@ -18,7 +18,7 @@ protected function outputFieldsSection(): NodeDefinition $node->isRequired()->requiresAtLeastOneElement(); $prototype = $node->useAttributeAsKey('name', false)->prototype('array'); - $this->resolverNormalization($prototype, 'resolver', 'resolve'); + $this->callbackNormalization($prototype, 'resolver', 'resolve'); /** @phpstan-ignore-next-line */ $prototype @@ -69,7 +69,7 @@ protected function outputFieldsSection(): NodeDefinition ->end() ->end() ->end() - ->append($this->resolverSection('resolver', 'GraphQL value resolver')) + ->append($this->callbackSection('resolver', 'GraphQL value resolver')) ->append($this->descriptionSection()) ->append($this->deprecationReasonSection()) ->variableNode('access') diff --git a/src/Config/UnionTypeDefinition.php b/src/Config/UnionTypeDefinition.php index fa48a922e..cde466f16 100644 --- a/src/Config/UnionTypeDefinition.php +++ b/src/Config/UnionTypeDefinition.php @@ -12,7 +12,7 @@ public function getDefinition(): ArrayNodeDefinition { /** @var ArrayNodeDefinition $node */ $node = self::createNode('_union_config'); - $this->resolverNormalization($node, 'typeResolver', 'resolveType'); + $this->callbackNormalization($node, 'typeResolver', 'resolveType'); /** @phpstan-ignore-next-line */ $node @@ -25,7 +25,7 @@ public function getDefinition(): ArrayNodeDefinition ->isRequired() ->requiresAtLeastOneElement() ->end() - ->append($this->resolverSection('typeResolver', 'GraphQL type resolver')) + ->append($this->callbackSection('typeResolver', 'GraphQL type resolver')) ->append($this->descriptionSection()) ->end(); diff --git a/src/DependencyInjection/Compiler/GraphQLServicesPass.php b/src/DependencyInjection/Compiler/GraphQLServicesPass.php index c91fff80c..bfa6a3067 100644 --- a/src/DependencyInjection/Compiler/GraphQLServicesPass.php +++ b/src/DependencyInjection/Compiler/GraphQLServicesPass.php @@ -8,7 +8,6 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Reference; use function is_string; use function sprintf; @@ -20,8 +19,6 @@ final class GraphQLServicesPass implements CompilerPassInterface */ public function process(ContainerBuilder $container): void { - $this->tagAllNotTaggedGraphQLServices($container); - $taggedServices = $container->findTaggedServiceIds('overblog_graphql.service', true); $locateableServices = []; @@ -56,82 +53,4 @@ public function process(ContainerBuilder $container): void $container->findDefinition(GraphQLServices::class)->addArgument(array_unique($locateableServices)); } - - private function tagAllNotTaggedGraphQLServices(ContainerBuilder $container): void - { - if (!$container->hasParameter('overblog_graphql_types.config')) { - return; - } - /** @var array $configs */ - $configs = $container->getParameter('overblog_graphql_types.config'); - foreach ($configs as &$typeConfig) { - switch ($typeConfig['type']) { - case 'object': - if (isset($typeConfig['config']['fieldResolver'])) { - $this->resolveServiceIdAndMethod($container, $typeConfig['config']['fieldResolver']); - } - - foreach ($typeConfig['config']['fields'] as &$field) { - if (isset($field['resolver'])) { - $this->resolveServiceIdAndMethod($container, $field['resolver']); - } - } - break; - - case 'interface': - case 'union': - if (isset($typeConfig['config']['typeResolver'])) { - $this->resolveServiceIdAndMethod($container, $typeConfig['config']['typeResolver']); - } - break; - } - } - $container->setParameter('overblog_graphql_types.config', $configs); - } - - private function resolveServiceIdAndMethod(ContainerBuilder $container, ?array &$resolver): void - { - if (!isset($resolver['id']) && !isset($resolver['method'])) { - return; - } - $originalId = $resolver['id'] ?? null; - $originalMethod = $resolver['method'] ?? null; - - if (null === $originalId) { - [$id, $method] = explode('::', $originalMethod, 2) + [null, null]; - $throw = false; - } else { - $id = $originalId; - $method = $originalMethod; - $throw = true; - } - - try { - $definition = $container->getDefinition($id); - } catch (ServiceNotFoundException $e) { - // get Alias real service ID - try { - $alias = $container->getAlias($id); - $id = (string) $alias; - $definition = $container->getDefinition($id); - } catch (ServiceNotFoundException | InvalidArgumentException $e) { - if ($throw) { - throw $e; - } - $resolver['id'] = null; - $resolver['method'] = $originalMethod; - - return; - } - } - if ( - !$definition->hasTag('overblog_graphql.service') - && !$definition->hasTag('overblog_graphql.global_variable') - ) { - $definition->addTag('overblog_graphql.service'); - } - - $resolver['id'] = $id; - $resolver['method'] = $method; - } } diff --git a/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php b/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php new file mode 100644 index 000000000..dfa04066d --- /dev/null +++ b/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php @@ -0,0 +1,94 @@ +<?php + +declare(strict_types=1); + +namespace Overblog\GraphQLBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; + +final class IdentifyCallbackServiceIdsPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container): void + { + if (!$container->hasParameter('overblog_graphql_types.config')) { + return; + } + /** @var array $configs */ + $configs = $container->getParameter('overblog_graphql_types.config'); + foreach ($configs as &$typeConfig) { + switch ($typeConfig['type']) { + case 'object': + if (isset($typeConfig['config']['fieldResolver'])) { + $this->resolveServiceIdAndMethod($container, $typeConfig['config']['fieldResolver']); + } + + foreach ($typeConfig['config']['fields'] as &$field) { + if (isset($field['resolver'])) { + $this->resolveServiceIdAndMethod($container, $field['resolver']); + } + } + break; + + case 'interface': + case 'union': + if (isset($typeConfig['config']['typeResolver'])) { + $this->resolveServiceIdAndMethod($container, $typeConfig['config']['typeResolver']); + } + break; + } + } + $container->setParameter('overblog_graphql_types.config', $configs); + } + + private function resolveServiceIdAndMethod(ContainerBuilder $container, ?array &$callback): void + { + if (!isset($callback['id']) && !isset($callback['method'])) { + return; + } + $originalId = $callback['id'] ?? null; + $originalMethod = $callback['method'] ?? null; + + if (null === $originalId) { + [$id, $method] = explode('::', $originalMethod, 2) + [null, null]; + $throw = false; + } else { + $id = $originalId; + $method = $originalMethod; + $throw = true; + } + + try { + $definition = $container->getDefinition($id); + } catch (ServiceNotFoundException $e) { + // get Alias real service ID + try { + $alias = $container->getAlias($id); + $id = (string) $alias; + $definition = $container->getDefinition($id); + } catch (ServiceNotFoundException | InvalidArgumentException $e) { + if ($throw) { + throw $e; + } + $callback['id'] = null; + $callback['method'] = $originalMethod; + + return; + } + } + if ( + !$definition->hasTag('overblog_graphql.service') + && !$definition->hasTag('overblog_graphql.global_variable') + ) { + $definition->addTag('overblog_graphql.service'); + } + + $callback['id'] = $id; + $callback['method'] = $method; + } +} diff --git a/src/OverblogGraphQLBundle.php b/src/OverblogGraphQLBundle.php index 8c406eeb7..2b79876c5 100644 --- a/src/OverblogGraphQLBundle.php +++ b/src/OverblogGraphQLBundle.php @@ -8,6 +8,7 @@ use Overblog\GraphQLBundle\DependencyInjection\Compiler\ConfigParserPass; use Overblog\GraphQLBundle\DependencyInjection\Compiler\ExpressionFunctionPass; use Overblog\GraphQLBundle\DependencyInjection\Compiler\GraphQLServicesPass; +use Overblog\GraphQLBundle\DependencyInjection\Compiler\IdentifyCallbackServiceIdsPass; use Overblog\GraphQLBundle\DependencyInjection\Compiler\MutationTaggedServiceMappingTaggedPass; use Overblog\GraphQLBundle\DependencyInjection\Compiler\QueryTaggedServiceMappingPass; use Overblog\GraphQLBundle\DependencyInjection\Compiler\ResolverMapTaggedServiceMappingPass; @@ -38,6 +39,7 @@ public function build(ContainerBuilder $container): void //TypeGeneratorPass must be before TypeTaggedServiceMappingPass $container->addCompilerPass(new ConfigParserPass()); + $container->addCompilerPass(new IdentifyCallbackServiceIdsPass()); $container->addCompilerPass(new GraphQLServicesPass()); $container->addCompilerPass(new ExpressionFunctionPass()); $container->addCompilerPass(new ResolverMethodAliasesPass()); diff --git a/src/Relay/Node/NodeDefinition.php b/src/Relay/Node/NodeDefinition.php index b7affe13f..1719d4a9d 100644 --- a/src/Relay/Node/NodeDefinition.php +++ b/src/Relay/Node/NodeDefinition.php @@ -11,13 +11,12 @@ final class NodeDefinition implements MappingInterface public function toMappingDefinition(array $config): array { $name = $config['name']; - $resolveType = empty($config['resolveType']) ? null : $config['resolveType']; return [ $name => [ 'type' => 'interface', 'config' => [ - 'name' => $config['name'], + 'name' => $name, 'description' => 'Fetches an object given its ID', 'fields' => [ 'id' => [ @@ -25,7 +24,7 @@ public function toMappingDefinition(array $config): array 'description' => 'The ID of an object', ], ], - 'resolveType' => $resolveType, + 'typeResolver' => $config['typeResolver'] ?? $config['resolveType'] ?? null, ], ], ]; diff --git a/tests/Functional/App/config/connection/mapping/connection.types.yaml b/tests/Functional/App/config/connection/mapping/connection.types.yaml index b45b9b4ce..7e2d4fea2 100644 --- a/tests/Functional/App/config/connection/mapping/connection.types.yaml +++ b/tests/Functional/App/config/connection/mapping/connection.types.yaml @@ -16,11 +16,14 @@ User: friends: type: friendConnection argsBuilder: "Relay::Connection" - resolver: 'overblog_graphql.test.resolver.node::friendsResolver' + resolver: + method: 'overblog_graphql.test.resolver.node::friendsResolver' friendsForward: type: userConnection argsBuilder: "Relay::ForwardConnection" - resolver: 'overblog_graphql.test.resolver.node::friendsResolver' + resolver: + id: 'overblog_graphql.test.resolver.node' + method: 'friendsResolver' friendsBackward: type: userConnection argsBuilder: "Relay::BackwardConnection" @@ -38,7 +41,7 @@ friendConnection: connectionFields: totalCount: type: Int - resolve: '@=query("connection")' + resolver: '@=query("connection")' userConnection: type: relay-connection From ed76483b1f1654ba497452b119ffc339131ed0c7 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <mcg-web@users.noreply.github.com> Date: Thu, 27 May 2021 09:12:54 +0200 Subject: [PATCH 05/14] Fix typo Co-authored-by: Timur Murtukov <murtukov@gmail.com> --- src/Config/TypeDefinition.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/TypeDefinition.php b/src/Config/TypeDefinition.php index 549ff46a2..38e4e0219 100644 --- a/src/Config/TypeDefinition.php +++ b/src/Config/TypeDefinition.php @@ -187,7 +187,7 @@ protected function callbackNormalization(NodeDefinition $node, string $new, stri ->validate() ->ifTrue(fn (array $v) => !empty($v[$new]) && !empty($v[$old])) ->thenInvalid(sprintf( - '"%s" and "%s" should not be use together in "%%s".', + '"%s" and "%s" should not be used together in "%%s".', $new, $old, )) From dfe4d29ad654db3455ad8764e5ba8659247ea5eb Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <mcg-web@users.noreply.github.com> Date: Thu, 27 May 2021 09:15:10 +0200 Subject: [PATCH 06/14] Improve syntax Co-authored-by: Timur Murtukov <murtukov@gmail.com> --- .../ExpressionFunction/Security/IsRememberMe.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/IsRememberMe.php b/src/ExpressionLanguage/ExpressionFunction/Security/IsRememberMe.php index ae22f7526..03afb8300 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/IsRememberMe.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/IsRememberMe.php @@ -14,7 +14,7 @@ public function __construct() { parent::__construct( 'isRememberMe', - fn () => "$this->gqlServices->get('".Security::class.'\')->isRememberMe()', + fn () => "$this->gqlServices->get('".Security::class."')->isRememberMe()", static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->isRememberMe() ); } From 55370a300b62184224a6d8a7cc78df6ed660c5c9 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <mcg-web@users.noreply.github.com> Date: Thu, 27 May 2021 09:15:27 +0200 Subject: [PATCH 07/14] Improve syntax Co-authored-by: Timur Murtukov <murtukov@gmail.com> --- .../ExpressionFunction/Security/IsAnonymous.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/IsAnonymous.php b/src/ExpressionLanguage/ExpressionFunction/Security/IsAnonymous.php index 0a7620237..26333605b 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/IsAnonymous.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/IsAnonymous.php @@ -14,7 +14,7 @@ public function __construct() { parent::__construct( 'isAnonymous', - fn () => "$this->gqlServices->get('".Security::class.'\')->isAnonymous()', + fn () => "$this->gqlServices->get('".Security::class."')->isAnonymous()", static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->isAnonymous() ); } From dfffaf6be4f90d0ec2954595c2ed760b70806272 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <mcg-web@users.noreply.github.com> Date: Thu, 27 May 2021 09:15:35 +0200 Subject: [PATCH 08/14] Improve syntax Co-authored-by: Timur Murtukov <murtukov@gmail.com> --- src/ExpressionLanguage/ExpressionFunction/Security/GetUser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExpressionLanguage/ExpressionFunction/Security/GetUser.php b/src/ExpressionLanguage/ExpressionFunction/Security/GetUser.php index 951580dda..9c53bc3f3 100644 --- a/src/ExpressionLanguage/ExpressionFunction/Security/GetUser.php +++ b/src/ExpressionLanguage/ExpressionFunction/Security/GetUser.php @@ -14,7 +14,7 @@ public function __construct() { parent::__construct( 'getUser', - fn () => "$this->gqlServices->get('".Security::class.'\')->getUser()', + fn () => "$this->gqlServices->get('".Security::class."')->getUser()", static fn (array $arguments) => $arguments[TypeGenerator::GRAPHQL_SERVICES]->get(Security::class)->getUser() ); } From 2f969dd570ba44b38c6b860fa4a0bed9de70ec82 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <jeremiah@overblog.com> Date: Wed, 22 Dec 2021 11:15:44 +0100 Subject: [PATCH 09/14] Fix after rebase --- .../Compiler/IdentifyCallbackServiceIdsPass.php | 2 +- src/Generator/TypeBuilder.php | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php b/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php index dfa04066d..45c4fe431 100644 --- a/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php +++ b/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php @@ -71,7 +71,7 @@ private function resolveServiceIdAndMethod(ContainerBuilder $container, ?array & $alias = $container->getAlias($id); $id = (string) $alias; $definition = $container->getDefinition($id); - } catch (ServiceNotFoundException | InvalidArgumentException $e) { + } catch (ServiceNotFoundException|InvalidArgumentException $e) { if ($throw) { throw $e; } diff --git a/src/Generator/TypeBuilder.php b/src/Generator/TypeBuilder.php index 7b683988d..523a51928 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -31,6 +31,7 @@ use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage as EL; use Overblog\GraphQLBundle\Generator\Config\Arg; use Overblog\GraphQLBundle\Generator\Config\Callback; +use Overblog\GraphQLBundle\Generator\Config\Config as GeneratorConfig; use Overblog\GraphQLBundle\Generator\Config\Field; use Overblog\GraphQLBundle\Generator\Config\Validation; use Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter; @@ -79,7 +80,7 @@ final class TypeBuilder private ExpressionConverter $expressionConverter; private PhpFile $file; private string $namespace; - private Config\Config $config; + private GeneratorConfig $config; private string $type; private string $currentField; private string $gqlServices = '$'.TypeGenerator::GRAPHQL_SERVICES; @@ -114,7 +115,7 @@ public function __construct(ExpressionConverter $expressionConverter, string $na public function build(array $config, string $type): PhpFile { // This values should be accessible from every method - $this->config = new Config\Config($config); + $this->config = new GeneratorConfig($config); $this->type = $type; $this->file = PhpFile::new()->setNamespace($this->namespace); @@ -439,7 +440,7 @@ private function buildScalarCallback($callback, string $fieldName) * * @throws GeneratorException */ - private function buildResolver(Callback $resolver, ?array $groups = null): ?GeneratorInterface + private function buildResolver(Callback $resolver, ?array $groups = null): GeneratorInterface { // TODO: before creating an input validator, check if any validation rules are defined return $this->buildCallback( @@ -874,7 +875,7 @@ private function buildTypeResolver(Callback $typeResolver): GeneratorInterface return $this->buildCallback($typeResolver, ['value', 'context', 'info']); } - protected function buildCallback(Callback $callback, array $argNames, ?callable $expressionBuilder = null): GeneratorInterface + private function buildCallback(Callback $callback, array $argNames, ?callable $expressionBuilder = null): GeneratorInterface { if (null !== $callback->expression) { if (null === $expressionBuilder) { From 19c9ac707aef4dd04fac84b78035f97af4099424 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <jeremiah@overblog.com> Date: Fri, 24 Dec 2021 21:22:24 +0100 Subject: [PATCH 10/14] Fix validator tests --- src/Generator/TypeBuilder.php | 7 +++---- .../Validator/InputValidatorTest.php | 20 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Generator/TypeBuilder.php b/src/Generator/TypeBuilder.php index 523a51928..18b1983c3 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -49,7 +49,6 @@ use function reset; use function rtrim; use function strtolower; -use function substr; /** * Service that exposes a single method `build` called for each GraphQL @@ -531,7 +530,7 @@ private function buildValidationRules(Validation $validationConfig): GeneratorIn $array = Collection::assoc(); if (null !== $validationConfig->link) { - if (str_contains($validationConfig->link, '::')) { + if (!str_contains($validationConfig->link, '::')) { // e.g. App\Entity\Droid $array->addItem('link', $validationConfig->link); } else { @@ -911,9 +910,9 @@ private function normalizeLink(string $link): array { [$fqcn, $classMember] = explode('::', $link); - if ('$' === $classMember[0]) { + if (str_starts_with($classMember, '$')) { return [$fqcn, ltrim($classMember, '$'), 'property']; - } elseif (')' === substr($classMember, -1)) { + } elseif (str_ends_with($classMember, ')')) { return [$fqcn, rtrim($classMember, '()'), 'getter']; } else { return [$fqcn, $classMember, 'member']; diff --git a/tests/Functional/Validator/InputValidatorTest.php b/tests/Functional/Validator/InputValidatorTest.php index 4af2d1ca1..58cbcd742 100644 --- a/tests/Functional/Validator/InputValidatorTest.php +++ b/tests/Functional/Validator/InputValidatorTest.php @@ -30,7 +30,7 @@ public function testNoValidation(): void $result = $this->executeGraphQLRequest($query); - $this->assertTrue(empty($result['errors'])); + $this->assertArrayNotHasKey('errors', $result); $this->assertTrue($result['data']['noValidation']); } @@ -47,7 +47,7 @@ public function testSimpleValidationPasses(): void $result = $this->executeGraphQLRequest($query); - $this->assertTrue(empty($result['errors'])); + $this->assertArrayNotHasKey('errors', $result); $this->assertTrue($result['data']['simpleValidation']); } @@ -82,7 +82,7 @@ public function testLinkedConstraintsValidationPasses(): void $result = $this->executeGraphQLRequest($query); - $this->assertTrue(empty($result['errors'])); + $this->assertArrayNotHasKey('errors', $result); $this->assertTrue($result['data']['linkedConstraintsValidation']); } @@ -128,7 +128,7 @@ public function testCollectionValidationPasses(): void $result = $this->executeGraphQLRequest($query); - $this->assertTrue(empty($result['errors'])); + $this->assertArrayNotHasKey('errors', $result); $this->assertTrue($result['data']['collectionValidation']); } @@ -189,7 +189,7 @@ public function testCascadeValidationWithGroupsPasses(): void $result = $this->executeGraphQLRequest($query); - $this->assertTrue(empty($result['errors'])); + $this->assertArrayNotHasKey('errors', $result); $this->assertTrue($result['data']['cascadeValidationWithGroups']); } @@ -249,7 +249,7 @@ public function testExpressionVariablesAccessible(): void $result = $this->executeGraphQLRequest($query); - $this->assertTrue(empty($result['errors'])); + $this->assertArrayNotHasKey('errors', $result); $this->assertTrue($result['data']['expressionVariablesValidation']); } @@ -266,7 +266,7 @@ public function testAutoValidationAutoThrowPasses(): void $result = $this->executeGraphQLRequest($query); - $this->assertTrue(empty($result['errors'])); + $this->assertArrayNotHasKey('errors', $result); $this->assertTrue($result['data']['autoValidationAutoThrow']); } @@ -296,7 +296,7 @@ public function testAutoValidationNoThrowNoErrors(): void $query = 'mutation { autoValidationNoThrow(username: "Andrew") }'; $result = $this->executeGraphQLRequest($query); - $this->assertTrue(empty($result['errors'])); + $this->assertArrayNotHasKey('errors', $result); $this->assertTrue(false === $result['data']['autoValidationNoThrow']); } @@ -309,7 +309,7 @@ public function testAutoValidationNoThrowHasErrors(): void $query = 'mutation { autoValidationNoThrow(username: "Tim") }'; $result = $this->executeGraphQLRequest($query); - $this->assertTrue(empty($result['errors'])); + $this->assertArrayNotHasKey('errors', $result); $this->assertTrue(true === $result['data']['autoValidationNoThrow']); } @@ -341,7 +341,7 @@ public function testAutoValidationAutoThrowWithGroupsPasses(): void $result = $this->executeGraphQLRequest($query); - $this->assertTrue(empty($result['errors'])); + $this->assertArrayNotHasKey('errors', $result); $this->assertTrue($result['data']['autoValidationAutoThrowWithGroups']); } From 2c5097723b8264ca06f99a80d91ae1dc2fae8387 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <jeremiah@overblog.com> Date: Fri, 24 Dec 2021 21:42:22 +0100 Subject: [PATCH 11/14] Fix code coverage --- src/Config/TypeDefinition.php | 9 --------- src/Generator/TypeBuilder.php | 7 ++----- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Config/TypeDefinition.php b/src/Config/TypeDefinition.php index 38e4e0219..17632f7c8 100644 --- a/src/Config/TypeDefinition.php +++ b/src/Config/TypeDefinition.php @@ -215,15 +215,6 @@ protected function callbackSection(string $name, string $info): ArrayNodeDefinit ->ifTrue(fn ($options) => is_string($options) && !ExpressionLanguage::stringHasTrigger($options)) ->then(fn ($options) => ['method' => $options]) ->end() - ->beforeNormalization() - // clean expression - ->ifTrue(fn ($options) => isset($options['expression']) && is_string($options['expression']) && ExpressionLanguage::stringHasTrigger($options['expression'])) - ->then(function ($options) { - $options['expression'] = ExpressionLanguage::unprefixExpression($options['expression']); - - return $options; - }) - ->end() ->children() ->scalarNode('method')->end() ->scalarNode('expression')->end() diff --git a/src/Generator/TypeBuilder.php b/src/Generator/TypeBuilder.php index 18b1983c3..243e7517b 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -887,11 +887,8 @@ private function buildCallback(Callback $callback, array $argNames, ?callable $e } } elseif (null !== $callback->id) { $fn = "$this->gqlServices->get('$callback->id')"; - if ($callback->method) { - return Collection::numeric([$fn, $callback->method]); - } else { - return Literal::new($fn); - } + + return Collection::numeric([$fn, $callback->method ?? '__invoke']); } else { return Literal::new("'$callback->method'"); } From 70103a2e03d46c96c0e9a1129a29db1055b73109 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <jeremiah@overblog.com> Date: Wed, 29 Dec 2021 22:35:04 +0100 Subject: [PATCH 12/14] Add Lazy behavior when using resolver as a service --- src/Generator/TypeBuilder.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Generator/TypeBuilder.php b/src/Generator/TypeBuilder.php index 243e7517b..5f73542ff 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -886,9 +886,14 @@ private function buildCallback(Callback $callback, array $argNames, ?callable $e return $expressionBuilder($callback->expression); } } elseif (null !== $callback->id) { - $fn = "$this->gqlServices->get('$callback->id')"; + $fnExpression = "$this->gqlServices->get('$callback->id')"; + if (null !== $callback->method) { + $fnExpression = "[$fnExpression, '$callback->method']"; + } - return Collection::numeric([$fn, $callback->method ?? '__invoke']); + return ArrowFunction::new() + ->addArguments(...$argNames) + ->setExpression(Literal::new("($fnExpression)(...\\func_get_args())")); } else { return Literal::new("'$callback->method'"); } From 5e82520d473aea7955eb8495942a95f2ae9d5878 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <jeremiah@overblog.com> Date: Sun, 2 Jan 2022 22:43:26 +0100 Subject: [PATCH 13/14] wip --- src/Config/TypeDefinition.php | 13 +++--- .../IdentifyCallbackServiceIdsPass.php | 23 ++-------- src/Generator/Config/Callback.php | 3 +- src/Generator/TypeBuilder.php | 37 +++++++++++---- .../connection/mapping/connection.types.yaml | 6 +-- .../Controller/GraphControllerTest.php | 46 ++++++++----------- 6 files changed, 61 insertions(+), 67 deletions(-) diff --git a/src/Config/TypeDefinition.php b/src/Config/TypeDefinition.php index 17632f7c8..4ec23ffa3 100644 --- a/src/Config/TypeDefinition.php +++ b/src/Config/TypeDefinition.php @@ -161,9 +161,9 @@ protected function callbackNormalization(NodeDefinition $node, string $new, stri ->then(function ($options) use ($old, $new) { if (is_callable($options[$old])) { if (is_array($options[$old])) { - $options[$new]['method'] = implode('::', $options[$old]); + $options[$new]['function'] = implode('::', $options[$old]); } else { - $options[$new]['method'] = $options[$old]; + $options[$new]['function'] = $options[$old]; } } elseif (is_string($options[$old])) { $options[$new]['expression'] = ExpressionLanguage::stringHasTrigger($options[$old]) ? @@ -203,8 +203,8 @@ protected function callbackSection(string $name, string $info): ArrayNodeDefinit $node ->info($info) ->validate() - ->ifTrue(fn (array $v) => !empty($v['method']) && !empty($v['expression'])) - ->thenInvalid('"method" and "expression" should not be use together.') + ->ifTrue(fn (array $v) => !empty($v['function']) && !empty($v['expression'])) + ->thenInvalid('"function" and "expression" should not be use together.') ->end() ->beforeNormalization() // Allow short syntax @@ -213,12 +213,11 @@ protected function callbackSection(string $name, string $info): ArrayNodeDefinit ->end() ->beforeNormalization() ->ifTrue(fn ($options) => is_string($options) && !ExpressionLanguage::stringHasTrigger($options)) - ->then(fn ($options) => ['method' => $options]) + ->then(fn ($options) => ['function' => $options]) ->end() ->children() - ->scalarNode('method')->end() + ->scalarNode('function')->end() ->scalarNode('expression')->end() - ->scalarNode('id')->end() ->end() ; diff --git a/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php b/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php index 45c4fe431..6330872ee 100644 --- a/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php +++ b/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php @@ -48,20 +48,10 @@ public function process(ContainerBuilder $container): void private function resolveServiceIdAndMethod(ContainerBuilder $container, ?array &$callback): void { - if (!isset($callback['id']) && !isset($callback['method'])) { + if (!isset($callback['function'])) { return; } - $originalId = $callback['id'] ?? null; - $originalMethod = $callback['method'] ?? null; - - if (null === $originalId) { - [$id, $method] = explode('::', $originalMethod, 2) + [null, null]; - $throw = false; - } else { - $id = $originalId; - $method = $originalMethod; - $throw = true; - } + [$id, $method] = explode('::', $callback['function'], 2) + [null, null]; try { $definition = $container->getDefinition($id); @@ -72,12 +62,6 @@ private function resolveServiceIdAndMethod(ContainerBuilder $container, ?array & $id = (string) $alias; $definition = $container->getDefinition($id); } catch (ServiceNotFoundException|InvalidArgumentException $e) { - if ($throw) { - throw $e; - } - $callback['id'] = null; - $callback['method'] = $originalMethod; - return; } } @@ -88,7 +72,6 @@ private function resolveServiceIdAndMethod(ContainerBuilder $container, ?array & $definition->addTag('overblog_graphql.service'); } - $callback['id'] = $id; - $callback['method'] = $method; + $callback['function'] = "$id::$method"; } } diff --git a/src/Generator/Config/Callback.php b/src/Generator/Config/Callback.php index a032b8224..e813d637b 100644 --- a/src/Generator/Config/Callback.php +++ b/src/Generator/Config/Callback.php @@ -6,7 +6,6 @@ final class Callback extends AbstractConfig { - public ?string $method = null; + public ?string $function = null; public ?string $expression = null; - public ?string $id = null; } diff --git a/src/Generator/TypeBuilder.php b/src/Generator/TypeBuilder.php index 5f73542ff..eaab2e435 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -17,6 +17,7 @@ use Murtukov\PHPCodeGenerator\Config; use Murtukov\PHPCodeGenerator\ConverterInterface; use Murtukov\PHPCodeGenerator\GeneratorInterface; +use Murtukov\PHPCodeGenerator\IfElse; use Murtukov\PHPCodeGenerator\Instance; use Murtukov\PHPCodeGenerator\Literal; use Murtukov\PHPCodeGenerator\PhpFile; @@ -885,17 +886,37 @@ private function buildCallback(Callback $callback, array $argNames, ?callable $e } else { return $expressionBuilder($callback->expression); } - } elseif (null !== $callback->id) { - $fnExpression = "$this->gqlServices->get('$callback->id')"; - if (null !== $callback->method) { - $fnExpression = "[$fnExpression, '$callback->method']"; + } else { + if (str_contains($callback->function, '::')) { + $function = explode('::', $callback->function, 2); + $isArray = true; + } else { + $function = $callback->function; + $isArray = false; } - return ArrowFunction::new() + $resolverExpression = IfElse::new("$this->gqlServices->has('".($isArray ? $function[0] : $function)."')"); + if ($isArray) { + $resolverExpression + ->append('$resolver = ', "[$this->gqlServices->get('$function[0]'), '$function[1]']") + ->createElse() + ->append('$resolver = ', "'$callback->function'") + ->end() + ; + } else { + $resolverExpression + ->append('$resolver = ', "$this->gqlServices->get('$function')") + ->createElse() + ->append('$resolver = ', "'$function'") + ->end() + ; + } + + return Closure::new() ->addArguments(...$argNames) - ->setExpression(Literal::new("($fnExpression)(...\\func_get_args())")); - } else { - return Literal::new("'$callback->method'"); + ->bindVar(TypeGenerator::GRAPHQL_SERVICES) + ->append($resolverExpression) + ->append('return $resolver(...\\func_get_args())'); } } diff --git a/tests/Functional/App/config/connection/mapping/connection.types.yaml b/tests/Functional/App/config/connection/mapping/connection.types.yaml index 7e2d4fea2..a9fc05ea8 100644 --- a/tests/Functional/App/config/connection/mapping/connection.types.yaml +++ b/tests/Functional/App/config/connection/mapping/connection.types.yaml @@ -17,13 +17,11 @@ User: type: friendConnection argsBuilder: "Relay::Connection" resolver: - method: 'overblog_graphql.test.resolver.node::friendsResolver' + function: 'overblog_graphql.test.resolver.node::friendsResolver' friendsForward: type: userConnection argsBuilder: "Relay::ForwardConnection" - resolver: - id: 'overblog_graphql.test.resolver.node' - method: 'friendsResolver' + resolver: 'overblog_graphql.test.resolver.node::friendsResolver' friendsBackward: type: userConnection argsBuilder: "Relay::BackwardConnection" diff --git a/tests/Functional/Controller/GraphControllerTest.php b/tests/Functional/Controller/GraphControllerTest.php index 5c06d6d6a..117d379bf 100644 --- a/tests/Functional/Controller/GraphControllerTest.php +++ b/tests/Functional/Controller/GraphControllerTest.php @@ -66,7 +66,7 @@ final class GraphControllerTest extends TestCase */ public function testEndpointAction(string $uri): void { - $client = static::createClient(['test_case' => 'connectionWithCORS']); + $client = self::createClient(['test_case' => 'connectionWithCORS']); $this->disableCatchExceptions($client); $client->request('GET', $uri, ['query' => $this->friendsQuery], [], ['CONTENT_TYPE' => 'application/graphql;charset=utf8', 'HTTP_Origin' => 'http://example.com']); @@ -87,7 +87,7 @@ public function testEndpointWithEmptyQuery(): void { $this->expectException(BadRequestHttpException::class); $this->expectExceptionMessage('Must provide query parameter'); - $client = static::createClient(); + $client = self::createClient(); $this->disableCatchExceptions($client); $client->request('GET', '/', []); $client->getResponse()->getContent(); @@ -97,14 +97,14 @@ public function testEndpointWithEmptyPostJsonBodyQuery(): void { $this->expectException(BadRequestHttpException::class); $this->expectExceptionMessage('The request content body must not be empty when using json content type request.'); - $client = static::createClient(); + $client = self::createClient(); $this->disableCatchExceptions($client); $client->request('POST', '/', [], [], ['CONTENT_TYPE' => 'application/json']); } public function testEndpointWithJsonContentTypeAndGetQuery(): void { - $client = static::createClient(['test_case' => 'connectionWithCORS']); + $client = self::createClient(['test_case' => 'connectionWithCORS']); $this->disableCatchExceptions($client); $client->request('GET', '/', ['query' => $this->friendsQuery], [], ['CONTENT_TYPE' => 'application/json']); $result = $client->getResponse()->getContent(); @@ -115,7 +115,7 @@ public function testEndpointWithInvalidBodyQuery(): void { $this->expectException(BadRequestHttpException::class); $this->expectExceptionMessage('POST body sent invalid JSON'); - $client = static::createClient(); + $client = self::createClient(); $this->disableCatchExceptions($client); $client->request('GET', '/', [], [], ['CONTENT_TYPE' => 'application/json'], '{'); $client->getResponse()->getContent(); @@ -123,7 +123,7 @@ public function testEndpointWithInvalidBodyQuery(): void public function testEndpointActionWithVariables(): void { - $client = static::createClient(['test_case' => 'connection']); + $client = self::createClient(['test_case' => 'connection']); $this->disableCatchExceptions($client); $query = <<<'EOF' @@ -151,7 +151,7 @@ public function testEndpointActionWithInvalidVariables(): void { $this->expectException(BadRequestHttpException::class); $this->expectExceptionMessage('Variables are invalid JSON'); - $client = static::createClient(['test_case' => 'connection']); + $client = self::createClient(['test_case' => 'connection']); $this->disableCatchExceptions($client); $query = <<<'EOF' @@ -167,7 +167,7 @@ public function testMultipleEndpointActionWithUnknownSchemaName(): void { $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('Could not find "fake" schema.'); - $client = static::createClient(['test_case' => 'connection']); + $client = self::createClient(['test_case' => 'connection']); $this->disableCatchExceptions($client); $query = <<<'EOF' @@ -181,7 +181,7 @@ public function testMultipleEndpointActionWithUnknownSchemaName(): void public function testEndpointActionWithOperationName(): void { - $client = static::createClient(['test_case' => 'connection']); + $client = self::createClient(['test_case' => 'connection']); $this->disableCatchExceptions($client); $query = $this->friendsQuery."\n".$this->friendsTotalCountQuery; @@ -196,7 +196,7 @@ public function testEndpointActionWithOperationName(): void */ public function testBatchEndpointAction(string $uri): void { - $client = static::createClient(['test_case' => 'connection']); + $client = self::createClient(['test_case' => 'connection']); $this->disableCatchExceptions($client); $data = [ @@ -233,7 +233,7 @@ public function testBatchEndpointWithEmptyQuery(): void { $this->expectException(BadRequestHttpException::class); $this->expectExceptionMessage('Must provide at least one valid query.'); - $client = static::createClient(); + $client = self::createClient(); $this->disableCatchExceptions($client); $client->request('GET', '/batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{}'); $client->getResponse()->getContent(); @@ -243,7 +243,7 @@ public function testBatchEndpointWrongContentType(): void { $this->expectException(BadRequestHttpException::class); $this->expectExceptionMessage('Batching parser only accepts "application/json" or "multipart/form-data" content-type but got "".'); - $client = static::createClient(); + $client = self::createClient(); $this->disableCatchExceptions($client); $client->request('GET', '/batch'); $client->getResponse()->getContent(); @@ -253,7 +253,7 @@ public function testBatchEndpointWithInvalidJson(): void { $this->expectException(BadRequestHttpException::class); $this->expectExceptionMessage('POST body sent invalid JSON'); - $client = static::createClient(); + $client = self::createClient(); $this->disableCatchExceptions($client); $client->request('GET', '/batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{'); $client->getResponse()->getContent(); @@ -263,7 +263,7 @@ public function testBatchEndpointWithInvalidQuery(): void { $this->expectException(BadRequestHttpException::class); $this->expectExceptionMessage('1 is not a valid query'); - $client = static::createClient(); + $client = self::createClient(); $this->disableCatchExceptions($client); $client->request('GET', '/batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{"test" : {"query": 1}}'); $client->getResponse()->getContent(); @@ -271,7 +271,7 @@ public function testBatchEndpointWithInvalidQuery(): void public function testPreflightedRequestWhenDisabled(): void { - $client = static::createClient(['test_case' => 'connection']); + $client = self::createClient(['test_case' => 'connection']); $this->disableCatchExceptions($client); $client->request('OPTIONS', '/', [], [], ['HTTP_Origin' => 'http://example.com']); $response = $client->getResponse(); @@ -281,7 +281,7 @@ public function testPreflightedRequestWhenDisabled(): void public function testUnAuthorizedMethod(): void { - $client = static::createClient(['test_case' => 'connection']); + $client = self::createClient(['test_case' => 'connection']); $this->disableCatchExceptions($client); $client->request('PUT', '/', [], [], ['HTTP_Origin' => 'http://example.com']); $this->assertSame(405, $client->getResponse()->getStatusCode()); @@ -289,7 +289,7 @@ public function testUnAuthorizedMethod(): void public function testPreflightedRequestWhenEnabled(): void { - $client = static::createClient(['test_case' => 'connectionWithCORS']); + $client = self::createClient(['test_case' => 'connectionWithCORS']); $this->disableCatchExceptions($client); $client->request('OPTIONS', '/batch', [], [], ['HTTP_Origin' => 'http://example.com']); $this->assertCORSHeadersExists($client); @@ -297,7 +297,7 @@ public function testPreflightedRequestWhenEnabled(): void public function testNoCORSHeadersIfOriginHeaderNotExists(): void { - $client = static::createClient(['test_case' => 'connectionWithCORS']); + $client = self::createClient(['test_case' => 'connectionWithCORS']); $this->disableCatchExceptions($client); $client->request('GET', '/', ['query' => $this->friendsQuery], [], ['CONTENT_TYPE' => 'application/graphql']); $result = $client->getResponse()->getContent(); @@ -305,10 +305,7 @@ public function testNoCORSHeadersIfOriginHeaderNotExists(): void $this->assertCORSHeadersNotExists($client); } - /** - * @param KernelBrowser $client - */ - private function assertCORSHeadersNotExists($client): void + private function assertCORSHeadersNotExists(KernelBrowser $client): void { $headers = $client->getResponse()->headers->all(); $this->assertArrayNotHasKey('access-control-allow-origin', $headers); @@ -318,10 +315,7 @@ private function assertCORSHeadersNotExists($client): void $this->assertArrayNotHasKey('access-control-max-age', $headers); } - /** - * @param KernelBrowser $client - */ - private function assertCORSHeadersExists($client): void + private function assertCORSHeadersExists(KernelBrowser $client): void { $response = $client->getResponse(); $this->assertSame(200, $response->getStatusCode()); From 1a9b1678d7f7dfe67fe39e85335e1ea41e70e6c5 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE <jeremiah@overblog.com> Date: Wed, 5 Jan 2022 16:17:48 +0100 Subject: [PATCH 14/14] Fix --- .../Compiler/IdentifyCallbackServiceIdsPass.php | 3 +++ .../App/config/connection/mapping/connection.types.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php b/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php index 6330872ee..e7645ab0b 100644 --- a/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php +++ b/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php @@ -52,6 +52,9 @@ private function resolveServiceIdAndMethod(ContainerBuilder $container, ?array & return; } [$id, $method] = explode('::', $callback['function'], 2) + [null, null]; + if (str_starts_with($id, '\\')) { + $id = ltrim($id, '\\'); + } try { $definition = $container->getDefinition($id); diff --git a/tests/Functional/App/config/connection/mapping/connection.types.yaml b/tests/Functional/App/config/connection/mapping/connection.types.yaml index a9fc05ea8..565e083d0 100644 --- a/tests/Functional/App/config/connection/mapping/connection.types.yaml +++ b/tests/Functional/App/config/connection/mapping/connection.types.yaml @@ -25,7 +25,7 @@ User: friendsBackward: type: userConnection argsBuilder: "Relay::BackwardConnection" - resolver: 'overblog_graphql.test.resolver.node::friendsResolver' + resolver: '\Overblog\GraphQLBundle\Tests\Functional\App\Resolver\ConnectionResolver::friendsResolver' friendConnection: type: relay-connection