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 === null || 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 !== null) { |
124
|
873 |
|
$this->resolvedTypes[$config->query->name] = $config->query; |
125
|
|
|
} |
126
|
890 |
|
if ($config->mutation !== null) { |
127
|
83 |
|
$this->resolvedTypes[$config->mutation->name] = $config->mutation; |
128
|
|
|
} |
129
|
890 |
|
if ($config->subscription !== null) { |
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() : ?Type |
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() : ?Type |
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() : ?Type |
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
|
|
|
public function getAstNode() : ?SchemaDefinitionNode |
450
|
|
|
{ |
451
|
|
|
return $this->config->getAstNode(); |
452
|
147 |
|
} |
453
|
|
|
|
454
|
147 |
|
/** |
455
|
|
|
* Validates schema. |
456
|
|
|
* |
457
|
|
|
* This operation requires full schema scan. Do not use in production environment. |
458
|
|
|
* |
459
|
|
|
* @throws InvariantViolation |
460
|
|
|
* |
461
|
|
|
* @api |
462
|
|
|
*/ |
463
|
|
|
public function assertValid() |
464
|
|
|
{ |
465
|
|
|
$errors = $this->validate(); |
466
|
11 |
|
|
467
|
|
|
if ($errors) { |
|
|
|
|
468
|
11 |
|
throw new InvariantViolation(implode("\n\n", $this->validationErrors)); |
469
|
|
|
} |
470
|
11 |
|
|
471
|
|
|
$internalTypes = Type::getStandardTypes() + Introspection::getTypes(); |
472
|
|
|
foreach ($this->getTypeMap() as $name => $type) { |
473
|
|
|
if (isset($internalTypes[$name])) { |
474
|
11 |
|
continue; |
475
|
11 |
|
} |
476
|
11 |
|
|
477
|
1 |
|
$type->assertValid(); |
478
|
|
|
|
479
|
|
|
// Make sure type loader returns the same instance as registered in other places of schema |
480
|
11 |
|
if (! $this->config->typeLoader) { |
481
|
|
|
continue; |
482
|
|
|
} |
483
|
11 |
|
|
484
|
10 |
|
Utils::invariant( |
485
|
|
|
$this->loadType($name) === $type, |
486
|
|
|
sprintf( |
487
|
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.', |
488
|
1 |
|
$name |
489
|
1 |
|
) |
490
|
1 |
|
); |
491
|
1 |
|
} |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
/** |
495
|
1 |
|
* Validates schema. |
496
|
|
|
* |
497
|
|
|
* This operation requires full schema scan. Do not use in production environment. |
498
|
|
|
* |
499
|
|
|
* @return InvariantViolation[]|Error[] |
500
|
|
|
* |
501
|
|
|
* @api |
502
|
|
|
*/ |
503
|
|
|
public function validate() |
504
|
|
|
{ |
505
|
|
|
// If this Schema has already been validated, return the previous results. |
506
|
89 |
|
if ($this->validationErrors !== null) { |
507
|
|
|
return $this->validationErrors; |
508
|
|
|
} |
509
|
89 |
|
// Validate the schema, producing a list of errors. |
510
|
|
|
$context = new SchemaValidationContext($this); |
511
|
|
|
$context->validateRootTypes(); |
512
|
|
|
$context->validateDirectives(); |
513
|
89 |
|
$context->validateTypes(); |
514
|
89 |
|
|
515
|
89 |
|
// Persist the results of validation before returning to ensure validation |
516
|
89 |
|
// does not run multiple times for this schema. |
517
|
|
|
$this->validationErrors = $context->getErrors(); |
518
|
|
|
|
519
|
|
|
return $this->validationErrors; |
520
|
89 |
|
} |
521
|
|
|
} |
522
|
|
|
|