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/Config/InterfaceTypeDefinition.php b/src/Config/InterfaceTypeDefinition.php index 42e6dffb1..fb2e5b83c 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->callbackNormalization($node, 'typeResolver', 'resolveType'); /** @phpstan-ignore-next-line */ $node ->children() ->append($this->nameSection()) ->append($this->outputFieldsSection()) - ->append($this->resolveTypeSection()) + ->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 8b4067bdc..8b83fcd5e 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->callbackNormalization($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->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 f02588750..4ec23ffa3 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,77 @@ protected function typeSection(bool $isRequired = false): ScalarNodeDefinition return $node; } + protected function callbackNormalization(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]['function'] = implode('::', $options[$old]); + } else { + $options[$new]['function'] = $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 used together in "%%s".', + $new, + $old, + )) + ->end() + ; + } + + protected function callbackSection(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['function']) && !empty($v['expression'])) + ->thenInvalid('"function" 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) => ['function' => $options]) + ->end() + ->children() + ->scalarNode('function')->end() + ->scalarNode('expression')->end() + ->end() + ; + + return $node; + } + /** * @return mixed * diff --git a/src/Config/TypeWithOutputFieldsDefinition.php b/src/Config/TypeWithOutputFieldsDefinition.php index 438f8da3f..d9dcd523e 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->callbackNormalization($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->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 47c9d32da..cde466f16 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->callbackNormalization($node, 'typeResolver', 'resolveType'); /** @phpstan-ignore-next-line */ $node @@ -24,7 +25,7 @@ public function getDefinition(): ArrayNodeDefinition ->isRequired() ->requiresAtLeastOneElement() ->end() - ->append($this->resolveTypeSection()) + ->append($this->callbackSection('typeResolver', 'GraphQL type resolver')) ->append($this->descriptionSection()) ->end(); 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..bfa6a3067 100644 --- a/src/DependencyInjection/Compiler/GraphQLServicesPass.php +++ b/src/DependencyInjection/Compiler/GraphQLServicesPass.php @@ -4,11 +4,10 @@ namespace Overblog\GraphQLBundle\DependencyInjection\Compiler; -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\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use function is_string; use function sprintf; @@ -27,27 +26,31 @@ 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); + $container->findDefinition(GraphQLServices::class)->addArgument(array_unique($locateableServices)); } } diff --git a/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php b/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php new file mode 100644 index 000000000..e7645ab0b --- /dev/null +++ b/src/DependencyInjection/Compiler/IdentifyCallbackServiceIdsPass.php @@ -0,0 +1,80 @@ +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['function'])) { + return; + } + [$id, $method] = explode('::', $callback['function'], 2) + [null, null]; + if (str_starts_with($id, '\\')) { + $id = ltrim($id, '\\'); + } + + 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) { + return; + } + } + if ( + !$definition->hasTag('overblog_graphql.service') + && !$definition->hasTag('overblog_graphql.global_variable') + ) { + $definition->addTag('overblog_graphql.service'); + } + + $callback['function'] = "$id::$method"; + } +} 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..9c53bc3f3 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..26333605b 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..03afb8300 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 */ public array $globalNames = []; - public function addGlobalName(string $index, string $name): void + /** @var array */ + 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 + */ + 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/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 @@ +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 @@ +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..e813d637b --- /dev/null +++ b/src/Generator/Config/Callback.php @@ -0,0 +1,11 @@ + '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 @@ + '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 @@ +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..eaab2e435 100644 --- a/src/Generator/TypeBuilder.php +++ b/src/Generator/TypeBuilder.php @@ -17,10 +17,12 @@ 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; 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; @@ -28,12 +30,16 @@ 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\Config as GeneratorConfig; +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; @@ -44,7 +50,6 @@ use function reset; use function rtrim; use function strtolower; -use function substr; /** * Service that exposes a single method `build` called for each GraphQL @@ -75,7 +80,7 @@ final class TypeBuilder private ExpressionConverter $expressionConverter; private PhpFile $file; private string $namespace; - private array $config; + private GeneratorConfig $config; private string $type; private string $currentField; private string $gqlServices = '$'.TypeGenerator::GRAPHQL_SERVICES; @@ -110,12 +115,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 GeneratorConfig($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 +132,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 +229,7 @@ private function wrapTypeRecursive($typeNode, bool &$isReference) * $services->getType('PostInterface'), * ... * ], - * 'resolveField' => {@see buildResolveField}, + * 'resolveField' => {@see buildResolver}, * ] * * Render example (input-object): @@ -248,7 +253,7 @@ private function wrapTypeRecursive($typeNode, bool &$isReference) * {@see buildField}, * ... * ], - * 'resolveType' => {@see buildResolveType}, + * 'resolveType' => {@see buildTypeResolver}, * ] * * Render example (union): @@ -260,7 +265,7 @@ private function wrapTypeRecursive($typeNode, bool &$isReference) * $services->getType('Photo'), * ... * ], - * 'resolveType' => {@see buildResolveType}, + * 'resolveType' => {@see buildTypeResolver}, * ] * * Render example (custom-scalar): @@ -290,69 +295,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 +390,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 +438,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)); + $closure->append('return ', $this->expressionConverter->convert($expression)); - return $closure; - } - - return ArrowFunction::new($resolve); + return $closure; + } + ); } /** @@ -498,14 +496,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 +524,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 +648,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 +730,30 @@ public function buildField(array $fieldConfig, string $fieldname) * ] * * - * @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; @@ -820,7 +787,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) ; } @@ -902,22 +869,55 @@ 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)); - } + private 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); + } + } else { + if (str_contains($callback->function, '::')) { + $function = explode('::', $callback->function, 2); + $isArray = true; + } else { + $function = $callback->function; + $isArray = false; + } - return $resolveType; + $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) + ->bindVar(TypeGenerator::GRAPHQL_SERVICES) + ->append($resolverExpression) + ->append('return $resolver(...\\func_get_args())'); + } } /** @@ -933,9 +933,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/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/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/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' => [ 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) { diff --git a/tests/Functional/App/config/connection/mapping/connection.types.yaml b/tests/Functional/App/config/connection/mapping/connection.types.yaml index ebb1b1db8..565e083d0 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,16 @@ User: friends: type: friendConnection argsBuilder: "Relay::Connection" - resolve: '@=query("friends", value, args)' + resolver: + function: '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\GraphQLBundle\Tests\Functional\App\Resolver\ConnectionResolver::friendsResolver' friendConnection: type: relay-connection @@ -38,7 +39,7 @@ friendConnection: connectionFields: totalCount: type: Int - resolve: '@=query("connection")' + resolver: '@=query("connection")' userConnection: 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' 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()); 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']); }