From f2748f1907ec0ddcccf6e0f21e0bb447b5f15dfb Mon Sep 17 00:00:00 2001 From: Yup Date: Wed, 23 Oct 2024 12:46:49 +0300 Subject: [PATCH] Add field config decorator when building schema from SDL --- docs/class-reference.md | 13 +++++++++++-- src/Utils/ASTDefinitionBuilder.php | 26 ++++++++++++++++++++++---- src/Utils/BuildSchema.php | 27 +++++++++++++++++++++------ src/Utils/SchemaExtender.php | 16 +++++++++++----- tests/Utils/BuildSchemaTest.php | 19 ++++++++++++++++++- tests/Utils/SchemaExtenderTest.php | 21 +++++++++++++++++++-- 6 files changed, 102 insertions(+), 20 deletions(-) diff --git a/docs/class-reference.md b/docs/class-reference.md index 222eea17c..6d39d1fde 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -2409,6 +2409,7 @@ Build instance of @see \GraphQL\Type\Schema out of schema language definition (s See [schema definition language docs](schema-definition-language.md) for details. @phpstan-import-type TypeConfigDecorator from ASTDefinitionBuilder +@phpstan-import-type FieldConfigDecorator from ASTDefinitionBuilder @phpstan-type BuildSchemaOptions array{ assumeValid?: bool, @@ -2439,6 +2440,7 @@ assumeValidSDL?: bool * @param DocumentNode|Source|string $source * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator + * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator * * @param array $options * @@ -2452,7 +2454,12 @@ assumeValidSDL?: bool * @throws InvariantViolation * @throws SyntaxError */ -static function build($source, ?callable $typeConfigDecorator = null, array $options = []): GraphQL\Type\Schema +static function build( + $source, + ?callable $typeConfigDecorator = null, + array $options = [], + ?callable $fieldConfigDecorator = null +): GraphQL\Type\Schema ``` ```php @@ -2465,6 +2472,7 @@ static function build($source, ?callable $typeConfigDecorator = null, array $opt * has no resolve methods, so execution will use default resolvers. * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator + * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator * * @param array $options * @@ -2480,7 +2488,8 @@ static function build($source, ?callable $typeConfigDecorator = null, array $opt static function buildAST( GraphQL\Language\AST\DocumentNode $ast, ?callable $typeConfigDecorator = null, - array $options = [] + array $options = [], + ?callable $fieldConfigDecorator = null ): GraphQL\Type\Schema ``` diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index 1e5aee27c..96dbb47e6 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -52,6 +52,7 @@ * * @phpstan-type ResolveType callable(string, Node|null): Type&NamedType * @phpstan-type TypeConfigDecorator callable(array, Node&TypeDefinitionNode, array): array + * @phpstan-type FieldConfigDecorator callable(UnnamedFieldDefinitionConfig, FieldDefinitionNode, ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode): UnnamedFieldDefinitionConfig */ class ASTDefinitionBuilder { @@ -72,6 +73,13 @@ class ASTDefinitionBuilder */ private $typeConfigDecorator; + /** + * @var callable|null + * + * @phpstan-var FieldConfigDecorator|null + */ + private $fieldConfigDecorator; + /** @var array */ private array $cache; @@ -91,12 +99,14 @@ public function __construct( array $typeDefinitionsMap, array $typeExtensionsMap, callable $resolveType, - ?callable $typeConfigDecorator = null + ?callable $typeConfigDecorator = null, + ?callable $fieldConfigDecorator = null ) { $this->typeDefinitionsMap = $typeDefinitionsMap; $this->typeExtensionsMap = $typeExtensionsMap; $this->resolveType = $resolveType; $this->typeConfigDecorator = $typeConfigDecorator; + $this->fieldConfigDecorator = $fieldConfigDecorator; $this->cache = Type::builtInTypes(); } @@ -355,7 +365,7 @@ private function makeFieldDefMap(array $nodes): array $map = []; foreach ($nodes as $node) { foreach ($node->fields as $field) { - $map[$field->name->value] = $this->buildField($field); + $map[$field->name->value] = $this->buildField($field, $node); } } @@ -363,12 +373,14 @@ private function makeFieldDefMap(array $nodes): array } /** + * @param ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode $node + * * @throws \Exception * @throws Error * * @return UnnamedFieldDefinitionConfig */ - public function buildField(FieldDefinitionNode $field): array + public function buildField(FieldDefinitionNode $field, object $node): array { // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation @@ -376,13 +388,19 @@ public function buildField(FieldDefinitionNode $field): array /** @var OutputType&Type $type */ $type = $this->buildWrappedType($field->type); - return [ + $config = [ 'type' => $type, 'description' => $field->description->value ?? null, 'args' => $this->makeInputValues($field->arguments), 'deprecationReason' => $this->getDeprecationReason($field), 'astNode' => $field, ]; + + if ($this->fieldConfigDecorator !== null) { + $config = ($this->fieldConfigDecorator)($config, $field, $node); + } + + return $config; } /** diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index dd1885891..87f7d73b9 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -25,6 +25,7 @@ * See [schema definition language docs](schema-definition-language.md) for details. * * @phpstan-import-type TypeConfigDecorator from ASTDefinitionBuilder + * @phpstan-import-type FieldConfigDecorator from ASTDefinitionBuilder * * @phpstan-type BuildSchemaOptions array{ * assumeValid?: bool, @@ -56,6 +57,13 @@ class BuildSchema */ private $typeConfigDecorator; + /** + * @var callable|null + * + * @phpstan-var FieldConfigDecorator|null + */ + private $fieldConfigDecorator; + /** * @var array * @@ -72,11 +80,13 @@ class BuildSchema public function __construct( DocumentNode $ast, ?callable $typeConfigDecorator = null, - array $options = [] + array $options = [], + ?callable $fieldConfigDecorator = null ) { $this->ast = $ast; $this->typeConfigDecorator = $typeConfigDecorator; $this->options = $options; + $this->fieldConfigDecorator = $fieldConfigDecorator; } /** @@ -86,6 +96,7 @@ public function __construct( * @param DocumentNode|Source|string $source * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator + * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator * * @param array $options * @@ -102,13 +113,14 @@ public function __construct( public static function build( $source, ?callable $typeConfigDecorator = null, - array $options = [] + array $options = [], + ?callable $fieldConfigDecorator = null ): Schema { $doc = $source instanceof DocumentNode ? $source : Parser::parse($source); - return self::buildAST($doc, $typeConfigDecorator, $options); + return self::buildAST($doc, $typeConfigDecorator, $options, $fieldConfigDecorator); } /** @@ -120,6 +132,7 @@ public static function build( * has no resolve methods, so execution will use default resolvers. * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator + * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator * * @param array $options * @@ -135,9 +148,10 @@ public static function build( public static function buildAST( DocumentNode $ast, ?callable $typeConfigDecorator = null, - array $options = [] + array $options = [], + ?callable $fieldConfigDecorator = null ): Schema { - return (new self($ast, $typeConfigDecorator, $options))->buildSchema(); + return (new self($ast, $typeConfigDecorator, $options, $fieldConfigDecorator))->buildSchema(); } /** @@ -200,7 +214,8 @@ public function buildSchema(): Schema static function (string $typeName): Type { throw self::unknownType($typeName); }, - $this->typeConfigDecorator + $this->typeConfigDecorator, + $this->fieldConfigDecorator ); $directives = \array_map( diff --git a/src/Utils/SchemaExtender.php b/src/Utils/SchemaExtender.php index d1bb1095d..a766d2e47 100644 --- a/src/Utils/SchemaExtender.php +++ b/src/Utils/SchemaExtender.php @@ -39,6 +39,7 @@ /** * @phpstan-import-type TypeConfigDecorator from ASTDefinitionBuilder + * @phpstan-import-type FieldConfigDecorator from ASTDefinitionBuilder * @phpstan-import-type UnnamedArgumentConfig from Argument * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField * @@ -58,6 +59,7 @@ class SchemaExtender * @param array $options * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator + * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator * * @api * @@ -68,15 +70,17 @@ public static function extend( Schema $schema, DocumentNode $documentAST, array $options = [], - ?callable $typeConfigDecorator = null + ?callable $typeConfigDecorator = null, + ?callable $fieldConfigDecorator = null ): Schema { - return (new static())->doExtend($schema, $documentAST, $options, $typeConfigDecorator); + return (new static())->doExtend($schema, $documentAST, $options, $typeConfigDecorator, $fieldConfigDecorator); } /** * @param array $options * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator + * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator * * @throws \Exception * @throws \ReflectionException @@ -87,7 +91,8 @@ protected function doExtend( Schema $schema, DocumentNode $documentAST, array $options = [], - ?callable $typeConfigDecorator = null + ?callable $typeConfigDecorator = null, + ?callable $fieldConfigDecorator = null ): Schema { if ( ! ($options['assumeValid'] ?? false) @@ -146,7 +151,8 @@ function (string $typeName) use ($schema): Type { return $this->extendNamedType($existingType); }, - $typeConfigDecorator + $typeConfigDecorator, + $fieldConfigDecorator ); $this->extendTypeCache = []; @@ -511,7 +517,7 @@ protected function extendFieldMap(Type $type): array ); foreach ($extension->fields as $field) { - $newFieldMap[$field->name->value] = $this->astBuilder->buildField($field); + $newFieldMap[$field->name->value] = $this->astBuilder->buildField($field, $extension); } } } diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index 71933ce40..508d9369e 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -11,6 +11,7 @@ use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\EnumTypeDefinitionNode; +use GraphQL\Language\AST\FieldDefinitionNode; use GraphQL\Language\AST\InputObjectTypeDefinitionNode; use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\Node; @@ -26,6 +27,7 @@ use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; +use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\NamedType; @@ -40,6 +42,9 @@ use GraphQL\Utils\SchemaPrinter; use GraphQL\Validator\Rules\KnownDirectives; +/** + * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition + */ final class BuildSchemaTest extends TestCaseBase { use ArraySubsetAsserts; @@ -1340,7 +1345,15 @@ interface Hello { return ['description' => 'My description of ' . $node->getName()->value] + $defaultConfig; }; - $schema = BuildSchema::buildAST($doc, $typeConfigDecorator); + $fieldResolver = static fn (): string => 'OK'; + $fieldConfigDecorator = static function (array $defaultConfig, FieldDefinitionNode $node) use (&$fieldResolver): array { + $defaultConfig['resolve'] = $fieldResolver; + + /** @var UnnamedFieldDefinitionConfig $defaultConfig */ + return $defaultConfig; + }; + + $schema = BuildSchema::buildAST($doc, $typeConfigDecorator, [], $fieldConfigDecorator); $schema->getTypeMap(); self::assertSame(['Query', 'Color', 'Hello'], $decorated); @@ -1358,6 +1371,10 @@ interface Hello { self::assertInstanceOf(ObjectType::class, $query); self::assertSame('My description of Query', $query->description); + self::assertSame($fieldResolver, $query->getFields()['str']->resolveFn); + self::assertSame($fieldResolver, $query->getFields()['color']->resolveFn); + self::assertSame($fieldResolver, $query->getFields()['hello']->resolveFn); + self::assertArrayHasKey(1, $calls); [$defaultConfig, $node, $allNodesMap] = $calls[1]; // enum Color self::assertInstanceOf(EnumTypeDefinitionNode::class, $node); diff --git a/tests/Utils/SchemaExtenderTest.php b/tests/Utils/SchemaExtenderTest.php index 4e10d5bd1..712328ccb 100644 --- a/tests/Utils/SchemaExtenderTest.php +++ b/tests/Utils/SchemaExtenderTest.php @@ -9,6 +9,7 @@ use GraphQL\Error\SyntaxError; use GraphQL\GraphQL; use GraphQL\Language\AST\DocumentNode; +use GraphQL\Language\AST\FieldDefinitionNode; use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\NodeList; use GraphQL\Language\AST\SchemaDefinitionNode; @@ -24,6 +25,7 @@ use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; +use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\NamedType; @@ -38,6 +40,9 @@ use GraphQL\Utils\SchemaPrinter; use GraphQL\Validator\Rules\KnownDirectives; +/** + * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition + */ final class SchemaExtenderTest extends TestCaseBase { /** @param NamedType|Schema $obj */ @@ -1785,7 +1790,7 @@ public function testSupportsTypeConfigDecorator(): void } extend type Query { - defaultValue: String + fieldDecorated: String foo: Foo } '); @@ -1799,7 +1804,17 @@ public function testSupportsTypeConfigDecorator(): void return $typeConfig; }; - $extendedSchema = SchemaExtender::extend($schema, $documentNode, [], $typeConfigDecorator); + $resolveFn = static fn (): string => 'coming from field decorated resolver'; + $fieldConfigDecorator = static function (array $typeConfig, FieldDefinitionNode $fieldDefinitionNode) use ($resolveFn) { + /** @var UnnamedFieldDefinitionConfig $typeConfig */ + if ($fieldDefinitionNode->name->value === 'fieldDecorated') { + $typeConfig['resolve'] = $resolveFn; + } + + return $typeConfig; + }; + + $extendedSchema = SchemaExtender::extend($schema, $documentNode, [], $typeConfigDecorator, $fieldConfigDecorator); $query = /** @lang GraphQL */ ' { @@ -1807,6 +1822,7 @@ public function testSupportsTypeConfigDecorator(): void foo { value } + fieldDecorated } '; $result = GraphQL::executeQuery($extendedSchema, $query); @@ -1817,6 +1833,7 @@ public function testSupportsTypeConfigDecorator(): void 'foo' => [ 'value' => $fooValue, ], + 'fieldDecorated' => 'coming from field decorated resolver', ], ], $result->toArray()); }