Skip to content

Commit

Permalink
Merge pull request #2492 from alanpoulain/graphql-custom-types
Browse files Browse the repository at this point in the history
[GraphQL] Custom types
  • Loading branch information
soyuka committed Feb 11, 2019
2 parents e1f92d0 + 7331f27 commit 2bb6f95
Show file tree
Hide file tree
Showing 15 changed files with 317 additions and 5 deletions.
2 changes: 2 additions & 0 deletions src/Bridge/Symfony/Bundle/ApiPlatformBundle.php
Expand Up @@ -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;

Expand All @@ -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());
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down
@@ -0,0 +1,44 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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 <contact@alanpoulain.eu>
*/
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));
}
}
15 changes: 15 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
Expand Up @@ -40,6 +40,20 @@
<argument type="service" id="api_platform.iri_converter" />
</service>

<!-- Type -->

<service id="api_platform.graphql.iterable_type" class="ApiPlatform\Core\GraphQl\Type\Definition\IterableType">
<tag name="api_platform.graphql.type" />
</service>

<service id="api_platform.graphql.type_locator" class="Symfony\Component\DependencyInjection\ServiceLocator">
<tag name="container.service_locator" />
</service>

<service id="api_platform.graphql.types_factory" class="ApiPlatform\Core\GraphQl\Type\TypesFactory">
<argument type="service" id="api_platform.graphql.type_locator" />
</service>

<service id="api_platform.graphql.schema_builder" class="ApiPlatform\Core\GraphQl\Type\SchemaBuilder" public="false">
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
Expand All @@ -51,6 +65,7 @@
<argument type="service" id="api_platform.graphql.resolver.resource_field" />
<argument type="service" id="api_platform.filter_locator" />
<argument>%api_platform.collection.pagination.enabled%</argument>
<argument type="service" id="api_platform.graphql.types_factory" />
</service>

<!-- Action -->
Expand Down
7 changes: 6 additions & 1 deletion src/GraphQl/Type/Definition/IterableType.php
Expand Up @@ -31,7 +31,7 @@
*
* @author Alan Poulain <contact@alanpoulain.eu>
*/
final class IterableType extends ScalarType
final class IterableType extends ScalarType implements TypeInterface
{
public function __construct()
{
Expand All @@ -41,6 +41,11 @@ public function __construct()
parent::__construct();
}

public function getName(): string
{
return $this->name;
}

/**
* {@inheritdoc}
*/
Expand Down
26 changes: 26 additions & 0 deletions src/GraphQl/Type/Definition/TypeInterface.php
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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 <contact@alanpoulain.eu>
*/
interface TypeInterface extends LeafType
{
public function getName(): string;
}
8 changes: 5 additions & 3 deletions src/GraphQl/Type/SchemaBuilder.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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 = [];

Expand Down
52 changes: 52 additions & 0 deletions src/GraphQl/Type/TypesFactory.php
@@ -0,0 +1,52 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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 <contact@alanpoulain.eu>
*/
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;
}
}
26 changes: 26 additions & 0 deletions src/GraphQl/Type/TypesFactoryInterface.php
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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 <contact@alanpoulain.eu>
*/
interface TypesFactoryInterface
{
public function getTypes(): array;
}
2 changes: 2 additions & 0 deletions tests/Bridge/Symfony/Bundle/ApiPlatformBundleTest.php
Expand Up @@ -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;
Expand All @@ -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());
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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',
Expand Down
@@ -0,0 +1,72 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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 <contact@alanpoulain.eu>
*/
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());
}
}

0 comments on commit 2bb6f95

Please sign in to comment.