Skip to content

Commit aec3604

Browse files
jpasquerstcarrio
andauthored
feat: entity type customization, type loader support (#33)
* feat: entity type customization, type loader support * feat: lazy union type loading * Update README.md Co-authored-by: Tom Carrio <tom@carrio.dev> * fix: type docs updating --------- Co-authored-by: Tom Carrio <tom@carrio.dev>
1 parent c1bc216 commit aec3604

File tree

8 files changed

+291
-56
lines changed

8 files changed

+291
-56
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ $query = 'query GetServiceSDL { _service { sdl } }';
105105
$result = GraphQL::executeQuery($schema, $query);
106106
```
107107

108+
#### Config
109+
110+
The config parameter for the `FederatedSchema` object is entirely compatible with the `Schema` config argument. On top of this, we support the following optional parameters:
111+
112+
1. `entityTypes` - the entity types (which extend `EntityObjectType`) which will form the `_Entity` type on the federated schema. If not provided, the Schema will scan the `Query` type tree for all types extending `EntityObjectType`.
113+
108114
## Disclaimer
109115

110116
Documentation in this project include content quoted directly from the [Apollo official documentation](https://www.apollographql.com/docs) to reduce redundancy.

src/FederatedSchema.php

Lines changed: 74 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44

55
namespace Apollo\Federation;
66

7+
use Apollo\Federation\Types\AnyType;
78
use GraphQL\Type\Schema;
8-
use GraphQL\Type\Definition\CustomScalarType;
99
use GraphQL\Type\Definition\Directive;
1010
use GraphQL\Type\Definition\ObjectType;
11-
use GraphQL\Type\Definition\UnionType;
1211
use GraphQL\Type\Definition\Type;
1312
use GraphQL\Utils\TypeInfo;
1413
use GraphQL\Utils\Utils;
1514

1615
use Apollo\Federation\Types\EntityObjectType;
17-
use Apollo\Federation\Utils\FederatedSchemaPrinter;
16+
use Apollo\Federation\Types\EntityUnionType;
17+
use Apollo\Federation\Types\ServiceDefinitionType;
1818

1919
/**
2020
* A federated GraphQL schema definition (see [related docs](https://www.apollographql.com/docs/apollo-server/federation/introduction))
@@ -55,18 +55,36 @@
5555
*/
5656
class FederatedSchema extends Schema
5757
{
58-
/** @var EntityObjectType[] */
58+
/** @var EntityObjectType[]|callable: EntityObjectType[] */
5959
protected $entityTypes;
6060

6161
/** @var Directive[] */
6262
protected $entityDirectives;
6363

64+
protected ServiceDefinitionType $serviceDefinitionType;
65+
protected EntityUnionType $entityUnionType;
66+
protected AnyType $anyType;
67+
68+
/**
69+
*
70+
* We will provide the parts that we need to operate against.
71+
*
72+
* @param array{?entityTypes: array<EntityObjectType>, ?typeLoader: callable, query: array} $config
73+
*/
6474
public function __construct($config)
6575
{
66-
$this->entityTypes = $this->extractEntityTypes($config);
76+
$this->entityTypes = $config['entityTypes'] ?? $this->lazyEntityTypeExtractor($config);
6777
$this->entityDirectives = array_merge(Directives::getDirectives(), Directive::getInternalDirectives());
68-
69-
$config = array_merge($config, $this->getEntityDirectivesConfig($config), $this->getQueryTypeConfig($config));
78+
79+
$this->serviceDefinitionType = new ServiceDefinitionType($this);
80+
$this->entityUnionType = new EntityUnionType($this->entityTypes);
81+
$this->anyType = new AnyType();
82+
83+
$config = array_merge($config,
84+
$this->getEntityDirectivesConfig($config),
85+
$this->getQueryTypeConfig($config),
86+
$this->supplementTypeLoader($config)
87+
);
7088

7189
parent::__construct($config);
7290
}
@@ -78,7 +96,9 @@ public function __construct($config)
7896
*/
7997
public function getEntityTypes(): array
8098
{
81-
return $this->entityTypes;
99+
return is_callable($this->entityTypes)
100+
? ($this->entityTypes)()
101+
: $this->entityTypes;
82102
}
83103

84104
/**
@@ -121,24 +141,42 @@ private function getQueryTypeConfig(array $config): array
121141
];
122142
}
123143

144+
/**
145+
* Add type loading functionality for the types required for the federated schema to function.
146+
*/
147+
private function supplementTypeLoader(array $config): array
148+
{
149+
if (!array_key_exists('typeLoader', $config) || !is_callable($config['typeLoader'])) {
150+
return [];
151+
}
152+
153+
return [
154+
'typeLoader' => function ($typeName) use ($config) {
155+
$map = $this->builtInTypeMap();
156+
if (array_key_exists($typeName, $map)) {
157+
return $map[$typeName];
158+
}
159+
160+
return $config['typeLoader']($typeName);
161+
}
162+
];
163+
}
164+
165+
private function builtInTypeMap(): array
166+
{
167+
return [
168+
EntityUnionType::getTypeName() => $this->entityUnionType,
169+
ServiceDefinitionType::getTypeName() => $this->serviceDefinitionType,
170+
AnyType::getTypeName() => $this->anyType
171+
];
172+
}
173+
124174
/** @var array */
125175
private function getQueryTypeServiceFieldConfig(): array
126176
{
127-
$serviceType = new ObjectType([
128-
'name' => '_Service',
129-
'fields' => [
130-
'sdl' => [
131-
'type' => Type::string(),
132-
'resolve' => function () {
133-
return FederatedSchemaPrinter::doPrint($this);
134-
}
135-
]
136-
]
137-
]);
138-
139177
return [
140178
'_service' => [
141-
'type' => Type::nonNull($serviceType),
179+
'type' => Type::nonNull($this->serviceDefinitionType),
142180
'resolve' => function () {
143181
return [];
144182
}
@@ -149,28 +187,12 @@ private function getQueryTypeServiceFieldConfig(): array
149187
/** @var array */
150188
private function getQueryTypeEntitiesFieldConfig(?array $config): array
151189
{
152-
if (!$this->hasEntityTypes()) {
153-
return [];
154-
}
155-
156-
$entityType = new UnionType([
157-
'name' => '_Entity',
158-
'types' => array_values($this->getEntityTypes())
159-
]);
160-
161-
$anyType = new CustomScalarType([
162-
'name' => '_Any',
163-
'serialize' => function ($value) {
164-
return $value;
165-
}
166-
]);
167-
168190
return [
169191
'_entities' => [
170-
'type' => Type::listOf($entityType),
192+
'type' => Type::listOf($this->entityUnionType),
171193
'args' => [
172194
'representations' => [
173-
'type' => Type::nonNull(Type::listOf(Type::nonNull($anyType)))
195+
'type' => Type::nonNull(Type::listOf(Type::nonNull($this->anyType)))
174196
]
175197
],
176198
'resolve' => function ($root, $args, $context, $info) use ($config) {
@@ -208,22 +230,25 @@ private function resolve($root, $args, $context, $info)
208230
return $r;
209231
}, $args['representations']);
210232
}
233+
211234
/**
212235
* @param array $config
213236
*
214-
* @return EntityObjectType[]
237+
* @return callable: EntityObjectType[]
215238
*/
216-
private function extractEntityTypes(array $config): array
239+
private function lazyEntityTypeExtractor(array $config): callable
217240
{
218-
$resolvedTypes = TypeInfo::extractTypes($config['query']);
219-
$entityTypes = [];
220-
221-
foreach ($resolvedTypes as $type) {
222-
if ($type instanceof EntityObjectType) {
223-
$entityTypes[$type->name] = $type;
241+
return function () use ($config) {
242+
$resolvedTypes = TypeInfo::extractTypes($config['query']);
243+
$entityTypes = [];
244+
245+
foreach ($resolvedTypes as $type) {
246+
if ($type instanceof EntityObjectType) {
247+
$entityTypes[$type->name] = $type;
248+
}
224249
}
225-
}
226250

227-
return $entityTypes;
251+
return $entityTypes;
252+
};
228253
}
229254
}

src/Types/AnyType.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Apollo\Federation\Types;
6+
7+
use GraphQL\Type\Definition\CustomScalarType;
8+
9+
/**
10+
* Simple representation of an agnostic scalar value.
11+
*/
12+
class AnyType extends CustomScalarType
13+
{
14+
public function __construct()
15+
{
16+
$config = [
17+
'name' => self::getTypeName(),
18+
'serialize' => function ($value) {
19+
return $value;
20+
}
21+
];
22+
parent::__construct($config);
23+
}
24+
25+
public static function getTypeName(): string
26+
{
27+
return '_Any';
28+
}
29+
}

src/Types/EntityUnionType.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Apollo\Federation\Types;
6+
7+
use GraphQL\Type\Definition\UnionType;
8+
9+
/**
10+
* The union of all entities defined within this schema.
11+
*/
12+
class EntityUnionType extends UnionType
13+
{
14+
15+
/**
16+
* @param array|callable $entityTypes all entity types or a callable to retrieve them
17+
*/
18+
public function __construct($entityTypes)
19+
{
20+
$config = [
21+
'name' => self::getTypeName(),
22+
'types' => is_callable($entityTypes)
23+
? fn () => array_values($entityTypes())
24+
: array_values($entityTypes)
25+
26+
];
27+
parent::__construct($config);
28+
}
29+
30+
public static function getTypeName(): string
31+
{
32+
return '_Entity';
33+
}
34+
}

src/Types/ServiceDefinitionType.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Apollo\Federation\Types;
6+
7+
use Apollo\Federation\Utils\FederatedSchemaPrinter;
8+
use GraphQL\Type\Definition\ObjectType;
9+
use GraphQL\Type\Definition\Type;
10+
use GraphQL\Type\Schema;
11+
12+
/**
13+
* The type of the service definition required for federated schemas.
14+
*/
15+
class ServiceDefinitionType extends ObjectType
16+
{
17+
18+
/**
19+
* @param Schema $schema - the schemas whose SDL should be printed.
20+
*/
21+
public function __construct(Schema $schema)
22+
{
23+
$config = [
24+
'name' => self::getTypeName(),
25+
'fields' => [
26+
'sdl' => [
27+
'type' => Type::string(),
28+
'resolve' => fn () => FederatedSchemaPrinter::doPrint($schema)
29+
]
30+
]
31+
];
32+
parent::__construct($config);
33+
}
34+
35+
public static function getTypeName(): string
36+
{
37+
return '_Service';
38+
}
39+
}

test/SchemaTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@
88
use Spatie\Snapshots\MatchesSnapshots;
99

1010
use GraphQL\GraphQL;
11-
use GraphQL\Type\Definition\Type;
1211
use GraphQL\Utils\SchemaPrinter;
1312

1413
use Apollo\Federation\Tests\StarWarsSchema;
15-
use Apollo\Federation\Tests\DungeonsAndDragonsSchema;
1614

1715
class SchemaTest extends TestCase
1816
{
@@ -88,6 +86,14 @@ public function testSchemaSdl()
8886
$this->assertMatchesSnapshot($schemaSdl);
8987
}
9088

89+
public function testSchemaSdlForProvidedEntities()
90+
{
91+
$schema = StarWarsSchema::getEpisodesSchemaWithProvidedEntities();
92+
$schemaSdl = SchemaPrinter::doPrint($schema);
93+
94+
$this->assertMatchesSnapshot($schemaSdl);
95+
}
96+
9197
public function testResolvingEntityReferences()
9298
{
9399
$schema = StarWarsSchema::getEpisodesSchema();

0 commit comments

Comments
 (0)