|
| 1 | +<?php |
| 2 | +declare(strict_types=1); |
| 3 | + |
| 4 | +namespace App\GraphQL\Directives; |
| 5 | + |
| 6 | +use App\Auth\Abilities\AbilityExposure; |
| 7 | +use GraphQL\Language\AST\EnumTypeDefinitionNode; |
| 8 | +use GraphQL\Language\AST\EnumValueDefinitionNode; |
| 9 | +use GraphQL\Language\AST\NameNode; |
| 10 | +use GraphQL\Language\AST\NodeList; |
| 11 | +use GraphQL\Language\AST\StringValueNode; |
| 12 | +use GraphQL\Language\AST\TypeDefinitionNode; |
| 13 | +use Nuwave\Lighthouse\Exceptions\DefinitionException; |
| 14 | +use Nuwave\Lighthouse\Schema\AST\DocumentAST; |
| 15 | +use Nuwave\Lighthouse\Schema\Directives\BaseDirective; |
| 16 | +use Nuwave\Lighthouse\Support\Contracts\TypeManipulator; |
| 17 | + |
| 18 | +/** |
| 19 | + * Builds a GraphQL ability enum's values from the {@see \App\Auth\Abilities\Exposed} |
| 20 | + * cases of a PHP ability enum, so the public ability vocabulary is generated from |
| 21 | + * the single source of truth and cannot drift. A new exposed case needs no schema |
| 22 | + * edit; an unannotated case never reaches the schema. |
| 23 | + * |
| 24 | + * Value names use {@see AbilityExposure::exposedName} (`Str::snake` of the case |
| 25 | + * name) — the same derivation the `abilities()` resolvers emit — and each value |
| 26 | + * carries the case's required `Exposed` description into introspection. |
| 27 | + */ |
| 28 | +class AbilityEnumDirective extends BaseDirective implements TypeManipulator |
| 29 | +{ |
| 30 | + /** |
| 31 | + * The directive's SDL definition. |
| 32 | + * |
| 33 | + * @return string |
| 34 | + */ |
| 35 | + public static function definition(): string |
| 36 | + { |
| 37 | + // phpcs:disable |
| 38 | + return /** @lang GraphQL */ <<<'GRAPHQL' |
| 39 | +""" |
| 40 | +Build this enum's values from the `Exposed` cases of the given PHP ability enum, |
| 41 | +named by `Str::snake` of the case name — the same derivation the `abilities()` |
| 42 | +resolvers emit. Keeps the public ability vocabulary locked to the PHP enum. |
| 43 | +""" |
| 44 | +directive @abilityEnum( |
| 45 | + """ |
| 46 | + Fully-qualified ability enum class, e.g. "App\\Auth\\Abilities\\SubmissionAbility". |
| 47 | + """ |
| 48 | + enum: String! |
| 49 | +) on ENUM |
| 50 | +GRAPHQL; |
| 51 | + // phpcs:enable |
| 52 | + } |
| 53 | + |
| 54 | + /** |
| 55 | + * Append one enum value per exposed PHP enum case. |
| 56 | + * |
| 57 | + * @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST |
| 58 | + * @param \GraphQL\Language\AST\TypeDefinitionNode&\GraphQL\Language\AST\Node $typeDefinition |
| 59 | + * @return void |
| 60 | + * @throws \Nuwave\Lighthouse\Exceptions\DefinitionException |
| 61 | + */ |
| 62 | + public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition): void |
| 63 | + { |
| 64 | + if (! $typeDefinition instanceof EnumTypeDefinitionNode) { |
| 65 | + throw new DefinitionException( |
| 66 | + '@abilityEnum may only decorate an enum type, used on ' . $typeDefinition->getName()->value . '.' |
| 67 | + ); |
| 68 | + } |
| 69 | + |
| 70 | + $enum = $this->directiveArgValue('enum'); |
| 71 | + if (! is_string($enum) || ! enum_exists($enum)) { |
| 72 | + throw new DefinitionException( |
| 73 | + "@abilityEnum on {$typeDefinition->getName()->value} requires a valid `enum` class, got " |
| 74 | + . var_export($enum, true) . '.' |
| 75 | + ); |
| 76 | + } |
| 77 | + |
| 78 | + $exposed = AbilityExposure::exposed($enum); |
| 79 | + if ($exposed === []) { |
| 80 | + throw new DefinitionException( |
| 81 | + "@abilityEnum on {$typeDefinition->getName()->value}: {$enum} exposes no cases." |
| 82 | + ); |
| 83 | + } |
| 84 | + |
| 85 | + $existing = []; |
| 86 | + foreach ($typeDefinition->values as $value) { |
| 87 | + $existing[$value->name->value] = true; |
| 88 | + } |
| 89 | + |
| 90 | + foreach ($exposed as $exposedName => $exposure) { |
| 91 | + if (isset($existing[$exposedName])) { |
| 92 | + continue; |
| 93 | + } |
| 94 | + // Built as AST nodes, not spliced into SDL and re-parsed: a |
| 95 | + // StringValueNode carries the description verbatim, so no |
| 96 | + // character in it can break block-string lexing. |
| 97 | + $typeDefinition->values[] = new EnumValueDefinitionNode([ |
| 98 | + 'name' => new NameNode(['value' => $exposedName]), |
| 99 | + 'description' => new StringValueNode([ |
| 100 | + 'value' => $exposure['description'], |
| 101 | + 'block' => true, |
| 102 | + ]), |
| 103 | + 'directives' => new NodeList([]), |
| 104 | + ]); |
| 105 | + } |
| 106 | + } |
| 107 | +} |
0 commit comments