2017-08-14 16:41:08 +03:00
|
|
|
<?php
|
|
|
|
namespace GraphQL\Tests\Executor;
|
|
|
|
|
|
|
|
require_once __DIR__ . '/TestClasses.php';
|
|
|
|
|
2017-08-14 19:52:17 +03:00
|
|
|
use GraphQL\Error\InvariantViolation;
|
2017-08-14 16:41:08 +03:00
|
|
|
use GraphQL\Error\Warning;
|
|
|
|
use GraphQL\Executor\ExecutionResult;
|
|
|
|
use GraphQL\Executor\Executor;
|
|
|
|
use GraphQL\Language\Parser;
|
2017-08-14 21:49:56 +03:00
|
|
|
use GraphQL\Type\Definition\CustomScalarType;
|
2017-08-14 16:41:08 +03:00
|
|
|
use GraphQL\Type\Definition\InterfaceType;
|
|
|
|
use GraphQL\Type\Definition\ObjectType;
|
|
|
|
use GraphQL\Type\Definition\Type;
|
2017-08-14 21:49:56 +03:00
|
|
|
use GraphQL\Type\Definition\UnionType;
|
2017-08-14 16:41:08 +03:00
|
|
|
use GraphQL\Type\Schema;
|
2018-07-29 18:43:10 +03:00
|
|
|
use PHPUnit\Framework\Error\Error;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
2017-08-14 16:41:08 +03:00
|
|
|
|
2018-07-29 18:43:10 +03:00
|
|
|
class ExecutorLazySchemaTest extends TestCase
|
2017-08-14 16:41:08 +03:00
|
|
|
{
|
2017-08-14 21:49:56 +03:00
|
|
|
public $SomeScalarType;
|
|
|
|
|
|
|
|
public $SomeObjectType;
|
|
|
|
|
|
|
|
public $OtherObjectType;
|
|
|
|
|
|
|
|
public $DeeperObjectType;
|
|
|
|
|
|
|
|
public $SomeUnionType;
|
|
|
|
|
|
|
|
public $SomeInterfaceType;
|
|
|
|
|
|
|
|
public $SomeEnumType;
|
|
|
|
|
|
|
|
public $SomeInputObjectType;
|
|
|
|
|
|
|
|
public $QueryType;
|
|
|
|
|
|
|
|
public $calls = [];
|
|
|
|
|
|
|
|
public $loadedTypes = [];
|
|
|
|
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testWarnsAboutSlowIsTypeOfForLazySchema() : void
|
2017-08-14 16:41:08 +03:00
|
|
|
{
|
|
|
|
// isTypeOf used to resolve runtime type for Interface
|
|
|
|
$petType = new InterfaceType([
|
|
|
|
'name' => 'Pet',
|
|
|
|
'fields' => function() {
|
|
|
|
return [
|
|
|
|
'name' => ['type' => Type::string()]
|
|
|
|
];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Added to interface type when defined
|
|
|
|
$dogType = new ObjectType([
|
|
|
|
'name' => 'Dog',
|
|
|
|
'interfaces' => [$petType],
|
|
|
|
'isTypeOf' => function($obj) { return $obj instanceof Dog; },
|
|
|
|
'fields' => function() {
|
|
|
|
return [
|
|
|
|
'name' => ['type' => Type::string()],
|
|
|
|
'woofs' => ['type' => Type::boolean()]
|
|
|
|
];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
$catType = new ObjectType([
|
|
|
|
'name' => 'Cat',
|
|
|
|
'interfaces' => [$petType],
|
|
|
|
'isTypeOf' => function ($obj) {
|
|
|
|
return $obj instanceof Cat;
|
|
|
|
},
|
|
|
|
'fields' => function() {
|
|
|
|
return [
|
|
|
|
'name' => ['type' => Type::string()],
|
|
|
|
'meows' => ['type' => Type::boolean()],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
$schema = new Schema([
|
|
|
|
'query' => new ObjectType([
|
|
|
|
'name' => 'Query',
|
|
|
|
'fields' => [
|
|
|
|
'pets' => [
|
|
|
|
'type' => Type::listOf($petType),
|
|
|
|
'resolve' => function () {
|
|
|
|
return [new Dog('Odie', true), new Cat('Garfield', false)];
|
|
|
|
}
|
|
|
|
]
|
|
|
|
]
|
|
|
|
]),
|
|
|
|
'types' => [$catType, $dogType],
|
|
|
|
'typeLoader' => function($name) use ($dogType, $petType, $catType) {
|
|
|
|
switch ($name) {
|
|
|
|
case 'Dog':
|
|
|
|
return $dogType;
|
|
|
|
case 'Pet':
|
|
|
|
return $petType;
|
|
|
|
case 'Cat':
|
|
|
|
return $catType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
$query = '{
|
|
|
|
pets {
|
|
|
|
name
|
|
|
|
... on Dog {
|
|
|
|
woofs
|
|
|
|
}
|
|
|
|
... on Cat {
|
|
|
|
meows
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}';
|
|
|
|
|
|
|
|
$expected = new ExecutionResult([
|
|
|
|
'pets' => [
|
|
|
|
['name' => 'Odie', 'woofs' => true],
|
|
|
|
['name' => 'Garfield', 'meows' => false]
|
|
|
|
],
|
|
|
|
]);
|
|
|
|
|
2017-08-19 19:01:46 +03:00
|
|
|
Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN);
|
2017-08-14 16:41:08 +03:00
|
|
|
$result = Executor::execute($schema, Parser::parse($query));
|
|
|
|
$this->assertEquals($expected, $result);
|
|
|
|
|
2017-08-19 19:01:46 +03:00
|
|
|
Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN);
|
2017-08-14 16:41:08 +03:00
|
|
|
$result = Executor::execute($schema, Parser::parse($query));
|
|
|
|
$this->assertEquals(1, count($result->errors));
|
2018-07-29 18:43:10 +03:00
|
|
|
$this->assertInstanceOf(Error::class, $result->errors[0]->getPrevious());
|
2017-08-14 16:41:08 +03:00
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
'GraphQL Interface Type `Pet` returned `null` from it`s `resolveType` function for value: instance of '.
|
|
|
|
'GraphQL\Tests\Executor\Dog. Switching to slow resolution method using `isTypeOf` of all possible '.
|
|
|
|
'implementations. It requires full schema scan and degrades query performance significantly. '.
|
|
|
|
'Make sure your `resolveType` always returns valid implementation or throws.',
|
|
|
|
$result->errors[0]->getMessage());
|
|
|
|
}
|
2017-08-14 19:52:17 +03:00
|
|
|
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testHintsOnConflictingTypeInstancesInDefinitions() : void
|
2017-08-14 19:52:17 +03:00
|
|
|
{
|
|
|
|
$calls = [];
|
|
|
|
$typeLoader = function($name) use (&$calls) {
|
|
|
|
$calls[] = $name;
|
|
|
|
switch ($name) {
|
|
|
|
case 'Test':
|
|
|
|
return new ObjectType([
|
|
|
|
'name' => 'Test',
|
|
|
|
'fields' => function() {
|
|
|
|
return [
|
|
|
|
'test' => Type::string(),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
default:
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
$query = new ObjectType([
|
|
|
|
'name' => 'Query',
|
|
|
|
'fields' => function() use ($typeLoader) {
|
|
|
|
return [
|
|
|
|
'test' => $typeLoader('Test')
|
|
|
|
];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
$schema = new Schema([
|
|
|
|
'query' => $query,
|
|
|
|
'typeLoader' => $typeLoader
|
|
|
|
]);
|
|
|
|
|
|
|
|
$query = '
|
|
|
|
{
|
|
|
|
test {
|
|
|
|
test
|
|
|
|
}
|
|
|
|
}
|
|
|
|
';
|
|
|
|
|
|
|
|
$this->assertEquals([], $calls);
|
|
|
|
$result = Executor::execute($schema, Parser::parse($query), ['test' => ['test' => 'value']]);
|
|
|
|
$this->assertEquals(['Test', 'Test'], $calls);
|
|
|
|
|
|
|
|
$this->assertEquals(
|
|
|
|
'Schema must contain unique named types but contains multiple types named "Test". '.
|
2017-08-20 18:28:17 +03:00
|
|
|
'Make sure that type loader returns the same instance as defined in Query.test '.
|
|
|
|
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
|
2017-08-14 19:52:17 +03:00
|
|
|
$result->errors[0]->getMessage()
|
|
|
|
);
|
|
|
|
$this->assertInstanceOf(
|
|
|
|
InvariantViolation::class,
|
|
|
|
$result->errors[0]->getPrevious()
|
|
|
|
);
|
|
|
|
}
|
2017-08-14 21:49:56 +03:00
|
|
|
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testSimpleQuery() : void
|
2017-08-14 21:49:56 +03:00
|
|
|
{
|
|
|
|
$schema = new Schema([
|
|
|
|
'query' => $this->loadType('Query'),
|
|
|
|
'typeLoader' => function($name) {
|
|
|
|
return $this->loadType($name, true);
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
$query = '{ object { string } }';
|
|
|
|
$result = Executor::execute(
|
|
|
|
$schema,
|
|
|
|
Parser::parse($query),
|
|
|
|
['object' => ['string' => 'test']]
|
|
|
|
);
|
|
|
|
|
|
|
|
$expected = [
|
|
|
|
'data' => ['object' => ['string' => 'test']],
|
|
|
|
];
|
|
|
|
$expectedExecutorCalls = [
|
|
|
|
'Query.fields',
|
|
|
|
'SomeObject',
|
|
|
|
'SomeObject.fields'
|
|
|
|
];
|
|
|
|
$this->assertEquals($expected, $result->toArray(true));
|
|
|
|
$this->assertEquals($expectedExecutorCalls, $this->calls);
|
|
|
|
}
|
|
|
|
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testDeepQuery() : void
|
2017-08-14 21:49:56 +03:00
|
|
|
{
|
|
|
|
$schema = new Schema([
|
|
|
|
'query' => $this->loadType('Query'),
|
|
|
|
'typeLoader' => function($name) {
|
|
|
|
return $this->loadType($name, true);
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
$query = '{ object { object { object { string } } } }';
|
|
|
|
$result = Executor::execute(
|
|
|
|
$schema,
|
|
|
|
Parser::parse($query),
|
|
|
|
['object' => ['object' => ['object' => ['string' => 'test']]]]
|
|
|
|
);
|
|
|
|
|
|
|
|
$expected = [
|
|
|
|
'data' => ['object' => ['object' => ['object' => ['string' => 'test']]]]
|
|
|
|
];
|
|
|
|
$expectedLoadedTypes = [
|
|
|
|
'Query' => true,
|
|
|
|
'SomeObject' => true,
|
|
|
|
'OtherObject' => true
|
|
|
|
];
|
|
|
|
|
|
|
|
$this->assertEquals($expected, $result->toArray(true));
|
|
|
|
$this->assertEquals($expectedLoadedTypes, $this->loadedTypes);
|
|
|
|
|
|
|
|
$expectedExecutorCalls = [
|
|
|
|
'Query.fields',
|
|
|
|
'SomeObject',
|
|
|
|
'SomeObject.fields'
|
|
|
|
];
|
|
|
|
$this->assertEquals($expectedExecutorCalls, $this->calls);
|
|
|
|
}
|
|
|
|
|
2018-08-31 12:07:29 +03:00
|
|
|
public function testResolveUnion() : void
|
2017-08-14 21:49:56 +03:00
|
|
|
{
|
|
|
|
$schema = new Schema([
|
|
|
|
'query' => $this->loadType('Query'),
|
|
|
|
'typeLoader' => function($name) {
|
|
|
|
return $this->loadType($name, true);
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
$query = '
|
|
|
|
{
|
|
|
|
other {
|
|
|
|
union {
|
|
|
|
scalar
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
';
|
|
|
|
$result = Executor::execute(
|
|
|
|
$schema,
|
|
|
|
Parser::parse($query),
|
|
|
|
['other' => ['union' => ['scalar' => 'test']]]
|
|
|
|
);
|
|
|
|
|
|
|
|
$expected = [
|
|
|
|
'data' => ['other' => ['union' => ['scalar' => 'test']]],
|
|
|
|
];
|
|
|
|
$expectedLoadedTypes = [
|
|
|
|
'Query' => true,
|
|
|
|
'SomeObject' => true,
|
|
|
|
'OtherObject' => true,
|
|
|
|
'SomeUnion' => true,
|
|
|
|
'SomeInterface' => true,
|
|
|
|
'DeeperObject' => true,
|
|
|
|
'SomeScalar' => true,
|
|
|
|
];
|
|
|
|
|
|
|
|
$this->assertEquals($expected, $result->toArray(true));
|
|
|
|
$this->assertEquals($expectedLoadedTypes, $this->loadedTypes);
|
|
|
|
|
|
|
|
$expectedCalls = [
|
|
|
|
'Query.fields',
|
|
|
|
'OtherObject',
|
|
|
|
'OtherObject.fields',
|
|
|
|
'SomeUnion',
|
|
|
|
'SomeUnion.resolveType',
|
|
|
|
'SomeUnion.types',
|
|
|
|
'DeeperObject',
|
|
|
|
'SomeScalar',
|
|
|
|
];
|
|
|
|
$this->assertEquals($expectedCalls, $this->calls);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function loadType($name, $isExecutorCall = false)
|
|
|
|
{
|
|
|
|
if ($isExecutorCall) {
|
|
|
|
$this->calls[] = $name;
|
|
|
|
}
|
|
|
|
$this->loadedTypes[$name] = true;
|
|
|
|
|
|
|
|
switch ($name) {
|
|
|
|
case 'Query':
|
|
|
|
return $this->QueryType ?: $this->QueryType = new ObjectType([
|
|
|
|
'name' => 'Query',
|
|
|
|
'fields' => function() {
|
|
|
|
$this->calls[] = 'Query.fields';
|
|
|
|
return [
|
|
|
|
'object' => ['type' => $this->loadType('SomeObject')],
|
|
|
|
'other' => ['type' => $this->loadType('OtherObject')],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
case 'SomeObject':
|
|
|
|
return $this->SomeObjectType ?: $this->SomeObjectType = new ObjectType([
|
|
|
|
'name' => 'SomeObject',
|
|
|
|
'fields' => function() {
|
|
|
|
$this->calls[] = 'SomeObject.fields';
|
|
|
|
return [
|
|
|
|
'string' => ['type' => Type::string()],
|
|
|
|
'object' => ['type' => $this->SomeObjectType]
|
|
|
|
];
|
|
|
|
},
|
|
|
|
'interfaces' => function() {
|
|
|
|
$this->calls[] = 'SomeObject.interfaces';
|
|
|
|
return [
|
|
|
|
$this->loadType('SomeInterface')
|
|
|
|
];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
case 'OtherObject':
|
|
|
|
return $this->OtherObjectType ?: $this->OtherObjectType = new ObjectType([
|
|
|
|
'name' => 'OtherObject',
|
|
|
|
'fields' => function() {
|
|
|
|
$this->calls[] = 'OtherObject.fields';
|
|
|
|
return [
|
|
|
|
'union' => ['type' => $this->loadType('SomeUnion')],
|
|
|
|
'iface' => ['type' => Type::nonNull($this->loadType('SomeInterface'))],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
case 'DeeperObject':
|
|
|
|
return $this->DeeperObjectType ?: $this->DeeperObjectType = new ObjectType([
|
|
|
|
'name' => 'DeeperObject',
|
|
|
|
'fields' => function() {
|
|
|
|
return [
|
|
|
|
'scalar' => ['type' => $this->loadType('SomeScalar')],
|
|
|
|
];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
case 'SomeScalar';
|
|
|
|
return $this->SomeScalarType ?: $this->SomeScalarType = new CustomScalarType([
|
|
|
|
'name' => 'SomeScalar',
|
|
|
|
'serialize' => function($value) {return $value;},
|
|
|
|
'parseValue' => function($value) {return $value;},
|
|
|
|
'parseLiteral' => function() {}
|
|
|
|
]);
|
|
|
|
case 'SomeUnion':
|
|
|
|
return $this->SomeUnionType ?: $this->SomeUnionType = new UnionType([
|
|
|
|
'name' => 'SomeUnion',
|
|
|
|
'resolveType' => function() {
|
|
|
|
$this->calls[] = 'SomeUnion.resolveType';
|
|
|
|
return $this->loadType('DeeperObject');
|
|
|
|
},
|
|
|
|
'types' => function() {
|
|
|
|
$this->calls[] = 'SomeUnion.types';
|
|
|
|
return [ $this->loadType('DeeperObject') ];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
case 'SomeInterface':
|
|
|
|
return $this->SomeInterfaceType ?: $this->SomeInterfaceType = new InterfaceType([
|
|
|
|
'name' => 'SomeInterface',
|
|
|
|
'resolveType' => function() {
|
|
|
|
$this->calls[] = 'SomeInterface.resolveType';
|
|
|
|
return $this->loadType('SomeObject');
|
|
|
|
},
|
|
|
|
'fields' => function() {
|
|
|
|
$this->calls[] = 'SomeInterface.fields';
|
|
|
|
return [
|
|
|
|
'string' => ['type' => Type::string() ]
|
|
|
|
];
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
default:
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2017-08-14 16:41:08 +03:00
|
|
|
}
|