Skip to content
Open
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
20 changes: 4 additions & 16 deletions src/Definition/Concerns/GraphQLDescribable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
102 changes: 5 additions & 97 deletions src/Definition/PhpEnumType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<UnitEnum> */
protected string $enumClass;

Expand All @@ -39,16 +30,16 @@ 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),
];
}

parent::__construct(
[
'name' => $name ?? $this->baseName($enumClass),
'values' => $enumDefinitions,
'description' => $this->extractDescription($reflection),
'description' => DescriptionExtractor::extract($reflection),
]
);
}
Expand Down Expand Up @@ -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;
}
}
22 changes: 22 additions & 0 deletions src/Utils/Reflection/DeprecationExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Worksome\GraphQLHelpers\Utils\Reflection;

use GraphQL\Type\Definition\Deprecated;
use ReflectionEnumUnitCase;

class DeprecationExtractor
{
public static function extract(ReflectionEnumUnitCase $reflection): string|null
{
$deprecations = $reflection->getAttributes(Deprecated::class);

if (count($deprecations) === 1) {
return $deprecations[0]->newInstance()->reason;
}
Comment thread
owenvoke marked this conversation as resolved.

return null;
}
}
53 changes: 53 additions & 0 deletions src/Utils/Reflection/DescribedByExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Worksome\GraphQLHelpers\Utils\Reflection;

use ReflectionEnumUnitCase;
use ValueError;
use Worksome\GraphQLHelpers\Definition\Attributes\CasesDescribedBy;

class DescribedByExtractor
{
public static function extract(ReflectionEnumUnitCase $reflection): string|null
{
$enum = $reflection->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;
}
}
36 changes: 36 additions & 0 deletions src/Utils/Reflection/DescriptionExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Worksome\GraphQLHelpers\Utils\Reflection;

use GraphQL\Type\Definition\Description;
use GraphQL\Utils\PhpDoc;
use ReflectionEnum;
use ReflectionEnumUnitCase;

class DescriptionExtractor
{
/** @phpstan-ignore-next-line */
public static function extract(
ReflectionEnum|ReflectionEnumUnitCase $reflection,
): string|null {
$descriptions = $reflection->getAttributes(Description::class);

Comment thread
owenvoke marked this conversation as resolved.
if (count($descriptions) === 1) {
return $descriptions[0]->newInstance()->description;
}

if (
$reflection instanceof ReflectionEnumUnitCase
&& $description = DescribedByExtractor::extract($reflection)
) {
return $description;
Comment thread
owenvoke marked this conversation as resolved.
}

$comment = $reflection->getDocComment();
$unpadded = PhpDoc::unpad($comment);

return PhpDoc::unwrap($unpadded);
Comment on lines +31 to +34

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to fallback to the PhpDoc comments? 👀

}
}
29 changes: 29 additions & 0 deletions tests/Unit/Utils/Reflection/DeprecationExtractorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Worksome\GraphQLHelpers\Tests\Unit\Utils\Reflection;

use GraphQL\Type\Definition\Deprecated;
use ReflectionEnumUnitCase;
use Worksome\GraphQLHelpers\Utils\Reflection\DeprecationExtractor;

enum DeprecationExtractorDummyEnum
{
#[Deprecated('This is deprecated.')]
case Deprecated;

case NotDeprecated;
}

it(
'can extract the deprecation reason from an enum case',
function (DeprecationExtractorDummyEnum $enum, string|null $reason) {
$reflection = new ReflectionEnumUnitCase($enum, $enum->name);

expect(DeprecationExtractor::extract($reflection))->toBe($reason);
}
)->with([
[DeprecationExtractorDummyEnum::Deprecated, 'This is deprecated.'],
[DeprecationExtractorDummyEnum::NotDeprecated, null],
]);
74 changes: 74 additions & 0 deletions tests/Unit/Utils/Reflection/DescribedByExtractorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php
Comment thread
owenvoke marked this conversation as resolved.

declare(strict_types=1);

namespace Worksome\GraphQLHelpers\Tests\Unit\Utils\Reflection;

use ReflectionEnumUnitCase;
use ValueError;
use Worksome\GraphQLHelpers\Definition\Attributes\CasesDescribedBy;
use Worksome\GraphQLHelpers\Utils\Reflection\DescribedByExtractor;

Comment thread
owenvoke marked this conversation as resolved.
#[CasesDescribedBy(describer: 'description')]
enum DescribedByExtractorDummyEnum
{
Comment thread
owenvoke marked this conversation as resolved.
case PascalCase;

public function description(): string
{
return 'Description from the describer';
}
}

#[CasesDescribedBy(describer: 'description')]
enum DescribedByExtractorDummyEnumMissingDescriber
{
case PascalCase;
}

#[CasesDescribedBy(describer: 'description')]
enum DescribedByExtractorDummyEnumWithNonStringReturnType
{
case PascalCase;

public function description(): int
{
return 123;
}
}
Comment thread
owenvoke marked this conversation as resolved.

it('can resolve descriptions from describer', function () {
$reflection = new ReflectionEnumUnitCase(DescribedByExtractorDummyEnum::class, 'PascalCase');

expect(DescribedByExtractor::extract($reflection))->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
)
);
Loading