Completed
Push — master ( a01b08...b72ba3 )
by Vladimir
16s queued 14s
created

ASTDefinitionBuilder::makeInputValues()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 14
c 1
b 0
f 0
dl 0
loc 24
rs 9.7998
ccs 13
cts 13
cp 1
cc 2
nc 1
nop 1
crap 2
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\NonNull;
34
use GraphQL\Type\Definition\ObjectType;
35
use GraphQL\Type\Definition\Type;
36
use GraphQL\Type\Definition\UnionType;
37
use Throwable;
38
use function array_reverse;
39
use function implode;
40
use function is_array;
41
use function is_string;
42
use function sprintf;
43
44
class ASTDefinitionBuilder
45
{
46
    /** @var Node[] */
47
    private $typeDefinitionsMap;
48
49
    /** @var callable */
50
    private $typeConfigDecorator;
51
52
    /** @var bool[] */
53
    private $options;
54
55
    /** @var callable */
56
    private $resolveType;
57
58
    /** @var Type[] */
59
    private $cache;
60
61
    /**
62
     * @param Node[] $typeDefinitionsMap
63
     * @param bool[] $options
64
     */
65 163
    public function __construct(
66
        array $typeDefinitionsMap,
67
        $options,
68
        callable $resolveType,
69
        ?callable $typeConfigDecorator = null
70
    ) {
71 163
        $this->typeDefinitionsMap  = $typeDefinitionsMap;
72 163
        $this->typeConfigDecorator = $typeConfigDecorator;
73 163
        $this->options             = $options;
74 163
        $this->resolveType         = $resolveType;
75
76 163
        $this->cache = Type::getAllBuiltInTypes();
77 163
    }
78
79 13
    public function buildDirective(DirectiveDefinitionNode $directiveNode)
80
    {
81 13
        return new Directive([
82 13
            'name'        => $directiveNode->name->value,
83 13
            'description' => $this->getDescription($directiveNode),
84 13
            'locations'   => Utils::map(
85 13
                $directiveNode->locations,
86
                static function ($node) {
87 13
                    return $node->value;
88 13
                }
89
            ),
90 13
            'args'        => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
91 13
            'astNode'     => $directiveNode,
92
        ]);
93
    }
94
95
    /**
96
     * Given an ast node, returns its string description.
97
     */
98 154
    private function getDescription($node)
99
    {
100 154
        if ($node->description) {
101 7
            return $node->description->value;
102
        }
103 151
        if (isset($this->options['commentDescriptions'])) {
104 3
            $rawValue = $this->getLeadingCommentBlock($node);
105 3
            if ($rawValue !== null) {
106 3
                return BlockString::value("\n" . $rawValue);
107
            }
108
        }
109
110 148
        return null;
111
    }
112
113 3
    private function getLeadingCommentBlock($node)
114
    {
115 3
        $loc = $node->loc;
116 3
        if (! $loc || ! $loc->startToken) {
117
            return null;
118
        }
119 3
        $comments = [];
120 3
        $token    = $loc->startToken->prev;
121 3
        while ($token &&
122 3
            $token->kind === Token::COMMENT &&
123 3
            $token->next && $token->prev &&
124 3
            $token->line + 1 === $token->next->line &&
125 3
            $token->line !== $token->prev->line
126
        ) {
127 3
            $value      = $token->value;
128 3
            $comments[] = $value;
129 3
            $token      = $token->prev;
130
        }
131
132 3
        return implode("\n", array_reverse($comments));
133
    }
134
135 142
    private function makeInputValues($values)
136
    {
137 142
        return Utils::keyValMap(
138 142
            $values,
139
            static function ($value) {
140 56
                return $value->name->value;
141 142
            },
142
            function ($value) {
143
                // Note: While this could make assertions to get the correctly typed
144
                // value, that would throw immediately while type system validation
145
                // with validateSchema() will produce more actionable results.
146 56
                $type = $this->internalBuildWrappedType($value->type);
147
148
                $config = [
149 56
                    'name'        => $value->name->value,
150 56
                    'type'        => $type,
151 56
                    'description' => $this->getDescription($value),
152 56
                    'astNode'     => $value,
153
                ];
154 56
                if (isset($value->defaultValue)) {
155 8
                    $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
156
                }
157
158 56
                return $config;
159 142
            }
160
        );
161
    }
162
163
    /**
164
     * @return Type|InputType
165
     *
166
     * @throws Error
167
     */
168 142
    private function internalBuildWrappedType(TypeNode $typeNode)
169
    {
170 142
        $typeDef = $this->buildType($this->getNamedTypeNode($typeNode));
171
172 139
        return $this->buildWrappedType($typeDef, $typeNode);
173
    }
174
175
    /**
176
     * @param string|NamedTypeNode $ref
177
     *
178
     * @return Type
179
     *
180
     * @throws Error
181
     */
182 153
    public function buildType($ref)
183
    {
184 153
        if (is_string($ref)) {
185 115
            return $this->internalBuildType($ref);
186
        }
187
188 149
        return $this->internalBuildType($ref->name->value, $ref);
189
    }
190
191
    /**
192
     * @param string             $typeName
193
     * @param NamedTypeNode|null $typeNode
194
     *
195
     * @return Type
196
     *
197
     * @throws Error
198
     */
199 153
    private function internalBuildType($typeName, $typeNode = null)
200
    {
201 153
        if (! isset($this->cache[$typeName])) {
202 138
            if (isset($this->typeDefinitionsMap[$typeName])) {
203 132
                $type = $this->makeSchemaDef($this->typeDefinitionsMap[$typeName]);
204 132
                if ($this->typeConfigDecorator) {
205 2
                    $fn = $this->typeConfigDecorator;
206
                    try {
207 2
                        $config = $fn($type->config, $this->typeDefinitionsMap[$typeName], $this->typeDefinitionsMap);
208
                    } catch (Throwable $e) {
209
                        throw new Error(
210
                            sprintf('Type config decorator passed to %s threw an error ', static::class) .
211
                            sprintf('when building %s type: %s', $typeName, $e->getMessage()),
212
                            null,
213
                            null,
214
                            null,
215
                            null,
216
                            $e
217
                        );
218
                    }
219 2
                    if (! is_array($config) || isset($config[0])) {
220
                        throw new Error(
221
                            sprintf(
222
                                'Type config decorator passed to %s is expected to return an array, but got %s',
223
                                static::class,
224
                                Utils::getVariableType($config)
225
                            )
226
                        );
227
                    }
228 2
                    $type = $this->makeSchemaDefFromConfig($this->typeDefinitionsMap[$typeName], $config);
229
                }
230 132
                $this->cache[$typeName] = $type;
231
            } else {
232 13
                $fn                     = $this->resolveType;
233 13
                $this->cache[$typeName] = $fn($typeName, $typeNode);
234
            }
235
        }
236
237 152
        return $this->cache[$typeName];
238
    }
239
240
    /**
241
     * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode|UnionTypeDefinitionNode $def
242
     *
243
     * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
244
     *
245
     * @throws Error
246
     */
247 132
    private function makeSchemaDef(Node $def)
248
    {
249
        switch (true) {
250 132
            case $def instanceof ObjectTypeDefinitionNode:
251 127
                return $this->makeTypeDef($def);
252 75
            case $def instanceof InterfaceTypeDefinitionNode:
253 32
                return $this->makeInterfaceDef($def);
254 50
            case $def instanceof EnumTypeDefinitionNode:
255 16
                return $this->makeEnumDef($def);
256 37
            case $def instanceof UnionTypeDefinitionNode:
257 16
                return $this->makeUnionDef($def);
258 25
            case $def instanceof ScalarTypeDefinitionNode:
259 5
                return $this->makeScalarDef($def);
260 21
            case $def instanceof InputObjectTypeDefinitionNode:
261 21
                return $this->makeInputObjectDef($def);
262
            default:
263
                throw new Error(sprintf('Type kind of %s not supported.', $def->kind));
264
        }
265
    }
266
267 127
    private function makeTypeDef(ObjectTypeDefinitionNode $def)
268
    {
269 127
        $typeName = $def->name->value;
270
271 127
        return new ObjectType([
272 127
            'name'        => $typeName,
273 127
            'description' => $this->getDescription($def),
274
            'fields'      => function () use ($def) {
275 119
                return $this->makeFieldDefMap($def);
276 127
            },
277
            'interfaces'  => function () use ($def) {
278 116
                return $this->makeImplementedInterfaces($def);
279 127
            },
280 127
            'astNode'     => $def,
281
        ]);
282
    }
283
284 120
    private function makeFieldDefMap($def)
285
    {
286 120
        return $def->fields
287 120
            ? Utils::keyValMap(
288 120
                $def->fields,
289
                static function ($field) {
290 120
                    return $field->name->value;
291 120
                },
292
                function ($field) {
293 120
                    return $this->buildField($field);
294 120
                }
295
            )
296 118
            : [];
297
    }
298
299 136
    public function buildField(FieldDefinitionNode $field)
300
    {
301
        return [
302
            // Note: While this could make assertions to get the correctly typed
303
            // value, that would throw immediately while type system validation
304
            // with validateSchema() will produce more actionable results.
305 136
            'type'              => $this->internalBuildWrappedType($field->type),
306 133
            'description'       => $this->getDescription($field),
307 133
            'args'              => $field->arguments ? $this->makeInputValues($field->arguments) : null,
308 133
            'deprecationReason' => $this->getDeprecationReason($field),
309 133
            'astNode'           => $field,
310
        ];
311
    }
312
313
    /**
314
     * Given a collection of directives, returns the string value for the
315
     * deprecation reason.
316
     *
317
     * @param EnumValueDefinitionNode | FieldDefinitionNode $node
318
     *
319
     * @return string
320
     */
321 136
    private function getDeprecationReason($node)
322
    {
323 136
        $deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);
324
325 136
        return $deprecated['reason'] ?? null;
326
    }
