diff --git a/examples/01-blog/Blog/Type/Scalar/EmailType.php b/examples/01-blog/Blog/Type/Scalar/EmailType.php index fd78ea796..ec304c168 100644 --- a/examples/01-blog/Blog/Type/Scalar/EmailType.php +++ b/examples/01-blog/Blog/Type/Scalar/EmailType.php @@ -6,15 +6,14 @@ use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Utils\Utils; -class EmailType +class EmailType extends CustomScalarType { - public static function create() + public function __construct(array $config = []) { - return new CustomScalarType([ - 'name' => 'Email', - 'serialize' => [__CLASS__, 'serialize'], - 'parseValue' => [__CLASS__, 'parseValue'], - 'parseLiteral' => [__CLASS__, 'parseLiteral'], + parent::__construct([ + 'serialize' => [__CLASS__, 's_serialize'], + 'parseValue' => [__CLASS__, 's_parseValue'], + 'parseLiteral' => [__CLASS__, 's_parseLiteral'], ]); } @@ -24,7 +23,7 @@ public static function create() * @param string $value * @return string */ - public static function serialize($value) + public static function s_serialize($value) { // Assuming internal representation of email is always correct: return $value; @@ -40,7 +39,7 @@ public static function serialize($value) * @param mixed $value * @return mixed */ - public static function parseValue($value) + public static function s_parseValue($value) { if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { throw new \UnexpectedValueException("Cannot represent value as email: " . Utils::printSafe($value)); @@ -55,7 +54,7 @@ public static function parseValue($value) * @return string * @throws Error */ - public static function parseLiteral($valueNode) + public static function s_parseLiteral($valueNode) { // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL // error location in query: diff --git a/examples/01-blog/Blog/Types.php b/examples/01-blog/Blog/Types.php index a8bb93aa5..9dddaec3b 100644 --- a/examples/01-blog/Blog/Types.php +++ b/examples/01-blog/Blog/Types.php @@ -1,13 +1,13 @@ Types::query() + 'query' => new QueryType(), + 'typeLoader' => function($name) { + return Types::byTypeName($name, true); + } ]); $result = GraphQL::executeQuery( diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index 1f6a33f42..02c7eb152 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -45,6 +45,7 @@ use function array_values; use function get_class; use function is_array; +use function is_callable; use function is_string; use function sprintf; @@ -528,7 +529,6 @@ private function resolveField(ObjectType $parentType, $rootValue, $fieldNodes, $ // The resolve function's optional 3rd argument is a context value that // is provided to every resolve function within an execution. It is commonly // used to represent an authenticated user, or request-specific caches. - $context = $exeContext->contextValue; // The resolve function's optional 4th argument is a collection of // information about the current execution state. $info = new ResolveInfo( @@ -938,10 +938,15 @@ private function completeLeafValue(LeafType $returnType, &$result) */ private function completeAbstractValue(AbstractType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result) { - $exeContext = $this->exeContext; - $runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info); - if ($runtimeType === null) { + $exeContext = $this->exeContext; + $typeCandidate = $returnType->resolveType($result, $exeContext->contextValue, $info); + + if ($typeCandidate === null) { $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType); + } elseif (is_callable($typeCandidate)) { + $runtimeType = Schema::resolveType($typeCandidate); + } else { + $runtimeType = $typeCandidate; } $promise = $this->getPromise($runtimeType); if ($promise !== null) { diff --git a/src/Type/Definition/FieldArgument.php b/src/Type/Definition/FieldArgument.php index 895504c94..c8cc49c14 100644 --- a/src/Type/Definition/FieldArgument.php +++ b/src/Type/Definition/FieldArgument.php @@ -6,6 +6,7 @@ use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\InputValueDefinitionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; use function array_key_exists; use function is_array; @@ -77,12 +78,9 @@ public static function createMap(array $config) : array return $map; } - /** - * @return InputType&Type - */ public function getType() : Type { - return $this->type; + return Schema::resolveType($this->type); } public function defaultValueExists() : bool diff --git a/src/Type/Definition/FieldDefinition.php b/src/Type/Definition/FieldDefinition.php index 39392cd53..2fb1c52bb 100644 --- a/src/Type/Definition/FieldDefinition.php +++ b/src/Type/Definition/FieldDefinition.php @@ -7,6 +7,7 @@ use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\FieldDefinitionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; use function is_array; use function is_callable; @@ -58,7 +59,7 @@ class FieldDefinition */ public $config; - /** @var OutputType&Type */ + /** @var callable|(OutputType&Type) */ public $type; /** @var callable|string */ @@ -174,12 +175,9 @@ public function getArg($name) return null; } - /** - * @return OutputType&Type - */ public function getType() : Type { - return $this->type; + return Schema::resolveType($this->type); } /** @@ -217,7 +215,7 @@ public function assertValid(Type $parentType) ) ); - $type = $this->type; + $type = $this->getType(); if ($type instanceof WrappingType) { $type = $type->getWrappedType(true); } diff --git a/src/Type/Definition/InputObjectField.php b/src/Type/Definition/InputObjectField.php index 0284a72b5..0eae4a2b3 100644 --- a/src/Type/Definition/InputObjectField.php +++ b/src/Type/Definition/InputObjectField.php @@ -7,6 +7,7 @@ use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\InputValueDefinitionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; use function array_key_exists; use function sprintf; @@ -55,7 +56,12 @@ public function __construct(array $opts) */ public function getType() : Type { - return $this->type; + /** + * TODO: Replace this cast with native assert + * + * @var Type&InputType + */ + return Schema::resolveType($this->type); } public function defaultValueExists() : bool diff --git a/src/Type/Definition/ListOfType.php b/src/Type/Definition/ListOfType.php index f9ca85b32..3f0945527 100644 --- a/src/Type/Definition/ListOfType.php +++ b/src/Type/Definition/ListOfType.php @@ -4,24 +4,38 @@ namespace GraphQL\Type\Definition; +use GraphQL\Type\Schema; +use function is_callable; + class ListOfType extends Type implements WrappingType, OutputType, NullableType, InputType { - /** @var Type */ + /** @var callable():Type|Type */ public $ofType; - public function __construct(Type $type) + /** + * @param callable():Type|Type $type + */ + public function __construct($type) { - $this->ofType = $type; + $this->ofType = is_callable($type) ? $type : Type::assertType($type); } public function toString() : string { - return '[' . $this->ofType->toString() . ']'; + return '[' . $this->getOfType()->toString() . ']'; + } + + public function getOfType() + { + return Schema::resolveType($this->ofType); } + /** + * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|(Type&WrappingType) + */ public function getWrappedType(bool $recurse = false) : Type { - $type = $this->ofType; + $type = $this->getOfType(); return $recurse && $type instanceof WrappingType ? $type->getWrappedType($recurse) diff --git a/src/Type/Definition/NonNull.php b/src/Type/Definition/NonNull.php index 716dac3d1..dd00fa2ac 100644 --- a/src/Type/Definition/NonNull.php +++ b/src/Type/Definition/NonNull.php @@ -4,12 +4,21 @@ namespace GraphQL\Type\Definition; +use GraphQL\Error\InvariantViolation; +use GraphQL\Type\Schema; +use function is_callable; + class NonNull extends Type implements WrappingType, OutputType, InputType { - /** @var NullableType&Type */ + /** @var callable|(NullableType&Type) */ private $ofType; - public function __construct(NullableType $type) + /** + * code sniffer doesn't understand this syntax. Pr with a fix here: waiting on https://github.com/squizlabs/PHP_CodeSniffer/pull/2919 + * phpcs:disable Squiz.Commenting.FunctionComment.SpacingAfterParamType + * @param (NullableType&Type)|callable $type + */ + public function __construct($type) { /** @var Type&NullableType $nullableType*/ $nullableType = $type; @@ -21,9 +30,14 @@ public function toString() : string return $this->getWrappedType()->toString() . '!'; } + public function getOfType() + { + return Schema::resolveType($this->ofType); + } + public function getWrappedType(bool $recurse = false) : Type { - $type = $this->ofType; + $type = $this->getOfType(); return $recurse && $type instanceof WrappingType ? $type->getWrappedType($recurse) diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index ce6097a09..b6febf026 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -5,10 +5,13 @@ namespace GraphQL\Type\Definition; use Exception; +use GraphQL\Deferred; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\AST\ObjectTypeExtensionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; +use function array_map; use function call_user_func; use function is_array; use function is_callable; @@ -171,6 +174,7 @@ private function getInterfaceMap() if ($this->interfaceMap === null) { $this->interfaceMap = []; foreach ($this->getInterfaces() as $interface) { + $interface = Schema::resolveType($interface); $this->interfaceMap[$interface->name] = $interface; } } @@ -195,7 +199,10 @@ public function getInterfaces() ); } - $this->interfaces = $interfaces ?? []; + /** @var InterfaceType[] $interfaces */ + $interfaces = array_map([Schema::class, 'resolveType'], $interfaces ?? []); + + $this->interfaces = $interfaces; } return $this->interfaces; @@ -205,7 +212,7 @@ public function getInterfaces() * @param mixed $value * @param mixed[]|null $context * - * @return bool|null + * @return bool|Deferred|null */ public function isTypeOf($value, $context, ResolveInfo $info) { diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php index 7049a1940..922725654 100644 --- a/src/Type/Definition/ResolveInfo.php +++ b/src/Type/Definition/ResolveInfo.php @@ -40,7 +40,7 @@ class ResolveInfo * Expected return type of the field being resolved. * * @api - * @var OutputType&Type + * @var Type */ public $returnType; diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index a779189e9..f79a49945 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -13,6 +13,7 @@ use ReflectionClass; use function array_keys; use function array_merge; +use function assert; use function implode; use function in_array; use function preg_replace; @@ -121,9 +122,11 @@ public static function listOf(Type $wrappedType) : ListOfType } /** + * @param callable|NullableType $wrappedType + * * @api */ - public static function nonNull(NullableType $wrappedType) : NonNull + public static function nonNull($wrappedType) : NonNull { return new NonNull($wrappedType); } @@ -280,29 +283,14 @@ public static function isAbstractType($type) : bool /** * @param mixed $type - * - * @return mixed */ - public static function assertType($type) + public static function assertType($type) : Type { - Utils::invariant( - self::isType($type), - 'Expected ' . Utils::printSafe($type) . ' to be a GraphQL type.' - ); + assert($type instanceof Type, new InvariantViolation('Expected ' . Utils::printSafe($type) . ' to be a GraphQL type.')); return $type; } - /** - * @param Type $type - * - * @api - */ - public static function isType($type) : bool - { - return $type instanceof Type; - } - /** * @api */ diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index f414bdbaa..345642158 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -7,7 +7,9 @@ use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\AST\UnionTypeExtensionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; +use function array_map; use function call_user_func; use function is_array; use function is_callable; @@ -41,7 +43,7 @@ public function __construct(array $config) /** * Optionally provide a custom type resolver function. If one is not provided, - * the default implemenation will call `isTypeOf` on each implementing + * the default implementation will call `isTypeOf` on each implementing * Object type. */ $this->name = $config['name']; @@ -90,7 +92,12 @@ public function getTypes() ); } - $this->types = $types; + $rawTypes = $types; + foreach ($rawTypes as $i => $rawType) { + $rawTypes[$i] = Schema::resolveType($rawType); + } + + $this->types = $rawTypes; } return $this->types; diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index 7f0b0f582..9a87d2c2a 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -506,7 +506,7 @@ public static function _field() ], 'type' => [ 'type' => Type::nonNull(self::_type()), - 'resolve' => static function (FieldDefinition $field) { + 'resolve' => static function (FieldDefinition $field) : Type { return $field->getType(); }, ], diff --git a/src/Type/Schema.php b/src/Type/Schema.php index e69b018db..eeeb3f0e8 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -57,7 +57,7 @@ class Schema */ private $resolvedTypes = []; - /** @var array>|null */ + /** @var array> */ private $possibleTypeMap; /** @@ -172,6 +172,7 @@ private function resolveAdditionalTypes() } foreach ($types as $index => $type) { + $type = self::resolveType($type); if (! $type instanceof Type) { throw new InvariantViolation(sprintf( 'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s', @@ -315,10 +316,11 @@ public function getType(string $name) : ?Type { if (! isset($this->resolvedTypes[$name])) { $type = $this->loadType($name); + if (! $type) { return null; } - $this->resolvedTypes[$name] = $type; + $this->resolvedTypes[$name] = self::resolveType($type); } return $this->resolvedTypes[$name]; @@ -342,12 +344,13 @@ private function loadType(string $typeName) : ?Type if (! $type instanceof Type) { throw new InvariantViolation( sprintf( - 'Type loader is expected to return valid type "%s", but it returned %s', + 'Type loader is expected to return a callable or valid type "%s", but it returned %s', $typeName, Utils::printSafe($type) ) ); } + if ($type->name !== $typeName) { throw new InvariantViolation( sprintf('Type loader is expected to return type "%s", but it returned "%s"', $typeName, $type->name) @@ -359,12 +362,24 @@ private function loadType(string $typeName) : ?Type private function defaultTypeLoader(string $typeName) : ?Type { - // Default type loader simply fallbacks to collecting all types + // Default type loader simply falls back to collecting all types $typeMap = $this->getTypeMap(); return $typeMap[$typeName] ?? null; } + /** + * @param Type|callable():Type $type + */ + public static function resolveType($type) : Type + { + if (is_callable($type)) { + return $type(); + } + + return $type; + } + /** * Returns all possible concrete types for given abstract type * (implementations for interfaces and members of union type for unions) @@ -385,11 +400,11 @@ public function getPossibleTypes(Type $abstractType) : array } /** - * @return array> + * @return array> */ private function getPossibleTypeMap() { - if ($this->possibleTypeMap === null) { + if (! isset($this->possibleTypeMap)) { $this->possibleTypeMap = []; foreach ($this->getTypeMap() as $type) { if ($type instanceof ObjectType) { diff --git a/src/Type/SchemaConfig.php b/src/Type/SchemaConfig.php index e6b0fb725..e90058a56 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -42,7 +42,7 @@ class SchemaConfig /** @var Directive[]|null */ public $directives; - /** @var callable|null */ + /** @var callable(string $name):Type|null */ public $typeLoader; /** @var SchemaDefinitionNode|null */ @@ -253,7 +253,7 @@ public function setDirectives(array $directives) } /** - * @return callable|null + * @return callable(string $name):Type|null * * @api */ diff --git a/src/Utils/SchemaExtender.php b/src/Utils/SchemaExtender.php index e24639ba2..5668f94d1 100644 --- a/src/Utils/SchemaExtender.php +++ b/src/Utils/SchemaExtender.php @@ -296,7 +296,7 @@ protected static function extendImplementedInterfaces(ObjectType $type) : array protected static function extendType($typeDef) { if ($typeDef instanceof ListOfType) { - return Type::listOf(static::extendType($typeDef->ofType)); + return Type::listOf(static::extendType($typeDef->getOfType())); } if ($typeDef instanceof NonNull) { diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index ef1f02301..e6a14761b 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -159,6 +159,7 @@ public static function extractTypes($type, ?array $typeMap = null) if ($type instanceof WrappingType) { return self::extractTypes($type->getWrappedType(true), $typeMap); } + if (! $type instanceof Type) { // Preserve these invalid types in map (at numeric index) to make them // detectable during $schema->validate() @@ -198,7 +199,7 @@ public static function extractTypes($type, ?array $typeMap = null) foreach ($type->getFields() as $fieldName => $field) { if (! empty($field->args)) { $fieldArgTypes = array_map( - static function (FieldArgument $arg) { + static function (FieldArgument $arg) : Type { return $arg->getType(); }, $field->args diff --git a/tests/Type/LazyTypeLoaderTest.php b/tests/Type/LazyTypeLoaderTest.php new file mode 100644 index 000000000..49934200e --- /dev/null +++ b/tests/Type/LazyTypeLoaderTest.php @@ -0,0 +1,381 @@ +loadedTypes[$name])) { + $type = null; + switch ($name) { + case 'Node': + $type = new InterfaceType([ + 'name' => 'Node', + 'fields' => function () : array { + $this->calls[] = 'Node.fields'; + + return [ + 'id' => Type::string(), + ]; + }, + 'resolveType' => static function () : void { + }, + ]); + break; + + case 'Content': + $type = new InterfaceType([ + 'name' => 'Content', + 'fields' => function () : array { + $this->calls[] = 'Content.fields'; + + return [ + 'title' => Type::string(), + 'body' => Type::string(), + ]; + }, + 'resolveType' => static function () : void { + }, + ]); + break; + + case 'BlogStory': + $type = new ObjectType([ + 'name' => 'BlogStory', + 'interfaces' => [ + $this->node, + $this->content, + ], + 'fields' => function () : array { + $this->calls[] = 'BlogStory.fields'; + + return [ + 'id' => Type::string(), + 'title' => Type::string(), + 'body' => Type::string(), + ]; + }, + ]); + break; + + case 'PostStoryMutation': + $type = new ObjectType([ + 'name' => 'PostStoryMutation', + 'fields' => [ + 'story' => $this->blogStory, + ], + ]); + break; + + case 'PostStoryMutationInput': + $type = new InputObjectType([ + 'name' => 'PostStoryMutationInput', + 'fields' => [ + 'title' => Type::string(), + 'body' => Type::string(), + 'author' => Type::id(), + 'category' => Type::id(), + ], + ]); + break; + } + $this->loadedTypes[$name] = $type; + } + + return $this->loadedTypes[$name]; + }; + } + + public function setUp() : void + { + $this->calls = []; + + $this->node = $this->lazyLoad('Node'); + $this->blogStory = $this->lazyLoad('BlogStory'); + $this->content = $this->lazyLoad('Content'); + $this->postStoryMutation = $this->lazyLoad('PostStoryMutation'); + $this->postStoryMutationInput = $this->lazyLoad('PostStoryMutationInput'); + $this->query = new ObjectType([ + 'name' => 'Query', + 'fields' => function () : array { + $this->calls[] = 'Query.fields'; + + return [ + 'latestContent' => $this->lazyLoad('Content'), + 'node' => $this->lazyLoad('Node'), + ]; + }, + ]); + + $this->mutation = new ObjectType([ + 'name' => 'Mutation', + 'fields' => function () : array { + $this->calls[] = 'Mutation.fields'; + + return [ + 'postStory' => [ + 'type' => $this->postStoryMutation, + 'args' => [ + 'input' => Type::nonNull($this->postStoryMutationInput), + 'clientRequestId' => Type::string(), + ], + ], + ]; + }, + ]); + + $this->typeLoader = function (string $name) : Type { + $this->calls[] = $name; + $prop = lcfirst($name); + + switch ($prop) { + case 'node': + return ($this->node)(); + case 'blogStory': + return ($this->blogStory)(); + case 'content': + return ($this->content)(); + case 'postStoryMutation': + return ($this->postStoryMutation)(); + case 'postStoryMutationInput': + return ($this->postStoryMutationInput)(); + } + + throw new InvalidArgument('Unknown type'); + }; + } + + public function testSchemaAcceptsTypeLoader() : void + { + $this->expectNotToPerformAssertions(); + new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => ['a' => Type::string()], + ]), + 'typeLoader' => static function () : void { + }, + ]); + } + + public function testSchemaRejectsNonCallableTypeLoader() : void + { + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('Schema type loader must be callable if provided but got: []'); + + new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => ['a' => Type::string()], + ]), + 'typeLoader' => [], + ]); + } + + public function testWorksWithoutTypeLoader() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'types' => [Schema::resolveType($this->blogStory)], + ]); + + $expected = [ + 'Query.fields', + 'Content.fields', + 'Node.fields', + 'Mutation.fields', + 'BlogStory.fields', + ]; + self::assertEquals($expected, $this->calls); + + self::assertSame($this->query, $schema->getType('Query')); + self::assertSame($this->mutation, $schema->getType('Mutation')); + self::assertSame(Schema::resolveType($this->node), $schema->getType('Node')); + self::assertSame(Schema::resolveType($this->content), $schema->getType('Content')); + self::assertSame(Schema::resolveType($this->blogStory), $schema->getType('BlogStory')); + self::assertSame(Schema::resolveType($this->postStoryMutation), $schema->getType('PostStoryMutation')); + self::assertSame(Schema::resolveType($this->postStoryMutationInput), $schema->getType('PostStoryMutationInput')); + + $expectedTypeMap = [ + 'Query' => $this->query, + 'Mutation' => $this->mutation, + 'Node' => Schema::resolveType($this->node), + 'String' => Type::string(), + 'Content' => Schema::resolveType($this->content), + 'BlogStory' => Schema::resolveType($this->blogStory), + 'PostStoryMutationInput' => Schema::resolveType($this->postStoryMutationInput), + ]; + + self::assertArraySubset($expectedTypeMap, $schema->getTypeMap()); + } + + public function testWorksWithTypeLoader() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'typeLoader' => $this->typeLoader, + ]); + self::assertEquals([], $this->calls); + + $node = $schema->getType('Node'); + self::assertSame(Schema::resolveType($this->node), $node); + self::assertEquals(['Node'], $this->calls); + + $content = $schema->getType('Content'); + self::assertSame(Schema::resolveType($this->content), $content); + self::assertEquals(['Node', 'Content'], $this->calls); + + $input = $schema->getType('PostStoryMutationInput'); + self::assertSame(Schema::resolveType($this->postStoryMutationInput), $input); + self::assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls); + + $result = $schema->isPossibleType( + Schema::resolveType($this->node), + Schema::resolveType($this->blogStory) + ); + self::assertTrue($result); + self::assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls); + } + + public function testOnlyCallsLoaderOnce() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => $this->typeLoader, + ]); + + $schema->getType('Node'); + self::assertEquals(['Node'], $this->calls); + + $schema->getType('Node'); + self::assertEquals(['Node'], $this->calls); + } + + public function testFailsOnNonExistentType() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => static function () : void { + }, + ]); + + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('Type loader is expected to return a callable or valid type "NonExistingType", but it returned null'); + + $schema->getType('NonExistingType'); + } + + public function testFailsOnNonType() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => static function () : stdClass { + return new stdClass(); + }, + ]); + + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('Type loader is expected to return a callable or valid type "Node", but it returned instance of stdClass'); + + $schema->getType('Node'); + } + + public function testPassesThroughAnExceptionInLoader() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => static function () : void { + throw new Exception('This is the exception we are looking for'); + }, + ]); + + $this->expectException(Throwable::class); + $this->expectExceptionMessage('This is the exception we are looking for'); + + $schema->getType('Node'); + } + + public function testReturnsIdenticalResults() : void + { + $withoutLoader = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + ]); + + $withLoader = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'typeLoader' => $this->typeLoader, + ]); + + self::assertSame($withoutLoader->getQueryType(), $withLoader->getQueryType()); + self::assertSame($withoutLoader->getMutationType(), $withLoader->getMutationType()); + self::assertSame($withoutLoader->getType('BlogStory'), $withLoader->getType('BlogStory')); + self::assertSame($withoutLoader->getDirectives(), $withLoader->getDirectives()); + } + + public function testSkipsLoaderForInternalTypes() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'typeLoader' => $this->typeLoader, + ]); + + $type = $schema->getType('ID'); + self::assertSame(Type::id(), $type); + self::assertEquals([], $this->calls); + } +} diff --git a/tests/Type/TypeLoaderTest.php b/tests/Type/TypeLoaderTest.php index ddf8a2137..516b4cb72 100644 --- a/tests/Type/TypeLoaderTest.php +++ b/tests/Type/TypeLoaderTest.php @@ -264,7 +264,7 @@ public function testFailsOnNonExistentType() : void ]); $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage('Type loader is expected to return valid type "NonExistingType", but it returned null'); + $this->expectExceptionMessage('Type loader is expected to return a callable or valid type "NonExistingType", but it returned null'); $schema->getType('NonExistingType'); } @@ -279,7 +279,7 @@ public function testFailsOnNonType() : void ]); $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage('Type loader is expected to return valid type "Node", but it returned instance of stdClass'); + $this->expectExceptionMessage('Type loader is expected to return a callable or valid type "Node", but it returned instance of stdClass'); $schema->getType('Node'); } diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index f374643e7..b696abb67 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -1348,28 +1348,6 @@ public function testRejectsWithReleventLocationsForANonOutputTypeAsAnObjectField ); } - // DESCRIBE: Type System: Interface fields must have output types - - /** - * @see it('rejects an Object implementing a non-type values') - */ - public function testRejectsAnObjectImplementingANonTypeValues() : void - { - $schema = new Schema([ - 'query' => new ObjectType([ - 'name' => 'BadObject', - 'interfaces' => [null], - 'fields' => ['a' => Type::string()], - ]), - ]); - $expected = ['message' => 'Type BadObject must only implement Interface types, it cannot implement null.']; - - $this->assertMatchesValidationMessage( - $schema->validate(), - [$expected] - ); - } - /** * @see it('rejects an Object implementing a non-Interface type') */