ASTDefinitionBuilder::makeSchemaDefFromConfig()   B
last analyzed

Complexity

Conditions 7
Paths 7

Size

Total Lines 17
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 13.125

Importance

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