327
328 116
    private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
329
    {
330 116
        if ($def->interfaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $def->interfaces of type GraphQL\Language\AST\NamedTypeNode[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
331
            // Note: While this could make early assertions to get the correctly
332
            // typed values, that would throw immediately while type system
333
            // validation with validateSchema() will produce more actionable results.
334 30
            return Utils::map(
335 30
                $def->interfaces,
336
                function ($iface) {
337 30
                    return $this->buildType($iface);
338 30
                }
339
            );
340
        }
341
342 112
        return null;
343
    }
344
345 32
    private function makeInterfaceDef(InterfaceTypeDefinitionNode $def)
346
    {
347 32
        $typeName = $def->name->value;
348
349 32
        return new InterfaceType([
350 32
            'name'        => $typeName,
351 32
            'description' => $this->getDescription($def),
352
            'fields'      => function () use ($def) {
353 32
                return $this->makeFieldDefMap($def);
354 32
            },
355 32
            'astNode'     => $def,
356
        ]);
357
    }
358
359 16
    private function makeEnumDef(EnumTypeDefinitionNode $def)
360
    {
361 16
        return new EnumType([
362 16
            'name'        => $def->name->value,
363 16
            'description' => $this->getDescription($def),
364 16
            'values'      => $def->values
365 16
                ? Utils::keyValMap(
366 16
                    $def->values,
367
                    static function ($enumValue) {
368 15
                        return $enumValue->name->value;
369 16
                    },
370
                    function ($enumValue) {
371
                        return [
372 15
                            'description'       => $this->getDescription($enumValue),
373 15
                            'deprecationReason' => $this->getDeprecationReason($enumValue),
374 15
                            'astNode'           => $enumValue,
375
                        ];
376 16
                    }
377
                )
378
                : [],
379 16
            'astNode'     => $def,
380
        ]);
381
    }
382
383 16
    private function makeUnionDef(UnionTypeDefinitionNode $def)
384
    {
385 16
        return new UnionType([
386 16
            'name'        => $def->name->value,
387 16
            'description' => $this->getDescription($def),
388
            // Note: While this could make assertions to get the correctly typed
389
            // values below, that would throw immediately while type system
390
            // validation with validateSchema() will produce more actionable results.
391 16
            'types'       => $def->types
392 15
                ? Utils::map(
393 15
                    $def->types,
394
                    function ($typeNode) {
395 15
                        return $this->buildType($typeNode);
396 15
                    }
397
                )
398
                : [],
399 15
            'astNode'     => $def,
400
        ]);
401
    }
402
403 5
    private function makeScalarDef(ScalarTypeDefinitionNode $def)
404
    {
405 5
        return new CustomScalarType([
406 5
            'name'        => $def->name->value,
407 5
            'description' => $this->getDescription($def),
408 5
            'astNode'     => $def,
409
            'serialize'   => static function ($value) {
410 1
                return $value;
411 5
            },
412
        ]);
413
    }
414
415 21
    private function makeInputObjectDef(InputObjectTypeDefinitionNode $def)
416
    {
417 21
        return new InputObjectType([
418 21
            'name'        => $def->name->value,
419 21
            'description' => $this->getDescription($def),
420
            'fields'      => function () use ($def) {
421 21
                return $def->fields
422 21
                    ? $this->makeInputValues($def->fields)
423 21
                    : [];
424 21
            },
425 21
            'astNode'     => $def,
426
        ]);
427
    }
428
429
    /**
430
     * @param mixed[] $config
431
     *
432
     * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
433
     *
434
     * @throws Error
435
     */
436 2
    private function makeSchemaDefFromConfig(Node $def, array $config)
437
    {
438
        switch (true) {
439 2
            case $def instanceof ObjectTypeDefinitionNode:
440 2
                return new ObjectType($config);
441 2
            case $def instanceof InterfaceTypeDefinitionNode:
442 2
                return new InterfaceType($config);
443 2
            case $def instanceof EnumTypeDefinitionNode:
444 2
                return new EnumType($config);
445
            case $def instanceof UnionTypeDefinitionNode:
446
                return new UnionType($config);
447
            case $def instanceof ScalarTypeDefinitionNode:
448
                return new CustomScalarType($config);
449
            case $def instanceof InputObjectTypeDefinitionNode:
450
                return new InputObjectType($config);
451
            default:
452
                throw new Error(sprintf('Type kind of %s not supported.', $def->kind));
453
        }
454
    }
455
456 142
    private function getNamedTypeNode(TypeNode $typeNode) : TypeNode
457
    {
458 142
        $namedType = $typeNode;
459 142
        while ($namedType instanceof ListTypeNode || $namedType instanceof NonNullTypeNode) {
460 25
            $namedType = $namedType->type;
461
        }
462
463 142
        return $namedType;
464
    }
465
466 139
    private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode) : Type
