Introduced type resolution strategies: eager and lazy (for #69)

This commit is contained in:
vladar 2016-12-10 07:49:41 +07:00
parent 97f9c1c201
commit 2043cc7e75
6 changed files with 955 additions and 130 deletions

View File

@ -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<string, ObjectType[]>
*/
private $implementations;
private $config;
/**
* @var array<string, array<string, boolean>>
*/
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;
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace GraphQL\Type;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\FieldArgument;
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\Utils;
class EagerResolution implements Resolution
{
/**
* @var Type[]
*/
private $typeMap = [];
/**
* @var array<string, ObjectType[]>
*/
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;
}
}

108
src/Type/LazyResolution.php Normal file
View File

@ -0,0 +1,108 @@
<?php
namespace GraphQL\Type;
use GraphQL\Error\InvariantViolation;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils;
class LazyResolution implements Resolution
{
/**
* @var array
*/
private $typeMap;
/**
* @var array
*/
private $possibleTypeMap;
/**
* @var callable
*/
private $typeLoader;
/**
* List of currently loaded types
*
* @var Type[]
*/
private $loadedTypes;
/**
* Map of $interfaceTypeName => $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];
}
}

25
src/Type/Resolution.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace GraphQL\Type;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
interface Resolution
{
/**
* Returns instance of type with given $name for GraphQL Schema
*
* @param string $name
* @return Type
*/
public function resolveType($name);
/**
* Returns instances of possible ObjectTypes for given InterfaceType or UnionType
*
* @param AbstractType $type
* @return ObjectType[]
*/
public function resolvePossibleTypes(AbstractType $type);
}

View File

@ -0,0 +1,596 @@
<?php
namespace GraphQL\Tests\Type;
use GraphQL\Error\InvariantViolation;
use GraphQL\Type\Definition\Config;
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\EagerResolution;
use GraphQL\Type\LazyResolution;
class ResolutionTest extends \PHPUnit_Framework_TestCase
{
/**
* @var ObjectType
*/
private $query;
/**
* @var ObjectType
*/
private $mutation;
/**
* @var InterfaceType
*/
private $node;
/**
* @var InterfaceType
*/
private $content;
/**
* @var ObjectType
*/
private $blogStory;
/**
* @var ObjectType
*/
private $link;
/**
* @var ObjectType
*/
private $video;
/**
* @var ObjectType
*/
private $videoMetadata;
/**
* @var ObjectType
*/
private $comment;
/**
* @var ObjectType
*/
private $user;
/**
* @var ObjectType
*/
private $category;
/**
* @var UnionType
*/
private $mention;
private $postStoryMutation;
private $postStoryMutationInput;
private $postCommentMutation;
private $postCommentMutationInput;
public function setUp()
{
Config::enableValidation(false);
$this->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()
);
}
}
}

View File

@ -17,6 +17,7 @@ class MyCustomType extends ObjectType
}
}
// Note: named OtherCustom vs OtherCustomType intentionally
class OtherCustom extends ObjectType
{
public function __construct()