Completed
Pull Request — master (#550)
by Jáchym
26:52 queued 22:59
created

AST::typeFromAST()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6.0359

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 17
rs 9.2222
c 0
b 0
f 0
ccs 9
cts 10
cp 0.9
cc 6
nc 6
nop 2
crap 6.0359
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Utils;
6
7
use ArrayAccess;
8
use Exception;
9
use GraphQL\Error\Error;
10
use GraphQL\Error\InvariantViolation;
11
use GraphQL\Language\AST\BooleanValueNode;
12
use GraphQL\Language\AST\DocumentNode;
13
use GraphQL\Language\AST\EnumValueNode;
14
use GraphQL\Language\AST\FloatValueNode;
15
use GraphQL\Language\AST\IntValueNode;
16
use GraphQL\Language\AST\ListTypeNode;
17
use GraphQL\Language\AST\ListValueNode;
18
use GraphQL\Language\AST\Location;
19
use GraphQL\Language\AST\NamedTypeNode;
20
use GraphQL\Language\AST\NameNode;
21
use GraphQL\Language\AST\Node;
22
use GraphQL\Language\AST\NodeKind;
23
use GraphQL\Language\AST\NodeList;
24
use GraphQL\Language\AST\NonNullTypeNode;
25
use GraphQL\Language\AST\NullValueNode;
26
use GraphQL\Language\AST\ObjectFieldNode;
27
use GraphQL\Language\AST\ObjectValueNode;
28
use GraphQL\Language\AST\OperationDefinitionNode;
29
use GraphQL\Language\AST\StringValueNode;
30
use GraphQL\Language\AST\ValueNode;
31
use GraphQL\Language\AST\VariableNode;
32
use GraphQL\Type\Definition\EnumType;
33
use GraphQL\Type\Definition\IDType;
34
use GraphQL\Type\Definition\InputObjectType;
35
use GraphQL\Type\Definition\InputType;
36
use GraphQL\Type\Definition\ListOfType;
37
use GraphQL\Type\Definition\NonNull;
38
use GraphQL\Type\Definition\ScalarType;
39
use GraphQL\Type\Definition\Type;
40
use GraphQL\Type\Schema;
41
use stdClass;
42
use Throwable;
43
use Traversable;
44
use function array_combine;
45
use function array_key_exists;
46
use function array_map;
47
use function count;
48
use function floatval;
49
use function intval;
50
use function is_array;
51
use function is_bool;
52
use function is_float;
53
use function is_int;
54
use function is_object;
55
use function is_string;
56
use function iterator_to_array;
57
use function property_exists;
58
59
/**
60
 * Various utilities dealing with AST
61
 */
62
class AST
63
{
64
    /**
65
     * Convert representation of AST as an associative array to instance of GraphQL\Language\AST\Node.
66
     *
67
     * For example:
68
     *
69
     * ```php
70
     * AST::fromArray([
71
     *     'kind' => 'ListValue',
72
     *     'values' => [
73
     *         ['kind' => 'StringValue', 'value' => 'my str'],
74
     *         ['kind' => 'StringValue', 'value' => 'my other str']
75
     *     ],
76
     *     'loc' => ['start' => 21, 'end' => 25]
77
     * ]);
78
     * ```
79
     *
80
     * Will produce instance of `ListValueNode` where `values` prop is a lazily-evaluated `NodeList`
81
     * returning instances of `StringValueNode` on access.
82
     *
83
     * This is a reverse operation for AST::toArray($node)
84
     *
85
     * @param mixed[] $node
86
     *
87
     * @api
88
     */
89 2
    public static function fromArray(array $node) : Node
90
    {
91 2
        if (! isset($node['kind']) || ! isset(NodeKind::$classMap[$node['kind']])) {
92
            throw new InvariantViolation('Unexpected node structure: ' . Utils::printSafeJson($node));
93
        }
94
95 2
        $kind     = $node['kind'] ?? null;
96 2
        $class    = NodeKind::$classMap[$kind];
97 2
        $instance = new $class([]);
98
99 2
        if (isset($node['loc'], $node['loc']['start'], $node['loc']['end'])) {
100 1
            $instance->loc = Location::create($node['loc']['start'], $node['loc']['end']);
101
        }
102
103 2
        foreach ($node as $key => $value) {
104 2
            if ($key === 'loc' || $key === 'kind') {
105 2
                continue;
106
            }
107 2
            if (is_array($value)) {
108 2
                if (isset($value[0]) || empty($value)) {
109 2
                    $value = new NodeList($value);
110
                } else {
111 2
                    $value = self::fromArray($value);
112
                }
113
            }
114 2
            $instance->{$key} = $value;
115
        }
116
117 2
        return $instance;
118
    }
119
120
    /**
121
     * Convert AST node to serializable array
122
     *
123
     * @return mixed[]
124
     *
125
     * @api
126
     */
127
    public static function toArray(Node $node)
128
    {
129
        return $node->toArray(true);
130
    }
131
132
    /**
133
     * Produces a GraphQL Value AST given a PHP value.
134
     *
135
     * Optionally, a GraphQL type may be provided, which will be used to
136
     * disambiguate between value primitives.
137
     *
138
     * | PHP Value     | GraphQL Value        |
139
     * | ------------- | -------------------- |
140
     * | Object        | Input Object         |
141
     * | Assoc Array   | Input Object         |
142
     * | Array         | List                 |
143
     * | Boolean       | Boolean              |
144
     * | String        | String / Enum Value  |
145
     * | Int           | Int                  |
146
     * | Float         | Int / Float          |
147
     * | Mixed         | Enum Value           |
148
     * | null          | NullValue            |
149
     *
150
     * @param Type|mixed|null $value
151
     *
152
     * @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode
153
     *
154
     * @api
155
     */
156 28
    public static function astFromValue($value, InputType $type)
157
    {
158 28
        if ($type instanceof NonNull) {
159 4
            $astValue = self::astFromValue($value, $type->getWrappedType());
0 ignored issues
show
Bug introduced by
$type->getWrappedType() of type GraphQL\Type\Definition\Type is incompatible with the type GraphQL\Type\Definition\InputType expected by parameter $type of GraphQL\Utils\AST::astFromValue(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

159
            $astValue = self::astFromValue($value, /** @scrutinizer ignore-type */ $type->getWrappedType());
Loading history...
160 4
            if ($astValue instanceof NullValueNode) {
161 4
                return null;
162
            }
163
164 1
            return $astValue;
165
        }
166
167 28
        if ($value === null) {
168 7
            return new NullValueNode([]);
169
        }
170
171
        // Convert PHP array to GraphQL list. If the GraphQLType is a list, but
172
        // the value is not an array, convert the value using the list's item type.
173 26
        if ($type instanceof ListOfType) {
174 2
            $itemType = $type->getWrappedType();
175 2
            if (is_array($value) || ($value instanceof Traversable)) {
176 1
                $valuesNodes = [];
177 1
                foreach ($value as $item) {
178 1
                    $itemNode = self::astFromValue($item, $itemType);
179 1
                    if (! $itemNode) {
180
                        continue;
181
                    }
182
183 1
                    $valuesNodes[] = $itemNode;
184
                }
185
186 1
                return new ListValueNode(['values' => new NodeList($valuesNodes)]);
187
            }
188
189 1
            return self::astFromValue($value, $itemType);
0 ignored issues
show
Bug introduced by
It seems like $itemType can also be of type GraphQL\Type\Definition\InterfaceType and GraphQL\Type\Definition\ObjectType and GraphQL\Type\Definition\UnionType; however, parameter $type of GraphQL\Utils\AST::astFromValue() does only seem to accept GraphQL\Type\Definition\InputType, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

189
            return self::astFromValue($value, /** @scrutinizer ignore-type */ $itemType);
Loading history...
190
        }
191
192
        // Populate the fields of the input object by creating ASTs from each value
193
        // in the PHP object according to the fields in the input type.
194 26
        if ($type instanceof InputObjectType) {
195 2
            $isArray     = is_array($value);
196 2
            $isArrayLike = $isArray || $value instanceof ArrayAccess;
197 2
            if ($value === null || (! $isArrayLike && ! is_object($value))) {
198
                return null;
199
            }
200 2
            $fields     = $type->getFields();
201 2
            $fieldNodes = [];
202 2
            foreach ($fields as $fieldName => $field) {
203 2
                if ($isArrayLike) {
204 2
                    $fieldValue = $value[$fieldName] ?? null;
205
                } else {
206 1
                    $fieldValue = $value->{$fieldName} ?? null;
207
                }
208
209
                // Have to check additionally if key exists, since we differentiate between
210
                // "no key" and "value is null":
211 2
                if ($fieldValue !== null) {
212 1
                    $fieldExists = true;
213 1
                } elseif ($isArray) {
214 1
                    $fieldExists = array_key_exists($fieldName, $value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type ArrayAccess and GraphQL\Type\Definition\Type and GraphQL\Type\Definition\Type&ArrayAccess; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

214
                    $fieldExists = array_key_exists($fieldName, /** @scrutinizer ignore-type */ $value);
Loading history...
215
                } elseif ($isArrayLike) {
216
                    /** @var ArrayAccess $value */
217
                    $fieldExists = $value->offsetExists($fieldName);
218
                } else {
219
                    $fieldExists = property_exists($value, $fieldName);
220
                }
221
222 2
                if (! $fieldExists) {
223 1
                    continue;
224
                }
225
226 2
                $fieldNode = self::astFromValue($fieldValue, $field->getType());
227
228 2
                if (! $fieldNode) {
229
                    continue;
230
                }
231
232 2
                $fieldNodes[] = new ObjectFieldNode([
233 2
                    'name'  => new NameNode(['value' => $fieldName]),
234 2
                    'value' => $fieldNode,
235
                ]);
236
            }
237
238 2
            return new ObjectValueNode(['fields' => new NodeList($fieldNodes)]);
239
        }
240
241 25
        if ($type instanceof ScalarType || $type instanceof EnumType) {
242
            // Since value is an internally represented value, it must be serialized
243
            // to an externally represented value before converting into an AST.
244
            try {
245 25
                $serialized = $type->serialize($value);
246 3
            } catch (Throwable $error) {
247 3
                if ($error instanceof Error && $type instanceof EnumType) {
248 1
                    return null;
249
                }
250 2
                throw $error;
251
            }
252
253
            // Others serialize based on their corresponding PHP scalar types.
254 23
            if (is_bool($serialized)) {
255 7
                return new BooleanValueNode(['value' => $serialized]);
256
            }
257 21
            if (is_int($serialized)) {
258 5
                return new IntValueNode(['value' => $serialized]);
259
            }
260 16
            if (is_float($serialized)) {
261
                // int cast with == used for performance reasons
262
                // phpcs:ignore
263 2
                if ((int) $serialized == $serialized) {
264 2
                    return new IntValueNode(['value' => $serialized]);
265
                }
266
267 1
                return new FloatValueNode(['value' => $serialized]);
268
            }
269 15
            if (is_string($serialized)) {
270
                // Enum types use Enum literals.
271 15
                if ($type instanceof EnumType) {
272 4
                    return new EnumValueNode(['value' => $serialized]);
273
                }
274
275
                // ID types can use Int literals.
276 13
                $asInt = (int) $serialized;
277 13
                if ($type instanceof IDType && (string) $asInt === $serialized) {
278 1
                    return new IntValueNode(['value' => $serialized]);
279
                }
280
281
                // Use json_encode, which uses the same string encoding as GraphQL,
282
                // then remove the quotes.
283 13
                return new StringValueNode(['value' => $serialized]);
284
            }
285
286
            throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized));
287
        }
288
289
        throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
290
    }
291
292
    /**
293
     * Produces a PHP value given a GraphQL Value AST.
294
     *
295
     * A GraphQL type must be provided, which will be used to interpret different
296
     * GraphQL Value literals.
297
     *
298
     * Returns `null` when the value could not be validly coerced according to
299
     * the provided type.
300
     *
301
     * | GraphQL Value        | PHP Value     |
302
     * | -------------------- | ------------- |
303
     * | Input Object         | Assoc Array   |
304
     * | List                 | Array         |
305
     * | Boolean              | Boolean       |
306
     * | String               | String        |
307
     * | Int / Float          | Int / Float   |
308
     * | Enum Value           | Mixed         |
309
     * | Null Value           | null          |
310
     *
311
     * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null $valueNode
312
     * @param mixed[]|null                                                                                                                             $variables
313
     *
314
     * @return mixed[]|stdClass|null
315
     *
316
     * @throws Exception
317
     *
318
     * @api
319
     */
320 106
    public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $variables = null)
321
    {
322 106
        $undefined = Utils::undefined();
323
324 106
        if ($valueNode === null) {
325
            // When there is no AST, then there is also no value.
326
            // Importantly, this is different from returning the GraphQL null value.
327 1
            return $undefined;
328
        }
329
330 105
        if ($type instanceof NonNull) {
331 39
            if ($valueNode instanceof NullValueNode) {
332
                // Invalid: intentionally return no value.
333 5
                return $undefined;
334
            }
335
336 38
            return self::valueFromAST($valueNode, $type->getWrappedType(), $variables);
337
        }
338
339 105
        if ($valueNode instanceof NullValueNode) {
340
            // This is explicitly returning the value null.
341 9
            return null;
342
        }
343
344 103
        if ($valueNode instanceof VariableNode) {
345 11
            $variableName = $valueNode->name->value;
346
347 11
            if (! $variables || ! array_key_exists($variableName, $variables)) {
348
                // No valid return value.
349 1
                return $undefined;
350
            }
351
352 11
            $variableValue = $variables[$variableName] ?? null;
353 11
            if ($variableValue === null && $type instanceof NonNull) {
354
                return $undefined; // Invalid: intentionally return no value.
355
            }
356
357
            // Note: This does no further checking that this variable is correct.
358
            // This assumes that this query has been validated and the variable
359
            // usage here is of the correct type.
360 11
            return $variables[$variableName];
361
        }
362
363 94
        if ($type instanceof ListOfType) {
364 7
            $itemType = $type->getWrappedType();
365
366 7
            if ($valueNode instanceof ListValueNode) {
367 7
                $coercedValues = [];
368 7
                $itemNodes     = $valueNode->values;
369 7
                foreach ($itemNodes as $itemNode) {
370 7
                    if (self::isMissingVariable($itemNode, $variables)) {
371
                        // If an array contains a missing variable, it is either coerced to
372
                        // null or if the item type is non-null, it considered invalid.
373 1
                        if ($itemType instanceof NonNull) {
374
                            // Invalid: intentionally return no value.
375 1
                            return $undefined;
376
                        }
377 1
                        $coercedValues[] = null;
378
                    } else {
379 7
                        $itemValue = self::valueFromAST($itemNode, $itemType, $variables);
380 7
                        if ($undefined === $itemValue) {
381
                            // Invalid: intentionally return no value.
382 4
                            return $undefined;
383
                        }
384 7
                        $coercedValues[] = $itemValue;
385
                    }
386
                }
387
388 7
                return $coercedValues;
389
            }
390 5
            $coercedValue = self::valueFromAST($valueNode, $itemType, $variables);
391 5
            if ($undefined === $coercedValue) {
392
                // Invalid: intentionally return no value.
393 4
                return $undefined;
394
            }
395
396 5
            return [$coercedValue];
397
        }
398
399 93
        if ($type instanceof InputObjectType) {
400 4
            if (! $valueNode instanceof ObjectValueNode) {
401
                // Invalid: intentionally return no value.
402 2
                return $undefined;
403
            }
404
405 4
            $coercedObj = [];
406 4
            $fields     = $type->getFields();
407 4
            $fieldNodes = Utils::keyMap(
408 4
                $valueNode->fields,
409
                static function ($field) {
410 4
                    return $field->name->value;
411 4
                }
412
            );
413 4
            foreach ($fields as $field) {
414
                /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $fieldNode */
415 4
                $fieldName = $field->name;
416 4
                $fieldNode = $fieldNodes[$fieldName] ?? null;
417
418 4
                if ($fieldNode === null || self::isMissingVariable($fieldNode->value, $variables)) {
419 4
                    if ($field->defaultValueExists()) {
420 2
                        $coercedObj[$fieldName] = $field->defaultValue;
421 4
                    } elseif ($field->getType() instanceof NonNull) {
422
                        // Invalid: intentionally return no value.
423 2
                        return $undefined;
424
                    }
425 4
                    continue;
426
                }
427
428 4
                $fieldValue = self::valueFromAST(
429 4
                    $fieldNode !== null ? $fieldNode->value : null,
430 4
                    $field->getType(),
431 4
                    $variables
432
                );
433
434 4
                if ($undefined === $fieldValue) {
435
                    // Invalid: intentionally return no value.
436 1
                    return $undefined;
437
                }
438 4
                $coercedObj[$fieldName] = $fieldValue;
439
            }
440
441 4
            return $coercedObj;
442
        }
443
444 93
        if ($type instanceof EnumType) {
445 8
            if (! $valueNode instanceof EnumValueNode) {
446 1
                return $undefined;
447
            }
448 8
            $enumValue = $type->getValue($valueNode->value);
449 8
            if (! $enumValue) {
450
                return $undefined;
451
            }
452
453 8
            return $enumValue->value;
454
        }
455
456 86
        if ($type instanceof ScalarType) {
457
            // Scalars fulfill parsing a literal value via parseLiteral().
458
            // Invalid values represent a failure to parse correctly, in which case
459
            // no value is returned.
460
            try {
461 86
                return $type->parseLiteral($valueNode, $variables);
462 8
            } catch (Throwable $error) {
463 8
                return $undefined;
464
            }
465
        }
466
467
        throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
468
    }
469
470
    /**
471
     * Returns true if the provided valueNode is a variable which is not defined
472
     * in the set of variables.
473
     *
474
     * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $valueNode
475
     * @param mixed[]                                                                                                                             $variables
476
     *
477
     * @return bool
478
     */
479 9
    private static function isMissingVariable(ValueNode $valueNode, $variables)
480
    {
481 9
        return $valueNode instanceof VariableNode &&
482 9
            (count($variables) === 0 || ! array_key_exists($valueNode->name->value, $variables));
483
    }
484
485
    /**
486
     * Produces a PHP value given a GraphQL Value AST.
487
     *
488
     * Unlike `valueFromAST()`, no type is provided. The resulting PHP value
489
     * will reflect the provided GraphQL value AST.
490
     *
491
     * | GraphQL Value        | PHP Value     |
492
     * | -------------------- | ------------- |
493
     * | Input Object         | Assoc Array   |
494
     * | List                 | Array         |
495
     * | Boolean              | Boolean       |
496
     * | String               | String        |
497
     * | Int / Float          | Int / Float   |
498
     * | Enum                 | Mixed         |
499
     * | Null                 | null          |
500
     *
501
     * @param Node         $valueNode
502
     * @param mixed[]|null $variables
503
     *
504
     * @return mixed
505
     *
506
     * @throws Exception
507
     *
508
     * @api
509
     */
510 6
    public static function valueFromASTUntyped($valueNode, ?array $variables = null)
511
    {
512
        switch (true) {
513 6
            case $valueNode instanceof NullValueNode:
514 2
                return null;
515 6
            case $valueNode instanceof IntValueNode:
516 3
                return intval($valueNode->value, 10);
517 5
            case $valueNode instanceof FloatValueNode:
518 2
                return floatval($valueNode->value);
519 5
            case $valueNode instanceof StringValueNode:
520 5
            case $valueNode instanceof EnumValueNode:
521 5
            case $valueNode instanceof BooleanValueNode:
522 4
                return $valueNode->value;
523 4
            case $valueNode instanceof ListValueNode:
524 4
                return array_map(
525
                    static function ($node) use ($variables) {
526 4
                        return self::valueFromASTUntyped($node, $variables);
527 4
                    },
528 4
                    iterator_to_array($valueNode->values)
529
                );
530 2
            case $valueNode instanceof ObjectValueNode:
531 2
                return array_combine(
532 2
                    array_map(
533
                        static function ($field) {
534 2
                            return $field->name->value;
535 2
                        },
536 2
                        iterator_to_array($valueNode->fields)
537
                    ),
538 2
                    array_map(
539
                        static function ($field) use ($variables) {
540 2
                            return self::valueFromASTUntyped($field->value, $variables);
541 2
                        },
542 2
                        iterator_to_array($valueNode->fields)
543
                    )
544
                );
545 1
            case $valueNode instanceof VariableNode:
546 1
                $variableName = $valueNode->name->value;
547
548 1
                return $variables && isset($variables[$variableName])
549 1
                    ? $variables[$variableName]
550 1
                    : null;
551
        }
552
553
        throw new Error('Unexpected value kind: ' . $valueNode->kind . '.');
554
    }
555
556
    /**
557
     * Returns type definition for given AST Type node
558
     *
559
     * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
560
     *
561
     * @return Type|null
562
     *
563
     * @throws Exception
564
     *
565
     * @api
566
     */
567 347
    public static function typeFromAST(Schema $schema, $inputTypeNode)
568
    {
569 347
        if ($inputTypeNode instanceof ListTypeNode) {
570 19
            $innerType = self::typeFromAST($schema, $inputTypeNode->type);
571
572 19
            return $innerType ? new ListOfType($innerType) : null;
573
        }
574 347
        if ($inputTypeNode instanceof NonNullTypeNode) {
575 43
            $innerType = self::typeFromAST($schema, $inputTypeNode->type);
576
577 43
            return $innerType ? new NonNull($innerType) : null;
578
        }
579 347
        if ($inputTypeNode instanceof NamedTypeNode) {
0 ignored issues
show
introduced by
$inputTypeNode is always a sub-type of GraphQL\Language\AST\NamedTypeNode.
Loading history...
580 347
            return $schema->getType($inputTypeNode->name->value);
581
        }
582
583
        throw new Error('Unexpected type kind: ' . $inputTypeNode->kind . '.');
584
    }
585
586
    /**
587
     * Returns operation type ("query", "mutation" or "subscription") given a document and operation name
588
     *
589
     * @param string $operationName
590
     *
591
     * @return bool
592
     *
593
     * @api
594
     */
595 23
    public static function getOperation(DocumentNode $document, $operationName = null)
596
    {
597 23
        if ($document->definitions) {
598 23
            foreach ($document->definitions as $def) {
599 23
                if (! ($def instanceof OperationDefinitionNode)) {
600
                    continue;
601
                }
602
603 23
                if (! $operationName || (isset($def->name->value) && $def->name->value === $operationName)) {
604 23
                    return $def->operation;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $def->operation returns the type string which is incompatible with the documented return type boolean.
Loading history...
605
                }
606
            }
607
        }
608
609
        return false;
610
    }
611
}
612