467
    {
468 139
        if ($inputTypeNode instanceof ListTypeNode) {
469 14
            return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type));
470
        }
471 139
        if ($inputTypeNode instanceof NonNullTypeNode) {
472 20
            $wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);
473
474 20
            return Type::nonNull(NonNull::assertNullableType($wrappedType));
475
        }
476
477 139
        return $innerType;
478
    }
479
480
    /**
481
     * @return mixed[]
482
     */
483 4
    public function buildInputField(InputValueDefinitionNode $value) : array
484
    {
485 4
        $type = $this->internalBuildWrappedType($value->type);
486
487
        $config = [
488 3
            'name' => $value->name->value,
489 3
            'type' => $type,
490 3
            'description' => $this->getDescription($value),
491 3
            'astNode' => $value,
492
        ];
493
494 3
        if ($value->defaultValue) {
495
            $config['defaultValue'] = $value->defaultValue;
496
        }
497
498 3
        return $config;
499
    }
500
501
    /**
502
     * @return mixed[]
503
     */
504 4
    public function buildEnumValue(EnumValueDefinitionNode $value) : array
505
    {
506
        return [
507 4
            'description' => $this->getDescription($value),
508 4
            'deprecationReason' => $this->getDeprecationReason($value),
509 4
            'astNode' => $value,
510
        ];
511
    }
512
}
513