1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace GraphQL\Utils; |
6
|
|
|
|
7
|
|
|
use GraphQL\Error\Error; |
8
|
|
|
use GraphQL\Executor\Values; |
9
|
|
|
use GraphQL\Language\AST\DirectiveDefinitionNode; |
10
|
|
|
use GraphQL\Language\AST\EnumTypeDefinitionNode; |
11
|
|
|
use GraphQL\Language\AST\EnumTypeExtensionNode; |
12
|
|
|
use GraphQL\Language\AST\EnumValueDefinitionNode; |
13
|
|
|
use GraphQL\Language\AST\FieldDefinitionNode; |
14
|
|
|
use GraphQL\Language\AST\InputObjectTypeDefinitionNode; |
15
|
|
|
use GraphQL\Language\AST\InputValueDefinitionNode; |
16
|
|
|
use GraphQL\Language\AST\InterfaceTypeDefinitionNode; |
17
|
|
|
use GraphQL\Language\AST\ListTypeNode; |
18
|
|
|
use GraphQL\Language\AST\NamedTypeNode; |
19
|
|
|
use GraphQL\Language\AST\Node; |
20
|
|
|
use GraphQL\Language\AST\NonNullTypeNode; |
21
|
|
|
use GraphQL\Language\AST\ObjectTypeDefinitionNode; |
22
|
|
|
use GraphQL\Language\AST\ScalarTypeDefinitionNode; |
23
|
|
|
use GraphQL\Language\AST\TypeNode; |
24
|
|
|
use GraphQL\Language\AST\UnionTypeDefinitionNode; |
25
|
|
|
use GraphQL\Language\Token; |
26
|
|
|
use GraphQL\Type\Definition\CustomScalarType; |
27
|
|
|
use GraphQL\Type\Definition\Directive; |
28
|
|
|
use GraphQL\Type\Definition\EnumType; |
29
|
|
|
use GraphQL\Type\Definition\FieldArgument; |
30
|
|
|
use GraphQL\Type\Definition\InputObjectType; |
31
|
|
|
use GraphQL\Type\Definition\InputType; |
32
|
|
|
use GraphQL\Type\Definition\InterfaceType; |
33
|
|
|
use GraphQL\Type\Definition\ObjectType; |
34
|
|
|
use GraphQL\Type\Definition\Type; |
35
|
|
|
use GraphQL\Type\Definition\UnionType; |
36
|
|
|
use Throwable; |
37
|
|
|
use function array_reverse; |
38
|
|
|
use function implode; |
39
|
|
|
use function is_array; |
40
|
|
|
use function is_string; |
41
|
|
|
use function sprintf; |
42
|
|
|
|
43
|
|
|
class ASTDefinitionBuilder |
44
|
|
|
{ |
45
|
|
|
/** @var Node[] */ |
46
|
|
|
private $typeDefinitionsMap; |
47
|
|
|
|
48
|
|
|
/** @var callable */ |
49
|
|
|
private $typeConfigDecorator; |
50
|
|
|
|
51
|
|
|
/** @var bool[] */ |
52
|
|
|
private $options; |
53
|
|
|
|
54
|
|
|
/** @var callable */ |
55
|
|
|
private $resolveType; |
56
|
|
|
|
57
|
|
|
/** @var Type[] */ |
58
|
|
|
private $cache; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @param Node[] $typeDefinitionsMap |
62
|
|
|
* @param bool[] $options |
63
|
|
|
*/ |
64
|
192 |
|
public function __construct( |
65
|
|
|
array $typeDefinitionsMap, |
66
|
|
|
$options, |
67
|
|
|
callable $resolveType, |
68
|
|
|
?callable $typeConfigDecorator = null |
69
|
|
|
) { |
70
|
192 |
|
$this->typeDefinitionsMap = $typeDefinitionsMap; |
71
|
192 |
|
$this->typeConfigDecorator = $typeConfigDecorator; |
72
|
192 |
|
$this->options = $options; |
73
|
192 |
|
$this->resolveType = $resolveType; |
74
|
|
|
|
75
|
192 |
|
$this->cache = Type::getAllBuiltInTypes(); |
76
|
192 |
|
} |
77
|
|
|
|
78
|
35 |
|
public function buildDirective(DirectiveDefinitionNode $directiveNode) |
79
|
|
|
{ |
80
|
35 |
|
return new Directive([ |
81
|
35 |
|
'name' => $directiveNode->name->value, |
82
|
35 |
|
'description' => $this->getDescription($directiveNode), |
83
|
35 |
|
'locations' => Utils::map( |
84
|
35 |
|
$directiveNode->locations, |
85
|
|
|
static function ($node) { |
86
|
35 |
|
return $node->value; |
87
|
35 |
|
} |
88
|
|
|
), |
89
|
35 |
|
'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null, |
90
|
35 |
|
'astNode' => $directiveNode, |
91
|
|
|
]); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Given an ast node, returns its string description. |
96
|
|
|
*/ |
97
|
182 |
|
private function getDescription($node) |
98
|
|
|
{ |
99
|
182 |
|
if ($node->description) { |
100
|
7 |
|
return $node->description->value; |
101
|
|
|
} |
102
|
179 |
|
if (isset($this->options['commentDescriptions'])) { |
103
|
3 |
|
$rawValue = $this->getLeadingCommentBlock($node); |
104
|
3 |
|
if ($rawValue !== null) { |
105
|
3 |
|
return BlockString::value("\n" . $rawValue); |
106
|
|
|
} |
107
|
|
|
} |
108
|
|
|
|
109
|
176 |
|
return null; |
110
|
|
|
} |
111
|
|
|
|
112
|
3 |
|
private function getLeadingCommentBlock($node) |
113
|
|
|
{ |
114
|
3 |
|
$loc = $node->loc; |
115
|
3 |
|
if (! $loc || ! $loc->startToken) { |
116
|
|
|
return null; |
117
|
|
|
} |
118
|
3 |
|
$comments = []; |
119
|
3 |
|
$token = $loc->startToken->prev; |
120
|
3 |
|
while ($token && |
121
|
3 |
|
$token->kind === Token::COMMENT && |
122
|
3 |
|
$token->next && $token->prev && |
123
|
3 |
|
$token->line + 1 === $token->next->line && |
124
|
3 |
|
$token->line !== $token->prev->line |
125
|
|
|
) { |
126
|
3 |
|
$value = $token->value; |
127
|
3 |
|
$comments[] = $value; |
128
|
3 |
|
$token = $token->prev; |
129
|
|
|
} |
130
|
|
|
|
131
|
3 |
|
return implode("\n", array_reverse($comments)); |
132
|
|
|
} |
133
|
|
|
|
134
|
167 |
|
private function makeInputValues($values) |
135
|
|
|
{ |
136
|
167 |
|
return Utils::keyValMap( |
137
|
167 |
|
$values, |
138
|
|
|
static function ($value) { |
139
|
59 |
|
return $value->name->value; |
140
|
167 |
|
}, |
141
|
|
|
function ($value) { |
142
|
|
|
// Note: While this could make assertions to get the correctly typed |
143
|
|
|
// value, that would throw immediately while type system validation |
144
|
|
|
// with validateSchema() will produce more actionable results. |
145
|
59 |
|
$type = $this->buildWrappedType($value->type); |
146
|
|
|
|
147
|
|
|
$config = [ |
148
|
59 |
|
'name' => $value->name->value, |
149
|
59 |
|
'type' => $type, |
150
|
59 |
|
'description' => $this->getDescription($value), |
151
|
59 |
|
'astNode' => $value, |
152
|
|
|
]; |
153
|
59 |
|
if (isset($value->defaultValue)) { |
154
|
8 |
|
$config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type); |
155
|
|
|
} |
156
|
|
|
|
157
|
59 |
|
return $config; |
158
|
167 |
|
} |
159
|
|
|
); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* @return Type|InputType |
164
|
|
|
* |
165
|
|
|
* @throws Error |
166
|
|
|
*/ |
167
|
150 |
|
private function buildWrappedType(TypeNode $typeNode) |
168
|
|
|
{ |
169
|
150 |
|
if ($typeNode instanceof ListTypeNode) { |
170
|
14 |
|
return Type::listOf($this->buildWrappedType($typeNode->type)); |
171
|
|
|
} |
172
|
150 |
|
if ($typeNode instanceof NonNullTypeNode) { |
173
|
21 |
|
return Type::nonNull($this->buildWrappedType($typeNode->type)); |
174
|
|
|
} |
175
|
|
|
|
176
|
150 |
|
return $this->buildType($typeNode); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* @param string|NamedTypeNode $ref |
181
|
|
|
* |
182
|
|
|
* @return Type |
183
|
|
|
* |
184
|
|
|
* @throws Error |
185
|
|
|
*/ |
186
|
168 |
|
public function buildType($ref) |
187
|
|
|
{ |
188
|
168 |
|
if (is_string($ref)) { |
189
|
130 |
|
return $this->internalBuildType($ref); |
190
|
|
|
} |
191
|
|
|
|
192
|
157 |
|
return $this->internalBuildType($ref->name->value, $ref); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* @param string $typeName |
197
|
|
|
* @param NamedTypeNode|null $typeNode |
198
|
|
|
* |
199
|
|
|
* @return Type |
200
|
|
|
* |
201
|
|
|
* @throws Error |
202
|
|
|
*/ |
203
|
168 |
|
private function internalBuildType($typeName, $typeNode = null) |
204
|
|
|
{ |
205
|
168 |
|
if (! isset($this->cache[$typeName])) { |
206
|
153 |
|
if (isset($this->typeDefinitionsMap[$typeName])) { |
207
|
147 |
|
$type = $this->makeSchemaDef($this->typeDefinitionsMap[$typeName]); |
208
|
147 |
|
if ($this->typeConfigDecorator) { |
209
|
2 |
|
$fn = $this->typeConfigDecorator; |
210
|
|
|
try { |
211
|
2 |
|
$config = $fn($type->config, $this->typeDefinitionsMap[$typeName], $this->typeDefinitionsMap); |
212
|
|
|
} catch (Throwable $e) { |
213
|
|
|
throw new Error( |
214
|
|
|
sprintf('Type config decorator passed to %s threw an error ', static::class) . |
215
|
|
|
sprintf('when building %s type: %s', $typeName, $e->getMessage()), |
216
|
|
|
null, |
217
|
|
|
null, |
218
|
|
|
null, |
219
|
|
|
null, |
220
|
|
|
$e |
221
|
|
|
); |
222
|
|
|
} |
223
|
2 |
|
if (! is_array($config) || isset($config[0])) { |
224
|
|
|
throw new Error( |
225
|
|
|
sprintf( |
226
|
|
|
'Type config decorator passed to %s is expected to return an array, but got %s', |
227
|
|
|
static::class, |
228
|
|
|
Utils::getVariableType($config) |
229
|
|
|
) |
230
|
|
|
); |
231
|
|
|
} |
232
|
2 |
|
$type = $this->makeSchemaDefFromConfig($this->typeDefinitionsMap[$typeName], $config); |
233
|
|
|
} |
234
|
147 |
|
$this->cache[$typeName] = $type; |
235
|
|
|
} else { |
236
|
16 |
|
$fn = $this->resolveType; |
237
|
16 |
|
$this->cache[$typeName] = $fn($typeName, $typeNode); |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
|
241
|
167 |
|
return $this->cache[$typeName]; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode|UnionTypeDefinitionNode $def |
246
|
|
|
* |
247
|
|
|
* @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType |
248
|
|
|
* |
249
|
|
|
* @throws Error |
250
|
|
|
*/ |
251
|
147 |
|
private function makeSchemaDef(Node $def) |
252
|
|
|
{ |
253
|
|
|
switch (true) { |
254
|
147 |
|
case $def instanceof ObjectTypeDefinitionNode: |
255
|
141 |
|
return $this->makeTypeDef($def); |
256
|
81 |
|
case $def instanceof InterfaceTypeDefinitionNode: |
257
|
36 |
|
return $this->makeInterfaceDef($def); |
258
|
53 |
|
case $def instanceof EnumTypeDefinitionNode: |
259
|
17 |
|
return $this->makeEnumDef($def); |
260
|
40 |
|
case $def instanceof UnionTypeDefinitionNode: |
261
|
18 |
|
return $this->makeUnionDef($def); |
262
|
27 |
|
case $def instanceof ScalarTypeDefinitionNode: |
263
|
6 |
|
return $this->makeScalarDef($def); |
264
|
23 |
|
case $def instanceof InputObjectTypeDefinitionNode: |
265
|
23 |
|
return $this->makeInputObjectDef($def); |
266
|
|
|
default: |
267
|
|
|
throw new Error(sprintf('Type kind of %s not supported.', $def->kind)); |
268
|
|
|
} |
269
|
|
|
} |
270
|
|
|
|
271
|
141 |
|
private function makeTypeDef(ObjectTypeDefinitionNode $def) |
272
|
|
|
{ |
273
|
141 |
|
$typeName = $def->name->value; |
274
|
|
|
|
275
|
141 |
|
return new ObjectType([ |
276
|
141 |
|
'name' => $typeName, |
277
|
141 |
|
'description' => $this->getDescription($def), |
278
|
|
|
'fields' => function () use ($def) { |
279
|
126 |
|
return $this->makeFieldDefMap($def); |
280
|
141 |
|
}, |
281
|
|
|
'interfaces' => function () use ($def) { |
282
|
123 |
|
return $this->makeImplementedInterfaces($def); |
283
|
141 |
|
}, |
284
|
141 |
|
'astNode' => $def, |
285
|
|
|
]); |
286
|
|
|
} |
287
|
|
|
|
288
|
127 |
|
private function makeFieldDefMap($def) |
289
|
|
|
{ |
290
|
127 |
|
return $def->fields |
291
|
127 |
|
? Utils::keyValMap( |
292
|
127 |
|
$def->fields, |
293
|
|
|
static function ($field) { |
294
|
127 |
|
return $field->name->value; |
295
|
127 |
|
}, |
296
|
|
|
function ($field) { |
297
|
127 |
|
return $this->buildField($field); |
298
|
127 |
|
} |
299
|
|
|
) |
300
|
126 |
|
: []; |
301
|
|
|
} |
302
|
|
|
|
303
|
143 |
|
public function buildField(FieldDefinitionNode $field) |
304
|
|
|
{ |
305
|
|
|
return [ |
306
|
|
|
// Note: While this could make assertions to get the correctly typed |
307
|
|
|
// value, that would throw immediately while type system validation |
308
|
|
|
// with validateSchema() will produce more actionable results. |
309
|
143 |
|
'type' => $this->buildWrappedType($field->type), |
310
|
141 |
|
'description' => $this->getDescription($field), |
311
|
141 |
|
'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null, |
312
|
141 |
|
'deprecationReason' => $this->getDeprecationReason($field), |
313
|
141 |
|
'astNode' => $field, |
314
|
|
|
]; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* Given a collection of directives, returns the string value for the |
319
|
|
|
* deprecation reason. |
320
|
|
|
* |
321
|
|
|
* @param EnumValueDefinitionNode | FieldDefinitionNode $node |
322
|
|
|
* |
323
|
|
|
* @return string |
324
|
|
|
*/ |
325
|
144 |
|
private function getDeprecationReason($node) |
326
|
|
|
{ |
327
|
144 |
|
$deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node); |
328
|
|
|
|
329
|
144 |
|
return $deprecated['reason'] ?? null; |
330
|
|
|
} |
331
|
|
|
|
332
|
123 |
|
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def) |
333
|
|
|
{ |
334
|
123 |
|
if ($def->interfaces) { |
|
|
|
|
335
|
|
|
// Note: While this could make early assertions to get the correctly |
336
|
|
|
// typed values, that would throw immediately while type system |
337
|
|
|
// validation with validateSchema() will produce more actionable results. |
338
|
34 |
|
return Utils::map( |
339
|
34 |
|
$def->interfaces, |
340
|
|
|
function ($iface) { |
341
|
34 |
|
return $this->buildType($iface); |
342
|
34 |
|
} |
343
|
|
|
); |
344
|
|
|
} |
345
|
|
|
|
346
|
119 |
|
return null; |
347
|
|
|
} |
348
|
|
|
|
349
|
36 |
|
private function makeInterfaceDef(InterfaceTypeDefinitionNode $def) |
350
|
|
|
{ |
351
|
36 |
|
$typeName = $def->name->value; |
352
|
|
|
|
353
|
36 |
|
return new InterfaceType([ |
354
|
36 |
|
'name' => $typeName, |
355
|
36 |
|
'description' => $this->getDescription($def), |
356
|
|
|
'fields' => function () use ($def) { |
357
|
36 |
|
return $this->makeFieldDefMap($def); |
358
|
36 |
|
}, |
359
|
36 |
|
'astNode' => $def, |
360
|
|
|
]); |
361
|
|
|
} |
362
|
|
|
|
363
|
17 |
|
private function makeEnumDef(EnumTypeDefinitionNode $def) |
364
|
|
|
{ |
365
|
17 |
|
return new EnumType([ |
366
|
17 |
|
'name' => $def->name->value, |
367
|
17 |
|
'description' => $this->getDescription($def), |
368
|
17 |
|
'values' => $def->values |
369
|
17 |
|
? Utils::keyValMap( |
370
|
17 |
|
$def->values, |
371
|
|
|
static function ($enumValue) { |
372
|
16 |
|
return $enumValue->name->value; |
373
|
17 |
|
}, |
374
|
|
|
function ($enumValue) { |
375
|
|
|
return [ |
376
|
16 |
|
'description' => $this->getDescription($enumValue), |
377
|
16 |
|
'deprecationReason' => $this->getDeprecationReason($enumValue), |
378
|
16 |
|
'astNode' => $enumValue, |
379
|
|
|
]; |
380
|
17 |
|
} |
381
|
|
|
) |
382
|
|
|
: [], |
383
|
17 |
|
'astNode' => $def, |
384
|
|
|
]); |
385
|
|
|
} |
386
|
|
|
|
387
|
18 |
|
private function makeUnionDef(UnionTypeDefinitionNode $def) |
388
|
|
|
{ |
389
|
18 |
|
return new UnionType([ |
390
|
18 |
|
'name' => $def->name->value, |
391
|
18 |
|
'description' => $this->getDescription($def), |
392
|
|
|
// Note: While this could make assertions to get the correctly typed |
393
|
|
|
// values below, that would throw immediately while type system |
394
|
|
|
// validation with validateSchema() will produce more actionable results. |
395
|
18 |
|
'types' => $def->types |
396
|
|
|
? function () use ($def) { |
397
|
17 |
|
return Utils::map( |
398
|
17 |
|
$def->types, |
399
|
|
|
function ($typeNode) { |
400
|
17 |
|
return $this->buildType($typeNode); |
401
|
17 |
|
} |
402
|
|
|
); |
403
|
17 |
|
} |
404
|
|
|
: [], |
405
|
18 |
|
'astNode' => $def, |
406
|
|
|
]); |
407
|
|
|
} |
408
|
|
|
|
409
|
6 |
|
private function makeScalarDef(ScalarTypeDefinitionNode $def) |
410
|
|
|
{ |
411
|
6 |
|
return new CustomScalarType([ |
412
|
6 |
|
'name' => $def->name->value, |
413
|
6 |
|
'description' => $this->getDescription($def), |
414
|
6 |
|
'astNode' => $def, |
415
|
|
|
'serialize' => static function ($value) { |
416
|
1 |
|
return $value; |
417
|
6 |
|
}, |
418
|
|
|
]); |
419
|
|
|
} |
420
|
|
|
|
421
|
23 |
|
private function makeInputObjectDef(InputObjectTypeDefinitionNode $def) |
422
|
|
|
{ |
423
|
23 |
|
return new InputObjectType([ |
424
|
23 |
|
'name' => $def->name->value, |
425
|
23 |
|
'description' => $this->getDescription($def), |
426
|
|
|
'fields' => function () use ($def) { |
427
|
23 |
|
return $def->fields |
428
|
23 |
|
? $this->makeInputValues($def->fields) |
429
|
23 |
|
: []; |
430
|
23 |
|
}, |
431
|
23 |
|
'astNode' => $def, |
432
|
|
|
]); |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
/** |
436
|
|
|
* @param mixed[] $config |
437
|
|
|
* |
438
|
|
|
* @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType |
439
|
|
|
* |
440
|
|
|
* @throws Error |
441
|
|
|
*/ |
442
|
2 |
|
private function makeSchemaDefFromConfig(Node $def, array $config) |
443
|
|
|
{ |
444
|
|
|
switch (true) { |
445
|
2 |
|
case $def instanceof ObjectTypeDefinitionNode: |
446
|
2 |
|
return new ObjectType($config); |
447
|
2 |
|
case $def instanceof InterfaceTypeDefinitionNode: |
448
|
2 |
|
return new InterfaceType($config); |
449
|
2 |
|
case $def instanceof EnumTypeDefinitionNode: |
450
|
2 |
|
return new EnumType($config); |
451
|
|
|
case $def instanceof UnionTypeDefinitionNode: |
452
|
|
|
return new UnionType($config); |
453
|
|
|
case $def instanceof ScalarTypeDefinitionNode: |
454
|
|
|
return new CustomScalarType($config); |
455
|
|
|
case $def instanceof InputObjectTypeDefinitionNode: |
456
|
|
|
return new InputObjectType($config); |
457
|
|
|
default: |
458
|
|
|
throw new Error(sprintf('Type kind of %s not supported.', $def->kind)); |
459
|
|
|
} |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
/** |
463
|
|
|
* @return mixed[] |
464
|
|
|
*/ |
465
|
4 |
|
public function buildInputField(InputValueDefinitionNode $value) : array |
466
|
|
|
{ |
467
|
4 |
|
$type = $this->buildWrappedType($value->type); |
468
|
|
|
|
469
|
|
|
$config = [ |
470
|
3 |
|
'name' => $value->name->value, |
471
|
3 |
|
'type' => $type, |
472
|
3 |
|
'description' => $this->getDescription($value), |
473
|
3 |
|
'astNode' => $value, |
474
|
|
|
]; |
475
|
|
|
|
476
|
3 |
|
if ($value->defaultValue) { |
477
|
|
|
$config['defaultValue'] = $value->defaultValue; |
478
|
|
|
} |
479
|
|
|
|
480
|
3 |
|
return $config; |
481
|
|
|
} |
482
|
|
|
|
483
|
|
|
/** |
484
|
|
|
* @return mixed[] |
485
|
|
|
*/ |
486
|
4 |
|
public function buildEnumValue(EnumValueDefinitionNode $value) : array |
487
|
|
|
{ |
488
|
|
|
return [ |
489
|
4 |
|
'description' => $this->getDescription($value), |
490
|
4 |
|
'deprecationReason' => $this->getDeprecationReason($value), |
491
|
4 |
|
'astNode' => $value, |
492
|
|
|
]; |
493
|
|
|
} |
494
|
|
|
} |
495
|
|
|
|
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.