Failed Conditions
Push — master ( a4f39b...12ee90 )
by Vladimir
11:17
created

ASTDefinitionBuilder::internalBuildWrappedType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
rs 10
ccs 3
cts 3
cp 1
cc 1
nc 1
nop 1
crap 1
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) {
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...
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