From 2043cc7e75e28141f38e175f5da0cb2ba9e1b842 Mon Sep 17 00:00:00 2001 From: vladar Date: Sat, 10 Dec 2016 07:49:41 +0700 Subject: [PATCH] Introduced type resolution strategies: eager and lazy (for #69) --- src/Schema.php | 201 ++++-------- src/Type/EagerResolution.php | 154 +++++++++ src/Type/LazyResolution.php | 108 ++++++ src/Type/Resolution.php | 25 ++ tests/Type/ResolutionTest.php | 596 ++++++++++++++++++++++++++++++++++ tests/Type/TestClasses.php | 1 + 6 files changed, 955 insertions(+), 130 deletions(-) create mode 100644 src/Type/EagerResolution.php create mode 100644 src/Type/LazyResolution.php create mode 100644 src/Type/Resolution.php create mode 100644 tests/Type/ResolutionTest.php diff --git a/src/Schema.php b/src/Schema.php index a1c527d..ee208e7 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -3,13 +3,11 @@ namespace GraphQL; use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; -use GraphQL\Type\Definition\InputObjectType; -use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\UnionType; -use GraphQL\Type\Definition\WrappingType; +use GraphQL\Type\EagerResolution; use GraphQL\Type\Introspection; +use GraphQL\Type\Resolution; /** * Schema Definition @@ -41,39 +39,26 @@ use GraphQL\Type\Introspection; class Schema { /** - * @var ObjectType + * @var array */ - private $queryType; - - /** - * @var ObjectType - */ - private $mutationType; - - /** - * @var ObjectType - */ - private $subscriptionType; - - /** - * @var Directive[] - */ - private $directives; - - /** - * @var Type[] - */ - private $typeMap; - - /** - * @var array - */ - private $implementations; + private $config; /** * @var array> */ - private $possibleTypeMap; + private $possibleTypeMap = []; + + /** + * @var Resolution + */ + private $typeResolutionStrategy; + + /** + * Required for `getTypeMap()` and `getDescriptor()` methods + * + * @var EagerResolution + */ + private $eagerTypeResolutionStrategy; /** * Schema constructor. @@ -96,6 +81,15 @@ class Schema ]; } + $config += [ + 'query' => null, + 'mutation' => null, + 'subscription' => null, + 'types' => [], + 'directives' => null, + 'typeResolution' => null + ]; + $this->init($config); } @@ -104,33 +98,20 @@ class Schema */ private function init(array $config) { - $config += [ - 'query' => null, - 'mutation' => null, - 'subscription' => null, - 'types' => [], - 'directives' => [], - 'validate' => true - ]; - Utils::invariant( $config['query'] instanceof ObjectType, "Schema query must be Object Type but got: " . Utils::getVariableType($config['query']) ); - $this->queryType = $config['query']; - Utils::invariant( !$config['mutation'] || $config['mutation'] instanceof ObjectType, "Schema mutation must be Object Type if provided but got: " . Utils::getVariableType($config['mutation']) ); - $this->mutationType = $config['mutation']; Utils::invariant( !$config['subscription'] || $config['subscription'] instanceof ObjectType, "Schema subscription must be Object Type if provided but got: " . Utils::getVariableType($config['subscription']) ); - $this->subscriptionType = $config['subscription']; Utils::invariant( !$config['types'] || is_array($config['types']), @@ -142,33 +123,14 @@ class Schema "Schema directives must be Directive[] if provided but got " . Utils::getVariableType($config['directives']) ); - $this->directives = $config['directives'] ?: GraphQL::getInternalDirectives(); + Utils::invariant( + !$config['typeResolution'] || $config['typeResolution'] instanceof Resolution, + "Type resolution strategy is expected to be instance of GraphQL\\Type\\Resolution, but got " . + Utils::getVariableType($config['typeResolution']) + ); - // Build type map now to detect any errors within this schema. - $initialTypes = [ - $config['query'], - $config['mutation'], - $config['subscription'], - Introspection::_schema() - ]; - if (!empty($config['types'])) { - $initialTypes = array_merge($initialTypes, $config['types']); - } - - foreach ($initialTypes as $type) { - $this->extractTypes($type); - } - $this->typeMap += Type::getInternalTypes(); - - // Keep track of all implementations by interface name. - $this->implementations = []; - foreach ($this->typeMap as $typeName => $type) { - if ($type instanceof ObjectType) { - foreach ($type->getInterfaces() as $iface) { - $this->implementations[$iface->name][] = $type; - } - } - } + $this->config = $config; + $this->typeResolutionStrategy = $config['typeResolution'] ?: $this->getEagerTypeResolutionStrategy(); } /** @@ -176,7 +138,7 @@ class Schema */ public function getQueryType() { - return $this->queryType; + return $this->config['query']; } /** @@ -184,7 +146,7 @@ class Schema */ public function getMutationType() { - return $this->mutationType; + return $this->config['mutation']; } /** @@ -192,15 +154,18 @@ class Schema */ public function getSubscriptionType() { - return $this->subscriptionType; + return $this->config['subscription']; } /** - * @return array + * Returns full map of types in this schema. + * Note: internally it will eager-load all types using GraphQL\Type\EagerResolution strategy + * + * @return Type[] */ public function getTypeMap() { - return $this->typeMap; + return $this->getEagerTypeResolutionStrategy()->getTypeMap(); } /** @@ -209,8 +174,17 @@ class Schema */ public function getType($name) { - $map = $this->getTypeMap(); - return isset($map[$name]) ? $map[$name] : null; + return $this->typeResolutionStrategy->resolveType($name); + } + + /** + * Returns serializable schema representation suitable for GraphQL\Type\LazyResolution + * + * @return array + */ + public function getDescriptor() + { + return $this->getEagerTypeResolutionStrategy()->getDescriptor(); } /** @@ -219,11 +193,7 @@ class Schema */ public function getPossibleTypes(AbstractType $abstractType) { - if ($abstractType instanceof UnionType) { - return $abstractType->getTypes(); - } - Utils::invariant($abstractType instanceof InterfaceType); - return isset($this->implementations[$abstractType->name]) ? $this->implementations[$abstractType->name] : []; + return $this->typeResolutionStrategy->resolvePossibleTypes($abstractType); } /** @@ -233,14 +203,10 @@ class Schema */ public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) { - if (null === $this->possibleTypeMap) { - $this->possibleTypeMap = []; - } - if (!isset($this->possibleTypeMap[$abstractType->name])) { $tmp = []; foreach ($this->getPossibleTypes($abstractType) as $type) { - $tmp[$type->name] = true; + $tmp[$type->name] = 1; } Utils::invariant( @@ -253,7 +219,6 @@ class Schema $this->possibleTypeMap[$abstractType->name] = $tmp; } - return !empty($this->possibleTypeMap[$abstractType->name][$possibleType->name]); } @@ -262,7 +227,7 @@ class Schema */ public function getDirectives() { - return $this->directives; + return isset($this->config['directives']) ? $this->config['directives'] : GraphQL::getInternalDirectives(); } /** @@ -279,49 +244,25 @@ class Schema return null; } - /** - * @param $type - * @return array - */ - private function extractTypes($type) + private function getEagerTypeResolutionStrategy() { - 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($arg) { return $arg->getType(); }, $field->args); - $nestedTypes = array_merge($nestedTypes, $fieldArgTypes); + if (!$this->eagerTypeResolutionStrategy) { + if ($this->typeResolutionStrategy instanceof EagerResolution) { + $this->eagerTypeResolutionStrategy = $this->typeResolutionStrategy; + } else { + // Build type map now to detect any errors within this schema. + $initialTypes = [ + $this->config['query'], + $this->config['mutation'], + $this->config['subscription'], + Introspection::_schema() + ]; + if (!empty($this->config['types'])) { + $initialTypes = array_merge($initialTypes, $this->config['types']); } - $nestedTypes[] = $field->getType(); + $this->eagerTypeResolutionStrategy = new EagerResolution($initialTypes); } } - foreach ($nestedTypes as $type) { - $this->extractTypes($type); - } - return $this->typeMap; + return $this->eagerTypeResolutionStrategy; } } diff --git a/src/Type/EagerResolution.php b/src/Type/EagerResolution.php new file mode 100644 index 0000000..ce06389 --- /dev/null +++ b/src/Type/EagerResolution.php @@ -0,0 +1,154 @@ + + */ + private $implementations = []; + + /** + * EagerResolution constructor. + * @param Type[] $initialTypes + */ + public function __construct(array $initialTypes) + { + foreach ($initialTypes as $type) { + $this->extractTypes($type); + } + $this->typeMap += Type::getInternalTypes(); + + // Keep track of all possible types for abstract types + foreach ($this->typeMap as $typeName => $type) { + if ($type instanceof ObjectType) { + foreach ($type->getInterfaces() as $iface) { + $this->implementations[$iface->name][] = $type; + } + } + } + } + + /** + * @inheritdoc + */ + public function resolveType($name) + { + return isset($this->typeMap[$name]) ? $this->typeMap[$name] : null; + } + + /** + * @inheritdoc + */ + public function resolvePossibleTypes(AbstractType $abstractType) + { + if (!isset($this->typeMap[$abstractType->name])) { + return []; + } + + if ($abstractType instanceof UnionType) { + return $abstractType->getTypes(); + } + + /** @var InterfaceType $abstractType */ + Utils::invariant($abstractType instanceof InterfaceType); + return isset($this->implementations[$abstractType->name]) ? $this->implementations[$abstractType->name] : []; + } + + /** + * @return Type[] + */ + public function getTypeMap() + { + return $this->typeMap; + } + + /** + * Returns serializable schema representation suitable for GraphQL\Type\LazyResolution + * + * @return array + */ + public function getDescriptor() + { + $typeMap = []; + $possibleTypesMap = []; + foreach ($this->getTypeMap() as $type) { + if ($type instanceof UnionType) { + foreach ($type->getTypes() as $innerType) { + $possibleTypesMap[$type->name][$innerType->name] = 1; + } + } else if ($type instanceof InterfaceType) { + foreach ($this->implementations[$type->name] as $obj) { + $possibleTypesMap[$type->name][$obj->name] = 1; + } + } + $typeMap[$type->name] = 1; + } + return [ + 'version' => '1.0', + 'typeMap' => $typeMap, + '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/Type/LazyResolution.php b/src/Type/LazyResolution.php new file mode 100644 index 0000000..3acccc4 --- /dev/null +++ b/src/Type/LazyResolution.php @@ -0,0 +1,108 @@ + $objectType[] + * + * @var array + */ + private $loadedPossibleTypes; + + /** + * LazyResolution constructor. + * @param array $descriptor + * @param callable $typeLoader + */ + public function __construct(array $descriptor, callable $typeLoader) + { + Utils::invariant( + isset($descriptor['typeMap'], $descriptor['possibleTypeMap'], $descriptor['version']) + ); + Utils::invariant( + $descriptor['version'] === '1.0' + ); + + $this->typeLoader = $typeLoader; + $this->typeMap = $descriptor['typeMap'] + Type::getInternalTypes(); + $this->possibleTypeMap = $descriptor['possibleTypeMap']; + $this->loadedTypes = Type::getInternalTypes(); + $this->loadedPossibleTypes = []; + } + + /** + * @inheritdoc + */ + public function resolveType($name) + { + if (!isset($this->typeMap[$name])) { + return null; + } + if (!isset($this->loadedTypes[$name])) { + $type = call_user_func($this->typeLoader, $name); + if (!$type instanceof Type && null !== $type) { + throw new InvariantViolation( + "Lazy Type Resolution Error: Expecting GraphQL Type instance, but got " . + Utils::getVariableType($type) + ); + } + + $this->loadedTypes[$name] = $type; + } + return $this->loadedTypes[$name]; + } + + /** + * @inheritdoc + */ + public function resolvePossibleTypes(AbstractType $type) + { + if (!isset($this->possibleTypeMap[$type->name])) { + return []; + } + if (!isset($this->loadedPossibleTypes[$type->name])) { + $tmp = []; + foreach ($this->possibleTypeMap[$type->name] as $typeName => $true) { + $obj = $this->resolveType($typeName); + if (!$obj instanceof ObjectType) { + throw new InvariantViolation( + "Lazy Type Resolution Error: Implementation {$typeName} of interface {$type->name} " . + "is expected to be instance of ObjectType, but got " . Utils::getVariableType($obj) + ); + } + $tmp[] = $obj; + } + $this->loadedPossibleTypes[$type->name] = $tmp; + } + return $this->loadedPossibleTypes[$type->name]; + } +} diff --git a/src/Type/Resolution.php b/src/Type/Resolution.php new file mode 100644 index 0000000..1f47655 --- /dev/null +++ b/src/Type/Resolution.php @@ -0,0 +1,25 @@ +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 testEagerTypeResolution() + { + // Has internal types by default: + $eagerTypeResolution = new EagerResolution([]); + $expectedTypeMap = [ + 'ID' => Type::id(), + 'String' => Type::string(), + 'Float' => Type::float(), + 'Int' => Type::int(), + 'Boolean' => Type::boolean() + ]; + $this->assertEquals($expectedTypeMap, $eagerTypeResolution->getTypeMap()); + + $expectedDescriptor = [ + 'version' => '1.0', + 'typeMap' => [ + 'ID' => 1, + 'String' => 1, + 'Float' => 1, + 'Int' => 1, + 'Boolean' => 1, + ], + 'possibleTypeMap' => [] + ]; + $this->assertEquals($expectedDescriptor, $eagerTypeResolution->getDescriptor()); + + $this->assertSame(null, $eagerTypeResolution->resolveType('User')); + $this->assertSame([], $eagerTypeResolution->resolvePossibleTypes($this->node)); + $this->assertSame([], $eagerTypeResolution->resolvePossibleTypes($this->content)); + $this->assertSame([], $eagerTypeResolution->resolvePossibleTypes($this->mention)); + + $eagerTypeResolution = new EagerResolution([$this->query, $this->mutation]); + + $this->assertSame($this->query, $eagerTypeResolution->resolveType('Query')); + $this->assertSame($this->mutation, $eagerTypeResolution->resolveType('Mutation')); + $this->assertSame($this->user, $eagerTypeResolution->resolveType('User')); + $this->assertSame($this->node, $eagerTypeResolution->resolveType('Node')); + $this->assertSame($this->node, $eagerTypeResolution->resolveType('Node')); + $this->assertSame($this->content, $eagerTypeResolution->resolveType('Content')); + $this->assertSame($this->comment, $eagerTypeResolution->resolveType('Comment')); + $this->assertSame($this->mention, $eagerTypeResolution->resolveType('Mention')); + $this->assertSame($this->blogStory, $eagerTypeResolution->resolveType('BlogStory')); + $this->assertSame($this->category, $eagerTypeResolution->resolveType('Category')); + $this->assertSame($this->postStoryMutation, $eagerTypeResolution->resolveType('PostStoryMutation')); + $this->assertSame($this->postStoryMutationInput, $eagerTypeResolution->resolveType('PostStoryMutationInput')); + $this->assertSame($this->postCommentMutation, $eagerTypeResolution->resolveType('PostCommentMutation')); + $this->assertSame($this->postCommentMutationInput, $eagerTypeResolution->resolveType('PostCommentMutationInput')); + + $this->assertEquals([$this->blogStory], $eagerTypeResolution->resolvePossibleTypes($this->content)); + $this->assertEquals([$this->user, $this->comment, $this->category, $this->blogStory], $eagerTypeResolution->resolvePossibleTypes($this->node)); + $this->assertEquals([$this->user, $this->category], $eagerTypeResolution->resolvePossibleTypes($this->mention)); + + $expectedTypeMap = [ + 'Query' => $this->query, + 'Mutation' => $this->mutation, + 'User' => $this->user, + 'Node' => $this->node, + 'String' => Type::string(), + 'Content' => $this->content, + 'Comment' => $this->comment, + 'Mention' => $this->mention, + 'BlogStory' => $this->blogStory, + 'Category' => $this->category, + 'PostStoryMutationInput' => $this->postStoryMutationInput, + 'ID' => Type::id(), + 'PostStoryMutation' => $this->postStoryMutation, + 'PostCommentMutationInput' => $this->postCommentMutationInput, + 'PostCommentMutation' => $this->postCommentMutation, + 'Float' => Type::float(), + 'Int' => Type::int(), + 'Boolean' => Type::boolean() + ]; + + $this->assertEquals($expectedTypeMap, $eagerTypeResolution->getTypeMap()); + + $expectedDescriptor = [ + 'version' => '1.0', + 'typeMap' => [ + 'Query' => 1, + 'Mutation' => 1, + 'User' => 1, + 'Node' => 1, + 'String' => 1, + 'Content' => 1, + 'Comment' => 1, + 'Mention' => 1, + 'BlogStory' => 1, + 'Category' => 1, + 'PostStoryMutationInput' => 1, + 'ID' => 1, + 'PostStoryMutation' => 1, + 'PostCommentMutationInput' => 1, + 'PostCommentMutation' => 1, + 'Float' => 1, + 'Int' => 1, + 'Boolean' => 1 + ], + 'possibleTypeMap' => [ + 'Node' => [ + 'User' => 1, + 'Comment' => 1, + 'Category' => 1, + 'BlogStory' => 1 + ], + 'Content' => [ + 'BlogStory' => 1 + ], + 'Mention' => [ + 'User' => 1, + 'Category' => 1 + ] + ] + ]; + + $this->assertEquals($expectedDescriptor, $eagerTypeResolution->getDescriptor()); + + // Ignores duplicates and nulls in initialTypes: + $eagerTypeResolution = new EagerResolution([null, $this->query, null, $this->query, $this->mutation, null]); + $this->assertEquals($expectedTypeMap, $eagerTypeResolution->getTypeMap()); + $this->assertEquals($expectedDescriptor, $eagerTypeResolution->getDescriptor()); + + // Those types are only part of interface + $this->assertEquals(null, $eagerTypeResolution->resolveType('Link')); + $this->assertEquals(null, $eagerTypeResolution->resolveType('Video')); + $this->assertEquals(null, $eagerTypeResolution->resolveType('VideoMetadata')); + + $this->assertEquals([$this->blogStory], $eagerTypeResolution->resolvePossibleTypes($this->content)); + $this->assertEquals([$this->user, $this->comment, $this->category, $this->blogStory], $eagerTypeResolution->resolvePossibleTypes($this->node)); + $this->assertEquals([$this->user, $this->category], $eagerTypeResolution->resolvePossibleTypes($this->mention)); + + $eagerTypeResolution = new EagerResolution([null, $this->video, null]); + $this->assertEquals($this->videoMetadata, $eagerTypeResolution->resolveType('VideoMetadata')); + $this->assertEquals($this->video, $eagerTypeResolution->resolveType('Video')); + + $this->assertEquals([$this->video], $eagerTypeResolution->resolvePossibleTypes($this->content)); + $this->assertEquals([$this->video, $this->user, $this->comment, $this->category], $eagerTypeResolution->resolvePossibleTypes($this->node)); + $this->assertEquals([], $eagerTypeResolution->resolvePossibleTypes($this->mention)); + + $expectedTypeMap = [ + 'Video' => $this->video, + 'Node' => $this->node, + 'String' => Type::string(), + 'Content' => $this->content, + 'User' => $this->user, + 'Comment' => $this->comment, + 'Category' => $this->category, + 'VideoMetadata' => $this->videoMetadata, + 'Float' => Type::float(), + 'ID' => Type::id(), + 'Int' => Type::int(), + 'Boolean' => Type::boolean() + ]; + $this->assertEquals($expectedTypeMap, $eagerTypeResolution->getTypeMap()); + + $expectedDescriptor = [ + 'version' => '1.0', + 'typeMap' => [ + 'Video' => 1, + 'Node' => 1, + 'String' => 1, + 'Content' => 1, + 'User' => 1, + 'Comment' => 1, + 'Category' => 1, + 'VideoMetadata' => 1, + 'Float' => 1, + 'ID' => 1, + 'Int' => 1, + 'Boolean' => 1 + ], + 'possibleTypeMap' => [ + 'Node' => [ + 'Video' => 1, + 'User' => 1, + 'Comment' => 1, + 'Category' => 1 + ], + 'Content' => [ + 'Video' => 1 + ] + ] + ]; + $this->assertEquals($expectedDescriptor, $eagerTypeResolution->getDescriptor()); + } + + public function testLazyResolutionFollowsEagerResolution() + { + // Lazy resolution should work the same way as eager resolution works, except that it should load types on demand + $eager = new EagerResolution([]); + $emptyDescriptor = $eager->getDescriptor(); + + $typeLoader = function($name) { + throw new \Exception("This should be never called for empty descriptor"); + }; + + $lazy = new LazyResolution($emptyDescriptor, $typeLoader); + $this->assertSame($eager->resolveType('User'), $lazy->resolveType('User')); + $this->assertSame($eager->resolvePossibleTypes($this->node), $lazy->resolvePossibleTypes($this->node)); + $this->assertSame($eager->resolvePossibleTypes($this->content), $lazy->resolvePossibleTypes($this->content)); + $this->assertSame($eager->resolvePossibleTypes($this->mention), $lazy->resolvePossibleTypes($this->mention)); + + $eager = new EagerResolution([$this->query, $this->mutation]); + + $called = 0; + $descriptor = $eager->getDescriptor(); + $typeLoader = function($name) use (&$called) { + $called++; + $prop = lcfirst($name); + return $this->{$prop}; + }; + + $lazy = new LazyResolution($descriptor, $typeLoader); + + $this->assertSame($eager->resolveType('Query'), $lazy->resolveType('Query')); + $this->assertSame(1, $called); + $this->assertSame($eager->resolveType('Mutation'), $lazy->resolveType('Mutation')); + $this->assertSame(2, $called); + $this->assertSame($eager->resolveType('User'), $lazy->resolveType('User')); + $this->assertSame(3, $called); + $this->assertSame($eager->resolveType('User'), $lazy->resolveType('User')); + $this->assertSame(3, $called); + $this->assertSame($eager->resolveType('Node'), $lazy->resolveType('Node')); + $this->assertSame($eager->resolveType('Node'), $lazy->resolveType('Node')); + $this->assertSame(4, $called); + $this->assertSame($eager->resolveType('Content'), $lazy->resolveType('Content')); + $this->assertSame($eager->resolveType('Comment'), $lazy->resolveType('Comment')); + $this->assertSame($eager->resolveType('Mention'), $lazy->resolveType('Mention')); + $this->assertSame($eager->resolveType('BlogStory'), $lazy->resolveType('BlogStory')); + $this->assertSame($eager->resolveType('Category'), $lazy->resolveType('Category')); + $this->assertSame($eager->resolveType('PostStoryMutation'), $lazy->resolveType('PostStoryMutation')); + $this->assertSame($eager->resolveType('PostStoryMutationInput'), $lazy->resolveType('PostStoryMutationInput')); + $this->assertSame($eager->resolveType('PostCommentMutation'), $lazy->resolveType('PostCommentMutation')); + $this->assertSame($eager->resolveType('PostCommentMutationInput'), $lazy->resolveType('PostCommentMutationInput')); + $this->assertSame(13, $called); + + $this->assertEquals($eager->resolvePossibleTypes($this->content), $lazy->resolvePossibleTypes($this->content)); + $this->assertEquals($eager->resolvePossibleTypes($this->node), $lazy->resolvePossibleTypes($this->node)); + $this->assertEquals($eager->resolvePossibleTypes($this->mention), $lazy->resolvePossibleTypes($this->mention)); + + $called = 0; + $eager = new EagerResolution([$this->video]); + $lazy = new LazyResolution($eager->getDescriptor(), $typeLoader); + + $this->assertEquals($eager->resolveType('VideoMetadata'), $lazy->resolveType('VideoMetadata')); + $this->assertEquals($eager->resolveType('Video'), $lazy->resolveType('Video')); + $this->assertEquals(2, $called); + + $this->assertEquals($eager->resolvePossibleTypes($this->content), $lazy->resolvePossibleTypes($this->content)); + $this->assertEquals($eager->resolvePossibleTypes($this->node), $lazy->resolvePossibleTypes($this->node)); + $this->assertEquals($eager->resolvePossibleTypes($this->mention), $lazy->resolvePossibleTypes($this->mention)); + } + + public function testLazyThrowsOnInvalidLoadedType() + { + $descriptor = [ + 'version' => '1.0', + 'typeMap' => [ + 'null' => 1, + 'int' => 1 + ], + 'possibleTypeMap' => [ + 'a' => [ + 'null' => 1, + ], + 'b' => [ + 'int' => 1 + ] + ] + ]; + + $invalidTypeLoader = function($name) { + switch ($name) { + case 'null': + return null; + case 'int': + return 7; + } + }; + + $lazy = new LazyResolution($descriptor, $invalidTypeLoader); + $value = $lazy->resolveType('null'); + $this->assertEquals(null, $value); + + try { + $lazy->resolveType('int'); + $this->fail('Expected exception not thrown'); + } catch (InvariantViolation $e) { + $this->assertEquals( + "Lazy Type Resolution Error: Expecting GraphQL Type instance, but got integer", + $e->getMessage() + ); + + } + + try { + $tmp = new InterfaceType(['name' => 'a', 'fields' => []]); + $lazy->resolvePossibleTypes($tmp); + $this->fail('Expected exception not thrown'); + } catch (InvariantViolation $e) { + $this->assertEquals( + 'Lazy Type Resolution Error: Implementation null of interface a is expected to be instance of ObjectType, but got NULL', + $e->getMessage() + ); + } + + try { + $tmp = new InterfaceType(['name' => 'b', 'fields' => []]); + $lazy->resolvePossibleTypes($tmp); + $this->fail('Expected exception not thrown'); + } catch (InvariantViolation $e) { + $this->assertEquals( + 'Lazy Type Resolution Error: Expecting GraphQL Type instance, but got integer', + $e->getMessage() + ); + } + } +} diff --git a/tests/Type/TestClasses.php b/tests/Type/TestClasses.php index affe6a4..b9c277c 100644 --- a/tests/Type/TestClasses.php +++ b/tests/Type/TestClasses.php @@ -17,6 +17,7 @@ class MyCustomType extends ObjectType } } +// Note: named OtherCustom vs OtherCustomType intentionally class OtherCustom extends ObjectType { public function __construct()