Skip to content

Commit 8af6044

Browse files
committed
Add support for interfaces
1 parent d55a1d4 commit 8af6044

File tree

11 files changed

+407
-14
lines changed

11 files changed

+407
-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: 79 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,15 @@ class SchemaClassGenerator
4445
*AND complete covering the schema scanner class
4546
* @var array
4647
*/
47-
private $generatedObjects;
48+
private $generatedObjects;
49+
50+
/**
51+
* This array is used to remember the generated interfaces or store objects that implement the interface
52+
* Array structure: [$objectName] => string[] | InterfaceObjectBuilder
53+
*AND complete covering the schema scanner class
54+
* @var array
55+
*/
56+
private $generatedInterfaces;
4857

4958
/**
5059
* SchemaClassGenerator constructor.
@@ -59,6 +68,7 @@ public function __construct(Client $client, string $writeDir = '', string $names
5968
$this->generatedObjects = [];
6069
$this->writeDir = $writeDir;
6170
$this->generationNamespace = $namespace;
71+
$this->generatedInterfaces = [];
6272
$this->setWriteDir();
6373
}
6474

@@ -141,6 +151,7 @@ protected function generateObject(string $objectName, string $objectKind): bool
141151
{
142152
switch ($objectKind) {
143153
case FieldTypeKindEnum::OBJECT:
154+
case FieldTypeKindEnum::INTERFACE_OBJECT:
144155
return $this->generateQueryObject($objectName);
145156
case FieldTypeKindEnum::INPUT_OBJECT:
146157
return $this->generateInputObject($objectName);
@@ -168,11 +179,25 @@ protected function generateQueryObject(string $objectName): bool
168179
$this->generatedObjects[$objectName] = true;
169180
$objectArray = $this->schemaInspector->getObjectSchema($objectName);
170181
$objectName = $objectArray['name'];
171-
$objectBuilder = new QueryObjectClassBuilder($this->writeDir, $objectName, $this->generationNamespace);
182+
183+
if ($objectArray['kind'] === FieldTypeKindEnum::INTERFACE_OBJECT) {
184+
$objectBuilder = new InterfaceObjectBuilder($this->writeDir, $objectName, $this->generationNamespace);
185+
$this->registerInterface($objectName, $objectBuilder);
186+
} else {
187+
$objectBuilder = new QueryObjectClassBuilder($this->writeDir, $objectName, $this->generationNamespace);
188+
}
172189

173190
$this->appendQueryObjectFields($objectBuilder, $objectName, $objectArray['fields']);
174191
$objectBuilder->build();
175192

193+
if (!empty($objectArray['interfaces'])) {
194+
$this->registerInterfaces($objectName, array_column($objectArray['interfaces'], 'name'));
195+
}
196+
197+
if ($objectArray['kind'] === FieldTypeKindEnum::INTERFACE_OBJECT) {
198+
$this->registerInterface($objectName, $objectBuilder);
199+
}
200+
176201
return true;
177202
}
178203

@@ -274,6 +299,32 @@ protected function generateUnionObject(string $objectName): bool
274299
return true;
275300
}
276301

302+
/**
303+
* @param string $objectName
304+
*
305+
* @return bool
306+
*/
307+
protected function generateInterfaceObject(string $objectName): bool
308+
{
309+
if (array_key_exists($objectName, $this->generatedObjects)) {
310+
return true;
311+
}
312+
313+
$this->generatedObjects[$objectName] = true;
314+
315+
$objectArray = $this->schemaInspector->getObjectSchema($objectName);
316+
$objectName = $objectArray['name'];
317+
$objectBuilder = new UnionObjectBuilder($this->writeDir, $objectName, $this->generationNamespace);
318+
319+
foreach ($objectArray['possibleTypes'] as $possibleType) {
320+
$this->generateObject($possibleType['name'], $possibleType['kind']);
321+
$objectBuilder->addPossibleType($possibleType['name']);
322+
}
323+
$objectBuilder->build();
324+
325+
return true;
326+
}
327+
277328
/**
278329
* @param string $argsObjectName
279330
* @param array $arguments
@@ -367,4 +418,30 @@ public function getWriteDir(): string
367418

368419
return $this->writeDir;
369420
}
421+
422+
private function registerInterfaces($objectName, array $interfaceNames): void
423+
{
424+
foreach ($interfaceNames as $interfaceName) {
425+
if (isset($this->generatedInterfaces[$interfaceName]) && is_object($this->generatedInterfaces[$interfaceName])) {
426+
// The interface was already processed, add this implementation.
427+
$this->generatedInterfaces[$interfaceName]->addImplementation($objectName);
428+
$this->generatedInterfaces[$interfaceName]->build();
429+
} else {
430+
// The interface is not yet processed, remember this implementation for later.
431+
$this->generatedInterfaces[$interfaceName][] = $objectName;
432+
}
433+
}
434+
}
435+
436+
private function registerInterface($interfaceName, InterfaceObjectBuilder $objectBuilder): void
437+
{
438+
// Check if any implementations were found before this interface was processed.
439+
if (isset($this->generatedInterfaces[$interfaceName])) {
440+
foreach ($this->generatedInterfaces[$interfaceName] as $implementation) {
441+
$objectBuilder->addImplementation($implementation);
442+
}
443+
}
444+
445+
$this->generatedInterfaces[$interfaceName] = $objectBuilder;
446+
}
370447
}

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
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+

0 commit comments

Comments
 (0)