diff --git a/src/Bridge/Symfony/Bundle/ApiPlatformBundle.php b/src/Bridge/Symfony/Bundle/ApiPlatformBundle.php index e3b8b20b306..27e2b473583 100644 --- a/src/Bridge/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Bridge/Symfony/Bundle/ApiPlatformBundle.php @@ -17,6 +17,7 @@ use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\DataProviderPass; use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\ElasticsearchClientPass; use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\FilterPass; +use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -38,5 +39,6 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new AnnotationFilterPass()); $container->addCompilerPass(new FilterPass()); $container->addCompilerPass(new ElasticsearchClientPass()); + $container->addCompilerPass(new GraphQlTypePass()); } } diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 1952d310ce9..327b3b707eb 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -26,6 +26,7 @@ use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; use ApiPlatform\Core\Exception\RuntimeException; +use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface as GraphQlTypeInterface; use Doctrine\Common\Annotations\Annotation; use Doctrine\ORM\Version; use Elasticsearch\Client; @@ -112,6 +113,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('api_platform.subresource_data_provider'); $container->registerForAutoconfiguration(FilterInterface::class) ->addTag('api_platform.filter'); + $container->registerForAutoconfiguration(GraphQlTypeInterface::class) + ->addTag('api_platform.graphql.type'); if (interface_exists(ValidatorInterface::class)) { $loader->load('validator.xml'); diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/GraphQlTypePass.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/GraphQlTypePass.php new file mode 100644 index 00000000000..6572dbb65c2 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Compiler/GraphQlTypePass.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Injects GraphQL types. + * + * @internal + * + * @author Alan Poulain + */ +final class GraphQlTypePass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $types = []; + foreach ($container->findTaggedServiceIds('api_platform.graphql.type', true) as $serviceId => $tags) { + foreach ($tags as $tag) { + $types[$tag['id'] ?? $serviceId] = new Reference($serviceId); + } + } + + $container->getDefinition('api_platform.graphql.type_locator')->addArgument($types); + $container->getDefinition('api_platform.graphql.types_factory')->addArgument(array_keys($types)); + } +} diff --git a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml index bdc491d1b38..1f5cdcdd8bc 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/graphql.xml @@ -40,6 +40,20 @@ + + + + + + + + + + + + + + @@ -51,6 +65,7 @@ %api_platform.collection.pagination.enabled% + diff --git a/src/GraphQl/Type/Definition/IterableType.php b/src/GraphQl/Type/Definition/IterableType.php index 871066cc958..a82979abba1 100644 --- a/src/GraphQl/Type/Definition/IterableType.php +++ b/src/GraphQl/Type/Definition/IterableType.php @@ -31,7 +31,7 @@ * * @author Alan Poulain */ -final class IterableType extends ScalarType +final class IterableType extends ScalarType implements TypeInterface { public function __construct() { @@ -41,6 +41,11 @@ public function __construct() parent::__construct(); } + public function getName(): string + { + return $this->name; + } + /** * {@inheritdoc} */ diff --git a/src/GraphQl/Type/Definition/TypeInterface.php b/src/GraphQl/Type/Definition/TypeInterface.php new file mode 100644 index 00000000000..fc4ff950306 --- /dev/null +++ b/src/GraphQl/Type/Definition/TypeInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\GraphQl\Type\Definition; + +use GraphQL\Type\Definition\LeafType; + +/** + * @experimental + * + * @author Alan Poulain + */ +interface TypeInterface extends LeafType +{ + public function getName(): string; +} diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index d92cbdeb41c..c8f345b7035 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -16,7 +16,6 @@ use ApiPlatform\Core\Exception\ResourceClassNotFoundException; use ApiPlatform\Core\GraphQl\Resolver\Factory\ResolverFactoryInterface; use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\Core\GraphQl\Type\Definition\IterableType; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; @@ -56,10 +55,11 @@ final class SchemaBuilder implements SchemaBuilderInterface private $itemMutationResolverFactory; private $defaultFieldResolver; private $filterLocator; + private $typesFactory; private $paginationEnabled; private $graphqlTypes = []; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, callable $itemResolver, callable $defaultFieldResolver, ContainerInterface $filterLocator = null, bool $paginationEnabled = true) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, callable $itemResolver, callable $defaultFieldResolver, ContainerInterface $filterLocator = null, bool $paginationEnabled = true, TypesFactoryInterface $typesFactory = null) { $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; $this->propertyMetadataFactory = $propertyMetadataFactory; @@ -70,12 +70,14 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName $this->itemMutationResolverFactory = $itemMutationResolverFactory; $this->defaultFieldResolver = $defaultFieldResolver; $this->filterLocator = $filterLocator; + $this->typesFactory = $typesFactory; $this->paginationEnabled = $paginationEnabled; } public function getSchema(): Schema { - $this->graphqlTypes['Iterable'] = new IterableType(); + $this->graphqlTypes += $this->typesFactory->getTypes(); + $queryFields = ['node' => $this->getNodeQueryField()]; $mutationFields = []; diff --git a/src/GraphQl/Type/TypesFactory.php b/src/GraphQl/Type/TypesFactory.php new file mode 100644 index 00000000000..40874744543 --- /dev/null +++ b/src/GraphQl/Type/TypesFactory.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\GraphQl\Type; + +use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface; +use Psr\Container\ContainerInterface; + +/** + * Get the registered services corresponding to GraphQL types. + * + * @experimental + * + * @author Alan Poulain + */ +final class TypesFactory implements TypesFactoryInterface +{ + private $typeLocator; + private $typeIds; + + /** + * @param string[] $typeIds + */ + public function __construct(ContainerInterface $typeLocator, array $typeIds) + { + $this->typeLocator = $typeLocator; + $this->typeIds = $typeIds; + } + + public function getTypes(): array + { + $types = []; + + foreach ($this->typeIds as $typeId) { + /** @var TypeInterface $type */ + $type = $this->typeLocator->get($typeId); + $types[$type->getName()] = $type; + } + + return $types; + } +} diff --git a/src/GraphQl/Type/TypesFactoryInterface.php b/src/GraphQl/Type/TypesFactoryInterface.php new file mode 100644 index 00000000000..4f34d888511 --- /dev/null +++ b/src/GraphQl/Type/TypesFactoryInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\GraphQl\Type; + +/** + * Get the GraphQL types. + * + * @experimental + * + * @author Alan Poulain + */ +interface TypesFactoryInterface +{ + public function getTypes(): array; +} diff --git a/tests/Bridge/Symfony/Bundle/ApiPlatformBundleTest.php b/tests/Bridge/Symfony/Bundle/ApiPlatformBundleTest.php index 2da81a1b0d9..778222ac10d 100644 --- a/tests/Bridge/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Bridge/Symfony/Bundle/ApiPlatformBundleTest.php @@ -18,6 +18,7 @@ use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\DataProviderPass; use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\ElasticsearchClientPass; use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\FilterPass; +use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -34,6 +35,7 @@ public function testBuild() $containerProphecy->addCompilerPass(Argument::type(AnnotationFilterPass::class))->shouldBeCalled(); $containerProphecy->addCompilerPass(Argument::type(FilterPass::class))->shouldBeCalled(); $containerProphecy->addCompilerPass(Argument::type(ElasticsearchClientPass::class))->shouldBeCalled(); + $containerProphecy->addCompilerPass(Argument::type(GraphQlTypePass::class))->shouldBeCalled(); $bundle = new ApiPlatformBundle(); $bundle->build($containerProphecy->reveal()); diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 5e72a38ca4b..75344dc61ca 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -57,6 +57,7 @@ use ApiPlatform\Core\Exception\FilterValidationException; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Exception\RuntimeException; +use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface as GraphQlTypeInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; @@ -634,6 +635,10 @@ private function getPartialContainerBuilderProphecy() ->willReturn($this->childDefinitionProphecy)->shouldBeCalledTimes(1); $this->childDefinitionProphecy->addTag('api_platform.filter')->shouldBeCalledTimes(1); + $containerBuilderProphecy->registerForAutoconfiguration(GraphQlTypeInterface::class) + ->willReturn($this->childDefinitionProphecy)->shouldBeCalledTimes(1); + $this->childDefinitionProphecy->addTag('api_platform.graphql.type')->shouldBeCalledTimes(1); + $containerBuilderProphecy->getParameter('kernel.bundles')->willReturn([ 'DoctrineBundle' => DoctrineBundle::class, ])->shouldBeCalled(); @@ -946,6 +951,9 @@ private function getBaseContainerBuilderProphecy() 'api_platform.graphql.resolver.factory.item_mutation', 'api_platform.graphql.resolver.item', 'api_platform.graphql.resolver.resource_field', + 'api_platform.graphql.iterable_type', + 'api_platform.graphql.type_locator', + 'api_platform.graphql.types_factory', 'api_platform.graphql.normalizer.item', 'api_platform.jsonld.normalizer.item', 'api_platform.jsonld.encoder', diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/GraphQlTypePassTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/GraphQlTypePassTest.php new file mode 100644 index 00000000000..38df999250e --- /dev/null +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/Compiler/GraphQlTypePassTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\DependencyInjection\Compiler; + +use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Alan Poulain + */ +class GraphQlTypePassTest extends TestCase +{ + public function testProcess() + { + $filterPass = new GraphQlTypePass(); + + $this->assertInstanceOf(CompilerPassInterface::class, $filterPass); + + $typeLocatorDefinitionProphecy = $this->prophesize(Definition::class); + $typeLocatorDefinitionProphecy->addArgument(Argument::that(function (array $arg) { + return !isset($arg['foo']) && isset($arg['my_id']) && $arg['my_id'] instanceof Reference; + }))->shouldBeCalled(); + + $typesFactoryDefinitionProphecy = $this->prophesize(Definition::class); + $typesFactoryDefinitionProphecy->addArgument(['my_id'])->shouldBeCalled(); + + $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); + $containerBuilderProphecy->findTaggedServiceIds('api_platform.graphql.type', true)->willReturn(['foo' => [], 'bar' => [['id' => 'my_id']]])->shouldBeCalled(); + $containerBuilderProphecy->getDefinition('api_platform.graphql.type_locator')->willReturn($typeLocatorDefinitionProphecy->reveal())->shouldBeCalled(); + $containerBuilderProphecy->getDefinition('api_platform.graphql.types_factory')->willReturn($typesFactoryDefinitionProphecy->reveal())->shouldBeCalled(); + + $filterPass->process($containerBuilderProphecy->reveal()); + } + + public function testIdNotExist() + { + $filterPass = new GraphQlTypePass(); + + $this->assertInstanceOf(CompilerPassInterface::class, $filterPass); + + $typeLocatorDefinitionProphecy = $this->prophesize(Definition::class); + $typeLocatorDefinitionProphecy->addArgument(Argument::that(function (array $arg) { + return !isset($arg['foo']) && isset($arg['bar']) && $arg['bar'] instanceof Reference; + }))->shouldBeCalled(); + + $typesFactoryDefinitionProphecy = $this->prophesize(Definition::class); + $typesFactoryDefinitionProphecy->addArgument(['bar'])->shouldBeCalled(); + + $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); + $containerBuilderProphecy->findTaggedServiceIds('api_platform.graphql.type', true)->willReturn(['foo' => [], 'bar' => [['hi' => 'hello']]])->shouldBeCalled(); + $containerBuilderProphecy->getDefinition('api_platform.graphql.type_locator')->willReturn($typeLocatorDefinitionProphecy->reveal())->shouldBeCalled(); + $containerBuilderProphecy->getDefinition('api_platform.graphql.types_factory')->willReturn($typesFactoryDefinitionProphecy->reveal())->shouldBeCalled(); + + $filterPass->process($containerBuilderProphecy->reveal()); + } +} diff --git a/tests/GraphQl/Type/Definition/IterableTypeTest.php b/tests/GraphQl/Type/Definition/IterableTypeTest.php index a847c818800..54a393d34f0 100644 --- a/tests/GraphQl/Type/Definition/IterableTypeTest.php +++ b/tests/GraphQl/Type/Definition/IterableTypeTest.php @@ -31,6 +31,13 @@ */ class IterableTypeTest extends TestCase { + public function testGetName() + { + $iterableType = new IterableType(); + + $this->assertEquals('Iterable', $iterableType->getName()); + } + public function testSerialize() { $iterableType = new IterableType(); diff --git a/tests/GraphQl/Type/SchemaBuilderTest.php b/tests/GraphQl/Type/SchemaBuilderTest.php index baf97ad07b6..49e13ae8e4a 100644 --- a/tests/GraphQl/Type/SchemaBuilderTest.php +++ b/tests/GraphQl/Type/SchemaBuilderTest.php @@ -15,7 +15,9 @@ use ApiPlatform\Core\Exception\ResourceClassNotFoundException; use ApiPlatform\Core\GraphQl\Resolver\Factory\ResolverFactoryInterface; +use ApiPlatform\Core\GraphQl\Type\Definition\IterableType; use ApiPlatform\Core\GraphQl\Type\SchemaBuilder; +use ApiPlatform\Core\GraphQl\Type\TypesFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; @@ -257,6 +259,7 @@ private function createSchemaBuilder($propertyMetadataMockBuilder, bool $paginat $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $collectionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); $itemMutationResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); + $typesFactoryProphecy = $this->prophesize(TypesFactoryInterface::class); $resourceClassNames = []; for ($i = 1; $i <= 3; ++$i) { @@ -293,6 +296,8 @@ private function createSchemaBuilder($propertyMetadataMockBuilder, bool $paginat $itemMutationResolverFactoryProphecy->__invoke(Argument::cetera())->willReturn(function () { }); + $typesFactoryProphecy->getTypes()->willReturn(['Iterable' => new IterableType()]); + return new SchemaBuilder( $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), @@ -305,7 +310,8 @@ function () { function () { }, null, - $paginationEnabled + $paginationEnabled, + $typesFactoryProphecy->reveal() ); } } diff --git a/tests/GraphQl/Type/TypesFactoryTest.php b/tests/GraphQl/Type/TypesFactoryTest.php new file mode 100644 index 00000000000..c36b56b223b --- /dev/null +++ b/tests/GraphQl/Type/TypesFactoryTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\GraphQl\Type; + +use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface; +use ApiPlatform\Core\GraphQl\Type\TypesFactory; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; + +/** + * @author Alan Poulain + */ +class TypesFactoryTest extends TestCase +{ + public function testGetTypes() + { + $typeProphecy = $this->prophesize(TypeInterface::class); + $typeProphecy->getName()->willReturn('Foo'); + $type = $typeProphecy->reveal(); + + $typeLocatorProphecy = $this->prophesize(ContainerInterface::class); + $typeLocatorProphecy->get('foo')->willReturn($type)->shouldBeCalled(); + + $typesFactory = new TypesFactory($typeLocatorProphecy->reveal(), ['foo']); + + $types = $typesFactory->getTypes(); + $this->assertArrayHasKey('Foo', $types); + $this->assertInstanceOf(TypeInterface::class, $types['Foo']); + $this->assertEquals(['Foo' => $type], $types); + } +}