1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace GraphQL\Type; |
6
|
|
|
|
7
|
|
|
use Generator; |
8
|
|
|
use GraphQL\Error\Error; |
9
|
|
|
use GraphQL\Error\InvariantViolation; |
10
|
|
|
use GraphQL\GraphQL; |
11
|
|
|
use GraphQL\Language\AST\SchemaDefinitionNode; |
12
|
|
|
use GraphQL\Language\AST\SchemaTypeExtensionNode; |
13
|
|
|
use GraphQL\Type\Definition\AbstractType; |
14
|
|
|
use GraphQL\Type\Definition\Directive; |
15
|
|
|
use GraphQL\Type\Definition\InterfaceType; |
16
|
|
|
use GraphQL\Type\Definition\ObjectType; |
17
|
|
|
use GraphQL\Type\Definition\Type; |
18
|
|
|
use GraphQL\Type\Definition\UnionType; |
19
|
|
|
use GraphQL\Utils\TypeInfo; |
20
|
|
|
use GraphQL\Utils\Utils; |
21
|
|
|
use Traversable; |
22
|
|
|
use function array_values; |
23
|
|
|
use function implode; |
24
|
|
|
use function is_array; |
25
|
|
|
use function is_callable; |
26
|
|
|
use function sprintf; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Schema Definition (see [related docs](type-system/schema.md)) |
30
|
|
|
* |
31
|
|
|
* A Schema is created by supplying the root types of each type of operation: |
32
|
|
|
* query, mutation (optional) and subscription (optional). A schema definition is |
33
|
|
|
* then supplied to the validator and executor. Usage Example: |
34
|
|
|
* |
35
|
|
|
* $schema = new GraphQL\Type\Schema([ |
36
|
|
|
* 'query' => $MyAppQueryRootType, |
37
|
|
|
* 'mutation' => $MyAppMutationRootType, |
38
|
|
|
* ]); |
39
|
|
|
* |
40
|
|
|
* Or using Schema Config instance: |
41
|
|
|
* |
42
|
|
|
* $config = GraphQL\Type\SchemaConfig::create() |
43
|
|
|
* ->setQuery($MyAppQueryRootType) |
44
|
|
|
* ->setMutation($MyAppMutationRootType); |
45
|
|
|
* |
46
|
|
|
* $schema = new GraphQL\Type\Schema($config); |
47
|
|
|
*/ |
48
|
|
|
class Schema |
49
|
|
|
{ |
50
|
|
|
/** @var SchemaConfig */ |
51
|
|
|
private $config; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Contains currently resolved schema types |
55
|
|
|
* |
56
|
|
|
* @var Type[] |
57
|
|
|
*/ |
58
|
|
|
private $resolvedTypes = []; |
59
|
|
|
|
60
|
|
|
/** @var array<string, array<string, ObjectType>>|null */ |
61
|
|
|
private $possibleTypeMap; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* True when $resolvedTypes contain all possible schema types |
65
|
|
|
* |
66
|
|
|
* @var bool |
67
|
|
|
*/ |
68
|
|
|
private $fullyLoaded = false; |
69
|
|
|
|
70
|
|
|
/** @var Error[] */ |
71
|
|
|
private $validationErrors; |
72
|
|
|
|
73
|
|
|
/** @var SchemaTypeExtensionNode[] */ |
74
|
|
|
public $extensionASTNodes; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @param mixed[]|SchemaConfig $config |
78
|
|
|
* |
79
|
|
|
* @api |
80
|
|
|
*/ |
81
|
891 |
|
public function __construct($config) |
82
|
|
|
{ |
83
|
891 |
|
if (is_array($config)) { |
84
|
891 |
|
$config = SchemaConfig::create($config); |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
// If this schema was built from a source known to be valid, then it may be |
88
|
|
|
// marked with assumeValid to avoid an additional type system validation. |
89
|
890 |
|
if ($config->getAssumeValid()) { |
90
|
|
|
$this->validationErrors = []; |
91
|
|
|
} else { |
92
|
|
|
// Otherwise check for common mistakes during construction to produce |
93
|
|
|
// clear and early error messages. |
94
|
890 |
|
Utils::invariant( |
95
|
890 |
|
$config instanceof SchemaConfig, |
96
|
890 |
|
'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s', |
97
|
890 |
|
implode( |
98
|
890 |
|
', ', |
99
|
|
|
[ |
100
|
890 |
|
'query', |
101
|
|
|
'mutation', |
102
|
|
|
'subscription', |
103
|
|
|
'types', |
104
|
|
|
'directives', |
105
|
|
|
'typeLoader', |
106
|
|
|
] |
107
|
|
|
), |
108
|
890 |
|
Utils::getVariableType($config) |
109
|
|
|
); |
110
|
890 |
|
Utils::invariant( |
111
|
890 |
|
! $config->types || is_array($config->types) || is_callable($config->types), |
112
|
890 |
|
'"types" must be array or callable if provided but got: ' . Utils::getVariableType($config->types) |
113
|
|
|
); |
114
|
890 |
|
Utils::invariant( |
115
|
890 |
|
! $config->directives || is_array($config->directives), |
|
|
|
|
116
|
890 |
|
'"directives" must be Array if provided but got: ' . Utils::getVariableType($config->directives) |
117
|
|
|
); |
118
|
|
|
} |
119
|
|
|
|
120
|
890 |
|
$this->config = $config; |
121
|
890 |
|
$this->extensionASTNodes = $config->extensionASTNodes; |
122
|
|
|
|
123
|
890 |
|
if ($config->query) { |
124
|
873 |
|
$this->resolvedTypes[$config->query->name] = $config->query; |
125
|
|
|
} |
126
|
890 |
|
if ($config->mutation) { |
127
|
83 |
|
$this->resolvedTypes[$config->mutation->name] = $config->mutation; |
128
|
|
|
} |
129
|
890 |
|
if ($config->subscription) { |
130
|
35 |
|
$this->resolvedTypes[$config->subscription->name] = $config->subscription; |
131
|
|
|
} |
132
|
890 |
|
if (is_array($this->config->types)) { |
133
|
157 |
|
foreach ($this->resolveAdditionalTypes() as $type) { |
134
|
157 |
|
if (isset($this->resolvedTypes[$type->name])) { |
135
|
57 |
|
Utils::invariant( |
136
|
57 |
|
$type === $this->resolvedTypes[$type->name], |
137
|
57 |
|
sprintf( |
138
|
57 |
|
'Schema must contain unique named types but contains multiple types named "%s" (see http://webonyx.github.io/graphql-php/type-system/#type-registry).', |
139
|
57 |
|
$type |
140
|
|
|
) |
141
|
|
|
); |
142
|
|
|
} |
143
|
157 |
|
$this->resolvedTypes[$type->name] = $type; |
144
|
|
|
} |
145
|
|
|
} |
146
|
889 |
|
$this->resolvedTypes += Type::getStandardTypes() + Introspection::getTypes(); |
147
|
|
|
|
148
|
889 |
|
if ($this->config->typeLoader) { |
149
|
161 |
|
return; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
// Perform full scan of the schema |
153
|
777 |
|
$this->getTypeMap(); |
154
|
773 |
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* @return Generator |
158
|
|
|
*/ |
159
|
254 |
|
private function resolveAdditionalTypes() |
160
|
|
|
{ |
161
|
254 |
|
$types = $this->config->types ?: []; |
162
|
|
|
|
163
|
254 |
|
if (is_callable($types)) { |
164
|
112 |
|
$types = $types(); |
165
|
|
|
} |
166
|
|
|
|
167
|
254 |
|
if (! is_array($types) && ! $types instanceof Traversable) { |
168
|
|
|
throw new InvariantViolation(sprintf( |
169
|
|
|
'Schema types callable must return array or instance of Traversable but got: %s', |
170
|
|
|
Utils::getVariableType($types) |
171
|
|
|
)); |
172
|
|
|
} |
173
|
|
|
|
174
|
254 |
|
foreach ($types as $index => $type) { |
175
|
254 |
|
if (! $type instanceof Type) { |
176
|
|
|
throw new InvariantViolation(sprintf( |
177
|
|
|
'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s', |
178
|
|
|
$index, |
179
|
|
|
Utils::printSafe($type) |
180
|
|
|
)); |
181
|
|
|
} |
182
|
254 |
|
yield $type; |
183
|
|
|
} |
184
|
253 |
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Returns array of all types in this schema. Keys of this array represent type names, values are instances |
188
|
|
|
* of corresponding type definitions |
189
|
|
|
* |
190
|
|
|
* This operation requires full schema scan. Do not use in production environment. |
191
|
|
|
* |
192
|
|
|
* @return Type[] |
193
|
|
|
* |
194
|
|
|
* @api |
195
|
|
|
*/ |
196
|
854 |
|
public function getTypeMap() |
197
|
|
|
{ |
198
|
854 |
|
if (! $this->fullyLoaded) { |
199
|
854 |
|
$this->resolvedTypes = $this->collectAllTypes(); |
200
|
847 |
|
$this->fullyLoaded = true; |
201
|
|
|
} |
202
|
|
|
|
203
|
847 |
|
return $this->resolvedTypes; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @return Type[] |
208
|
|
|
*/ |
209
|
854 |
|
private function collectAllTypes() |
210
|
|
|
{ |
211
|
854 |
|
$typeMap = []; |
212
|
854 |
|
foreach ($this->resolvedTypes as $type) { |
213
|
854 |
|
$typeMap = TypeInfo::extractTypes($type, $typeMap); |
214
|
|
|
} |
215
|
847 |
|
foreach ($this->getDirectives() as $directive) { |
216
|
847 |
|
if (! ($directive instanceof Directive)) { |
217
|
1 |
|
continue; |
218
|
|
|
} |
219
|
|
|
|
220
|
846 |
|
$typeMap = TypeInfo::extractTypesFromDirectives($directive, $typeMap); |
|
|
|
|
221
|
|
|
} |
222
|
|
|
// When types are set as array they are resolved in constructor |
223
|
847 |
|
if (is_callable($this->config->types)) { |
224
|
112 |
|
foreach ($this->resolveAdditionalTypes() as $type) { |
225
|
112 |
|
$typeMap = TypeInfo::extractTypes($type, $typeMap); |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
|
229
|
847 |
|
return $typeMap; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Returns a list of directives supported by this schema |
234
|
|
|
* |
235
|
|
|
* @return Directive[] |
236
|
|
|
* |
237
|
|
|
* @api |
238
|
|
|
*/ |
239
|
868 |
|
public function getDirectives() |
240
|
|
|
{ |
241
|
868 |
|
return $this->config->directives ?: GraphQL::getStandardDirectives(); |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* @param string $operation |
246
|
|
|
* |
247
|
|
|
* @return ObjectType|null |
248
|
|
|
*/ |
249
|
|
|
public function getOperationType($operation) |
250
|
|
|
{ |
251
|
|
|
switch ($operation) { |
252
|
|
|
case 'query': |
253
|
|
|
return $this->getQueryType(); |
254
|
|
|
case 'mutation': |
255
|
|
|
return $this->getMutationType(); |
256
|
|
|
case 'subscription': |
257
|
|
|
return $this->getSubscriptionType(); |
258
|
|
|
default: |
259
|
|
|
return null; |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* Returns schema query type |
265
|
|
|
* |
266
|
|
|
* @return ObjectType |
267
|
|
|
* |
268
|
|
|
* @api |
269
|
|
|
*/ |
270
|
771 |
|
public function getQueryType() |
271
|
|
|
{ |
272
|
771 |
|
return $this->config->query; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Returns schema mutation type |
277
|
|
|
* |
278
|
|
|
* @return ObjectType|null |
279
|
|
|
* |
280
|
|
|
* @api |
281
|
|
|
*/ |
282
|
215 |
|
public function getMutationType() |
283
|
|
|
{ |
284
|
215 |
|
return $this->config->mutation; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Returns schema subscription |
289
|
|
|
* |
290
|
|
|
* @return ObjectType|null |
291
|
|
|
* |
292
|
|
|
* @api |
293
|
|
|
*/ |
294
|
208 |
|
public function getSubscriptionType() |
295
|
|
|
{ |
296
|
208 |
|
return $this->config->subscription; |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* @return SchemaConfig |
301
|
|
|
* |
302
|
|
|
* @api |
303
|
|
|
*/ |
304
|
8 |
|
public function getConfig() |
305
|
|
|
{ |
306
|
8 |
|
return $this->config; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* Returns type by its name |
311
|
|
|
* |
312
|
|
|
* @api |
313
|
|
|
*/ |
314
|
559 |
|
public function getType(string $name) : ?Type |
315
|
|
|
{ |
316
|
559 |
|
if (! isset($this->resolvedTypes[$name])) { |
317
|
97 |
|
$type = $this->loadType($name); |
318
|
92 |
|
if (! $type) { |
319
|
70 |
|
return null; |
320
|
|
|
} |
321
|
22 |
|
$this->resolvedTypes[$name] = $type; |
322
|
|
|
} |
323
|
|
|
|
324
|
512 |
|
return $this->resolvedTypes[$name]; |
325
|
|
|
} |
326
|
|
|
|
327
|
35 |
|
public function hasType(string $name) : bool |
328
|
|
|
{ |
329
|
35 |
|
return $this->getType($name) !== null; |
330
|
|
|
} |
331
|
|
|
|
332
|
98 |
|
private function loadType(string $typeName) : ?Type |
333
|
|
|
{ |
334
|
98 |
|
$typeLoader = $this->config->typeLoader; |
335
|
|
|
|
336
|
98 |
|
if (! $typeLoader) { |
337
|
70 |
|
return $this->defaultTypeLoader($typeName); |
338
|
|
|
} |
339
|
|
|
|
340
|
28 |
|
$type = $typeLoader($typeName); |
341
|
|
|
|
342
|
26 |
|
if (! $type instanceof Type) { |
343
|
2 |
|
throw new InvariantViolation( |
344
|
2 |
|
sprintf( |
345
|
2 |
|
'Type loader is expected to return valid type "%s", but it returned %s', |
346
|
2 |
|
$typeName, |
347
|
2 |
|
Utils::printSafe($type) |
348
|
|
|
) |
349
|
|
|
); |
350
|
|
|
} |
351
|
24 |
|
if ($type->name !== $typeName) { |
352
|
1 |
|
throw new InvariantViolation( |
353
|
1 |
|
sprintf('Type loader is expected to return type "%s", but it returned "%s"', $typeName, $type->name) |
354
|
|
|
); |
355
|
|
|
} |
356
|
|
|
|
357
|
23 |
|
return $type; |
358
|
|
|
} |
359
|
|
|
|
360
|
70 |
|
private function defaultTypeLoader(string $typeName) : ?Type |
361
|
|
|
{ |
362
|
|
|
// Default type loader simply fallbacks to collecting all types |
363
|
70 |
|
$typeMap = $this->getTypeMap(); |
364
|
|
|
|
365
|
70 |
|
return $typeMap[$typeName] ?? null; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* Returns all possible concrete types for given abstract type |
370
|
|
|
* (implementations for interfaces and members of union type for unions) |
371
|
|
|
* |
372
|
|
|
* This operation requires full schema scan. Do not use in production environment. |
373
|
|
|
* |
374
|
|
|
* @param InterfaceType|UnionType $abstractType |
375
|
|
|
* |
376
|
|
|
* @return array<Type&ObjectType> |
377
|
|
|
* |
378
|
|
|
* @api |
379
|
|
|
*/ |
380
|
18 |
|
public function getPossibleTypes(Type $abstractType) : array |
381
|
|
|
{ |
382
|
18 |
|
$possibleTypeMap = $this->getPossibleTypeMap(); |
383
|
|
|
|
384
|
18 |
|
return array_values($possibleTypeMap[$abstractType->name] ?? []); |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* @return array<string, array<string, ObjectType>> |
389
|
|
|
*/ |
390
|
18 |
|
private function getPossibleTypeMap() |
391
|
|
|
{ |
392
|
18 |
|
if ($this->possibleTypeMap === null) { |
393
|
18 |
|
$this->possibleTypeMap = []; |
394
|
18 |
|
foreach ($this->getTypeMap() as $type) { |
395
|
18 |
|
if ($type instanceof ObjectType) { |
396
|
18 |
|
foreach ($type->getInterfaces() as $interface) { |
397
|
16 |
|
if (! ($interface instanceof InterfaceType)) { |
398
|
|
|
continue; |
399
|
|
|
} |
400
|
|
|
|
401
|
18 |
|
$this->possibleTypeMap[$interface->name][$type->name] = $type; |
402
|
|
|
} |
403
|
18 |
|
} elseif ($type instanceof UnionType) { |
404
|
12 |
|
foreach ($type->getTypes() as $innerType) { |
405
|
18 |
|
$this->possibleTypeMap[$type->name][$innerType->name] = $innerType; |
406
|
|
|
} |
407
|
|
|
} |
408
|
|
|
} |
409
|
|
|
} |
410
|
|
|
|
411
|
18 |
|
return $this->possibleTypeMap; |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
/** |
415
|
|
|
* Returns true if object type is concrete type of given abstract type |
416
|
|
|
* (implementation for interfaces and members of union type for unions) |
417
|
|
|
* |
418
|
|
|
* @api |
419
|
|
|
*/ |
420
|
49 |
|
public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) : bool |
421
|
|
|
{ |
422
|
49 |
|
if ($abstractType instanceof InterfaceType) { |
423
|
33 |
|
return $possibleType->implementsInterface($abstractType); |
424
|
|
|
} |
425
|
|
|
|
426
|
17 |
|
if ($abstractType instanceof UnionType) { |
427
|
17 |
|
return $abstractType->isPossibleType($possibleType); |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
throw InvariantViolation::shouldNotHappen(); |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* Returns instance of directive by name |
435
|
|
|
* |
436
|
|
|
* @api |
437
|
|
|
*/ |
438
|
65 |
|
public function getDirective(string $name) : ?Directive |
439
|
|
|
{ |
440
|
65 |
|
foreach ($this->getDirectives() as $directive) { |
441
|
65 |
|
if ($directive->name === $name) { |
442
|
65 |
|
return $directive; |
443
|
|
|
} |
444
|
|
|
} |
445
|
|
|
|
446
|
28 |
|
return null; |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* @return SchemaDefinitionNode |
451
|
|
|
*/ |
452
|
147 |
|
public function getAstNode() |
453
|
|
|
{ |
454
|
147 |
|
return $this->config->getAstNode(); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* Validates schema. |
459
|
|
|
* |
460
|
|
|
* This operation requires full schema scan. Do not use in production environment. |
461
|
|
|
* |
462
|
|
|
* @throws InvariantViolation |
463
|
|
|
* |
464
|
|
|
* @api |
465
|
|
|
*/ |
466
|
11 |
|
public function assertValid() |
467
|
|
|
{ |
468
|
11 |
|
$errors = $this->validate(); |
469
|
|
|
|
470
|
11 |
|
if ($errors) { |
|
|
|
|
471
|
|
|
throw new InvariantViolation(implode("\n\n", $this->validationErrors)); |
472
|
|
|
} |
473
|
|
|
|
474
|
11 |
|
$internalTypes = Type::getStandardTypes() + Introspection::getTypes(); |
475
|
11 |
|
foreach ($this->getTypeMap() as $name => $type) { |
476
|
11 |
|
if (isset($internalTypes[$name])) { |
477
|
1 |
|
continue; |
478
|
|
|
} |
479
|
|
|
|
480
|
11 |
|
$type->assertValid(); |
481
|
|
|
|
482
|
|
|
// Make sure type loader returns the same instance as registered in other places of schema |
483
|
11 |
|
if (! $this->config->typeLoader) { |
484
|
10 |
|
continue; |
485
|
|
|
} |
486
|
|
|
|
487
|
1 |
|
Utils::invariant( |
488
|
1 |
|
$this->loadType($name) === $type, |
489
|
1 |
|
sprintf( |
490
|
1 |
|
'Type loader returns different instance for %s than field/argument definitions. Make sure you always return the same instance for the same type name.', |
491
|
1 |
|
$name |
492
|
|
|
) |
493
|
|
|
); |
494
|
|
|
} |
495
|
1 |
|
} |
496
|
|
|
|
497
|
|
|
/** |
498
|
|
|
* Validates schema. |
499
|
|
|
* |
500
|
|
|
* This operation requires full schema scan. Do not use in production environment. |
501
|
|
|
* |
502
|
|
|
* @return InvariantViolation[]|Error[] |
503
|
|
|
* |
504
|
|
|
* @api |
505
|
|
|
*/ |
506
|
89 |
|
public function validate() |
507
|
|
|
{ |
508
|
|
|
// If this Schema has already been validated, return the previous results. |
509
|
89 |
|
if ($this->validationErrors !== null) { |
510
|
|
|
return $this->validationErrors; |
511
|
|
|
} |
512
|
|
|
// Validate the schema, producing a list of errors. |
513
|
89 |
|
$context = new SchemaValidationContext($this); |
514
|
89 |
|
$context->validateRootTypes(); |
515
|
89 |
|
$context->validateDirectives(); |
516
|
89 |
|
$context->validateTypes(); |
517
|
|
|
|
518
|
|
|
// Persist the results of validation before returning to ensure validation |
519
|
|
|
// does not run multiple times for this schema. |
520
|
89 |
|
$this->validationErrors = $context->getErrors(); |
521
|
|
|
|
522
|
89 |
|
return $this->validationErrors; |
523
|
|
|
} |
524
|
|
|
} |
525
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.