Passed
Pull Request — master (#169)
by Quang
03:16
created

ValuesHelper::coerceValueForVariableNode()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 29
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 8.439
c 0
b 0
f 0
cc 5
eloc 13
nc 4
nop 5
1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\CoercingException;
6
use Digia\GraphQL\Error\ExecutionException;
7
use Digia\GraphQL\Error\GraphQLException;
8
use Digia\GraphQL\Error\InvalidTypeException;
9
use Digia\GraphQL\Error\InvariantException;
10
use Digia\GraphQL\Language\Node\ArgumentNode;
11
use Digia\GraphQL\Language\Node\ArgumentsAwareInterface;
12
use Digia\GraphQL\Language\Node\NameAwareInterface;
13
use Digia\GraphQL\Language\Node\NodeInterface;
14
use Digia\GraphQL\Language\Node\VariableDefinitionNode;
15
use Digia\GraphQL\Language\Node\VariableNode;
16
use Digia\GraphQL\Schema\Schema;
17
use Digia\GraphQL\Type\Definition\Directive;
18
use Digia\GraphQL\Type\Definition\EnumType;
19
use Digia\GraphQL\Type\Definition\EnumValue;
20
use Digia\GraphQL\Type\Definition\Field;
21
use Digia\GraphQL\Type\Definition\InputObjectType;
22
use Digia\GraphQL\Type\Definition\ListType;
23
use Digia\GraphQL\Type\Definition\NonNullType;
24
use Digia\GraphQL\Type\Definition\ScalarType;
25
use Digia\GraphQL\Type\Definition\TypeInterface;
26
use Digia\GraphQL\Type\Definition\WrappingTypeInterface;
27
use function Digia\GraphQL\Util\find;
28
use function Digia\GraphQL\Util\keyMap;
29
use function Digia\GraphQL\Util\suggestionList;
30
use function Digia\GraphQL\Util\typeFromAST;
31
use function Digia\GraphQL\Util\valueFromAST;
32
33
class ValuesHelper
34
{
35
    /**
36
     * Prepares an object map of argument values given a list of argument
37
     * definitions and list of argument AST nodes.
38
     *
39
     * @see http://facebook.github.io/graphql/October2016/#CoerceArgumentValues()
40
     *
41
     * @param Field|Directive         $definition
42
     * @param ArgumentsAwareInterface $node
43
     * @param array                   $variableValues
44
     * @return array
45
     * @throws ExecutionException
46
     * @throws InvalidTypeException
47
     * @throws InvariantException
48
     */
49
    public function coerceArgumentValues($definition, ArgumentsAwareInterface $node, array $variableValues = []): array
50
    {
51
        $coercedValues       = [];
52
        $argumentDefinitions = $definition->getArguments();
53
        $argumentNodes       = $node->getArguments();
54
55
        if (empty($argumentDefinitions) || $argumentNodes === null) {
56
            return $coercedValues;
57
        }
58
59
        $argumentNodeMap = keyMap($argumentNodes, function (ArgumentNode $value) {
60
            return $value->getNameValue();
61
        });
62
63
        foreach ($argumentDefinitions as $argumentDefinition) {
64
            $argumentName = $argumentDefinition->getName();
65
            $argumentType = $argumentDefinition->getType();
66
            /** @var ArgumentNode $argumentNode */
67
            $argumentNode = $argumentNodeMap[$argumentName] ?? null;
68
            $defaultValue = $argumentDefinition->getDefaultValue();
69
70
            if (null === $argumentNode) {
71
                if (null !== $defaultValue) {
72
                    $coercedValues[$argumentName] = $defaultValue;
73
                } elseif ($argumentType instanceof NonNullType) {
74
                    throw new ExecutionException(
75
                        sprintf('Argument "%s" of required type "%s" was not provided.', $argumentName,
76
                            $argumentType),
77
                        [$node]
78
                    );
79
                }
80
            } elseif ($argumentNode->getValue() instanceof VariableNode) {
81
                $coercedValues[$argumentName] = $this->coerceValueForVariableNode(
82
                    $argumentNode->getValue(),
83
                    $argumentType,
84
                    $argumentName,
85
                    $variableValues,
86
                    $defaultValue
87
                );
88
            } else {
89
                $coercedValue = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $coercedValue is dead and can be removed.
Loading history...
90
91
                try {
92
                    $coercedValue = valueFromAST($argumentNode->getValue(), $argumentType, $variableValues);
93
                } catch (\Exception $ex) {
94
                    // Value nodes that cannot be resolved should be treated as invalid values
95
                    // because there is no undefined value in PHP so that we throw an exception
96
97
                    throw new ExecutionException(
98
                        sprintf('Argument "%s" has invalid value %s.',
99
                            $argumentName,
100
                            (string)$argumentNode->getValue()),
101
                        [$argumentNode->getValue()],
102
                        null, null, null, $ex
103
                    );
104
                }
105
106
                $coercedValues[$argumentName] = $coercedValue;
107
            }
108
        }
109
110
        return $coercedValues;
111
    }
112
113
    /**
114
     * Prepares an object map of argument values given a directive definition
115
     * and a AST node which may contain directives. Optionally also accepts a map
116
     * of variable values.
117
     *
118
     * If the directive does not exist on the node, returns null.
119
     *
120
     * @param Directive $directive
121
     * @param mixed     $node
122
     * @param array     $variableValues
123
     * @return array|null
124
     * @throws ExecutionException
125
     * @throws InvalidTypeException
126
     * @throws InvariantException
127
     */
128
    public function coerceDirectiveValues(
129
        Directive $directive,
130
        $node,
131
        array $variableValues = []
132
    ): ?array {
133
        $directiveNode = $node->hasDirectives()
134
            ? find($node->getDirectives(), function (NameAwareInterface $value) use ($directive) {
135
                return $value->getNameValue() === $directive->getName();
136
            }) : null;
137
138
        if (null !== $directiveNode) {
139
            return $this->coerceArgumentValues($directive, $directiveNode, $variableValues);
140
        }
141
142
        return null;
143
    }
144
145
    /**
146
     * Prepares an object map of variableValues of the correct type based on the
147
     * provided variable definitions and arbitrary input. If the input cannot be
148
     * parsed to match the variable definitions, a GraphQLError will be thrown.
149
     *
150
     * @param Schema                         $schema
151
     * @param array|VariableDefinitionNode[] $variableDefinitionNodes
152
     * @param                                $input
153
     * @return CoercedValue
154
     * @throws \Exception
155
     */
156
    public function coerceVariableValues(Schema $schema, array $variableDefinitionNodes, array $inputs): CoercedValue
157
    {
158
        $coercedValues = [];
159
        $errors        = [];
160
161
        foreach ($variableDefinitionNodes as $variableDefinitionNode) {
162
            $variableName = $variableDefinitionNode->getVariable()->getNameValue();
163
            $variableType = typeFromAST($schema, $variableDefinitionNode->getType());
164
165
            if (!$this->isInputType($variableType)) {
166
                $variableTypeName = (string)$variableType;
167
168
                if ($variableTypeName === '') {
169
                    $variableTypeName = (string)$variableDefinitionNode;
170
                }
171
172
                $errors[] = $this->buildCoerceException(
173
                    sprintf(
174
                        'Variable "$%s" expected value of type "%s" which cannot be used as an input type',
175
                        $variableName,
176
                        $variableTypeName
177
                    ),
178
                    $variableDefinitionNode,
179
                    null
180
                );
181
            } else {
182
                if (!array_key_exists($variableName, $inputs)) {
183
                    if ($variableType instanceof NonNullType) {
184
                        $errors[] = $this->buildCoerceException(
185
                            sprintf(
186
                                'Variable "$%s" of required type "%s" was not provided',
187
                                $variableName,
188
                                (string)$variableType
189
                            ),
190
                            $variableDefinitionNode,
191
                            null
192
                        );
193
                    } elseif ($variableDefinitionNode->getDefaultValue() !== null) {
194
                        $coercedValues[$variableName] = valueFromAST(
195
                            $variableDefinitionNode->getDefaultValue(),
196
                            $variableType
197
                        );
198
                    }
199
                } else {
200
                    $value        = $inputs[$variableName];
201
                    $coercedValue = $this->coerceValue($value, $variableType, $variableDefinitionNode);
202
                    if ($coercedValue->hasErrors()) {
203
                        $messagePrelude = sprintf(
204
                            'Variable "$%s" got invalid value %s',
205
                            $variableName, json_encode($value)
206
                        );
207
                        foreach ($coercedValue->getErrors() as $error) {
208
                            $errors[] = $this->buildCoerceException(
209
                                $messagePrelude,
210
                                $variableDefinitionNode,
211
                                null,
212
                                $error->getMessage(),
213
                                $error
214
                            );
215
                        }
216
                    } else {
217
                        $coercedValues[$variableName] = $coercedValue->getValue();
218
                    }
219
                }
220
            }
221
        }
222
223
        return new CoercedValue($coercedValues, $errors);
224
    }
225
226
227
    /**
228
     * @param TypeInterface|null $type
229
     * @return bool
230
     */
231
    protected function isInputType(?TypeInterface $type)
232
    {
233
        return ($type instanceof ScalarType) ||
234
            ($type instanceof EnumType) ||
235
            ($type instanceof InputObjectType) ||
236
            (($type instanceof WrappingTypeInterface) && $this->isInputType($type->getOfType()));
237
    }
238
239
    /**
240
     * Returns either a value which is valid for the provided type or a list of
241
     * encountered coercion errors.
242
     *
243
     * @param  mixed|array $value
244
     * @param              $type
245
     * @param              $blameNode
246
     * @param array        $path
247
     * @return CoercedValue
248
     * @throws InvariantException
249
     * @throws GraphQLException
250
     */
251
    private function coerceValue($value, $type, $blameNode, ?Path $path = null): CoercedValue
252
    {
253
        if ($type instanceof NonNullType) {
254
            return $this->coerceValueForNonNullType($value, $type, $blameNode, $path);
255
        }
256
257
        if (empty($value)) {
258
            return new CoercedValue(null, null);
259
        }
260
261
        if ($type instanceof ScalarType) {
262
            return $this->coerceValueForScalarType($value, $type, $blameNode, $path);
263
        }
264
265
        if ($type instanceof EnumType) {
266
            return $this->coerceValueForEnumType($value, $type, $blameNode, $path);
267
        }
268
269
        if ($type instanceof ListType) {
270
            return $this->coerceValueForListType($value, $type, $blameNode, $path);
271
        }
272
273
        if ($type instanceof InputObjectType) {
274
            return $this->coerceValueForInputObjectType($value, $type, $blameNode, $path);
275
        }
276
277
        throw new GraphQLException('Unexpected type.');
278
    }
279
280
    /**
281
     * @param               $value
282
     * @param NonNullType   $type
283
     * @param NodeInterface $blameNode
284
     * @param array|null    $path
285
     * @return CoercedValue
286
     * @throws GraphQLException
287
     * @throws InvariantException
288
     */
289
    protected function coerceValueForNonNullType(
290
        $value,
291
        NonNullType $type,
292
        NodeInterface $blameNode,
293
        ?Path $path
294
    ): CoercedValue {
295
        if (empty($value)) {
296
            return new CoercedValue(null, [
297
                $this->buildCoerceException(
298
                    sprintf('Expected non-nullable type %s not to be null', (string)$type),
299
                    $blameNode,
300
                    $path
301
                )
302
            ]);
303
        }
304
        return $this->coerceValue($value, $type->getOfType(), $blameNode, $path);
305
    }
306
307
    /**
308
     * Scalars determine if a value is valid via parseValue(), which can
309
     * throw to indicate failure. If it throws, maintain a reference to
310
     * the original error.
311
     *
312
     * @param               $value
313
     * @param ScalarType    $type
314
     * @param NodeInterface $blameNode
315
     * @param array|null    $path
316
     * @return CoercedValue
317
     */
318
    protected function coerceValueForScalarType(
319
        $value,
320
        ScalarType $type,
321
        NodeInterface $blameNode,
322
        ?Path $path
323
    ): CoercedValue {
324
        try {
325
            $parseResult = $type->parseValue($value);
326
            if (empty($parseResult)) {
327
                return new CoercedValue(null, [
328
                    new GraphQLException(sprintf('Expected type %s', (string)$type))
329
                ]);
330
            }
331
            return new CoercedValue($parseResult, null);
332
        } catch (InvalidTypeException|CoercingException $ex) {
333
            return new CoercedValue(null, [
334
                $this->buildCoerceException(
335
                    sprintf('Expected type %s', (string)$type),
336
                    $blameNode,
337
                    $path,
338
                    $ex->getMessage(),
339
                    $ex
340
                )
341
            ]);
342
        }
343
    }
344
345
    /**
346
     * @param               $value
347
     * @param EnumType      $type
348
     * @param NodeInterface $blameNode
349
     * @param array|null    $path
350
     * @return CoercedValue
351
     * @throws InvariantException
352
     */
353
    protected function coerceValueForEnumType(
354
        $value,
355
        EnumType $type,
356
        NodeInterface $blameNode,
357
        ?Path $path
358
    ): CoercedValue {
359
        if (is_string($value)) {
360
            $enumValue = $type->getValue($value);
361
            if ($enumValue !== null) {
362
                return new CoercedValue($enumValue, null);
363
            }
364
        }
365
366
        $suggestions = suggestionList((string)$value, array_map(function (EnumValue $enumValue) {
367
            return $enumValue->getName();
368
        }, $type->getValues()));
369
370
        $didYouMean = (!empty($suggestions))
371
            ? 'did you mean' . implode(',', $suggestions)
372
            : null;
373
374
        return new CoercedValue(null, [
375
            $this->buildCoerceException(sprintf('Expected type %s', $type->getName()), $blameNode, $path, $didYouMean)
376
        ]);
377
    }
378
379
    /**
380
     * @param               $value
381
     * @param ListType      $type
382
     * @param NodeInterface $blameNode
383
     * @param array|null    $path
384
     * @return CoercedValue
385
     * @throws GraphQLException
386
     * @throws InvariantException
387
     */
388
    protected function coerceValueForInputObjectType(
389
        $value,
390
        InputObjectType $type,
391
        NodeInterface $blameNode,
392
        ?Path $path
393
    ): CoercedValue {
394
        $errors        = [];
395
        $coercedValues = [];
396
        $fields        = $type->getFields();
397
398
        // Ensure every defined field is valid.
399
        foreach ($fields as $field) {
400
            if (empty($value[$field->getName()])) {
401
                if (!empty($field->getDefaultValue())) {
402
                    $coercedValue[$field->getName()] = $field->getDefaultValue();
403
                } elseif ($field->getType() instanceof NonNullType) {
404
                    $errors[] = new GraphQLException(
405
                        sprintf(
406
                            "Field %s of required type %s! was not provided.",
407
                            $this->printPath(new Path($path, $field->getName())),
408
                            $field->getType()->getOfType()
0 ignored issues
show
Bug introduced by
$field->getType()->getOfType() of type Digia\GraphQL\Type\Definition\TypeInterface is incompatible with the type string expected by parameter $args of sprintf(). ( Ignorable by Annotation )

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

408
                            /** @scrutinizer ignore-type */ $field->getType()->getOfType()
Loading history...
409
                        )
410
                    );
411
                }
412
            } else {
413
                $fieldValue   = $value[$field->getName()];
414
                $coercedValue = $this->coerceValue(
415
                    $fieldValue,
416
                    $field->getType(),
417
                    $blameNode,
418
                    new Path($path, $field->getName())
419
                );
420
421
                if ($coercedValue->hasErrors()) {
422
                    $errors = array_merge($errors, $coercedValue->getErrors());
423
                } elseif (empty($errors)) {
424
                    $coercedValues[$field->getName()] = $coercedValue->getValue();
425
                }
426
            }
427
        }
428
429
        // Ensure every provided field is defined.
430
        foreach ($value as $fieldName => $fieldValue) {
431
            if (!isset($fields[$fieldName])) {
432
                $suggestions = suggestionList($fieldName, array_keys($fields));
433
                $didYouMean  = (!empty($suggestions))
434
                    ? 'did you mean' . implode(',', $suggestions)
435
                    : null;
436
437
                $errors[] = $this->buildCoerceException(
438
                    sprintf('Field "%s" is not defined by type %s', $fieldName, $type->getName()),
439
                    $blameNode,
440
                    $path,
441
                    $didYouMean
442
                );
443
            }
444
        }
445
446
        return new CoercedValue($coercedValues, $errors);
447
    }
448
449
    /**
450
     * @param $value
451
     * @param $type
452
     * @return CoercedValue
453
     * @throws GraphQLException
454
     * @throws InvariantException
455
     */
456
    protected function coerceValueForListType(
457
        $value,
458
        ListType $type,
459
        NodeInterface $blameNode,
460
        ?Path $path
461
    ): CoercedValue {
462
        $itemType = $type->getOfType();
463
464
        if (is_array($value) || $value instanceof \Traversable) {
465
            $errors        = [];
466
            $coercedValues = [];
467
            foreach ($value as $index => $itemValue) {
468
                $coercedValue = $this->coerceValue($itemValue, $itemType, $blameNode, new Path($path, $index));
469
470
                if ($coercedValue->hasErrors()) {
471
                    $errors = array_merge($errors, $coercedValue->getErrors());
472
                } else {
473
                    $coercedValues[] = $coercedValue->getValue();
474
                }
475
            }
476
477
            return new CoercedValue($coercedValues, $errors);
478
        }
479
480
        // Lists accept a non-list value as a list of one.
481
        $coercedValue = $this->coerceValue($value, $itemType, $blameNode);
482
483
        return new CoercedValue([$coercedValue->getValue()], $coercedValue->getErrors());
484
    }
485
486
    /**
487
     * @param string                $message
488
     * @param NodeInterface         $blameNode
489
     * @param array|null            $path
490
     * @param null|string           $subMessage
491
     * @param GraphQLException|null $origin $originalException
492
     * @return GraphQLException
493
     */
494
    protected function buildCoerceException(
495
        string $message,
496
        NodeInterface $blameNode,
497
        ?Path $path,
498
        ?string $subMessage = null,
499
        ?GraphQLException $originalException = null
500
    ) {
501
        $stringPath = $this->printPath($path);
502
503
        return new CoercingException(
504
            $message .
505
            (($stringPath !== '') ? ' at ' . $stringPath : $stringPath) .
506
            (($subMessage !== null) ? '; ' . $subMessage : '.'),
507
            [$blameNode],
508
            null,
509
            null,
510
            [],
511
            $originalException
512
        );
513
    }
514
515
    /**
516
     * @param array|null $path
517
     * @return string
518
     */
519
    protected function printPath(?Path $path)
520
    {
521
        $stringPath  = '';
522
        $currentPath = $path;
523
524
        while ($currentPath) {
525
            if (is_string($currentPath->getKey())) {
526
                $stringPath = '.' . $currentPath->getKey() . $stringPath;
527
            } else {
528
                $stringPath = '[' . (string)$currentPath->getKey() . ']' . $stringPath;
529
            }
530
531
            $currentPath = $currentPath->getPrevious();
532
        }
533
534
        return !empty($stringPath) ? 'value' . $stringPath : '';
535
    }
536
537
    /**
538
     * @param VariableNode  $variableNode
539
     * @param TypeInterface $argumentType
540
     * @param string        $argumentName
541
     * @param array         $variableValues
542
     * @param mixed         $defaultValue
543
     * @return mixed
544
     * @throws ExecutionException
545
     */
546
    protected function coerceValueForVariableNode(
547
        VariableNode $variableNode,
548
        TypeInterface $argumentType,
549
        string $argumentName,
550
        array $variableValues,
551
        $defaultValue
552
    ) {
553
        $variableName = $variableNode->getNameValue();
554
555
        if (!empty($variableValues) && isset($variableValues[$variableName])) {
556
            // Note: this does not check that this variable value is correct.
557
            // This assumes that this query has been validated and the variable
558
            // usage here is of the correct type.
559
            return $variableValues[$variableName];
560
        }
561
562
        if (null !== $defaultValue) {
563
            return $defaultValue;
564
        }
565
566
        if ($argumentType instanceof NonNullType) {
567
            throw new ExecutionException(
568
                \sprintf(
569
                    'Argument "%s" of required type "%s" was provided the variable "$%s" which was not provided a runtime value.',
570
                    $argumentName,
571
                    $argumentType,
572
                    $variableName
573
                ),
574
                [$variableNode]
575
            );
576
        }
577
    }
578
}
579