diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a6c93c4..4549c2861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ You can find and compare releases at the [GitHub release page](https://github.co - Handle `null` parent of list in `ValuesOfCorrectType::getVisitor` - Allow sending both `query` and `queryId`, ignore `queryId` in that case - Fix `extend()` to preserve `repeatable` (#931) +- Preserve extended methods from class-based types in `SchemaExtender::extend()` ### Removed diff --git a/UPGRADE.md b/UPGRADE.md index 612565e15..796d16571 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -810,7 +810,7 @@ Starting from v0.7.0 the signature has changed to (note the new `$context` argum /** * @param mixed $object The parent resolved object * @param array $args Input arguments - * @param mixed $context The context object hat was passed to GraphQL::execute + * @param mixed $context The context object that was passed to GraphQL::execute * @param ResolveInfo $info ResolveInfo object * @return mixed */ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8e7fed45c..dbc05649a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -56,12 +56,12 @@ parameters: path: src/Language/AST/Node.php - - message: "#^Variable property access on GraphQL\\\\Language\\\\AST\\\\Node\\|null\\.$#" + message: "#^Variable property access on GraphQL\\\\Language\\\\AST\\\\Node\\.$#" count: 1 path: src/Language/Visitor.php - - message: "#^Variable property access on GraphQL\\\\Language\\\\AST\\\\Node\\.$#" + message: "#^Variable property access on GraphQL\\\\Language\\\\AST\\\\Node\\|null\\.$#" count: 1 path: src/Language/Visitor.php @@ -80,6 +80,11 @@ parameters: count: 2 path: src/Utils/AST.php + - + message: "#^Method GraphQL\\\\Utils\\\\SchemaExtender\\:\\:extendType\\(\\) should return GraphQL\\\\Type\\\\Definition\\\\ListOfType\\|\\(GraphQL\\\\Type\\\\Definition\\\\NamedType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|GraphQL\\\\Type\\\\Definition\\\\NonNull but returns GraphQL\\\\Type\\\\Definition\\\\Type\\.$#" + count: 1 + path: src/Utils/SchemaExtender.php + - message: "#^Variable property access on object\\.$#" count: 1 diff --git a/src/Type/Definition/AbstractType.php b/src/Type/Definition/AbstractType.php index e02571549..ba5c6581c 100644 --- a/src/Type/Definition/AbstractType.php +++ b/src/Type/Definition/AbstractType.php @@ -12,12 +12,12 @@ interface AbstractType { /** - * Resolves concrete ObjectType for given object value + * Resolves the concrete ObjectType for the given value. * - * @param object $objectValue - * @param mixed[] $context + * @param mixed $objectValue The resolved value for the object type + * @param mixed $context The context that was passed to GraphQL::execute() * - * @return mixed + * @return ObjectType|callable(): ObjectType|null */ public function resolveType($objectValue, $context, ResolveInfo $info); } diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index 59413b762..1e92df348 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -116,14 +116,6 @@ public function getInterfaces(): array return $this->interfaces; } - /** - * Resolves concrete ObjectType for given object value - * - * @param object $objectValue - * @param mixed $context - * - * @return Type|mixed|null - */ public function resolveType($objectValue, $context, ResolveInfo $info) { if (isset($this->config['resolveType'])) { diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index 73cda6cf3..13a4b32ac 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -161,16 +161,16 @@ public function getInterfaces(): array } /** - * @param mixed $value - * @param mixed $context + * @param mixed $objectValue The resolved value for the object type + * @param mixed $context The context that was passed to GraphQL::execute() * * @return bool|Deferred|null */ - public function isTypeOf($value, $context, ResolveInfo $info) + public function isTypeOf($objectValue, $context, ResolveInfo $info) { return isset($this->config['isTypeOf']) ? $this->config['isTypeOf']( - $value, + $objectValue, $context, $info ) diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index 60ab553f5..63a5befed 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -110,14 +110,6 @@ public function getTypes(): array return $this->types; } - /** - * Resolves concrete ObjectType for given object value - * - * @param object $objectValue - * @param mixed $context - * - * @return callable|mixed|null - */ public function resolveType($objectValue, $context, ResolveInfo $info) { if (isset($this->config['resolveType'])) { diff --git a/src/Utils/SchemaExtender.php b/src/Utils/SchemaExtender.php index f8c0132dc..55379ecfe 100644 --- a/src/Utils/SchemaExtender.php +++ b/src/Utils/SchemaExtender.php @@ -45,40 +45,31 @@ class SchemaExtender { public const SCHEMA_EXTENSION = 'SchemaExtension'; - /** @var Type[] */ - protected static $extendTypeCache; + /** @var array */ + protected static array $extendTypeCache; - /** @var mixed[] */ - protected static $typeExtensionsMap; + /** @var array> */ + protected static array $typeExtensionsMap; - /** @var ASTDefinitionBuilder */ - protected static $astBuilder; + protected static ASTDefinitionBuilder $astBuilder; /** - * @return TypeExtensionNode[]|null + * @param Type &NamedType $type + * + * @return array|null */ protected static function getExtensionASTNodes(NamedType $type): ?array { - if (! $type instanceof Type) { - return null; - } - - $name = $type->name; - if ($type->extensionASTNodes !== null) { - if (isset(static::$typeExtensionsMap[$name])) { - return array_merge($type->extensionASTNodes, static::$typeExtensionsMap[$name]); - } - - return $type->extensionASTNodes; - } - - return static::$typeExtensionsMap[$name] ?? null; + return array_merge( + $type->extensionASTNodes ?? [], + static::$typeExtensionsMap[$type->name] ?? [] + ); } /** * @throws Error */ - protected static function checkExtensionNode(Type $type, Node $node): void + protected static function assertTypeMatchesExtension(Type $type, Node $node): void { switch (true) { case $node instanceof ObjectTypeExtensionNode: @@ -134,10 +125,10 @@ protected static function extendScalarType(ScalarType $type): CustomScalarType return new CustomScalarType([ 'name' => $type->name, 'description' => $type->description, + 'serialize' => [$type, 'serialize'], + 'parseValue' => [$type, 'parseValue'], + 'parseLiteral' => [$type, 'parseLiteral'], 'astNode' => $type->astNode, - 'serialize' => $type->config['serialize'] ?? null, - 'parseValue' => $type->config['parseValue'] ?? null, - 'parseLiteral' => $type->config['parseLiteral'] ?? null, 'extensionASTNodes' => static::getExtensionASTNodes($type), ]); } @@ -147,11 +138,9 @@ protected static function extendUnionType(UnionType $type): UnionType return new UnionType([ 'name' => $type->name, 'description' => $type->description, - 'types' => static function () use ($type): array { - return static::extendPossibleTypes($type); - }, + 'types' => static fn (): array => static::extendUnionPossibleTypes($type), + 'resolveType' => [$type, 'resolveType'], 'astNode' => $type->astNode, - 'resolveType' => $type->config['resolveType'] ?? null, 'extensionASTNodes' => static::getExtensionASTNodes($type), ]); } @@ -161,7 +150,7 @@ protected static function extendEnumType(EnumType $type): EnumType return new EnumType([ 'name' => $type->name, 'description' => $type->description, - 'values' => static::extendValueMap($type), + 'values' => static::extendEnumValueMap($type), 'astNode' => $type->astNode, 'extensionASTNodes' => static::getExtensionASTNodes($type), ]); @@ -172,16 +161,14 @@ protected static function extendInputObjectType(InputObjectType $type): InputObj return new InputObjectType([ 'name' => $type->name, 'description' => $type->description, - 'fields' => static function () use ($type): array { - return static::extendInputFieldMap($type); - }, + 'fields' => static fn (): array => static::extendInputFieldMap($type), 'astNode' => $type->astNode, 'extensionASTNodes' => static::getExtensionASTNodes($type), ]); } /** - * @return mixed[] + * @return array> */ protected static function extendInputFieldMap(InputObjectType $type): array { @@ -201,9 +188,13 @@ protected static function extendInputFieldMap(InputObjectType $type): array $newFieldMap[$fieldName]['defaultValue'] = $field->defaultValue; } - $extensions = static::$typeExtensionsMap[$type->name] ?? null; - if ($extensions !== null) { - foreach ($extensions as $extension) { + if (isset(static::$typeExtensionsMap[$type->name])) { + /** + * Proven by @see assertTypeMatchesExtension(). + * + * @var InputObjectTypeExtensionNode $extension + */ + foreach (static::$typeExtensionsMap[$type->name] as $extension) { foreach ($extension->fields as $field) { $fieldName = $field->name->value; if (isset($oldFieldMap[$fieldName])) { @@ -219,12 +210,12 @@ protected static function extendInputFieldMap(InputObjectType $type): array } /** - * @return mixed[] + * @return array> */ - protected static function extendValueMap(EnumType $type): array + protected static function extendEnumValueMap(EnumType $type): array { $newValueMap = []; - /** @var EnumValueDefinition[] $oldValueMap */ + /** @var array $oldValueMap */ $oldValueMap = []; foreach ($type->getValues() as $value) { $oldValueMap[$value->name] = $value; @@ -240,9 +231,13 @@ protected static function extendValueMap(EnumType $type): array ]; } - $extensions = static::$typeExtensionsMap[$type->name] ?? null; - if ($extensions !== null) { - foreach ($extensions as $extension) { + if (isset(static::$typeExtensionsMap[$type->name])) { + /** + * Proven by @see assertTypeMatchesExtension(). + * + * @var EnumTypeExtensionNode $extension + */ + foreach (static::$typeExtensionsMap[$type->name] as $extension) { foreach ($extension->values as $value) { $valueName = $value->name->value; if (isset($oldValueMap[$valueName])) { @@ -258,18 +253,22 @@ protected static function extendValueMap(EnumType $type): array } /** - * @return ObjectType[] + * @return array Should be ObjectType, will be caught in schema validation */ - protected static function extendPossibleTypes(UnionType $type): array + protected static function extendUnionPossibleTypes(UnionType $type): array { $possibleTypes = array_map( [static::class, 'extendNamedType'], $type->getTypes() ); - $extensions = static::$typeExtensionsMap[$type->name] ?? null; - if ($extensions !== null) { - foreach ($extensions as $extension) { + if (isset(static::$typeExtensionsMap[$type->name])) { + /** + * Proven by @see assertTypeMatchesExtension(). + * + * @var UnionTypeExtensionNode $extension + */ + foreach (static::$typeExtensionsMap[$type->name] as $extension) { foreach ($extension->types as $namedType) { $possibleTypes[] = static::$astBuilder->buildType($namedType); } @@ -282,7 +281,7 @@ protected static function extendPossibleTypes(UnionType $type): array /** * @param ObjectType|InterfaceType $type * - * @return array + * @return array Should be InterfaceType, will be caught in schema validation */ protected static function extendImplementedInterfaces(ImplementingType $type): array { @@ -291,12 +290,17 @@ protected static function extendImplementedInterfaces(ImplementingType $type): a $type->getInterfaces() ); - $extensions = static::$typeExtensionsMap[$type->name] ?? null; - if ($extensions !== null) { - /** @var ObjectTypeExtensionNode|InterfaceTypeExtensionNode $extension */ - foreach ($extensions as $extension) { + if (isset(static::$typeExtensionsMap[$type->name])) { + /** + * Proven by @see assertTypeMatchesExtension(). + * + * @var ObjectTypeExtensionNode|InterfaceTypeExtensionNode $extension + */ + foreach (static::$typeExtensionsMap[$type->name] as $extension) { foreach ($extension->interfaces as $namedType) { - $interfaces[] = static::$astBuilder->buildType($namedType); + /** @var InterfaceType $interface we know this, but PHP templates cannot express it */ + $interface = static::$astBuilder->buildType($namedType); + $interfaces[] = $interface; } } } @@ -304,7 +308,12 @@ protected static function extendImplementedInterfaces(ImplementingType $type): a return $interfaces; } - protected static function extendType($typeDef) + /** + * @param ListOfType|NonNull|(Type &NamedType) $typeDef + * + * @return ListOfType|NonNull|(Type&NamedType) + */ + protected static function extendType(Type $typeDef): Type { if ($typeDef instanceof ListOfType) { return Type::listOf(static::extendType($typeDef->getOfType())); @@ -318,41 +327,38 @@ protected static function extendType($typeDef) } /** - * @param FieldArgument[] $args + * @param array $args * - * @return mixed[] + * @return array> */ protected static function extendArgs(array $args): array { - return Utils::keyValMap( - $args, - static function (FieldArgument $arg): string { - return $arg->name; - }, - static function (FieldArgument $arg): array { - $def = [ - 'type' => static::extendType($arg->getType()), - 'description' => $arg->description, - 'astNode' => $arg->astNode, - ]; - - if ($arg->defaultValueExists()) { - $def['defaultValue'] = $arg->defaultValue; - } + $extended = []; + foreach ($args as $arg) { + $def = [ + 'type' => static::extendType($arg->getType()), + 'description' => $arg->description, + 'astNode' => $arg->astNode, + ]; - return $def; + if ($arg->defaultValueExists()) { + $def['defaultValue'] = $arg->defaultValue; } - ); + + $extended[$arg->name] = $def; + } + + return $extended; } /** * @param InterfaceType|ObjectType $type * - * @return mixed[] + * @return array> * * @throws Error */ - protected static function extendFieldMap($type): array + protected static function extendFieldMap(Type $type): array { $newFieldMap = []; $oldFieldMap = $type->getFields(); @@ -366,14 +372,18 @@ protected static function extendFieldMap($type): array 'deprecationReason' => $field->deprecationReason, 'type' => static::extendType($field->getType()), 'args' => static::extendArgs($field->args), - 'astNode' => $field->astNode, 'resolve' => $field->resolveFn, + 'astNode' => $field->astNode, ]; } - $extensions = static::$typeExtensionsMap[$type->name] ?? null; - if ($extensions !== null) { - foreach ($extensions as $extension) { + if (isset(static::$typeExtensionsMap[$type->name])) { + /** + * Proven by @see assertTypeMatchesExtension(). + * + * @var ObjectTypeExtensionNode|InputObjectTypeExtensionNode $extension + */ + foreach (static::$typeExtensionsMap[$type->name] as $extension) { foreach ($extension->fields as $field) { $fieldName = $field->name->value; if (isset($oldFieldMap[$fieldName])) { @@ -393,16 +403,12 @@ protected static function extendObjectType(ObjectType $type): ObjectType return new ObjectType([ 'name' => $type->name, 'description' => $type->description, - 'interfaces' => static function () use ($type): array { - return static::extendImplementedInterfaces($type); - }, - 'fields' => static function () use ($type): array { - return static::extendFieldMap($type); - }, + 'interfaces' => static fn (): array => static::extendImplementedInterfaces($type), + 'fields' => static fn (): array => static::extendFieldMap($type), + 'isTypeOf' => [$type, 'isTypeOf'], + 'resolveField' => $type->resolveFieldFn ?? null, 'astNode' => $type->astNode, 'extensionASTNodes' => static::getExtensionASTNodes($type), - 'isTypeOf' => $type->config['isTypeOf'] ?? null, - 'resolveField' => $type->resolveFieldFn ?? null, ]); } @@ -411,15 +417,11 @@ protected static function extendInterfaceType(InterfaceType $type): InterfaceTyp return new InterfaceType([ 'name' => $type->name, 'description' => $type->description, - 'interfaces' => static function () use ($type): array { - return static::extendImplementedInterfaces($type); - }, - 'fields' => static function () use ($type): array { - return static::extendFieldMap($type); - }, + 'interfaces' => static fn (): array => static::extendImplementedInterfaces($type), + 'fields' => static fn (): array => static::extendFieldMap($type), + 'resolveType' => [$type, 'resolveType'], 'astNode' => $type->astNode, 'extensionASTNodes' => static::getExtensionASTNodes($type), - 'resolveType' => $type->config['resolveType'] ?? null, ]); } @@ -435,7 +437,14 @@ protected static function isSpecifiedScalarType(Type $type): bool ); } - protected static function extendNamedType(Type $type) + /** + * @param T $type + * + * @return T + * + * @template T of Type + */ + protected static function extendNamedType($type) { if (Introspection::isIntrospectionType($type) || static::isSpecifiedScalarType($type)) { return $type; @@ -458,13 +467,18 @@ protected static function extendNamedType(Type $type) } } + // @phpstan-ignore-next-line the lines above ensure only matching subtypes get in here return static::$extendTypeCache[$name]; } /** - * @return mixed|null + * @param T|null $type + * + * @return T|null + * + * @template T of Type */ - protected static function extendMaybeNamedType(?NamedType $type = null) + protected static function extendMaybeNamedType(?Type $type = null): ?Type { if ($type !== null) { return static::extendNamedType($type); @@ -474,9 +488,9 @@ protected static function extendMaybeNamedType(?NamedType $type = null) } /** - * @param DirectiveDefinitionNode[] $directiveDefinitions + * @param array $directiveDefinitions * - * @return Directive[] + * @return array */ protected static function getMergedDirectives(Schema $schema, array $directiveDefinitions): array { @@ -501,8 +515,8 @@ protected static function extendDirective(Directive $directive): Directive 'description' => $directive->description, 'locations' => $directive->locations, 'args' => static::extendArgs($directive->args), - 'astNode' => $directive->astNode, 'isRepeatable' => $directive->isRepeatable, + 'astNode' => $directive->astNode, ]); } @@ -528,12 +542,7 @@ public static function extend( /** @var array $schemaExtensions */ $schemaExtensions = []; - $definitionsCount = count($documentAST->definitions); - for ($i = 0; $i < $definitionsCount; $i++) { - - /** @var Node $def */ - $def = $documentAST->definitions[$i]; - + foreach ($documentAST->definitions as $def) { if ($def instanceof SchemaDefinitionNode) { $schemaDef = $def; } elseif ($def instanceof SchemaTypeExtensionNode) { @@ -553,16 +562,14 @@ public static function extend( $typeDefinitionMap[$typeName] = $def; } elseif ($def instanceof TypeExtensionNode) { - $extendedTypeName = isset($def->name) ? $def->name->value : null; + $extendedTypeName = $def->name->value; $existingType = $schema->getType($extendedTypeName); if ($existingType === null) { throw new Error('Cannot extend type "' . $extendedTypeName . '" because it does not exist in the existing schema.', [$def]); } - static::checkExtensionNode($existingType, $def); - - $existingTypeExtensions = static::$typeExtensionsMap[$extendedTypeName] ?? null; - static::$typeExtensionsMap[$extendedTypeName] = $existingTypeExtensions !== null ? array_merge($existingTypeExtensions, [$def]) : [$def]; + static::assertTypeMatchesExtension($existingType, $def); + static::$typeExtensionsMap[$extendedTypeName][] = $def; } elseif ($def instanceof DirectiveDefinitionNode) { $directiveName = $def->name->value; $existingDirective = $schema->getDirective($directiveName); diff --git a/tests/Type/SchemaTest.php b/tests/Type/SchemaTest.php index a58173077..e653d55f8 100644 --- a/tests/Type/SchemaTest.php +++ b/tests/Type/SchemaTest.php @@ -18,23 +18,17 @@ class SchemaTest extends TestCase { - /** @var InterfaceType */ - private $interfaceType; + private InterfaceType $interfaceType; - /** @var ObjectType */ - private $implementingType; + private ObjectType $implementingType; - /** @var InputObjectType */ - private $directiveInputType; + private InputObjectType $directiveInputType; - /** @var InputObjectType */ - private $wrappedDirectiveInputType; + private InputObjectType $wrappedDirectiveInputType; - /** @var Directive */ - private $directive; + private Directive $directive; - /** @var Schema */ - private $schema; + private Schema $schema; public function setUp(): void { diff --git a/tests/Utils/SchemaExtenderTest.php b/tests/Utils/SchemaExtenderTest.php index 6f7c09109..c1be0933d 100644 --- a/tests/Utils/SchemaExtenderTest.php +++ b/tests/Utils/SchemaExtenderTest.php @@ -4,6 +4,7 @@ namespace GraphQL\Tests\Utils; +use GraphQL\Error\DebugFlag; use GraphQL\Error\Error; use GraphQL\GraphQL; use GraphQL\Language\AST\DefinitionNode; @@ -13,6 +14,10 @@ use GraphQL\Language\DirectiveLocation; use GraphQL\Language\Parser; use GraphQL\Language\Printer; +use GraphQL\Tests\Utils\SchemaExtenderTest\SomeInterfaceClassType; +use GraphQL\Tests\Utils\SchemaExtenderTest\SomeObjectClassType; +use GraphQL\Tests\Utils\SchemaExtenderTest\SomeScalarClassType; +use GraphQL\Tests\Utils\SchemaExtenderTest\SomeUnionClassType; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; @@ -29,6 +34,7 @@ use GraphQL\Utils\SchemaExtender; use GraphQL\Utils\SchemaPrinter; use PHPUnit\Framework\TestCase; +use stdClass; use function array_filter; use function array_map; @@ -44,17 +50,14 @@ class SchemaExtenderTest extends TestCase { - /** @var Schema */ - protected $testSchema; + protected Schema $testSchema; - /** @var string[] */ - protected $testSchemaDefinitions; + /** @var array */ + protected array $testSchemaDefinitions; - /** @var ObjectType */ - protected $FooType; + protected ObjectType $FooType; - /** @var Directive */ - protected $FooDirective; + protected Directive $FooDirective; public function setUp(): void { @@ -62,9 +65,7 @@ public function setUp(): void $SomeScalarType = new CustomScalarType([ 'name' => 'SomeScalar', - 'serialize' => static function ($x) { - return $x; - }, + 'serialize' => static fn ($x) => $x, ]); $SomeInterfaceType = new InterfaceType([ @@ -1940,16 +1941,14 @@ public function testOriginalResolversArePreserved(): void 'fields' => [ 'hello' => [ 'type' => Type::string(), - 'resolve' => static function (): string { - return 'Hello World!'; - }, + 'resolve' => static fn (): string => 'Hello World!', ], ], ]); $schema = new Schema(['query' => $queryType]); - $documentNode = Parser::parse(' + $documentNode = Parser::parse(/** @lang GraphQL */ ' extend type Query { misc: String } @@ -1960,7 +1959,7 @@ public function testOriginalResolversArePreserved(): void self::assertIsCallable($helloResolveFn); - $query = '{ hello }'; + $query = /** @lang GraphQL */ '{ hello }'; $result = GraphQL::executeQuery($extendedSchema, $query); self::assertSame(['data' => ['hello' => 'Hello World!']], $result->toArray()); } @@ -1974,14 +1973,12 @@ public function testOriginalResolveFieldIsPreserved(): void 'type' => Type::string(), ], ], - 'resolveField' => static function (): string { - return 'Hello World!'; - }, + 'resolveField' => static fn (): string => 'Hello World!', ]); $schema = new Schema(['query' => $queryType]); - $documentNode = Parser::parse(' + $documentNode = Parser::parse(/** @lang GraphQL */ ' extend type Query { misc: String } @@ -1992,7 +1989,7 @@ public function testOriginalResolveFieldIsPreserved(): void self::assertIsCallable($queryResolveFieldFn); - $query = '{ hello }'; + $query = /** @lang GraphQL */ '{ hello }'; $result = GraphQL::executeQuery($extendedSchema, $query); self::assertSame(['data' => ['hello' => 'Hello World!']], $result->toArray()); } @@ -2002,10 +1999,11 @@ public function testOriginalResolveFieldIsPreserved(): void */ public function testShouldBeAbleToIntroduceNewTypesThroughExtension(): void { - $sdl = ' + $sdl = /** @lang GraphQL */' type Query { defaultValue: String } + type Foo { value: Int } @@ -2014,7 +2012,7 @@ public function testShouldBeAbleToIntroduceNewTypesThroughExtension(): void $documentNode = Parser::parse($sdl); $schema = BuildSchema::build($documentNode); - $extensionSdl = ' + $extensionSdl = /** @lang GraphQL */ ' type Bar { foo: Foo } @@ -2023,7 +2021,7 @@ public function testShouldBeAbleToIntroduceNewTypesThroughExtension(): void $extendedDocumentNode = Parser::parse($extensionSdl); $extendedSchema = SchemaExtender::extend($schema, $extendedDocumentNode); - $expected = ' + $expected = /** @lang GraphQL */' type Bar { foo: Foo } @@ -2045,7 +2043,7 @@ public function testShouldBeAbleToIntroduceNewTypesThroughExtension(): void */ public function testPreservesRepeatableInDirective(): void { - $schema = BuildSchema::build(' + $schema = BuildSchema::build(/** @lang GraphQL */ ' directive @test(arg: Int) repeatable on FIELD | SCALAR '); @@ -2065,17 +2063,16 @@ public function testSupportsTypeConfigDecorator(): void 'type' => Type::string(), ], ], - 'resolveField' => static function (): string { - return 'Hello World!'; - }, + 'resolveField' => static fn (): string => 'Hello World!', ]); $schema = new Schema(['query' => $queryType]); - $documentNode = Parser::parse(' + $documentNode = Parser::parse(/** @lang GraphQL */ ' type Foo { value: String } + extend type Query { defaultValue: String foo: Foo @@ -2096,14 +2093,172 @@ public function testSupportsTypeConfigDecorator(): void $extendedSchema = SchemaExtender::extend($schema, $documentNode, [], $typeConfigDecorator); - $query = '{ + $query = /** @lang GraphQL */' + { hello foo { value } - }'; + } + '; $result = GraphQL::executeQuery($extendedSchema, $query); self::assertSame(['data' => ['hello' => 'Hello World!', 'foo' => ['value' => 'bar']]], $result->toArray()); } + + public function testPreservesScalarClassMethods(): void + { + $SomeScalarClassType = new SomeScalarClassType(); + + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'someScalarClass' => ['type' => $SomeScalarClassType], + ], + ]); + + $schema = new Schema(['query' => $queryType]); + + $documentNode = Parser::parse(/** @lang GraphQL */ ' + extend type Query { + foo: ID + } + '); + + $extendedSchema = SchemaExtender::extend($schema, $documentNode); + $extendedScalar = $extendedSchema->getType('SomeScalarClass'); + + self::assertInstanceOf(CustomScalarType::class, $extendedScalar); + self::assertSame(SomeScalarClassType::SERIALIZE_RETURN, $extendedScalar->serialize(null)); + self::assertSame(SomeScalarClassType::PARSE_VALUE_RETURN, $extendedScalar->parseValue(null)); + self::assertSame(SomeScalarClassType::PARSE_LITERAL_RETURN, $extendedScalar->parseLiteral($documentNode)); + } + + public function testPreservesResolveTypeMethod(): void + { + $SomeInterfaceClassType = new SomeInterfaceClassType([ + 'name' => 'SomeInterface', + 'fields' => [ + 'foo' => [ 'type' => Type::string() ], + ], + ]); + + $FooType = new ObjectType([ + 'name' => 'Foo', + 'interfaces' => [$SomeInterfaceClassType], + 'fields' => [ + 'foo' => [ 'type' => Type::string() ], + ], + ]); + + $BarType = new ObjectType([ + 'name' => 'Bar', + 'fields' => [ + 'bar' => [ 'type' => Type::string() ], + ], + ]); + + $SomeUnionClassType = new SomeUnionClassType([ + 'name' => 'SomeUnion', + 'types' => [$FooType, $BarType], + ]); + + $QueryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'someUnion' => ['type' => $SomeUnionClassType], + 'someInterface' => ['type' => $SomeInterfaceClassType], + ], + 'resolveField' => static fn (): stdClass => new stdClass(), + ]); + + $schema = new Schema(['query' => $QueryType]); + + $documentNode = Parser::parse(/** @lang GraphQL */ ' + extend type Query { + foo: ID + } + '); + + $extendedSchema = SchemaExtender::extend($schema, $documentNode); + + $ExtendedFooType = $extendedSchema->getType('Foo'); + self::assertInstanceOf(ObjectType::class, $ExtendedFooType); + + $SomeInterfaceClassType->concrete = $ExtendedFooType; + $SomeUnionClassType->concrete = $ExtendedFooType; + + $query = /** @lang GraphQL */' + { + someUnion { + __typename + } + someInterface { + __typename + } + } + '; + $result = GraphQL::executeQuery($extendedSchema, $query); + + self::assertSame([ + 'data' => [ + 'someUnion' => ['__typename' => 'Foo'], + 'someInterface' => ['__typename' => 'Foo'], + ], + ], $result->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)); + } + + public function testPreservesIsTypeOfMethod(): void + { + $SomeInterfaceType = new InterfaceType([ + 'name' => 'SomeInterface', + 'fields' => [ + 'foo' => [ 'type' => Type::string() ], + ], + ]); + + $FooClassType = new SomeObjectClassType([ + 'name' => 'Foo', + 'interfaces' => [$SomeInterfaceType], + 'fields' => [ + 'foo' => [ 'type' => Type::string() ], + ], + ]); + + $QueryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'someInterface' => ['type' => $SomeInterfaceType], + ], + 'resolveField' => static fn (): stdClass => new stdClass(), + ]); + + $schema = new Schema([ + 'query' => $QueryType, + 'types' => [$FooClassType], + ]); + + $documentNode = Parser::parse(/** @lang GraphQL */ ' + extend type Query { + foo: ID + } + '); + + $extendedSchema = SchemaExtender::extend($schema, $documentNode); + + $query = /** @lang GraphQL */' + { + someInterface { + __typename + } + } + '; + $result = GraphQL::executeQuery($extendedSchema, $query); + + self::assertSame([ + 'data' => [ + 'someInterface' => ['__typename' => 'Foo'], + ], + ], $result->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)); + } } diff --git a/tests/Utils/SchemaExtenderTest/SomeInterfaceClassType.php b/tests/Utils/SchemaExtenderTest/SomeInterfaceClassType.php new file mode 100644 index 000000000..60c3dd0d5 --- /dev/null +++ b/tests/Utils/SchemaExtenderTest/SomeInterfaceClassType.php @@ -0,0 +1,22 @@ +concrete; + } +} diff --git a/tests/Utils/SchemaExtenderTest/SomeObjectClassType.php b/tests/Utils/SchemaExtenderTest/SomeObjectClassType.php new file mode 100644 index 000000000..0003b9d47 --- /dev/null +++ b/tests/Utils/SchemaExtenderTest/SomeObjectClassType.php @@ -0,0 +1,19 @@ +concrete; + } +}