From fb1cc4b8e5e9d0663873537220fd973af0c27863 Mon Sep 17 00:00:00 2001 From: Owen Voke Date: Fri, 27 Feb 2026 15:14:38 +0000 Subject: [PATCH] feat: add extractor classes for reflection --- .../Concerns/GraphQLDescribable.php | 20 +--- src/Definition/PhpEnumType.php | 102 +----------------- src/Utils/Reflection/DeprecationExtractor.php | 22 ++++ src/Utils/Reflection/DescribedByExtractor.php | 53 +++++++++ src/Utils/Reflection/DescriptionExtractor.php | 36 +++++++ .../Reflection/DeprecationExtractorTest.php | 29 +++++ .../Reflection/DescribedByExtractorTest.php | 74 +++++++++++++ .../Reflection/DescriptionExtractorTest.php | 63 +++++++++++ 8 files changed, 286 insertions(+), 113 deletions(-) create mode 100644 src/Utils/Reflection/DeprecationExtractor.php create mode 100644 src/Utils/Reflection/DescribedByExtractor.php create mode 100644 src/Utils/Reflection/DescriptionExtractor.php create mode 100644 tests/Unit/Utils/Reflection/DeprecationExtractorTest.php create mode 100644 tests/Unit/Utils/Reflection/DescribedByExtractorTest.php create mode 100644 tests/Unit/Utils/Reflection/DescriptionExtractorTest.php diff --git a/src/Definition/Concerns/GraphQLDescribable.php b/src/Definition/Concerns/GraphQLDescribable.php index 46069ca..45f6ddc 100644 --- a/src/Definition/Concerns/GraphQLDescribable.php +++ b/src/Definition/Concerns/GraphQLDescribable.php @@ -4,11 +4,10 @@ namespace Worksome\GraphQLHelpers\Definition\Concerns; -use Exception; -use GraphQL\Type\Definition\Description; use Illuminate\Support\Str; -use ReflectionEnum; +use ReflectionEnumUnitCase; use UnitEnum; +use Worksome\GraphQLHelpers\Utils\Reflection\DescriptionExtractor; /** * @mixin UnitEnum @@ -19,20 +18,9 @@ trait GraphQLDescribable { public function description(): string { - $reflection = new ReflectionEnum(static::class); - $constReflection = $reflection->getCase($this->name); + $reflection = new ReflectionEnumUnitCase(static::class, $this->name); - $descriptionAttributes = $constReflection->getAttributes(Description::class); - - return match (count($descriptionAttributes)) { - 0 => self::friendlyDescription($this->name), - 1 => $descriptionAttributes[0]->newInstance()->description, - default => throw new Exception( - 'You cannot use more than 1 description attribute on ' . class_basename( - static::class - ) . '::' . $this->name - ), - }; + return DescriptionExtractor::extract($reflection) ?? self::friendlyDescription($this->name); } private function friendlyDescription(string $name): string diff --git a/src/Definition/PhpEnumType.php b/src/Definition/PhpEnumType.php index 21eea39..f40d14a 100644 --- a/src/Definition/PhpEnumType.php +++ b/src/Definition/PhpEnumType.php @@ -4,27 +4,18 @@ namespace Worksome\GraphQLHelpers\Definition; -use Exception; use GraphQL\Error\SerializationError; -use GraphQL\Type\Definition\Deprecated; -use GraphQL\Type\Definition\Description; use GraphQL\Type\Definition\EnumType; -use GraphQL\Utils\PhpDoc; use GraphQL\Utils\Utils; -use ReflectionClassConstant; use ReflectionEnum; -use ReflectionEnumUnitCase; use UnitEnum; -use Worksome\GraphQLHelpers\Definition\Attributes\CasesDescribedBy; +use Worksome\GraphQLHelpers\Utils\Reflection\DeprecationExtractor; +use Worksome\GraphQLHelpers\Utils\Reflection\DescriptionExtractor; use Worksome\GraphQLHelpers\Utils\UpperSnakeCaseConverter; /** @phpstan-import-type PartialEnumValueConfig from EnumType */ class PhpEnumType extends EnumType { - public const string MULTIPLE_DESCRIPTIONS_DISALLOWED = 'Using more than 1 Description attribute is not supported.'; - - public const string MULTIPLE_DEPRECATIONS_DISALLOWED = 'Using more than 1 Deprecated attribute is not supported.'; - /** @var class-string */ protected string $enumClass; @@ -39,8 +30,8 @@ public function __construct(string $enumClass, string|null $name = null) foreach ($reflection->getCases() as $case) { $enumDefinitions[UpperSnakeCaseConverter::convert($case->name)] = [ 'value' => $case->getValue(), - 'description' => $this->extractDescription($case), - 'deprecationReason' => $this->deprecationReason($case), + 'description' => DescriptionExtractor::extract($case), + 'deprecationReason' => DeprecationExtractor::extract($case), ]; } @@ -48,7 +39,7 @@ public function __construct(string $enumClass, string|null $name = null) [ 'name' => $name ?? $this->baseName($enumClass), 'values' => $enumDefinitions, - 'description' => $this->extractDescription($reflection), + 'description' => DescriptionExtractor::extract($reflection), ] ); } @@ -83,87 +74,4 @@ protected function baseName(string $class): string return end($parts); } - - /** @phpstan-ignore-next-line */ - protected function extractDescription( - ReflectionEnum|ReflectionEnumUnitCase $reflection, - ): string|null { - $descriptions = $reflection->getAttributes(Description::class); - - if (count($descriptions) === 1) { - return $descriptions[0]->newInstance()->description; - } - - if (count($descriptions) > 1) { - throw new Exception(self::MULTIPLE_DESCRIPTIONS_DISALLOWED); - } - - if ( - $reflection instanceof ReflectionEnumUnitCase - && $description = $this->resolveDescriptionFromDescriber($reflection) - ) { - return $description; - } - - $comment = $reflection->getDocComment(); - $unpadded = PhpDoc::unpad($comment); - - return PhpDoc::unwrap($unpadded); - } - - protected function deprecationReason(ReflectionClassConstant $reflection): string|null - { - $deprecations = $reflection->getAttributes(Deprecated::class); - - if (count($deprecations) === 1) { - return $deprecations[0]->newInstance()->reason; - } - - if (count($deprecations) > 1) { - throw new Exception(self::MULTIPLE_DEPRECATIONS_DISALLOWED); - } - - return null; - } - - private function resolveDescriptionFromDescriber(ReflectionEnumUnitCase $reflection): string|null - { - $enum = $reflection->getEnum(); - - $describers = $reflection->getEnum()->getAttributes(CasesDescribedBy::class); - - if (count($describers) === 0) { - return null; - } - - $describer = $describers[0]->newInstance()->describer; - - if (! $enum->hasMethod($describer)) { - throw new Exception( - sprintf( - 'The describer method `%s` does not exist on `%s`', - $describer, - $enum->name - ) - ); - } - - $description = $enum->getMethod($describer)->invoke($reflection->getValue()); - - if ($description === null) { - return null; - } - - if (! is_string($description)) { - throw new Exception( - sprintf( - 'The describer method `%s` on `%s` must return a string', - $describer, - $enum->name - ) - ); - } - - return $description; - } } diff --git a/src/Utils/Reflection/DeprecationExtractor.php b/src/Utils/Reflection/DeprecationExtractor.php new file mode 100644 index 0000000..7dbe762 --- /dev/null +++ b/src/Utils/Reflection/DeprecationExtractor.php @@ -0,0 +1,22 @@ +getAttributes(Deprecated::class); + + if (count($deprecations) === 1) { + return $deprecations[0]->newInstance()->reason; + } + + return null; + } +} diff --git a/src/Utils/Reflection/DescribedByExtractor.php b/src/Utils/Reflection/DescribedByExtractor.php new file mode 100644 index 0000000..d307b2c --- /dev/null +++ b/src/Utils/Reflection/DescribedByExtractor.php @@ -0,0 +1,53 @@ +getEnum(); + + $describers = $enum->getAttributes(CasesDescribedBy::class); + + if (count($describers) === 0) { + return null; + } + + $describer = $describers[0]->newInstance()->describer; + + if (! $enum->hasMethod($describer)) { + throw new ValueError( + sprintf( + 'The describer method `%s` does not exist on `%s`', + $describer, + $enum->name + ) + ); + } + + $description = $enum->getMethod($describer)->invoke($reflection->getValue()); + + if ($description === null) { + return null; + } + + if (! is_string($description)) { + throw new ValueError( + sprintf( + 'The describer method `%s` on `%s` must return a string', + $describer, + $enum->name + ) + ); + } + + return $description; + } +} diff --git a/src/Utils/Reflection/DescriptionExtractor.php b/src/Utils/Reflection/DescriptionExtractor.php new file mode 100644 index 0000000..435b453 --- /dev/null +++ b/src/Utils/Reflection/DescriptionExtractor.php @@ -0,0 +1,36 @@ +getAttributes(Description::class); + + if (count($descriptions) === 1) { + return $descriptions[0]->newInstance()->description; + } + + if ( + $reflection instanceof ReflectionEnumUnitCase + && $description = DescribedByExtractor::extract($reflection) + ) { + return $description; + } + + $comment = $reflection->getDocComment(); + $unpadded = PhpDoc::unpad($comment); + + return PhpDoc::unwrap($unpadded); + } +} diff --git a/tests/Unit/Utils/Reflection/DeprecationExtractorTest.php b/tests/Unit/Utils/Reflection/DeprecationExtractorTest.php new file mode 100644 index 0000000..f82c815 --- /dev/null +++ b/tests/Unit/Utils/Reflection/DeprecationExtractorTest.php @@ -0,0 +1,29 @@ +name); + + expect(DeprecationExtractor::extract($reflection))->toBe($reason); + } +)->with([ + [DeprecationExtractorDummyEnum::Deprecated, 'This is deprecated.'], + [DeprecationExtractorDummyEnum::NotDeprecated, null], +]); diff --git a/tests/Unit/Utils/Reflection/DescribedByExtractorTest.php b/tests/Unit/Utils/Reflection/DescribedByExtractorTest.php new file mode 100644 index 0000000..c50e61e --- /dev/null +++ b/tests/Unit/Utils/Reflection/DescribedByExtractorTest.php @@ -0,0 +1,74 @@ +toBe('Description from the describer'); +}); + +it('throws a ValueError when the describer method does not exist', function () { + $reflection = new ReflectionEnumUnitCase( + DescribedByExtractorDummyEnumMissingDescriber::class, + 'PascalCase' + ); + + DescribedByExtractor::extract($reflection); +})->throws( + ValueError::class, + sprintf( + 'The describer method `description` does not exist on `%s`', + DescribedByExtractorDummyEnumMissingDescriber::class + ) +); + +it('throws a ValueError when the describer does not return a string', function () { + $reflection = new ReflectionEnumUnitCase( + DescribedByExtractorDummyEnumWithNonStringReturnType::class, + 'PascalCase' + ); + + DescribedByExtractor::extract($reflection); +})->throws( + ValueError::class, + sprintf( + 'The describer method `description` on `%s` must return a string', + DescribedByExtractorDummyEnumWithNonStringReturnType::class + ) +); diff --git a/tests/Unit/Utils/Reflection/DescriptionExtractorTest.php b/tests/Unit/Utils/Reflection/DescriptionExtractorTest.php new file mode 100644 index 0000000..7bb1317 --- /dev/null +++ b/tests/Unit/Utils/Reflection/DescriptionExtractorTest.php @@ -0,0 +1,63 @@ +toBe('Dummy enum description'); +}); + +it( + 'can extract the description from an enum case', + function (DescriptionExtractorDummyEnum $enum, string|null $description) { + $reflection = new ReflectionEnumUnitCase($enum, $enum->name); + + expect(DescriptionExtractor::extract($reflection))->toBe($description); + } +)->with([ + [DescriptionExtractorDummyEnum::PascalCase, 'PascalCase description'], + [DescriptionExtractorDummyEnum::SCREAMING_SNAKE_CASE, 'SCREAMING_SNAKE_CASE description'], + [DescriptionExtractorDummyEnum::snake_case, 'snake_case description'], + [DescriptionExtractorDummyEnum::NoDescription, null], +]); + +it('can extract the description from an enum case with a describer', function () { + $reflection = new ReflectionEnumUnitCase(DescriptionExtractorDummyEnumWithDescribedBy::class, 'PascalCase'); + + expect(DescriptionExtractor::extract($reflection))->toBe('Description from the describer'); +});