diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index dc313c8345..0f8a4297ab 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: install-args: ['', '--prefer-lowest'] - php-version: ['7.4', '8.0'] + php-version: ['7.4', '8.0', '8.1'] fail-fast: false steps: diff --git a/.gitignore b/.gitignore index be23bb574f..ee8d2be718 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/build/ /vendor/ /composer.lock /src/Tests/ diff --git a/composer.json b/composer.json index 850be3bd8f..636fee238c 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,8 @@ "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-webmozart-assert": "^1.0", - "phpunit/phpunit": "^8.5.19||^9.5.8", + "phpunit/phpunit": "^8.5.19 || ^9.5.8", + "symfony/var-dumper": "^5.4 || ^6.0", "thecodingmachine/phpstan-strict-rules": "^1.0" }, "suggest": { diff --git a/phpcs.xml.dist b/phpcs.xml.dist index c3814ecf82..2a564ed45b 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -28,6 +28,7 @@ + diff --git a/phpstan.neon b/phpstan.neon index dbf086d187..95333255bd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,6 +4,10 @@ parameters: tmpDir: .phpstan-cache paths: - src + excludePaths: + # TODO: exlude only for PHP < 8.1 + - src/Mappers/Root/EnumTypeMapper.php + - src/Types/EnumType.php level: 8 checkGenericClassInNonGenericObjectType: false reportUnmatchedIgnoredErrors: false diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index 9ab3c376de..a96145f940 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -7,7 +7,6 @@ use Doctrine\Common\Annotations\AnnotationException; use Doctrine\Common\Annotations\Reader; use InvalidArgumentException; -use MyCLabs\Enum\Enum; use ReflectionClass; use ReflectionMethod; use ReflectionParameter; @@ -568,9 +567,6 @@ static function ($attribute) { return $toAddAnnotations; } - /** - * @param ReflectionClass $refClass - */ public function getEnumTypeAnnotation(ReflectionClass $refClass): ?EnumType { return $this->getClassAnnotation($refClass, EnumType::class); diff --git a/src/Annotations/EnumType.php b/src/Annotations/EnumType.php index 3ce3e2dc10..ad63e47778 100644 --- a/src/Annotations/EnumType.php +++ b/src/Annotations/EnumType.php @@ -9,6 +9,8 @@ /** * The EnumType annotation is useful to change the name of the generated "enum" type. * + * @deprecated Use @Type on a native PHP 8.1 Enum instead. Support will be removed in future release. + * * @Annotation * @Target({"CLASS"}) * @Attributes({ @@ -21,12 +23,16 @@ class EnumType /** @var string|null */ private $name; + /** @var bool */ + private $useValues; + /** * @param mixed[] $attributes */ - public function __construct(array $attributes = [], ?string $name = null) + public function __construct(array $attributes = [], ?string $name = null, ?bool $useValues = null) { $this->name = $name ?? $attributes['name'] ?? null; + $this->useValues = $useValues ?? $attributes['useValues'] ?? false; } /** @@ -36,4 +42,12 @@ public function getName(): ?string { return $this->name; } + + /** + * Returns true if the enum type should expose backed values instead of case names. + */ + public function useValues(): bool + { + return $this->useValues; + } } diff --git a/src/Annotations/Type.php b/src/Annotations/Type.php index 6d1c3d1ed0..f7c72af129 100644 --- a/src/Annotations/Type.php +++ b/src/Annotations/Type.php @@ -45,12 +45,21 @@ class Type */ private $selfType = false; + /** @var bool */ + private $useEnumValues = false; + /** * @param mixed[] $attributes * @param class-string|null $class */ - public function __construct(array $attributes = [], ?string $class = null, ?string $name = null, ?bool $default = null, ?bool $external = null) - { + public function __construct( + array $attributes = [], + ?string $class = null, + ?string $name = null, + ?bool $default = null, + ?bool $external = null, + ?bool $useEnumValues = null + ) { $external = $external ?? $attributes['external'] ?? null; $class = $class ?? $attributes['class'] ?? null; if ($class !== null) { @@ -63,6 +72,7 @@ public function __construct(array $attributes = [], ?string $class = null, ?stri // If no value is passed for default, "default" = true $this->default = $default ?? $attributes['default'] ?? true; + $this->useEnumValues = $useEnumValues ?? $attributes['useEnumValues'] ?? false; if ($external === null) { return; @@ -123,4 +133,12 @@ public function isDefault(): bool { return $this->default; } + + /** + * Returns true if this enum type + */ + public function useEnumValues(): bool + { + return $this->useEnumValues; + } } diff --git a/src/Mappers/CompositeTypeMapper.php b/src/Mappers/CompositeTypeMapper.php index eea9ca476c..f21dddb892 100644 --- a/src/Mappers/CompositeTypeMapper.php +++ b/src/Mappers/CompositeTypeMapper.php @@ -65,6 +65,7 @@ public function mapClassToType(string $className, ?OutputType $subType): Mutable return $typeMapper->mapClassToType($className, $subType); } } + throw CannotMapTypeException::createForType($className); } diff --git a/src/Mappers/RecursiveTypeMapper.php b/src/Mappers/RecursiveTypeMapper.php index cf7247164f..88966849e5 100644 --- a/src/Mappers/RecursiveTypeMapper.php +++ b/src/Mappers/RecursiveTypeMapper.php @@ -114,6 +114,7 @@ public function mapClassToType(string $className, ?OutputType $subType): Mutable if ($closestClassName === null) { throw CannotMapTypeException::createForType($className); } + $type = $this->typeMapper->mapClassToType($closestClassName, $subType); // In the event this type was already part of cache, let's not extend it. @@ -452,8 +453,10 @@ public function getOutputTypes(): array $types = []; $typeNames = []; foreach ($this->typeMapper->getSupportedClasses() as $supportedClass) { - $type = $this->mapClassToType($supportedClass, null); + $type = $this->mapClassToType($supportedClass, null); + $types[$supportedClass] = $type; + if (isset($typeNames[$type->name])) { throw DuplicateMappingException::createForTypeName($type->name, $typeNames[$type->name], $supportedClass); } diff --git a/src/Mappers/Root/EnumTypeMapper.php b/src/Mappers/Root/EnumTypeMapper.php new file mode 100644 index 0000000000..0381baf5d5 --- /dev/null +++ b/src/Mappers/Root/EnumTypeMapper.php @@ -0,0 +1,214 @@ +=8.1) + */ +class EnumTypeMapper implements RootTypeMapperInterface +{ + /** @var array, EnumType> */ + private $cache = []; + /** @var array */ + private $cacheByName = []; + /** @var array> */ + private $nameToClassMapping; + /** @var RootTypeMapperInterface */ + private $next; + /** @var AnnotationReader */ + private $annotationReader; + /** @var array|NS[] */ + private $namespaces; + /** @var CacheInterface */ + private $cacheService; + + /** + * @param NS[] $namespaces List of namespaces containing enums. Used when searching an enum by name. + */ + public function __construct( + RootTypeMapperInterface $next, + AnnotationReader $annotationReader, + CacheInterface $cacheService, + array $namespaces + ) { + $this->next = $next; + $this->annotationReader = $annotationReader; + $this->cacheService = $cacheService; + $this->namespaces = $namespaces; + } + + /** + * @param (OutputType&GraphQLType)|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector + * + * @return OutputType&GraphQLType + */ + public function toGraphQLOutputType( + Type $type, + ?OutputType $subType, + $reflector, + DocBlock $docBlockObj + ): OutputType { + $result = $this->map($type); + if ($result === null) { + return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); + } + + return $result; + } + + /** + * Maps into the appropriate InputType + * + * @param InputType|GraphQLType|null $subType + * @param ReflectionMethod|ReflectionProperty $reflector + * + * @return InputType|GraphQLType + */ + public function toGraphQLInputType( + Type $type, + ?InputType $subType, + string $argumentName, + $reflector, + DocBlock $docBlockObj + ): InputType + { + $result = $this->map($type); + if ($result === null) { + return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); + } + + return $result; + } + + private function map(Type $type): ?EnumType + { + if (! $type instanceof Object_) { + return null; + } + $fqsen = $type->getFqsen(); + if ($fqsen === null) { + return null; + } + + /** @var class-string $enumClass */ + $enumClass = (string) $fqsen; + + return $this->mapByClassName($enumClass); + } + + /** + * @param class-string $enumClass + */ + private function mapByClassName(string $enumClass): ?EnumType + { + if (isset($this->cache[$enumClass])) { + return $this->cache[$enumClass]; + } + + if (! enum_exists($enumClass)) { + return null; + } + + // phpcs:disable SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable + /** @var class-string $enumClass */ + // phpcs:enable SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable + + $reflectionEnum = new ReflectionEnum($enumClass); + + $typeAnnotation = $this->annotationReader->getTypeAnnotation($reflectionEnum); + $typeName = ($typeAnnotation !== null ? $typeAnnotation->getName() : null) ?? $reflectionEnum->getShortName(); + + // Expose values instead of names if specifically configured to and if enum is string-backed + $useValues = $typeAnnotation !== null && + $typeAnnotation->useEnumValues() && + $reflectionEnum->isBacked() && + (string) $reflectionEnum->getBackingType() === 'string'; + + $type = new EnumType($enumClass, $typeName, $useValues); + + return $this->cacheByName[$typeName] = $this->cache[$enumClass] = $type; + } + + private function getTypeName(ReflectionClass $reflectionClass): string + { + $typeAnnotation = $this->annotationReader->getTypeAnnotation($reflectionClass); + + return ($typeAnnotation !== null ? $typeAnnotation->getName() : null) ?? $reflectionClass->getShortName(); + } + + /** + * Returns a GraphQL type by name. + * If this root type mapper can return this type in "toGraphQLOutputType" or "toGraphQLInputType", it should + * also map these types by name in the "mapNameToType" method. + * + * @param string $typeName The name of the GraphQL type + */ + public function mapNameToType(string $typeName): NamedType + { + // This is a hack to make sure "$schema->assertValid()" returns true. + // The mapNameToType will fail if the mapByClassName method was not called before. + // This is actually not an issue in real life scenarios where enum types are never queried by type name. + if (isset($this->cacheByName[$typeName])) { + return $this->cacheByName[$typeName]; + } + + $nameToClassMapping = $this->getNameToClassMapping(); + if (isset($this->nameToClassMapping[$typeName])) { + $className = $nameToClassMapping[$typeName]; + $type = $this->mapByClassName($className); + assert($type !== null); + return $type; + } + + return $this->next->mapNameToType($typeName); + } + + /** + * Go through all classes in the defined namespaces and loads the cache. + * + * @return array> + */ + private function getNameToClassMapping(): array + { + if ($this->nameToClassMapping === null) { + $this->nameToClassMapping = $this->cacheService->get('enum_name_to_class', function () { + $nameToClassMapping = []; + foreach ($this->namespaces as $ns) { + foreach ($ns->getClassList() as $className => $classRef) { + if (! enum_exists($className)) { + continue; + } + + $nameToClassMapping[$this->getTypeName($classRef)] = $className; + } + } + return $nameToClassMapping; + }); + } + + return $this->nameToClassMapping; + } +} diff --git a/src/SchemaFactory.php b/src/SchemaFactory.php index 82bf8bd0a1..dd9b3550cc 100644 --- a/src/SchemaFactory.php +++ b/src/SchemaFactory.php @@ -28,6 +28,7 @@ use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Root\BaseTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Root\CompoundTypeMapper; +use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Root\IteratorTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper; @@ -51,10 +52,12 @@ use TheCodingMachine\GraphQLite\Types\TypeResolver; use TheCodingMachine\GraphQLite\Utils\NamespacedCache; use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory; +use UnitEnum; use function array_map; use function array_reverse; use function class_exists; +use function interface_exists; use function md5; use function substr; @@ -361,6 +364,11 @@ public function createSchema(): Schema $errorRootTypeMapper = new FinalRootTypeMapper($recursiveTypeMapper); $rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $recursiveTypeMapper, $topRootTypeMapper); + + if (interface_exists(UnitEnum::class)) { + $rootTypeMapper = new EnumTypeMapper($rootTypeMapper, $annotationReader, $symfonyCache, $nsList); + } + if (class_exists(Enum::class)) { $rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $annotationReader, $symfonyCache, $nsList); } diff --git a/src/Types/EnumType.php b/src/Types/EnumType.php new file mode 100644 index 0000000000..a1a413b580 --- /dev/null +++ b/src/Types/EnumType.php @@ -0,0 +1,62 @@ + $enumName + */ + public function __construct(string $enumName, string $typeName, bool $useValues = false) + { + $this->useValues = $useValues; + + $values = []; + foreach ($enumName::cases() as $case) { + /** @var UnitEnum $case */ + $values[$this->serialize($case)] = ['value' => $case]; + } + + parent::__construct([ + 'name' => $typeName, + 'values' => $values, + ]); + } + + // phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + + /** + * @param mixed $value + */ + public function serialize($value): string + { + if (! $value instanceof UnitEnum) { + throw new InvalidArgumentException('Expected a Myclabs Enum instance'); + } + + if (! $this->useValues) { + return $value->name; + } + + assert($value instanceof BackedEnum); + assert(is_string($value->value)); + + return $value->value; + } +} diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index 6587b2f70d..ac524e016e 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -77,6 +77,14 @@ public function getClassList(): array } $refClass = new ReflectionClass($className); + // Enum's are not classes + if (interface_exists(\UnitEnum::class)) { + // @phpstan-ignore-next-line - Remove this after minimum supported PHP version is >= 8.1 + if ($refClass->isEnum()) { + continue; + } + } + $this->classes[$className] = $refClass; } } diff --git a/tests/AbstractQueryProviderTest.php b/tests/AbstractQueryProviderTest.php index 6f166f779f..304edd9caf 100644 --- a/tests/AbstractQueryProviderTest.php +++ b/tests/AbstractQueryProviderTest.php @@ -35,6 +35,7 @@ use TheCodingMachine\GraphQLite\Mappers\Root\BaseTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Root\CompositeRootTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Root\CompoundTypeMapper; +use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Root\IteratorTypeMapper; use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper; @@ -60,6 +61,7 @@ use TheCodingMachine\GraphQLite\Types\TypeResolver; use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory; use TheCodingMachine\GraphQLite\Utils\Namespaces\NS; +use UnitEnum; use function array_reverse; abstract class AbstractQueryProviderTest extends TestCase @@ -335,6 +337,9 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface $errorRootTypeMapper = new FinalRootTypeMapper($this->getTypeMapper()); $rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $this->getTypeMapper(), $topRootTypeMapper); $rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $this->getAnnotationReader(), $arrayAdapter, []); + if (interface_exists(UnitEnum::class)) { + $rootTypeMapper = new EnumTypeMapper($rootTypeMapper, $this->getAnnotationReader(), $arrayAdapter, []); + } $rootTypeMapper = new CompoundTypeMapper($rootTypeMapper, $topRootTypeMapper, $this->getTypeRegistry(), $this->getTypeMapper()); $rootTypeMapper = new IteratorTypeMapper($rootTypeMapper, $topRootTypeMapper); diff --git a/tests/Fixtures81/Integration/Controllers/ButtonController.php b/tests/Fixtures81/Integration/Controllers/ButtonController.php new file mode 100644 index 0000000000..d2b9e31971 --- /dev/null +++ b/tests/Fixtures81/Integration/Controllers/ButtonController.php @@ -0,0 +1,22 @@ +color = $color; + $this->size = $size; + $this->state = $state; + } + + /** + * @Field + */ + public function getColor(): Color + { + return $this->color; + } + + /** + * @Field + */ + public function getSize(): Size + { + return $this->size; + } + + /** + * @Field + */ + public function getState(): Position + { + return $this->state; + } +} diff --git a/tests/Fixtures81/Integration/Models/Color.php b/tests/Fixtures81/Integration/Models/Color.php new file mode 100644 index 0000000000..1d6c4d4a1a --- /dev/null +++ b/tests/Fixtures81/Integration/Models/Color.php @@ -0,0 +1,17 @@ +get(QueryProviderInterface::class), $container->get(RecursiveTypeMapperInterface::class), $container->get(TypeResolver::class), $container->get(RootTypeMapperInterface::class)); }, QueryProviderInterface::class => function(ContainerInterface $container) { - return new GlobControllerQueryProvider('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers', $container->get(FieldsBuilder::class), - $container->get(BasicAutoWiringContainer::class), $container->get(AnnotationReader::class), new Psr16Cache(new ArrayAdapter())); + $queryProvider = new GlobControllerQueryProvider( + 'TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Controllers', + $container->get(FieldsBuilder::class), + $container->get(BasicAutoWiringContainer::class), + $container->get(AnnotationReader::class), + new Psr16Cache(new ArrayAdapter()) + ); + + if (interface_exists(UnitEnum::class)) { + $queryProvider = new AggregateQueryProvider([ + $queryProvider, + new GlobControllerQueryProvider( + 'TheCodingMachine\\GraphQLite\\Fixtures81\\Integration\\Controllers', + $container->get(FieldsBuilder::class), + $container->get(BasicAutoWiringContainer::class), + $container->get(AnnotationReader::class), + new Psr16Cache(new ArrayAdapter()) + ) + ]); + } + return $queryProvider; }, FieldsBuilder::class => function(ContainerInterface $container) { return new FieldsBuilder( @@ -175,6 +197,7 @@ public function createContainer(array $overloadedServices = []): ContainerInterf new Psr16Cache($arrayAdapter) ); }, + // We use a second type mapper here so we can target the Models dir GlobTypeMapper::class.'2' => function(ContainerInterface $container) { $arrayAdapter = new ArrayAdapter(); $arrayAdapter->setLogger(new ExceptionLogger()); @@ -192,6 +215,17 @@ public function createContainer(array $overloadedServices = []): ContainerInterf PorpaginasTypeMapper::class => function(ContainerInterface $container) { return new PorpaginasTypeMapper($container->get(RecursiveTypeMapperInterface::class)); }, + EnumTypeMapper::class => function(ContainerInterface $container) { + return new EnumTypeMapper( + $container->get(RootTypeMapperInterface::class), + $container->get(AnnotationReader::class), + new ArrayAdapter(), + [ + $container->get(NamespaceFactory::class) + ->createNamespace('TheCodingMachine\\GraphQLite\\Fixtures81\\Integration\\Models') + ] + ); + }, TypeGenerator::class => function(ContainerInterface $container) { return new TypeGenerator( $container->get(AnnotationReader::class), @@ -232,9 +266,13 @@ public function createContainer(array $overloadedServices = []): ContainerInterf return new NullableTypeMapperAdapter(); }, 'rootTypeMapper' => function(ContainerInterface $container) { + // These are in reverse order of execution $errorRootTypeMapper = new FinalRootTypeMapper($container->get(RecursiveTypeMapperInterface::class)); $rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $container->get(RecursiveTypeMapperInterface::class), $container->get(RootTypeMapperInterface::class)); $rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $container->get(AnnotationReader::class), new ArrayAdapter(), [ $container->get(NamespaceFactory::class)->createNamespace('TheCodingMachine\\GraphQLite\\Fixtures\\Integration\\Models') ]); + if (interface_exists(UnitEnum::class)) { + $rootTypeMapper = new EnumTypeMapper($rootTypeMapper, $container->get(AnnotationReader::class), new ArrayAdapter(), [ $container->get(NamespaceFactory::class)->createNamespace('TheCodingMachine\\GraphQLite\\Fixtures81\\Integration\\Models') ]); + } $rootTypeMapper = new CompoundTypeMapper($rootTypeMapper, $container->get(RootTypeMapperInterface::class), $container->get(TypeRegistry::class), $container->get(RecursiveTypeMapperInterface::class)); $rootTypeMapper = new IteratorTypeMapper($rootTypeMapper, $container->get(RootTypeMapperInterface::class)); return $rootTypeMapper; @@ -261,10 +299,33 @@ public function createContainer(array $overloadedServices = []): ContainerInterf return $parameterMiddlewarePipe; } ]; + + if (interface_exists(UnitEnum::class)) { + // Register another instance of GlobTypeMapper to process our PHP 8.1 enums and/or other + // 8.1 supported features. + $services[GlobTypeMapper::class.'3'] = function(ContainerInterface $container) { + $arrayAdapter = new ArrayAdapter(); + $arrayAdapter->setLogger(new ExceptionLogger()); + return new GlobTypeMapper($container->get(NamespaceFactory::class)->createNamespace('TheCodingMachine\\GraphQLite\\Fixtures81\\Integration\\Models'), + $container->get(TypeGenerator::class), + $container->get(InputTypeGenerator::class), + $container->get(InputTypeUtils::class), + $container->get(BasicAutoWiringContainer::class), + $container->get(AnnotationReader::class), + $container->get(NamingStrategyInterface::class), + $container->get(RecursiveTypeMapperInterface::class), + new Psr16Cache($arrayAdapter) + ); + }; + } + $container = new Picotainer($overloadedServices + $services); $container->get(TypeResolver::class)->registerSchema($container->get(Schema::class)); $container->get(TypeMapperInterface::class)->addTypeMapper($container->get(GlobTypeMapper::class)); $container->get(TypeMapperInterface::class)->addTypeMapper($container->get(GlobTypeMapper::class.'2')); + if (interface_exists(UnitEnum::class)) { + $container->get(TypeMapperInterface::class)->addTypeMapper($container->get(GlobTypeMapper::class.'3')); + } $container->get(TypeMapperInterface::class)->addTypeMapper($container->get(PorpaginasTypeMapper::class)); $container->get(RootTypeMapperInterface::class)->setNext($container->get('rootTypeMapper')); @@ -1147,6 +1208,42 @@ public function testEndToEndEnums3(): void ], $this->getSuccessResult($result)); } + /** + * @requires PHP >= 8.1 + * + * @group test-only + */ + public function testEndToEndNativeEnums(): void + { + /** + * @var Schema $schema + */ + $schema = $this->mainContainer->get(Schema::class); + + $gql = ' + query { + button(color: red, size: M, state: Off) { + color + size + state + } + } + '; + + $result = GraphQL::executeQuery( + $schema, + $gql + ); + + $this->assertSame([ + 'button' => [ + 'color' => 'red', + 'size' => 'M', + 'state' => 'Off', + ] + ], $this->getSuccessResult($result)); + } + public function testEndToEndDateTime(): void { /** diff --git a/website/docs/annotations-reference.md b/website/docs/annotations-reference.md index 7423bdc9fc..fc44b410a3 100644 --- a/website/docs/annotations-reference.md +++ b/website/docs/annotations-reference.md @@ -31,13 +31,14 @@ name | *no* | string | The name of the mutation. If skipped, the ## @Type annotation -The `@Type` annotation is used to declare a GraphQL object type. +The `@Type` annotation is used to declare a GraphQL object type. This is used with standard output +types, as well as enum types. For input types, use the [@Input annotation](#input-annotation) directly on the input type or a [@Factory annoation](#factory-annotation) to make/return an input type. **Applies on**: classes. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- -class | *no* | string | The targeted class. If no class is passed, the type applies to the current class. The current class is assumed to be an entity. If the "class" attribute is passed, [the class annotated with `@Type` is a service](external-type-declaration.mdx). +class | *no* | string | The targeted class/enum for the actual type. If no "class" attribute is passed, the type applies to the current class/enum. The current class/enum is assumed to be an entity (not service). If the "class" attribute *is passed*, [the class/enum annotated with `@Type` becomes a service](external-type-declaration.mdx). name | *no* | string | The name of the GraphQL type generated. If not passed, the name of the class is used. If the class ends with "Type", the "Type" suffix is removed default | *no* | bool | Defaults to *true*. Whether the targeted PHP class should be mapped by default to this type. external | *no* | bool | Whether this is an [external type declaration](external-type-declaration.mdx) or not. You usually do not need to use this attribute since this value defaults to true if a "class" attribute is set. This is only useful if you are declaring a type with no PHP class mapping using the "name" attribute. @@ -268,7 +269,9 @@ Attribute | Compulsory | Type | Definition *for* | *yes* | string | The name of the PHP parameter *constraint* | *yes | annotation | One (or many) Symfony validation annotations. -## @EnumType annotation +## ~~@EnumType annotation~~ + +*Deprecated: Use [PHP 8.1's native Enums](https://www.php.net/manual/en/language.types.enumerations.php) instead with a [@Type](#type-annotation).* The `@EnumType` annotation is used to change the name of a "Enum" type. Note that if you do not want to change the name, the annotation is optionnal. Any object extending `MyCLabs\Enum\Enum` diff --git a/website/docs/type-mapping.mdx b/website/docs/type-mapping.mdx index 4d467150cb..c569119196 100644 --- a/website/docs/type-mapping.mdx +++ b/website/docs/type-mapping.mdx @@ -423,13 +423,61 @@ public function companyOrContact(int $id) ## Enum types -Available in GraphQLite 4.0+ +PHP 8.1 introduced native support for Enums. GraphQLite now also supports native enums as of version 5.1. + +```php +#[Type] +enum Status: string +{ + case ON = 'on'; + case OFF = 'off'; + case PENDING = 'pending'; +} +``` + +```php +/** + * @return User[] + */ +#[Query] +public function users(Status $status): array +{ + if ($status === Status::ON) { + // Your logic + } + // ... +} +``` + +```graphql +query users($status: Status!) {} + users(status: $status) { + id + } +} +``` + +By default, the name of the GraphQL enum type will be the name of the class. If you have a naming conflict (two classes +that live in different namespaces with the same class name), you can solve it using the `name` property on the `@Type` annotation: + +```php +namespace Model\User; + +#[Type(name: "UserStatus")] +enum Status: string +{ + // ... +} +``` + -PHP has no native support for enum types. Hopefully, there are a number of PHP libraries that emulate enums in PHP. -The most commonly used library is [myclabs/php-enum](https://github.com/myclabs/php-enum) and GraphQLite comes with -native support for it. +### Enum types with myclabs/php-enum -You will first need to install [myclabs/php-enum](https://github.com/myclabs/php-enum): +
+ This implementation is now deprecated and will be removed in the future. You are advised to use native enums instead. +
+ +*Prior to version 5.1, GraphQLite only supported Enums through the 3rd party library, [myclabs/php-enum](https://github.com/myclabs/php-enum). If you'd like to use this implementation you'll first need to add this library as a dependency to your application.* ```bash $ composer require myclabs/php-enum @@ -557,12 +605,6 @@ in your project. By default, GraphQLite will look for "Enum" classes in the name reason, your enum classes MUST be in one of the namespaces declared for the types in your GraphQLite configuration file. - -
There are many enumeration library in PHP and you might be using another library. -If you want to add support for your own library, this is not extremely difficult to do. You need to register a custom -"RootTypeMapper" with GraphQLite. You can learn more about type mappers in the "internals" documentation -and copy/paste and adapt the root type mapper used for myclabs/php-enum.
- ## Deprecation of fields You can mark a field as deprecated in your GraphQL Schema by just annotating it with the `@deprecated` PHPDoc annotation. Note that a description (reason) is required for the annotation to be rendered.