'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] ], ]); Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN); $result = Executor::execute($schema, Parser::parse($query)); $this->assertEquals($expected, $result); Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN); $result = Executor::execute($schema, Parser::parse($query)); $this->assertEquals(1, count($result->errors)); $this->assertInstanceOf(Error::class, $result->errors[0]->getPrevious()); $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()); } public function testHintsOnConflictingTypeInstancesInDefinitions() { $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". '. '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).', $result->errors[0]->getMessage() ); $this->assertInstanceOf( InvariantViolation::class, $result->errors[0]->getPrevious() ); } public function testSimpleQuery() { $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); } public function testDeepQuery() { $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); } public function testResolveUnion() { $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; } } }