Skip to content

feat: add GraphQL enum support #5199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
->notPath('src/Annotation/ApiResource.php') // temporary
->notPath('src/Annotation/ApiSubresource.php') // temporary
->notPath('tests/Fixtures/TestBundle/Entity/DummyPhp8.php') // temporary
->notPath('tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php') // PHPDoc on enum cases
->notPath('tests/Fixtures/TestBundle/Enum/GamePlayMode.php') // PHPDoc on enum cases
->notPath('tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php') // PHPDoc on enum cases
->append([
'tests/Fixtures/app/console',
]);
Expand Down
55 changes: 55 additions & 0 deletions features/graphql/introspection.feature
Original file line number Diff line number Diff line change
Expand Up @@ -566,3 +566,58 @@ Feature: GraphQL introspection support
And the JSON node "errors[0].debugMessage" should be equal to 'Type with id "VoDummyInspectionCursorConnection" is not present in the types container'
And the JSON node "data.typeNotAvailable" should be null
And the JSON node "data.typeOwner.fields[1].type.name" should be equal to "VoDummyInspectionCursorConnection"

Scenario: Introspect an enum
When I send the following GraphQL request:
"""
{
person: __type(name: "Person") {
name
fields {
name
type {
name
description
enumValues {
name
description
}
}
}
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "data.person.fields[1].type.name" should be equal to "GenderTypeEnum"
#And the JSON node "data.person.fields[1].type.description" should be equal to "An enumeration of genders."
And the JSON node "data.person.fields[1].type.enumValues[0].name" should be equal to "MALE"
#And the JSON node "data.person.fields[1].type.enumValues[0].description" should be equal to "The male gender."
And the JSON node "data.person.fields[1].type.enumValues[1].name" should be equal to "FEMALE"
And the JSON node "data.person.fields[1].type.enumValues[1].description" should be equal to "The female gender."

Scenario: Introspect an enum resource
When I send the following GraphQL request:
"""
{
videoGame: __type(name: "VideoGame") {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "data.videoGame.fields[3].type.ofType.name" should be equal to "GamePlayMode"
63 changes: 63 additions & 0 deletions features/graphql/mutation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,69 @@ Feature: GraphQL mutation support
And the JSON node "data.createDummy.dummy.arrayData[1]" should be equal to baz
And the JSON node "data.createDummy.clientMutationId" should be equal to "myId"

Scenario: Create an item with an enum
When I send the following GraphQL request:
"""
mutation {
createPerson(input: {name: "Mob", genderType: FEMALE}) {
person {
id
name
genderType
}
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "data.createPerson.person.id" should be equal to "/people/1"
And the JSON node "data.createPerson.person.name" should be equal to "Mob"
And the JSON node "data.createPerson.person.genderType" should be equal to "FEMALE"

Scenario: Create an item with an enum as a resource
When I send the following GraphQL request:
"""
{
gamePlayModes {
id
name
}
gamePlayMode(id: "/game_play_modes/SINGLE_PLAYER") {
name
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "data.gamePlayModes" should have 3 elements
And the JSON node "data.gamePlayModes[2].id" should be equal to "/game_play_modes/SINGLE_PLAYER"
And the JSON node "data.gamePlayModes[2].name" should be equal to "SINGLE_PLAYER"
And the JSON node "data.gamePlayMode.name" should be equal to "SINGLE_PLAYER"
When I send the following GraphQL request:
"""
mutation {
createVideoGame(input: {name: "Baten Kaitos", playMode: "/game_play_modes/SINGLE_PLAYER"}) {
videoGame {
id
name
playMode {
id
name
}
}
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "data.createVideoGame.videoGame.id" should be equal to "/video_games/1"
And the JSON node "data.createVideoGame.videoGame.name" should be equal to "Baten Kaitos"
And the JSON node "data.createVideoGame.videoGame.playMode.id" should be equal to "/game_play_modes/SINGLE_PLAYER"
And the JSON node "data.createVideoGame.videoGame.playMode.name" should be equal to "SINGLE_PLAYER"

Scenario: Delete an item through a mutation
When I send the following GraphQL request:
"""
Expand Down
8 changes: 8 additions & 0 deletions features/openapi/docs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ Feature: Documentation support
"nullable": true
}
"""
And the "playMode" property exists for the OpenAPI class "VideoGame"
And the "playMode" property for the OpenAPI class "VideoGame" should be equal to:
"""
{
"type": "string",
"format": "iri-reference"
}
"""
# Enable these tests when SF 4.4 / PHP 7.1 support is dropped
#And the "isDummyBoolean" property exists for the OpenAPI class "DummyBoolean"
#And the "isDummyBoolean" property is not read only for the OpenAPI class "DummyBoolean"
Expand Down
41 changes: 38 additions & 3 deletions src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@
*
* @author Alan Poulain <contact@alanpoulain.eu>
*/
final class FieldsBuilder implements FieldsBuilderInterface
final class FieldsBuilder implements FieldsBuilderInterface, FieldsBuilderEnumInterface
{
public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator)
private readonly TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder;

public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator)
{
if ($typeBuilder instanceof TypeBuilderInterface) {
@trigger_error(sprintf('$typeBuilder argument of FieldsBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED);
}
$this->typeBuilder = $typeBuilder;
}

/**
Expand Down Expand Up @@ -226,6 +232,26 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o
return $fields;
}

/**
* {@inheritdoc}
*/
public function getEnumFields(string $enumClass): array
{
$rEnum = new \ReflectionEnum($enumClass);

$enumCases = [];
foreach ($rEnum->getCases() as $rCase) {
$enumCase = ['value' => $rCase->getBackingValue()];
$propertyMetadata = $this->propertyMetadataFactory->create($enumClass, $rCase->getName());
if ($enumCaseDescription = $propertyMetadata->getDescription()) {
$enumCase['description'] = $enumCaseDescription;
}
$enumCases[$rCase->getName()] = $enumCase;
}

return $enumCases;
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -481,7 +507,16 @@ private function convertType(Type $type, bool $input, Operation $resourceOperati
}

if ($this->typeBuilder->isCollection($type)) {
return $this->pagination->isGraphQlEnabled($resourceOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation) : GraphQLType::listOf($graphqlType);
if (!$input && $this->pagination->isGraphQlEnabled($resourceOperation)) {
// Deprecated path, to remove in API Platform 4.
if ($this->typeBuilder instanceof TypeBuilderInterface) {
return $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation);
}

return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation);
}

return GraphQLType::listOf($graphqlType);
}

return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName())
Expand Down
64 changes: 64 additions & 0 deletions src/GraphQl/Type/FieldsBuilderEnumInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?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\GraphQl\Type;

use ApiPlatform\Metadata\GraphQl\Operation;

/**
* Interface implemented to build GraphQL fields.
*
* @author Alan Poulain <contact@alanpoulain.eu>
*/
interface FieldsBuilderEnumInterface
{
/**
* Gets the fields of a node for a query.
*/
public function getNodeQueryFields(): array;

/**
* Gets the item query fields of the schema.
*/
public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array;

/**
* Gets the collection query fields of the schema.
*/
public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array;

/**
* Gets the mutation fields of the schema.
*/
public function getMutationFields(string $resourceClass, Operation $operation): array;

/**
* Gets the subscription fields of the schema.
*/
public function getSubscriptionFields(string $resourceClass, Operation $operation): array;

/**
* Gets the fields of the type of the given resource.
*/
public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, int $depth = 0, ?array $ioMetadata = null): array;

/**
* Gets the fields (cases) of the enum.
*/
public function getEnumFields(string $enumClass): array;

/**
* Resolve the args of a resource by resolving its types.
*/
public function resolveResourceArgs(array $args, Operation $operation): array;
}
2 changes: 2 additions & 0 deletions src/GraphQl/Type/FieldsBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
* Interface implemented to build GraphQL fields.
*
* @author Alan Poulain <contact@alanpoulain.eu>
*
* @deprecated Since API Platform 3.1. Use @see FieldsBuilderEnumInterface instead.
*/
interface FieldsBuilderInterface
{
Expand Down
5 changes: 4 additions & 1 deletion src/GraphQl/Type/SchemaBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@
*/
final class SchemaBuilder implements SchemaBuilderInterface
{
public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly TypesFactoryInterface $typesFactory, private readonly TypesContainerInterface $typesContainer, private readonly FieldsBuilderInterface $fieldsBuilder)
public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly TypesFactoryInterface $typesFactory, private readonly TypesContainerInterface $typesContainer, private readonly FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder)
{
if ($this->fieldsBuilder instanceof FieldsBuilderInterface) {
@trigger_error(sprintf('$fieldsBuilder argument of SchemaBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED);
}
}

public function getSchema(): Schema
Expand Down
49 changes: 48 additions & 1 deletion src/GraphQl/Type/TypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use ApiPlatform\Metadata\GraphQl\Subscription;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\State\Pagination\Pagination;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\NonNull;
Expand All @@ -35,7 +36,7 @@
*
* @author Alan Poulain <contact@alanpoulain.eu>
*/
final class TypeBuilder implements TypeBuilderInterface
final class TypeBuilder implements TypeBuilderInterface, TypeBuilderEnumInterface
{
private $defaultFieldResolver;

Expand Down Expand Up @@ -201,6 +202,16 @@ public function getNodeInterface(): InterfaceType
* {@inheritdoc}
*/
public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType
{
@trigger_error('Using getResourcePaginatedCollectionType method of TypeBuilder is deprecated since API Platform 3.1. Use getPaginatedCollectionType method instead.', \E_USER_DEPRECATED);

return $this->getPaginatedCollectionType($resourceType, $operation);
}

/**
* {@inheritdoc}
*/
public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType
{
$shortName = $resourceType->name;
$paginationType = $this->pagination->getGraphQlPaginationType($operation);
Expand All @@ -226,6 +237,42 @@ public function getResourcePaginatedCollectionType(GraphQLType $resourceType, st
return $resourcePaginatedCollectionType;
}

public function getEnumType(Operation $operation): GraphQLType
{
$enumName = $operation->getShortName();
$enumKey = $enumName;
if (!str_ends_with($enumName, 'Enum')) {
$enumKey = sprintf('%sEnum', $enumName);
}

if ($this->typesContainer->has($enumKey)) {
return $this->typesContainer->get($enumKey);
}

/** @var FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder */
$fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder');
$enumCases = [];
// Remove the condition in API Platform 4.
if ($fieldsBuilder instanceof FieldsBuilderEnumInterface) {
$enumCases = $fieldsBuilder->getEnumFields($operation->getClass());
} else {
@trigger_error(sprintf('api_platform.graphql.fields_builder service implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED);
}

$enumConfig = [
'name' => $enumName,
'values' => $enumCases,
];
if ($enumDescription = $operation->getDescription()) {
$enumConfig['description'] = $enumDescription;
}

$enumType = new EnumType($enumConfig);
$this->typesContainer->set($enumKey, $enumType);

return $enumType;
}

/**
* {@inheritdoc}
*/
Expand Down
Loading