diff --git a/src/Type/EagerResolution.php b/src/Type/EagerResolution.php index e7a9aa9..ed15faf 100644 --- a/src/Type/EagerResolution.php +++ b/src/Type/EagerResolution.php @@ -36,10 +36,11 @@ class EagerResolution implements Resolution */ public function __construct(array $initialTypes) { + $typeMap = []; foreach ($initialTypes as $type) { - $this->extractTypes($type); + $typeMap = Utils\TypeInfo::extractTypes($type, $typeMap); } - $this->typeMap += Type::getInternalTypes(); + $this->typeMap = $typeMap + Type::getInternalTypes(); // Keep track of all possible types for abstract types foreach ($this->typeMap as $typeName => $type) { @@ -112,50 +113,4 @@ class EagerResolution implements Resolution 'possibleTypeMap' => $possibleTypesMap ]; } - - /** - * @param $type - * @return array - */ - private function extractTypes($type) - { - if (!$type) { - return $this->typeMap; - } - - if ($type instanceof WrappingType) { - return $this->extractTypes($type->getWrappedType(true)); - } - - if (!empty($this->typeMap[$type->name])) { - Utils::invariant( - $this->typeMap[$type->name] === $type, - "Schema must contain unique named types but contains multiple types named \"$type\"." - ); - return $this->typeMap; - } - $this->typeMap[$type->name] = $type; - - $nestedTypes = []; - - if ($type instanceof UnionType) { - $nestedTypes = $type->getTypes(); - } - if ($type instanceof ObjectType) { - $nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); - } - if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) { - foreach ((array) $type->getFields() as $fieldName => $field) { - if (isset($field->args)) { - $fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args); - $nestedTypes = array_merge($nestedTypes, $fieldArgTypes); - } - $nestedTypes[] = $field->getType(); - } - } - foreach ($nestedTypes as $type) { - $this->extractTypes($type); - } - return $this->typeMap; - } } diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index e052c04..929caa1 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -20,6 +20,8 @@ use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\UnionType; +use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Introspection; use GraphQL\Utils; @@ -162,6 +164,66 @@ class TypeInfo return $schema->getType($inputTypeNode->name->value); } + /** + * Given root type scans through all fields to find nested types. Returns array where keys are for type name + * and value contains corresponding type instance. + * + * Example output: + * [ + * 'String' => $instanceOfStringType, + * 'MyType' => $instanceOfMyType, + * ... + * ] + * + * @param Type $type + * @param array|null $typeMap + * @return array + */ + public static function extractTypes($type, array $typeMap = null) + { + if (!$typeMap) { + $typeMap = []; + } + if (!$type) { + return $typeMap; + } + + if ($type instanceof WrappingType) { + return self::extractTypes($type->getWrappedType(true), $typeMap); + } + + if (!empty($typeMap[$type->name])) { + Utils::invariant( + $typeMap[$type->name] === $type, + "Schema must contain unique named types but contains multiple types named \"$type\"." + ); + return $typeMap; + } + $typeMap[$type->name] = $type; + + $nestedTypes = []; + + if ($type instanceof UnionType) { + $nestedTypes = $type->getTypes(); + } + if ($type instanceof ObjectType) { + $nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); + } + if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) { + foreach ((array) $type->getFields() as $fieldName => $field) { + if (isset($field->args)) { + $fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args); + $nestedTypes = array_merge($nestedTypes, $fieldArgTypes); + } + $nestedTypes[] = $field->getType(); + } + } + foreach ($nestedTypes as $type) { + $typeMap = self::extractTypes($type, $typeMap); + } + return $typeMap; + } + /** * Not exactly the same as the executor's definition of getFieldDef, in this * statically evaluated environment we do not always have an Object type, diff --git a/tests/Utils/ExtractTypesTest.php b/tests/Utils/ExtractTypesTest.php new file mode 100644 index 0000000..4d19cc0 --- /dev/null +++ b/tests/Utils/ExtractTypesTest.php @@ -0,0 +1,338 @@ +node = new InterfaceType([ + 'name' => 'Node', + 'fields' => [ + 'id' => Type::string() + ] + ]); + + $this->content = new InterfaceType([ + 'name' => 'Content', + 'fields' => function() { + return [ + 'title' => Type::string(), + 'body' => Type::string(), + 'author' => $this->user, + 'comments' => Type::listOf($this->comment), + 'categories' => Type::listOf($this->category) + ]; + } + ]); + + $this->blogStory = new ObjectType([ + 'name' => 'BlogStory', + 'interfaces' => [ + $this->node, + $this->content + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + $this->content->getField('title'), + $this->content->getField('body'), + $this->content->getField('author'), + $this->content->getField('comments'), + $this->content->getField('categories') + ]; + }, + ]); + + $this->link = new ObjectType([ + 'name' => 'Link', + 'interfaces' => [ + $this->node, + $this->content + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + $this->content->getField('title'), + $this->content->getField('body'), + $this->content->getField('author'), + $this->content->getField('comments'), + $this->content->getField('categories'), + 'url' => Type::string() + ]; + }, + ]); + + $this->video = new ObjectType([ + 'name' => 'Video', + 'interfaces' => [ + $this->node, + $this->content + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + $this->content->getField('title'), + $this->content->getField('body'), + $this->content->getField('author'), + $this->content->getField('comments'), + $this->content->getField('categories'), + 'streamUrl' => Type::string(), + 'downloadUrl' => Type::string(), + 'metadata' => $this->videoMetadata = new ObjectType([ + 'name' => 'VideoMetadata', + 'fields' => [ + 'lat' => Type::float(), + 'lng' => Type::float() + ] + ]) + ]; + } + ]); + + $this->comment = new ObjectType([ + 'name' => 'Comment', + 'interfaces' => [ + $this->node + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + 'author' => $this->user, + 'text' => Type::string(), + 'replies' => Type::listOf($this->comment), + 'parent' => $this->comment, + 'content' => $this->content + ]; + } + ]); + + $this->user = new ObjectType([ + 'name' => 'User', + 'interfaces' => [ + $this->node + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + 'name' => Type::string(), + ]; + } + ]); + + $this->category = new ObjectType([ + 'name' => 'Category', + 'interfaces' => [ + $this->node + ], + 'fields' => function() { + return [ + $this->node->getField('id'), + 'name' => Type::string() + ]; + } + ]); + + $this->mention = new UnionType([ + 'name' => 'Mention', + 'types' => [ + $this->user, + $this->category + ] + ]); + + $this->query = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'viewer' => $this->user, + 'latestContent' => $this->content, + 'node' => $this->node, + 'mentions' => Type::listOf($this->mention) + ] + ]); + + $this->mutation = new ObjectType([ + 'name' => 'Mutation', + 'fields' => [ + 'postStory' => [ + 'type' => $this->postStoryMutation = new ObjectType([ + 'name' => 'PostStoryMutation', + 'fields' => [ + 'story' => $this->blogStory + ] + ]), + 'args' => [ + 'input' => Type::nonNull($this->postStoryMutationInput = new InputObjectType([ + 'name' => 'PostStoryMutationInput', + 'fields' => [ + 'title' => Type::string(), + 'body' => Type::string(), + 'author' => Type::id(), + 'category' => Type::id() + ] + ])), + 'clientRequestId' => Type::string() + ] + ], + 'postComment' => [ + 'type' => $this->postCommentMutation = new ObjectType([ + 'name' => 'PostCommentMutation', + 'fields' => [ + 'comment' => $this->comment + ] + ]), + 'args' => [ + 'input' => Type::nonNull($this->postCommentMutationInput = new InputObjectType([ + 'name' => 'PostCommentMutationInput', + 'fields' => [ + 'text' => Type::nonNull(Type::string()), + 'author' => Type::nonNull(Type::id()), + 'content' => Type::id(), + 'parent' => Type::id() + ] + ])), + 'clientRequestId' => Type::string() + ] + ] + ] + ]); + } + + public function testExtractTypesFromQuery() + { + $expectedTypeMap = [ + 'Query' => $this->query, + 'User' => $this->user, + 'Node' => $this->node, + 'String' => Type::string(), + 'Content' => $this->content, + 'Comment' => $this->comment, + 'Mention' => $this->mention, + 'Category' => $this->category, + ]; + + $actualTypeMap = TypeInfo::extractTypes($this->query); + $this->assertEquals($expectedTypeMap, $actualTypeMap); + } + + public function testExtractTypesFromMutation() + { + $expectedTypeMap = [ + 'Mutation' => $this->mutation, + 'User' => $this->user, + 'Node' => $this->node, + 'String' => Type::string(), + 'Content' => $this->content, + 'Comment' => $this->comment, + 'BlogStory' => $this->blogStory, + 'Category' => $this->category, + 'PostStoryMutationInput' => $this->postStoryMutationInput, + 'ID' => Type::id(), + 'PostStoryMutation' => $this->postStoryMutation, + 'PostCommentMutationInput' => $this->postCommentMutationInput, + 'PostCommentMutation' => $this->postCommentMutation, + ]; + + $actualTypeMap = TypeInfo::extractTypes($this->mutation); + $this->assertEquals($expectedTypeMap, $actualTypeMap); + } + + public function testThrowsOnMultipleTypesWithSameName() + { + $otherUserType = new ObjectType([ + 'name' => 'User', + 'fields' => [] + ]); + + $queryType = new ObjectType([ + 'name' => 'Test', + 'fields' => [ + 'otherUser' => $otherUserType, + 'user' => $this->user + ] + ]); + + $this->setExpectedException( + '\GraphQL\Error\InvariantViolation', + "Schema must contain unique named types but contains multiple types named \"User\"" + ); + TypeInfo::extractTypes($queryType); + } +}