Skip to content

Commit 0439dc2

Browse files
committed
Add support for interfaces
1 parent d55a1d4 commit 0439dc2

File tree

11 files changed

+375
-14
lines changed

11 files changed

+375
-14
lines changed

src/Enumeration/FieldTypeKindEnum.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
*/
1010
class FieldTypeKindEnum
1111
{
12-
const SCALAR = 'SCALAR';
13-
const LIST = 'LIST';
14-
const NON_NULL = 'NON_NULL';
15-
const OBJECT = 'OBJECT';
16-
const INPUT_OBJECT = 'INPUT_OBJECT';
17-
const ENUM_OBJECT = 'ENUM';
18-
const UNION_OBJECT = 'UNION';
12+
const SCALAR = 'SCALAR';
13+
const LIST = 'LIST';
14+
const NON_NULL = 'NON_NULL';
15+
const OBJECT = 'OBJECT';
16+
const INPUT_OBJECT = 'INPUT_OBJECT';
17+
const ENUM_OBJECT = 'ENUM';
18+
const UNION_OBJECT = 'UNION';
19+
const INTERFACE_OBJECT = 'INTERFACE';
1920
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace GraphQL\SchemaGenerator\CodeGenerator;
4+
5+
use GraphQL\SchemaGenerator\CodeGenerator\CodeFile\ClassFile;
6+
use GraphQL\Util\StringLiteralFormatter;
7+
8+
/**
9+
* Class InterfaceObjectBuilder
10+
*
11+
* @package GraphQL\SchemaGenerator\CodeGenerator
12+
*/
13+
class InterfaceObjectBuilder extends QueryObjectClassBuilder
14+
{
15+
/**
16+
* @var ClassFile
17+
*/
18+
protected $classFile;
19+
20+
/**
21+
* EnumObjectBuilder constructor.
22+
*
23+
* @param string $writeDir
24+
* @param string $objectName
25+
* @param string $namespace
26+
*/
27+
public function __construct(string $writeDir, string $objectName, string $namespace = self::DEFAULT_NAMESPACE)
28+
{
29+
$className = $objectName . 'QueryObject';
30+
31+
$this->classFile = new ClassFile($writeDir, $className);
32+
$this->classFile->setNamespace($namespace);
33+
if ($namespace !== self::DEFAULT_NAMESPACE) {
34+
$this->classFile->addImport('GraphQL\\SchemaObject\\InterfaceObject');
35+
}
36+
$this->classFile->extendsClass('InterfaceObject');
37+
}
38+
39+
/**
40+
* @param string $typeName
41+
*/
42+
public function addImplementation(string $typeName)
43+
{
44+
$upperCamelCaseTypeName = StringLiteralFormatter::formatUpperCamelCase($typeName);
45+
$objectClassName = $typeName . 'QueryObject';
46+
$method = "public function on$upperCamelCaseTypeName(): $objectClassName
47+
{
48+
return \$this->addImplementation($objectClassName::class);
49+
}";
50+
$this->classFile->addMethod($method);
51+
}
52+
53+
/**
54+
* @return void
55+
*/
56+
public function build(): void
57+
{
58+
$this->classFile->writeFile();
59+
}
60+
}

src/SchemaGenerator/SchemaClassGenerator.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use GraphQL\SchemaGenerator\CodeGenerator\ArgumentsObjectClassBuilder;
88
use GraphQL\SchemaGenerator\CodeGenerator\EnumObjectBuilder;
99
use GraphQL\SchemaGenerator\CodeGenerator\InputObjectClassBuilder;
10+
use GraphQL\SchemaGenerator\CodeGenerator\InterfaceObjectBuilder;
1011
use GraphQL\SchemaGenerator\CodeGenerator\ObjectBuilderInterface;
1112
use GraphQL\SchemaGenerator\CodeGenerator\QueryObjectClassBuilder;
1213
use GraphQL\SchemaGenerator\CodeGenerator\UnionObjectBuilder;
@@ -44,7 +45,7 @@ class SchemaClassGenerator
4445
*AND complete covering the schema scanner class
4546
* @var array
4647
*/
47-
private $generatedObjects;
48+
private $generatedObjects;
4849

4950
/**
5051
* SchemaClassGenerator constructor.
@@ -141,6 +142,7 @@ protected function generateObject(string $objectName, string $objectKind): bool
141142
{
142143
switch ($objectKind) {
143144
case FieldTypeKindEnum::OBJECT:
145+
case FieldTypeKindEnum::INTERFACE_OBJECT:
144146
return $this->generateQueryObject($objectName);
145147
case FieldTypeKindEnum::INPUT_OBJECT:
146148
return $this->generateInputObject($objectName);
@@ -168,7 +170,15 @@ protected function generateQueryObject(string $objectName): bool
168170
$this->generatedObjects[$objectName] = true;
169171
$objectArray = $this->schemaInspector->getObjectSchema($objectName);
170172
$objectName = $objectArray['name'];
171-
$objectBuilder = new QueryObjectClassBuilder($this->writeDir, $objectName, $this->generationNamespace);
173+
174+
if ($objectArray['kind'] === FieldTypeKindEnum::INTERFACE_OBJECT) {
175+
$objectBuilder = new InterfaceObjectBuilder($this->writeDir, $objectName, $this->generationNamespace);
176+
foreach ($objectArray['possibleTypes'] as $possibleType) {
177+
$objectBuilder->addImplementation($possibleType['name']);
178+
}
179+
} else {
180+
$objectBuilder = new QueryObjectClassBuilder($this->writeDir, $objectName, $this->generationNamespace);
181+
}
172182

173183
$this->appendQueryObjectFields($objectBuilder, $objectName, $objectArray['fields']);
174184
$objectBuilder->build();
@@ -274,6 +284,32 @@ protected function generateUnionObject(string $objectName): bool
274284
return true;
275285
}
276286

287+
/**
288+
* @param string $objectName
289+
*
290+
* @return bool
291+
*/
292+
protected function generateInterfaceObject(string $objectName): bool
293+
{
294+
if (array_key_exists($objectName, $this->generatedObjects)) {
295+
return true;
296+
}
297+
298+
$this->generatedObjects[$objectName] = true;
299+
300+
$objectArray = $this->schemaInspector->getObjectSchema($objectName);
301+
$objectName = $objectArray['name'];
302+
$objectBuilder = new UnionObjectBuilder($this->writeDir, $objectName, $this->generationNamespace);
303+
304+
foreach ($objectArray['possibleTypes'] as $possibleType) {
305+
$this->generateObject($possibleType['name'], $possibleType['kind']);
306+
$objectBuilder->addPossibleType($possibleType['name']);
307+
}
308+
$objectBuilder->build();
309+
310+
return true;
311+
}
312+
277313
/**
278314
* @param string $argsObjectName
279315
* @param array $arguments

src/SchemaGenerator/SchemaInspector.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ public function getObjectSchema(string $objectName): array
9696
__type(name: \"$objectName\") {
9797
name
9898
kind
99+
interfaces{
100+
name
101+
}
99102
fields(includeDeprecated: true){
100103
name
101104
description

src/SchemaObject/InterfaceObject.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace GraphQL\SchemaObject;
4+
5+
use GraphQL\InlineFragment;
6+
7+
/**
8+
* Class InterfaceObject
9+
*
10+
* @package GraphQL\SchemaObject
11+
*/
12+
abstract class InterfaceObject extends QueryObject
13+
{
14+
private $implementations = [];
15+
16+
protected function addImplementation(string $implementationTypeClassName)
17+
{
18+
if (!isset($this->implementations[$implementationTypeClassName])) {
19+
$implementationType = new $implementationTypeClassName();
20+
$fragment = new InlineFragment($implementationType::OBJECT_NAME, $implementationType);
21+
$this->selectField($fragment);
22+
$this->implementations[$implementationTypeClassName] = $implementationType;
23+
}
24+
25+
return $this->implementations[$implementationTypeClassName];
26+
}
27+
}

tests/InterfaceObjectTest.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace GraphQL\Tests;
4+
5+
use GraphQL\SchemaObject\QueryObject;
6+
use GraphQL\SchemaObject\InterfaceObject;
7+
use PHPUnit\Framework\TestCase;
8+
9+
/**
10+
* Class QueryObjectTest
11+
*
12+
* @package GraphQL\Tests
13+
*/
14+
class InterfaceObjectTest extends TestCase
15+
{
16+
/**
17+
* @covers \GraphQL\SchemaObject\InterfaceObject
18+
*/
19+
public function testInterfaceObject()
20+
{
21+
$object = new SimpleInterfaceObject('interface');
22+
$object->onType1()->selectScalar();
23+
$object->onType2()->selectAnotherScalar();
24+
$object->onType2()->selectScalar();
25+
$this->assertEquals(
26+
'query {
27+
interface {
28+
... on Type1 {
29+
scalar
30+
}
31+
... on Type2 {
32+
anotherScalar
33+
scalar
34+
}
35+
}
36+
}',
37+
(string) $object->getQuery());
38+
}
39+
}
40+
41+
class SimpleInterfaceObject extends InterfaceObject
42+
{
43+
const OBJECT_NAME = 'Simple';
44+
45+
46+
47+
public function onType1(): InterfaceType1QueryObject
48+
{
49+
return $this->addImplementation(InterfaceType1QueryObject::class);
50+
}
51+
52+
public function onType2(): InterfaceType2QueryObject
53+
{
54+
return $this->addImplementation(InterfaceType2QueryObject::class);
55+
}
56+
}
57+
58+
abstract class InterfaceSimpleSubTypeQueryObject extends QueryObject
59+
{
60+
public function selectScalar()
61+
{
62+
$this->selectField('scalar');
63+
64+
return $this;
65+
}
66+
67+
public function selectAnotherScalar()
68+
{
69+
$this->selectField('anotherScalar');
70+
71+
return $this;
72+
}
73+
}
74+
75+
class InterfaceType1QueryObject extends InterfaceSimpleSubTypeQueryObject
76+
{
77+
const OBJECT_NAME = 'Type1';
78+
}
79+
80+
class InterfaceType2QueryObject extends InterfaceSimpleSubTypeQueryObject
81+
{
82+
const OBJECT_NAME = 'Type2';
83+
}
84+

