diff --git a/Command/DebugCommand.php b/Command/DebugCommand.php index 3c059ff95..07c53c1be 100644 --- a/Command/DebugCommand.php +++ b/Command/DebugCommand.php @@ -74,38 +74,34 @@ protected function execute(InputInterface $input, OutputInterface $output) /** @var FluentResolverInterface $resolver */ $resolver = $this->{$category.'Resolver'}; - - $solutions = $this->retrieveSolutions($resolver); - $this->renderTable($tableHeaders, $solutions, $io); + $this->renderTable($resolver, $tableHeaders, $io); } } - private function renderTable(array $tableHeaders, array $solutions, SymfonyStyle $io) + private function renderTable(FluentResolverInterface $resolver, array $tableHeaders, SymfonyStyle $io) { $tableRows = []; - foreach ($solutions as $id => &$options) { - ksort($options['aliases']); - $tableRows[] = [$id, implode("\n", $options['aliases'])]; + $solutionIDs = array_keys($resolver->getSolutions()); + sort($solutionIDs); + foreach ($solutionIDs as $solutionID) { + $aliases = $resolver->getSolutionAliases($solutionID); + $aliases[] = $solutionID; + $options = $resolver->getSolutionOptions($solutionID); + $tableRows[$options['id']] = [$options['id'], self::serializeAliases($aliases, $options)]; } + ksort($tableRows); $io->table($tableHeaders, $tableRows); $io->write("\n\n"); } - private function retrieveSolutions(FluentResolverInterface $resolver) + private static function serializeAliases(array $aliases, array $options) { - $data = []; - foreach ($resolver->getSolutions() as $alias => $solution) { - $options = $resolver->getSolutionOptions($alias); - - $id = $options['id']; - if (!isset($data[$id]['aliases'])) { - $data[$id]['aliases'] = []; - } - $data[$id]['aliases'][] = $options['alias'].(isset($options['method']) ? ' (method: '.$options['method'].')' : ''); - } - ksort($data); + ksort($aliases); + $aliases = array_map(function ($alias) use ($options) { + return $alias.(isset($options['method']) ? ' (method: '.$options['method'].')' : ''); + }, $aliases); - return $data; + return implode("\n", $aliases); } public static function getCategories() diff --git a/Config/Processor/NamedConfigProcessor.php b/Config/Processor/NamedConfigProcessor.php index 1e854cebd..121cab27b 100644 --- a/Config/Processor/NamedConfigProcessor.php +++ b/Config/Processor/NamedConfigProcessor.php @@ -10,8 +10,9 @@ final class NamedConfigProcessor implements ProcessorInterface public static function process(array $configs) { foreach ($configs as $name => &$config) { - $config['config'] = isset($config['config']) && is_array($config['config']) ? $config['config'] : []; - $config['config']['name'] = $name; + if (empty($config['config']['name'])) { + $config['config']['name'] = $name; + } } return $configs; diff --git a/Config/TypeDefinition.php b/Config/TypeDefinition.php index 4a8a5e097..6a977fd19 100644 --- a/Config/TypeDefinition.php +++ b/Config/TypeDefinition.php @@ -37,7 +37,7 @@ protected function nameSection() ->ifTrue(function ($name) { return !preg_match('/^[_a-z][_0-9a-z]*$/i', $name); }) - ->thenInvalid('Invalid type name "%s". (see https://facebook.github.io/graphql/#Name)') + ->thenInvalid('Invalid type name "%s". (see http://facebook.github.io/graphql/October2016/#Name)') ->end(); return $node; diff --git a/DependencyInjection/Compiler/ConfigTypesPass.php b/DependencyInjection/Compiler/ConfigTypesPass.php index 6f0b620ed..5e6a1f6f1 100644 --- a/DependencyInjection/Compiler/ConfigTypesPass.php +++ b/DependencyInjection/Compiler/ConfigTypesPass.php @@ -18,18 +18,16 @@ public function process(ContainerBuilder $container) ->compile(TypeGenerator::MODE_MAPPING_ONLY); foreach ($generatedClasses as $class => $file) { - $aliases = [preg_replace('/Type$/', '', substr(strrchr($class, '\\'), 1))]; - $this->setTypeServiceDefinition($container, $class, $aliases); + $alias = preg_replace('/Type$/', '', substr(strrchr($class, '\\'), 1)); + $this->setTypeServiceDefinition($container, $class, $alias); } } - private function setTypeServiceDefinition(ContainerBuilder $container, $class, array $aliases) + private function setTypeServiceDefinition(ContainerBuilder $container, $class, $alias) { $definition = $container->setDefinition($class, new Definition($class)); $definition->setPublic(false); $definition->setArguments([new Reference(ConfigProcessor::class), new Reference(GlobalVariables::class)]); - foreach ($aliases as $alias) { - $definition->addTag(TypeTaggedServiceMappingPass::TAG_NAME, ['alias' => $alias, 'generated' => true]); - } + $definition->addTag(TypeTaggedServiceMappingPass::TAG_NAME, ['alias' => $alias, 'generated' => true]); } } diff --git a/DependencyInjection/Compiler/TaggedServiceMappingPass.php b/DependencyInjection/Compiler/TaggedServiceMappingPass.php index ee47a440b..f2fd5e3c4 100644 --- a/DependencyInjection/Compiler/TaggedServiceMappingPass.php +++ b/DependencyInjection/Compiler/TaggedServiceMappingPass.php @@ -5,6 +5,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; abstract class TaggedServiceMappingPass implements CompilerPassInterface @@ -18,23 +19,22 @@ private function getTaggedServiceMapping(ContainerBuilder $container, $tagName) foreach ($taggedServices as $id => $tags) { $className = $container->findDefinition($id)->getClass(); - foreach ($tags as $tag) { - $this->checkRequirements($id, $tag); - $tag = array_merge($tag, ['id' => $id]); - if (!$isType) { - $tag['method'] = isset($tag['method']) ? $tag['method'] : '__invoke'; + foreach ($tags as $attributes) { + $this->checkRequirements($id, $attributes); + $attributes = self::resolveAttributes($attributes, $id, !$isType); + $solutionID = $className; + + if (!$isType && '__invoke' !== $attributes['method']) { + $solutionID = sprintf('%s::%s', $className, $attributes['method']); } - if (isset($tag['alias'])) { - $serviceMapping[$tag['alias']] = $tag; + + if (!isset($serviceMapping[$solutionID])) { + $serviceMapping[$solutionID] = $attributes; } - // add FQCN alias - $alias = $className; - if (!$isType && '__invoke' !== $tag['method']) { - $alias .= '::'.$tag['method']; + if (isset($attributes['alias']) && $solutionID !== $attributes['alias']) { + $serviceMapping[$solutionID]['aliases'][] = $attributes['alias']; } - $tag['alias'] = $alias; - $serviceMapping[$tag['alias']] = $tag; } } @@ -46,35 +46,19 @@ public function process(ContainerBuilder $container) $mapping = $this->getTaggedServiceMapping($container, $this->getTagName()); $resolverDefinition = $container->findDefinition($this->getResolverServiceID()); - foreach ($mapping as $name => $options) { - $cleanOptions = $options; - $solutionID = $options['id']; + foreach ($mapping as $solutionID => $attributes) { + $attributes['aliases'] = array_unique($attributes['aliases']); + $aliases = $attributes['aliases']; + $serviceID = $attributes['id']; - $solutionDefinition = $container->findDefinition($solutionID); + $solutionDefinition = $container->findDefinition($serviceID); // make solution service public to improve lazy loading $solutionDefinition->setPublic(true); - - $methods = array_map( - function ($methodCall) { - return $methodCall[0]; - }, - $solutionDefinition->getMethodCalls() - ); - if ( - empty($options['generated']) // false is consider as empty - && is_subclass_of($solutionDefinition->getClass(), ContainerAwareInterface::class) - && !in_array('setContainer', $methods) - ) { - @trigger_error( - 'Autowire custom tagged (type, resolver or mutation) services is deprecated as of 0.9 and will be removed in 1.0. Use AutoMapping or set it manually instead.', - E_USER_DEPRECATED - ); - $solutionDefinition->addMethodCall('setContainer', [new Reference('service_container')]); - } + $this->autowireSolutionImplementingContainerAwareInterface($solutionDefinition, empty($attributes['generated'])); $resolverDefinition->addMethodCall( 'addSolution', - [$name, [new Reference('service_container'), 'get'], [$solutionID], $cleanOptions] + [$solutionID, [[new Reference('service_container'), 'get'], [$serviceID]], $aliases, $attributes] ); } } @@ -88,6 +72,52 @@ protected function checkRequirements($id, array $tag) } } + /** + * @param array $attributes + * @param string $id + * @param bool $withMethod + * + * @return array + */ + private static function resolveAttributes(array $attributes, $id, $withMethod) + { + $default = ['id' => $id, 'aliases' => []]; + if ($withMethod) { + $default['method'] = '__invoke'; + } + $attributes = array_replace($default, $attributes); + + return $attributes; + } + + /** + * @param Definition $solutionDefinition + * @param bool $isGenerated + */ + private function autowireSolutionImplementingContainerAwareInterface(Definition $solutionDefinition, $isGenerated) + { + $methods = array_map( + function ($methodCall) { + return $methodCall[0]; + }, + $solutionDefinition->getMethodCalls() + ); + if ( + $isGenerated + && is_subclass_of($solutionDefinition->getClass(), ContainerAwareInterface::class) + && !in_array('setContainer', $methods) + ) { + @trigger_error( + sprintf( + 'Autowire method "%s::setContainer" for custom tagged (type, resolver or mutation) services is deprecated as of 0.9 and will be removed in 1.0.', + ContainerAwareInterface::class + ), + E_USER_DEPRECATED + ); + $solutionDefinition->addMethodCall('setContainer', [new Reference('service_container')]); + } + } + abstract protected function getTagName(); /** diff --git a/Resolver/AbstractResolver.php b/Resolver/AbstractResolver.php index 5019c69eb..1e11281e7 100644 --- a/Resolver/AbstractResolver.php +++ b/Resolver/AbstractResolver.php @@ -7,39 +7,51 @@ abstract class AbstractResolver implements FluentResolverInterface /** @var array */ private $solutions = []; + private $aliases = []; + /** @var array */ private $solutionOptions = []; /** @var array */ private $fullyLoadedSolutions = []; - public function addSolution($name, callable $solutionFunc, array $solutionFuncArgs = [], array $options = []) + public function addSolution($id, $solutionOrFactory, array $aliases = [], array $options = []) { - $this->fullyLoadedSolutions[$name] = false; - $this->solutions[$name] = function () use ($name, $solutionFunc, $solutionFuncArgs) { - $solution = call_user_func_array($solutionFunc, $solutionFuncArgs); - $this->checkSolution($name, $solution); + $this->fullyLoadedSolutions[$id] = false; + $this->addAliases($id, $aliases); + + $this->solutions[$id] = function () use ($id, $solutionOrFactory) { + $solution = $solutionOrFactory; + if (self::isSolutionFactory($solutionOrFactory)) { + if (!isset($solutionOrFactory[1])) { + $solutionOrFactory[1] = []; + } + $solution = call_user_func_array(...$solutionOrFactory); + } + $this->checkSolution($id, $solution); return $solution; }; - $this->solutionOptions[$name] = $options; + $this->solutionOptions[$id] = $options; return $this; } - public function hasSolution($name) + public function hasSolution($id) { - return isset($this->solutions[$name]); + $id = $this->resolveAlias($id); + + return isset($this->solutions[$id]); } /** - * @param $name + * @param $id * * @return mixed */ - public function getSolution($name) + public function getSolution($id) { - return $this->loadSolution($name); + return $this->loadSolution($id); } /** @@ -50,38 +62,63 @@ public function getSolutions() return $this->loadSolutions(); } + public function getSolutionAliases($id) + { + return array_keys($this->aliases, $id); + } + /** - * @param $name + * @param $id * * @return mixed */ - public function getSolutionOptions($name) + public function getSolutionOptions($id) { - return isset($this->solutionOptions[$name]) ? $this->solutionOptions[$name] : []; + $id = $this->resolveAlias($id); + + return isset($this->solutionOptions[$id]) ? $this->solutionOptions[$id] : []; } /** - * @param string $name + * @param string $id * * @return mixed */ - private function loadSolution($name) + private function loadSolution($id) { - if (!$this->hasSolution($name)) { + $id = $this->resolveAlias($id); + if (!$this->hasSolution($id)) { return null; } - if ($this->fullyLoadedSolutions[$name]) { - return $this->solutions[$name]; + if ($this->fullyLoadedSolutions[$id]) { + return $this->solutions[$id]; } else { - $loader = $this->solutions[$name]; - $this->solutions[$name] = $loader(); - $this->fullyLoadedSolutions[$name] = true; + $loader = $this->solutions[$id]; + $this->solutions[$id] = $loader(); + $this->fullyLoadedSolutions[$id] = true; + + return $this->solutions[$id]; + } + } - return $this->solutions[$name]; + private function addAliases($id, $aliases) + { + foreach ($aliases as $alias) { + $this->aliases[$alias] = $id; } } + private static function isSolutionFactory($solutionOrFactory) + { + return is_array($solutionOrFactory) && isset($solutionOrFactory[0]) && is_callable($solutionOrFactory[0]); + } + + private function resolveAlias($alias) + { + return isset($this->aliases[$alias]) ? $this->aliases[$alias] : $alias; + } + /** * @return mixed[] */ @@ -106,11 +143,11 @@ protected function supportsSolution($solution) return null === $supportedClass || $solution instanceof $supportedClass; } - protected function checkSolution($name, $solution) + protected function checkSolution($id, $solution) { if (!$this->supportsSolution($solution)) { throw new UnsupportedResolverException( - sprintf('Resolver "%s" must be "%s" "%s" given.', $name, $this->supportedSolutionClass(), get_class($solution)) + sprintf('Resolver "%s" must be "%s" "%s" given.', $id, $this->supportedSolutionClass(), get_class($solution)) ); } } @@ -122,6 +159,6 @@ protected function checkSolution($name, $solution) */ protected function supportedSolutionClass() { - return; + return null; } } diff --git a/Resolver/FluentResolverInterface.php b/Resolver/FluentResolverInterface.php index 958576ecb..53c643a00 100644 --- a/Resolver/FluentResolverInterface.php +++ b/Resolver/FluentResolverInterface.php @@ -6,11 +6,23 @@ interface FluentResolverInterface { public function resolve($input); - public function addSolution($name, callable $solutionFunc, array $solutionFuncArgs = [], array $options = []); + /** + * Add a solution to resolver. + * + * @param string $id the solution identifier + * @param array|mixed $solutionOrFactory the solution itself or array with a factory and it arguments if needed [$factory] or [$factory, $factoryArgs] + * @param string[] $aliases the solution aliases + * @param array $options extra options + * + * @return $this + */ + public function addSolution($id, $solutionOrFactory, array $aliases = [], array $options = []); - public function getSolution($name); + public function getSolution($id); public function getSolutions(); - public function getSolutionOptions($name); + public function getSolutionAliases($id); + + public function getSolutionOptions($id); } diff --git a/Tests/Functional/Command/fixtures/case-sensitive/debug-resolver.txt b/Tests/Functional/Command/fixtures/case-sensitive/debug-resolver.txt index aa134bd49..2d4d49640 100644 --- a/Tests/Functional/Command/fixtures/case-sensitive/debug-resolver.txt +++ b/Tests/Functional/Command/fixtures/case-sensitive/debug-resolver.txt @@ -5,14 +5,14 @@ GraphQL Resolvers Services ------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------- id aliases ------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------- - Overblog\GraphQLBundle\GraphQL\Relay\Mutation\MutationFieldResolver Overblog\GraphQLBundle\GraphQL\Relay\Mutation\MutationFieldResolver (method: __invoke) - relay_mutation_field (method: __invoke) - Overblog\GraphQLBundle\GraphQL\Relay\Node\GlobalIdFieldResolver Overblog\GraphQLBundle\GraphQL\Relay\Node\GlobalIdFieldResolver (method: __invoke) - relay_globalid_field (method: __invoke) - Overblog\GraphQLBundle\GraphQL\Relay\Node\NodeFieldResolver Overblog\GraphQLBundle\GraphQL\Relay\Node\NodeFieldResolver (method: __invoke) - relay_node_field (method: __invoke) - Overblog\GraphQLBundle\GraphQL\Relay\Node\PluralIdentifyingRootFieldResolver Overblog\GraphQLBundle\GraphQL\Relay\Node\PluralIdentifyingRootFieldResolver (method: __invoke) - relay_plural_identifying_field (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Mutation\MutationFieldResolver relay_mutation_field (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Mutation\MutationFieldResolver (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Node\GlobalIdFieldResolver relay_globalid_field (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Node\GlobalIdFieldResolver (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Node\NodeFieldResolver relay_node_field (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Node\NodeFieldResolver (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Node\PluralIdentifyingRootFieldResolver relay_plural_identifying_field (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Node\PluralIdentifyingRootFieldResolver (method: __invoke) ------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------- diff --git a/Tests/Functional/Command/fixtures/debug-resolver.txt b/Tests/Functional/Command/fixtures/debug-resolver.txt index 97e036e1e..6fa4fe9e8 100644 --- a/Tests/Functional/Command/fixtures/debug-resolver.txt +++ b/Tests/Functional/Command/fixtures/debug-resolver.txt @@ -5,14 +5,14 @@ GraphQL Resolvers Services ------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------- id aliases ------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------- - overblog\graphqlbundle\graphql\relay\mutation\mutationfieldresolver Overblog\GraphQLBundle\GraphQL\Relay\Mutation\MutationFieldResolver (method: __invoke) - relay_mutation_field (method: __invoke) - overblog\graphqlbundle\graphql\relay\node\globalidfieldresolver Overblog\GraphQLBundle\GraphQL\Relay\Node\GlobalIdFieldResolver (method: __invoke) - relay_globalid_field (method: __invoke) - overblog\graphqlbundle\graphql\relay\node\nodefieldresolver Overblog\GraphQLBundle\GraphQL\Relay\Node\NodeFieldResolver (method: __invoke) - relay_node_field (method: __invoke) - overblog\graphqlbundle\graphql\relay\node\pluralidentifyingrootfieldresolver Overblog\GraphQLBundle\GraphQL\Relay\Node\PluralIdentifyingRootFieldResolver (method: __invoke) - relay_plural_identifying_field (method: __invoke) + overblog\graphqlbundle\graphql\relay\mutation\mutationfieldresolver relay_mutation_field (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Mutation\MutationFieldResolver (method: __invoke) + overblog\graphqlbundle\graphql\relay\node\globalidfieldresolver relay_globalid_field (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Node\GlobalIdFieldResolver (method: __invoke) + overblog\graphqlbundle\graphql\relay\node\nodefieldresolver relay_node_field (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Node\NodeFieldResolver (method: __invoke) + overblog\graphqlbundle\graphql\relay\node\pluralidentifyingrootfieldresolver relay_plural_identifying_field (method: __invoke) + Overblog\GraphQLBundle\GraphQL\Relay\Node\PluralIdentifyingRootFieldResolver (method: __invoke) ------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------- diff --git a/Tests/Resolver/AbstractProxyResolverTest.php b/Tests/Resolver/AbstractProxyResolverTest.php index 1bd94123e..68e7c7219 100644 --- a/Tests/Resolver/AbstractProxyResolverTest.php +++ b/Tests/Resolver/AbstractProxyResolverTest.php @@ -7,7 +7,7 @@ abstract class AbstractProxyResolverTest extends AbstractResolverTest protected function getResolverSolutionsMapping() { return [ - 'Toto' => ['solutionFunc' => [$this, 'createToto'], 'solutionFuncArgs' => [], 'method' => 'resolve'], + 'Toto' => ['factory' => [[$this, 'createToto'], []], 'aliases' => ['foo', 'bar', 'baz'], 'method' => 'resolve'], ]; } @@ -23,6 +23,22 @@ public function testResolveKnownMutation() $this->assertEquals(['my', 'resolve', 'test'], $result); } + /** + * @param string $alias + * + * @dataProvider aliasProvider + */ + public function testResolveAliasesMutation($alias) + { + $result = $this->resolver->resolve([$alias, ['my', 'resolve', 'test']]); + $this->assertSame( + $this->resolver->getSolution('Toto'), + $this->resolver->getSolution($alias) + ); + + $this->assertEquals(['my', 'resolve', 'test'], $result); + } + /** * @expectedException \Overblog\GraphQLBundle\Resolver\UnresolvableException */ @@ -30,4 +46,11 @@ public function testResolveUnknownMutation() { $this->resolver->resolve('Fake'); } + + public function aliasProvider() + { + yield ['foo']; + yield ['bar']; + yield ['baz']; + } } diff --git a/Tests/Resolver/AbstractResolverTest.php b/Tests/Resolver/AbstractResolverTest.php index f8d972dd6..94edfe8e6 100644 --- a/Tests/Resolver/AbstractResolverTest.php +++ b/Tests/Resolver/AbstractResolverTest.php @@ -19,7 +19,7 @@ public function setUp() $this->resolver = $this->createResolver(); foreach ($this->getResolverSolutionsMapping() as $name => $options) { - $this->resolver->addSolution($name, $options['solutionFunc'], $options['solutionFuncArgs'], $options); + $this->resolver->addSolution($name, $options['factory'], isset($options['aliases']) ? $options['aliases'] : [], $options); } } } diff --git a/Tests/Resolver/TypeResolverTest.php b/Tests/Resolver/TypeResolverTest.php index ac1e6d07b..27944ddce 100644 --- a/Tests/Resolver/TypeResolverTest.php +++ b/Tests/Resolver/TypeResolverTest.php @@ -17,8 +17,8 @@ protected function createResolver() protected function getResolverSolutionsMapping() { return [ - 'Toto' => ['solutionFunc' => [$this, 'createObjectType'], 'solutionFuncArgs' => [['name' => 'Toto']]], - 'Tata' => ['solutionFunc' => [$this, 'createObjectType'], 'solutionFuncArgs' => [['name' => 'Tata']]], + 'Toto' => ['factory' => [[$this, 'createObjectType'], [['name' => 'Toto']]], 'aliases' => ['foo']], + 'Tata' => ['factory' => [[$this, 'createObjectType'], [['name' => 'Tata']]], 'aliases' => ['bar']], ]; } @@ -45,9 +45,7 @@ public function testErrorLoadingType() */ public function testAddNotSupportedSolution() { - $this->resolver->addSolution('not-supported', function () { - return new \stdClass(); - }); + $this->resolver->addSolution('not-supported', new \stdClass()); $this->resolver->getSolution('not-supported'); } @@ -134,4 +132,24 @@ public function testResolveWitListOfListOfWrapper() $this->assertInstanceOf(ListOfType::class, $type->getWrappedType()); $this->assertEquals('Toto', $type->getWrappedType()->getWrappedType()); } + + public function testAliases() + { + $this->assertSame( + $this->resolver->resolve('Tata'), + $this->resolver->resolve('bar') + ); + $this->assertSame( + $this->resolver->getSolution('Tata'), + $this->resolver->getSolution('bar') + ); + $this->assertSame( + $this->resolver->resolve('Toto'), + $this->resolver->resolve('foo') + ); + $this->assertSame( + $this->resolver->getSolution('Toto'), + $this->resolver->getSolution('foo') + ); + } }