From 6dcec26392f35594796de417334615de0c4cfc8a Mon Sep 17 00:00:00 2001 From: Dorian Savina Date: Thu, 25 Nov 2021 15:30:03 +0100 Subject: [PATCH 01/23] Implement native enum type + mapper --- src/AnnotationReader.php | 4 - src/Annotations/EnumType.php | 14 +- src/Mappers/Root/EnumTypeMapper.php | 198 ++++++++++++++++++++++++++++ src/SchemaFactory.php | 8 ++ src/Types/EnumType.php | 62 +++++++++ 5 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 src/Mappers/Root/EnumTypeMapper.php create mode 100644 src/Types/EnumType.php diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index a2becbc567..9fa9e9065c 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; @@ -567,9 +566,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..c340f8013c 100644 --- a/src/Annotations/EnumType.php +++ b/src/Annotations/EnumType.php @@ -21,12 +21,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 +40,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/Mappers/Root/EnumTypeMapper.php b/src/Mappers/Root/EnumTypeMapper.php new file mode 100644 index 0000000000..c38669177f --- /dev/null +++ b/src/Mappers/Root/EnumTypeMapper.php @@ -0,0 +1,198 @@ +=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; + } + + /** + * @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->getEnumTypeAnnotation($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->useValues() && + $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->getEnumTypeAnnotation($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 b80e066fe8..315c8ae50c 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; @@ -50,10 +51,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; @@ -338,6 +341,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; + } +} From 73f3420eb87b9c3094dfa6295f3530965dea3fe0 Mon Sep 17 00:00:00 2001 From: Dorian Savina Date: Mon, 14 Feb 2022 13:20:20 +0100 Subject: [PATCH 02/23] Ignore test output in directory 'build/' --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index be23bb574f..ee8d2be718 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/build/ /vendor/ /composer.lock /src/Tests/ From 02769b2aa5f01e3d1c5bb2ba8d8153b05d4e50eb Mon Sep 17 00:00:00 2001 From: Dorian Savina Date: Mon, 14 Feb 2022 13:26:43 +0100 Subject: [PATCH 03/23] Run tests on PHP 8.1 --- .github/workflows/continuous_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 8dd971d5a6..caec6f4061 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.2', '7.3', '7.4', '8.0'] + php-version: ['7.2', '7.3', '7.4', '8.0', '8.1'] fail-fast: false steps: From d4439e044e062068a4cb7e4136d1db2126ad6dc5 Mon Sep 17 00:00:00 2001 From: Dorian Savina Date: Mon, 14 Feb 2022 13:09:07 +0100 Subject: [PATCH 04/23] Test native enum support if available - PHP >= 8.1 - interface_exists(UnitEnum) --- tests/AbstractQueryProviderTest.php | 5 ++ .../Controllers/ButtonController.php | 22 +++++ .../Fixtures81/Integration/Models/Button.php | 60 +++++++++++++ tests/Fixtures81/Integration/Models/Color.php | 16 ++++ .../Integration/Models/Position.php | 16 ++++ tests/Fixtures81/Integration/Models/Size.php | 17 ++++ tests/Integration/EndToEndTest.php | 84 ++++++++++++++++++- 7 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures81/Integration/Controllers/ButtonController.php create mode 100644 tests/Fixtures81/Integration/Models/Button.php create mode 100644 tests/Fixtures81/Integration/Models/Color.php create mode 100644 tests/Fixtures81/Integration/Models/Position.php create mode 100644 tests/Fixtures81/Integration/Models/Size.php 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..1bc1f4d6a9 --- /dev/null +++ b/tests/Fixtures81/Integration/Models/Color.php @@ -0,0 +1,16 @@ +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( @@ -233,6 +255,9 @@ public function createContainer(array $overloadedServices = []): ContainerInterf $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; @@ -259,10 +284,31 @@ public function createContainer(array $overloadedServices = []): ContainerInterf return $parameterMiddlewarePipe; } ]; + + if (interface_exists(UnitEnum::class)) { + $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')); @@ -1146,6 +1192,40 @@ public function testEndToEndEnums3(): void ], $this->getSuccessResult($result)); } + /** + * @requires PHP >= 8.1 + */ + 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 { /** From 453b85cb2ccbec7e6fc919084ccaab3f8dc5aa0d Mon Sep 17 00:00:00 2001 From: Dorian Savina Date: Mon, 14 Feb 2022 17:33:30 +0100 Subject: [PATCH 05/23] Disable static code analysis on 8.1-specific code files --- phpstan.neon | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/phpstan.neon b/phpstan.neon index d572736ede..ddea5b2dc4 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: max checkGenericClassInNonGenericObjectType: false reportUnmatchedIgnoredErrors: false From c3a692014e26e9e902b19131e0fa580d7e6ddc6e Mon Sep 17 00:00:00 2001 From: Dorian Savina Date: Thu, 24 Feb 2022 12:08:43 +0100 Subject: [PATCH 06/23] [wip] Use default Type annotation for enums instead of EnumType --- src/Annotations/Type.php | 15 ++++++++++++++- src/Mappers/Root/EnumTypeMapper.php | 6 +++--- tests/Fixtures81/Integration/Models/Color.php | 4 ++-- tests/Fixtures81/Integration/Models/Position.php | 4 ++-- tests/Fixtures81/Integration/Models/Size.php | 4 ++-- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Annotations/Type.php b/src/Annotations/Type.php index 6d1c3d1ed0..e63b2d90f5 100644 --- a/src/Annotations/Type.php +++ b/src/Annotations/Type.php @@ -45,11 +45,14 @@ 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; @@ -69,6 +72,8 @@ public function __construct(array $attributes = [], ?string $class = null, ?stri } $this->selfType = ! $external; + + $this->useEnumValues = $useEnumValues ?? $attributes['useEnumValues'] ?? false; } /** @@ -123,4 +128,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/Root/EnumTypeMapper.php b/src/Mappers/Root/EnumTypeMapper.php index c38669177f..08b68bd456 100644 --- a/src/Mappers/Root/EnumTypeMapper.php +++ b/src/Mappers/Root/EnumTypeMapper.php @@ -122,12 +122,12 @@ private function mapByClassName(string $enumClass): ?EnumType $reflectionEnum = new ReflectionEnum($enumClass); - $typeAnnotation = $this->annotationReader->getEnumTypeAnnotation($reflectionEnum); + $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->useValues() && + $typeAnnotation->useEnumValues() && $reflectionEnum->isBacked() && (string) $reflectionEnum->getBackingType() === 'string'; @@ -138,7 +138,7 @@ private function mapByClassName(string $enumClass): ?EnumType private function getTypeName(ReflectionClass $reflectionClass): string { - $typeAnnotation = $this->annotationReader->getEnumTypeAnnotation($reflectionClass); + $typeAnnotation = $this->annotationReader->getTypeAnnotation($reflectionClass); return ($typeAnnotation !== null ? $typeAnnotation->getName() : null) ?? $reflectionClass->getShortName(); } diff --git a/tests/Fixtures81/Integration/Models/Color.php b/tests/Fixtures81/Integration/Models/Color.php index 1bc1f4d6a9..af36e4ce8a 100644 --- a/tests/Fixtures81/Integration/Models/Color.php +++ b/tests/Fixtures81/Integration/Models/Color.php @@ -4,10 +4,10 @@ namespace TheCodingMachine\GraphQLite\Fixtures81\Integration\Models; -use TheCodingMachine\GraphQLite\Annotations\EnumType; +use TheCodingMachine\GraphQLite\Annotations\Type; /** - * @EnumType(useValues=true) + * @Type(useEnumValues=true) */ enum Color: string { diff --git a/tests/Fixtures81/Integration/Models/Position.php b/tests/Fixtures81/Integration/Models/Position.php index 61b66e752f..07e9638929 100644 --- a/tests/Fixtures81/Integration/Models/Position.php +++ b/tests/Fixtures81/Integration/Models/Position.php @@ -4,10 +4,10 @@ namespace TheCodingMachine\GraphQLite\Fixtures81\Integration\Models; -use TheCodingMachine\GraphQLite\Annotations\EnumType; +use TheCodingMachine\GraphQLite\Annotations\Type; /** - * @EnumType + * @Type */ enum Position: int { diff --git a/tests/Fixtures81/Integration/Models/Size.php b/tests/Fixtures81/Integration/Models/Size.php index bde64bdefc..677e8d5eb2 100644 --- a/tests/Fixtures81/Integration/Models/Size.php +++ b/tests/Fixtures81/Integration/Models/Size.php @@ -4,10 +4,10 @@ namespace TheCodingMachine\GraphQLite\Fixtures81\Integration\Models; -use TheCodingMachine\GraphQLite\Annotations\EnumType; +use TheCodingMachine\GraphQLite\Annotations\Type; /** - * @EnumType + * @Type */ enum Size { From 178ee95958f5e779cded80762e693b6363b97f32 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 00:38:01 -0400 Subject: [PATCH 07/23] Added symfony/var-dumper so we can actually debug --- composer.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 880145d16b..f076682afe 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,8 @@ "phpstan/phpstan": "^0.12.94", "phpstan/phpstan-webmozart-assert": "^0.12.15", "phpunit/phpunit": "^8.5.19||^9.5.8", - "thecodingmachine/phpstan-strict-rules": "^0.12.1" + "thecodingmachine/phpstan-strict-rules": "^0.12.1", + "symfony/var-dumper": "^6.0" }, "suggest": { "beberlei/porpaginas": "If you want automatic pagination in your GraphQL types", @@ -67,5 +68,11 @@ "branch-alias": { "dev-master": "5.0.x-dev" } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "phpstan/extension-installer": true + } } } From 8cd54b860223fcd25634c36984a48daf8bb076f9 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 05:01:51 -0400 Subject: [PATCH 08/23] Excluded a bad rule for 8.1 code style --- phpcs.xml.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 7b358ea907..0f93d0209c 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -26,6 +26,7 @@ + From 8cb51384e393e7b868f42882842cdca184a7a54d Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 05:04:58 -0400 Subject: [PATCH 09/23] CS fixes --- src/Mappers/CompositeTypeMapper.php | 1 + src/Mappers/RecursiveTypeMapper.php | 8 +++++++- src/Mappers/Root/EnumTypeMapper.php | 30 ++++++++++++++++++++++------- 3 files changed, 31 insertions(+), 8 deletions(-) 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 4f59b94d64..2123d551c5 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. @@ -450,8 +451,13 @@ 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 index 08b68bd456..0381baf5d5 100644 --- a/src/Mappers/Root/EnumTypeMapper.php +++ b/src/Mappers/Root/EnumTypeMapper.php @@ -47,8 +47,12 @@ class EnumTypeMapper implements RootTypeMapperInterface /** * @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) - { + public function __construct( + RootTypeMapperInterface $next, + AnnotationReader $annotationReader, + CacheInterface $cacheService, + array $namespaces + ) { $this->next = $next; $this->annotationReader = $annotationReader; $this->cacheService = $cacheService; @@ -61,8 +65,12 @@ public function __construct(RootTypeMapperInterface $next, AnnotationReader $ann * * @return OutputType&GraphQLType */ - public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType - { + 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); @@ -72,12 +80,20 @@ public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector } /** - * @param (InputType&GraphQLType)|null $subType + * Maps into the appropriate InputType + * + * @param InputType|GraphQLType|null $subType * @param ReflectionMethod|ReflectionProperty $reflector * - * @return InputType&GraphQLType + * @return InputType|GraphQLType */ - public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType + public function toGraphQLInputType( + Type $type, + ?InputType $subType, + string $argumentName, + $reflector, + DocBlock $docBlockObj + ): InputType { $result = $this->map($type); if ($result === null) { From 408db3ff56e60c419df7257ea4ddf1e4226f9b5e Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 05:05:33 -0400 Subject: [PATCH 10/23] Additional comments --- tests/Integration/EndToEndTest.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index 1a9aa65342..96e9bc7651 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -195,6 +195,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()); @@ -212,6 +213,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), @@ -252,6 +264,7 @@ 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') ]); @@ -286,6 +299,8 @@ public function createContainer(array $overloadedServices = []): ContainerInterf ]; 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()); @@ -819,14 +834,14 @@ public function testEndToEnd2Iterators(): void } count } - + products { items { name price unauthorized } - count + count } } '; From db91015da51a1e5fbd76a0f9502710ea8aad54e5 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 05:07:35 -0400 Subject: [PATCH 11/23] Ignore Enum type mapping outside root --- src/Mappers/AbstractTypeMapper.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Mappers/AbstractTypeMapper.php b/src/Mappers/AbstractTypeMapper.php index 03d6eec4e2..fda6d5a0ab 100644 --- a/src/Mappers/AbstractTypeMapper.php +++ b/src/Mappers/AbstractTypeMapper.php @@ -31,6 +31,8 @@ use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; use Webmozart\Assert\Assert; +use function interface_exists; + /** * Analyzes classes and uses the @Type annotation to find the types automatically. * @@ -139,6 +141,14 @@ private function buildMap(): GlobTypeMapperCache $classes = $this->getClassList(); foreach ($classes as $className => $refClass) { + // Enum's are processed through the EnumTypeMapper. It may make more sense to handle + // this through the individual type mappers implemeenting this abstract. + if (interface_exists(UnitEnum::class)) { + if ($refClass->isEnum()) { + continue; + } + } + $annotationsCache = $this->mapClassToAnnotationsCache->get($refClass, function () use ($refClass, $className) { $annotationsCache = new GlobAnnotationsCache(); From 380d61e09759f6c2e3c3d6af790988bcbdbd88b8 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 05:14:23 -0400 Subject: [PATCH 12/23] Define UnitEnum namespace --- src/Mappers/AbstractTypeMapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mappers/AbstractTypeMapper.php b/src/Mappers/AbstractTypeMapper.php index fda6d5a0ab..713c1e928c 100644 --- a/src/Mappers/AbstractTypeMapper.php +++ b/src/Mappers/AbstractTypeMapper.php @@ -143,7 +143,7 @@ private function buildMap(): GlobTypeMapperCache foreach ($classes as $className => $refClass) { // Enum's are processed through the EnumTypeMapper. It may make more sense to handle // this through the individual type mappers implemeenting this abstract. - if (interface_exists(UnitEnum::class)) { + if (interface_exists(\UnitEnum::class)) { if ($refClass->isEnum()) { continue; } From bca078ee964d19a5255481c726cb509a80f288a5 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 05:55:13 -0400 Subject: [PATCH 13/23] Moved logic into more optimal location --- src/Mappers/AbstractTypeMapper.php | 8 -------- src/Utils/Namespaces/NS.php | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Mappers/AbstractTypeMapper.php b/src/Mappers/AbstractTypeMapper.php index 713c1e928c..76ed2624dd 100644 --- a/src/Mappers/AbstractTypeMapper.php +++ b/src/Mappers/AbstractTypeMapper.php @@ -141,14 +141,6 @@ private function buildMap(): GlobTypeMapperCache $classes = $this->getClassList(); foreach ($classes as $className => $refClass) { - // Enum's are processed through the EnumTypeMapper. It may make more sense to handle - // this through the individual type mappers implemeenting this abstract. - if (interface_exists(\UnitEnum::class)) { - if ($refClass->isEnum()) { - continue; - } - } - $annotationsCache = $this->mapClassToAnnotationsCache->get($refClass, function () use ($refClass, $className) { $annotationsCache = new GlobAnnotationsCache(); diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index d991ddc95e..d2aa64e556 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -76,6 +76,13 @@ public function getClassList(): array } $refClass = new ReflectionClass($className); + // Enum's are not classes + if (interface_exists(\UnitEnum::class)) { + if ($refClass->isEnum()) { + continue; + } + } + $this->classes[$className] = $refClass; } } From 185bc9ce110a57e6bc3eb23a5358c78814df14fb Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 17:47:25 -0400 Subject: [PATCH 14/23] Removed unused use statement --- src/Mappers/AbstractTypeMapper.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Mappers/AbstractTypeMapper.php b/src/Mappers/AbstractTypeMapper.php index 76ed2624dd..03d6eec4e2 100644 --- a/src/Mappers/AbstractTypeMapper.php +++ b/src/Mappers/AbstractTypeMapper.php @@ -31,8 +31,6 @@ use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; use Webmozart\Assert\Assert; -use function interface_exists; - /** * Analyzes classes and uses the @Type annotation to find the types automatically. * From 9c418d02013c419b09d960754274910606aa104d Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 17:55:46 -0400 Subject: [PATCH 15/23] Support v5 of var-dumper for < PHP8 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 5b14f83522..636fee238c 100644 --- a/composer.json +++ b/composer.json @@ -40,8 +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", - "symfony/var-dumper": "^6.0", + "phpunit/phpunit": "^8.5.19 || ^9.5.8", + "symfony/var-dumper": "^5.4 || ^6.0", "thecodingmachine/phpstan-strict-rules": "^1.0" }, "suggest": { From 7a22c4565e6f7b3f74c3ce79f7a1883662d4e39a Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 18:45:17 -0400 Subject: [PATCH 16/23] Remove root namespace for `UnitEnum` interface check --- src/Utils/Namespaces/NS.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index 19df8ca617..274cf80af3 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -78,7 +78,7 @@ public function getClassList(): array $refClass = new ReflectionClass($className); // Enum's are not classes - if (interface_exists(\UnitEnum::class)) { + if (interface_exists(UnitEnum::class)) { if ($refClass->isEnum()) { continue; } From 8a88dfcb22bd53c7d30487bb2b135286ec0331bf Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 18:52:26 -0400 Subject: [PATCH 17/23] Ignore PHPStan check for isEnum --- src/Utils/Namespaces/NS.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index 274cf80af3..5fb644facf 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -79,6 +79,7 @@ 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; } From f3018f1dfed905efdefe9a0753c9d98490ece259 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 18:58:48 -0400 Subject: [PATCH 18/23] Added root namespce back for UnitEnum --- src/Utils/Namespaces/NS.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index 5fb644facf..ac524e016e 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -78,7 +78,7 @@ public function getClassList(): array $refClass = new ReflectionClass($className); // Enum's are not classes - if (interface_exists(UnitEnum::class)) { + if (interface_exists(\UnitEnum::class)) { // @phpstan-ignore-next-line - Remove this after minimum supported PHP version is >= 8.1 if ($refClass->isEnum()) { continue; From c77866143812f237e6059c2dca87334551fe0a11 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Tue, 29 Mar 2022 20:28:20 -0400 Subject: [PATCH 19/23] Deprecated EnumType annotation and updated docs --- src/Annotations/EnumType.php | 2 + website/docs/annotations-reference.md | 7 ++- website/docs/type-mapping.mdx | 64 ++++++++++++++++++++++----- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/Annotations/EnumType.php b/src/Annotations/EnumType.php index c340f8013c..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({ diff --git a/website/docs/annotations-reference.md b/website/docs/annotations-reference.md index 7423bdc9fc..4471122557 100644 --- a/website/docs/annotations-reference.md +++ b/website/docs/annotations-reference.md @@ -31,7 +31,8 @@ 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. @@ -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. From 2109afa126bfb530fb630507491b2808981e9c79 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Mon, 4 Apr 2022 00:20:04 -0400 Subject: [PATCH 20/23] CS fixes --- src/Mappers/RecursiveTypeMapper.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Mappers/RecursiveTypeMapper.php b/src/Mappers/RecursiveTypeMapper.php index 6a79db687f..88966849e5 100644 --- a/src/Mappers/RecursiveTypeMapper.php +++ b/src/Mappers/RecursiveTypeMapper.php @@ -457,9 +457,6 @@ public function getOutputTypes(): array $types[$supportedClass] = $type; - - - if (isset($typeNames[$type->name])) { throw DuplicateMappingException::createForTypeName($type->name, $typeNames[$type->name], $supportedClass); } From 6256fbcc67866920d20e9e597e3be4579ec31fca Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Mon, 4 Apr 2022 00:37:52 -0400 Subject: [PATCH 21/23] Fixed failing test --- tests/Integration/EndToEndTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index 3d8c828e41..5b1926cdaa 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -1210,6 +1210,8 @@ public function testEndToEndEnums3(): void /** * @requires PHP >= 8.1 + * + * @group test-only */ public function testEndToEndNativeEnums(): void { @@ -1220,7 +1222,7 @@ public function testEndToEndNativeEnums(): void $gql = ' query { - button(color: red, size: M, state: Off) { + button(color: Red, size: M, state: Off) { color size state @@ -1235,7 +1237,7 @@ public function testEndToEndNativeEnums(): void $this->assertSame([ 'button' => [ - 'color' => 'red', + 'color' => 'Red', 'size' => 'M', 'state' => 'Off', ] From 3e01865669269582540173beb62da1244a03fdb3 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Mon, 4 Apr 2022 01:19:25 -0400 Subject: [PATCH 22/23] Fixed bug causing useEnumValues to not never be assigned --- src/Annotations/Type.php | 13 +++++++++---- tests/Fixtures81/Integration/Models/Color.php | 7 ++++--- tests/Integration/EndToEndTest.php | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Annotations/Type.php b/src/Annotations/Type.php index e63b2d90f5..f7c72af129 100644 --- a/src/Annotations/Type.php +++ b/src/Annotations/Type.php @@ -52,8 +52,14 @@ class Type * @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, ?bool $useEnumValues = 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) { @@ -66,14 +72,13 @@ 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; } $this->selfType = ! $external; - - $this->useEnumValues = $useEnumValues ?? $attributes['useEnumValues'] ?? false; } /** diff --git a/tests/Fixtures81/Integration/Models/Color.php b/tests/Fixtures81/Integration/Models/Color.php index af36e4ce8a..1d6c4d4a1a 100644 --- a/tests/Fixtures81/Integration/Models/Color.php +++ b/tests/Fixtures81/Integration/Models/Color.php @@ -6,9 +6,10 @@ use TheCodingMachine\GraphQLite\Annotations\Type; -/** - * @Type(useEnumValues=true) - */ +#[Type( + name: 'Color', + useEnumValues: true, +)] enum Color: string { case Green = 'green'; diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index 5b1926cdaa..330451e6f5 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -1222,7 +1222,7 @@ public function testEndToEndNativeEnums(): void $gql = ' query { - button(color: Red, size: M, state: Off) { + button(color: red, size: M, state: Off) { color size state @@ -1237,7 +1237,7 @@ public function testEndToEndNativeEnums(): void $this->assertSame([ 'button' => [ - 'color' => 'Red', + 'color' => 'red', 'size' => 'M', 'state' => 'Off', ] From 351619a40730e927c71b47c230f87920841f96a6 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Wed, 6 Apr 2022 21:26:40 -0400 Subject: [PATCH 23/23] Improved documentation around the class attribute for @Type --- website/docs/annotations-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/annotations-reference.md b/website/docs/annotations-reference.md index 4471122557..fc44b410a3 100644 --- a/website/docs/annotations-reference.md +++ b/website/docs/annotations-reference.md @@ -38,7 +38,7 @@ types, as well as enum types. For input types, use the [@Input annotation](#inp 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.