tests/SchemaClassGeneratorTest.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,104 @@ public function testGenerateUnionObject()
887887
);
888888
}
889889

890+
/**
891+
* @covers \GraphQL\SchemaGenerator\SchemaClassGenerator::generateInterfaceObject
892+
*/
893+
public function testGenerateInterfaceObject()
894+
{
895+
$objectName = 'TestInterface';
896+
897+
// Add mock responses
898+
$this->mockHandler->append(new Response(200, [], json_encode([
899+
'data' => [
900+
'__type' => [
901+
'name' => 'InterfaceObject1',
902+
'kind' => FieldTypeKindEnum::OBJECT,
903+
'interfaces' => [
904+
['name' => $objectName],
905+
],
906+
'fields' => [
907+
[
908+
'name' => 'value',
909+
'description' => null,
910+
'isDeprecated' => false,
911+
'deprecationReason' => null,
912+
'type' => [
913+
'name' => 'String',
914+
'kind' => FieldTypeKindEnum::SCALAR,
915+
'description' => null,
916+
'ofType' => null,
917+
],
918+
'args' => null,
919+
],
920+
],
921+
]
922+
]
923+
])));
924+
$this->mockHandler->append(new Response(200, [], json_encode([
925+
'data' => [
926+
'__type' => [
927+
'name' => $objectName,
928+
'kind' => FieldTypeKindEnum::INTERFACE_OBJECT,
929+
'fields' => [
930+
[
931+
'name' => 'interface_field',
932+
'description' => null,
933+
'isDeprecated' => false,
934+
'deprecationReason' => null,
935+
'type' => [
936+
'name' => 'String',
937+
'kind' => FieldTypeKindEnum::SCALAR,
938+
'description' => null,
939+
'ofType' => null,
940+
],
941+
'args' => null,
942+
]
943+
],
944+
'possibleTypes' => [
945+
[
946+
'kind' => FieldTypeKindEnum::OBJECT,
947+
'name' => 'InterfaceObject1',
948+
], [
949+
'kind' => FieldTypeKindEnum::OBJECT,
950+
'name' => 'InterfaceObject2',
951+
],
952+
]
953+
]
954+
]
955+
])));
956+
$this->mockHandler->append(new Response(200, [], json_encode([
957+
'data' => [
958+
'__type' => [
959+
'name' => 'InterfaceObject2',
960+
'kind' => FieldTypeKindEnum::OBJECT,
961+
'interfaces' => [
962+
['name' => $objectName],
963+
],
964+
'fields' => [],
965+
]
966+
]
967+
])));
968+
969+
$this->classGenerator->generateObject('InterfaceObject1', FieldTypeKindEnum::INTERFACE_OBJECT);
970+
$this->classGenerator->generateObject($objectName, FieldTypeKindEnum::INTERFACE_OBJECT);
971+
$this->classGenerator->generateObject('InterfaceObject2', FieldTypeKindEnum::INTERFACE_OBJECT);
972+
973+
$objectName .= 'QueryObject';
974+
$this->assertFileEquals(
975+
static::getExpectedFilesDir() . "/interface_objects/InterfaceObject1QueryObject.php",
976+
static::getGeneratedFilesDir() . "/InterfaceObject1QueryObject.php"
977+
);
978+
$this->assertFileEquals(
979+
static::getExpectedFilesDir() . "/interface_objects/InterfaceObject2QueryObject.php",
980+
static::getGeneratedFilesDir() . "/InterfaceObject2QueryObject.php"
981+
);
982+
$this->assertFileEquals(
983+
static::getExpectedFilesDir() . "/interface_objects/$objectName.php",
984+
static::getGeneratedFilesDir() . "/$objectName.php"
985+
);
986+
}
987+
890988
///**
891989
// * @covers \GraphQL\SchemaGenerator\SchemaClassGenerator::generateObject
892990
// */

0 commit comments

Comments
 (0)