Passed
Pull Request — master (#321)
by Christoffer
04:53
created

ValuesResolver::coerceValue()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 13
nc 7
nop 4
dl 0
loc 27
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\GraphQLException;
6
use Digia\GraphQL\Error\InvalidTypeException;
7
use Digia\GraphQL\Error\InvariantException;
8
use Digia\GraphQL\Language\Node\ArgumentNode;
9
use Digia\GraphQL\Language\Node\ArgumentsAwareInterface;
10
use Digia\GraphQL\Language\Node\NameAwareInterface;
11
use Digia\GraphQL\Language\Node\NodeInterface;
12
use Digia\GraphQL\Language\Node\VariableDefinitionNode;
13
use Digia\GraphQL\Language\Node\VariableNode;
14
use Digia\GraphQL\Schema\Schema;
15
use Digia\GraphQL\Type\Coercer\CoercingException;
16
use Digia\GraphQL\Type\Definition\Directive;
17
use Digia\GraphQL\Type\Definition\EnumType;
18
use Digia\GraphQL\Type\Definition\EnumValue;
19
use Digia\GraphQL\Type\Definition\Field;
20
use Digia\GraphQL\Type\Definition\InputObjectType;
21
use Digia\GraphQL\Type\Definition\ListType;
22
use Digia\GraphQL\Type\Definition\NonNullType;
23
use Digia\GraphQL\Type\Definition\ScalarType;
24
use Digia\GraphQL\Type\Definition\TypeInterface;
25
use Digia\GraphQL\Type\Definition\WrappingTypeInterface;
26
use Digia\GraphQL\Util\ConversionException;
27
use Digia\GraphQL\Util\TypeASTConverter;
28
use Digia\GraphQL\Util\ValueASTConverter;
29
use function Digia\GraphQL\Util\find;
30
use function Digia\GraphQL\Util\keyMap;
31
use function Digia\GraphQL\Util\suggestionList;
32
33
class ValuesResolver
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 https://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)) {
56
            return $coercedValues;
57
        }
58
59
        /** @var ArgumentNode[] $argumentNodeMap */
60
        $argumentNodeMap = keyMap($argumentNodes, function (ArgumentNode $value) {
61
            return $value->getNameValue();
62
        });
63
64
        foreach ($argumentDefinitions as $argumentDefinition) {
65
            $argumentName  = $argumentDefinition->getName();
66
            $argumentType  = $argumentDefinition->getType();
67
            $argumentNode  = $argumentNodeMap[$argumentName] ?? null;
68
            $defaultValue  = $argumentDefinition->getDefaultValue();
69
            $argumentValue = null !== $argumentNode ? $argumentNode->getValue() : null;
70
71
            if (null === $argumentNode) {
72
                if (null !== $defaultValue) {
73
                    $coercedValues[$argumentName] = $defaultValue;
74
                } elseif ($argumentType instanceof NonNullType) {
75
                    throw new ExecutionException(
76
                        sprintf(
77
                            'Argument "%s" of required type "%s" was not provided.',
78
                            $argumentName,
79
                            $argumentType
80
                        ),
81
                        [$node]
82
                    );
83
                }
84
            } elseif ($argumentValue instanceof VariableNode) {
85
                $coercedValues[$argumentName] = $this->coerceValueForVariableNode(
86
                    $argumentValue,
87
                    $argumentType,
0 ignored issues
show
Bug introduced by
It seems like $argumentType can also be of type null; however, parameter $argumentType of Digia\GraphQL\Execution\...eValueForVariableNode() does only seem to accept Digia\GraphQL\Type\Definition\TypeInterface, 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

87
                    /** @scrutinizer ignore-type */ $argumentType,
Loading history...
88
                    $argumentName,
89
                    $variableValues,
90
                    $defaultValue
91
                );
92
            } else {
93
                try {
94
                    $coercedValues[$argumentName] = ValueASTConverter::convert(
95
                        $argumentNode->getValue(),
96
                        $argumentType,
0 ignored issues
show
Bug introduced by
It seems like $argumentType can also be of type null; however, parameter $type of Digia\GraphQL\Util\ValueASTConverter::convert() does only seem to accept Digia\GraphQL\Type\Definition\TypeInterface, 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

96
                        /** @scrutinizer ignore-type */ $argumentType,
Loading history...
97
                        $variableValues
98
                    );
99
                } catch (\Exception $ex) {
100
                    // Value nodes that cannot be resolved should be treated as invalid values
101
                    // because there is no undefined value in PHP so that we throw an exception
102
                    throw new ExecutionException(
103
                        sprintf(
104
                            'Argument "%s" has invalid value %s.',
105
                            $argumentName,
106
                            (string)$argumentNode->getValue()
107
                        ),
108
                        [$argumentNode->getValue()],
109
                        null,
110
                        null,
111
                        null,
112
                        null,
113
                        $ex
114
                    );
115
                }
116
            }
117
        }
118
119
        return $coercedValues;
120
    }
121
122
    /**
123
     * Prepares an object map of argument values given a directive definition
124
     * and a AST node which may contain directives. Optionally also accepts a map
125
     * of variable values.
126
     *
127
     * If the directive does not exist on the node, returns null.
128
     *
129
     * @param Directive $directive
130
     * @param mixed     $node
131
     * @param array     $variableValues
132
     * @return array|null
133
     * @throws ExecutionException
134
     * @throws InvalidTypeException
135
     * @throws InvariantException
136
     */
137
    public function coerceDirectiveValues(
138
        Directive $directive,
139
        $node,
140
        array $variableValues = []
141
    ): ?array {
142
        $directiveNode = $node->hasDirectives()
143
            ? find($node->getDirectives(), function (NameAwareInterface $value) use ($directive) {
144
                return $value->getNameValue() === $directive->getName();
145
            }) : null;
146
147
        if (null !== $directiveNode) {
148
            return $this->coerceArgumentValues($directive, $directiveNode, $variableValues);
149
        }
150
151
        return null;
152
    }
153
154
    /**
155
     * Prepares an object map of variableValues of the correct type based on the
156
     * provided variable definitions and arbitrary input. If the input cannot be
157
     * parsed to match the variable definitions, a GraphQLError will be thrown.
158
     *
159
     * @param Schema                         $schema
160
     * @param array|VariableDefinitionNode[] $variableDefinitionNodes
161
     * @param array                          $inputs
162
     * @return CoercedValue
163
     * @throws GraphQLException
164
     * @throws InvariantException
165
     * @throws ConversionException
166
     */
167
    public function coerceVariableValues(Schema $schema, array $variableDefinitionNodes, array $inputs): CoercedValue
168
    {
169
        $coercedValues = [];
170
        $errors        = [];
171
172
        foreach ($variableDefinitionNodes as $variableDefinitionNode) {
173
            $variableName = $variableDefinitionNode->getVariable()->getNameValue();
174
            $variableType = TypeASTConverter::convert($schema, $variableDefinitionNode->getType());
175
176
            if (!$this->isInputType($variableType)) {
177
                $variableTypeName = (string)$variableType;
178
179
                if ($variableTypeName === '') {
180
                    $variableTypeName = (string)$variableDefinitionNode;
181
                }
182
183
                $errors[] = $this->buildCoerceException(
184
                    sprintf(
185
                        'Variable "$%s" expected value of type "%s" which cannot be used as an input type',
186
                        $variableName,
187
                        $variableTypeName
188
                    ),
189
                    $variableDefinitionNode,
190
                    null
191
                );
192
            } else {
193
                if (!array_key_exists($variableName, $inputs)) {
194
                    if ($variableType instanceof NonNullType) {
195
                        $errors[] = $this->buildCoerceException(
196
                            sprintf(
197
                                'Variable "$%s" of required type "%s" was not provided',
198
                                $variableName,
199
                                (string)$variableType
200
                            ),
201
                            $variableDefinitionNode,
202
                            null
203
                        );
204
                    } elseif ($variableDefinitionNode->getDefaultValue() !== null) {
205
                        $coercedValues[$variableName] = ValueASTConverter::convert(
206
                            $variableDefinitionNode->getDefaultValue(),
207
                            $variableType
208
                        );
209
                    }
210
                } else {
211
                    $value        = $inputs[$variableName];
212
                    $coercedValue = $this->coerceValue($value, $variableType, $variableDefinitionNode);
213
                    if ($coercedValue->hasErrors()) {
214
                        $messagePrelude = sprintf(
215
                            'Variable "$%s" got invalid value %s',
216
                            $variableName, json_encode($value)
217
                        );
218
                        foreach ($coercedValue->getErrors() as $error) {
219
                            $errors[] = $this->buildCoerceException(
220
                                $messagePrelude,
221
                                $variableDefinitionNode,
222
                                null,
223
                                $error->getMessage(),
224
                                $error
225
                            );
226
                        }
227
                    } else {
228
                        $coercedValues[$variableName] = $coercedValue->getValue();
229
                    }
230
                }
231
            }
232
        }
233
234
        return new CoercedValue($coercedValues, $errors);
235
    }
236
237
238
    /**
239
     * @param TypeInterface|null $type
240
     * @return bool
241
     */
242
    protected function isInputType(?TypeInterface $type)
243
    {
244
        return ($type instanceof ScalarType) ||
245
            ($type instanceof EnumType) ||
246
            ($type instanceof InputObjectType) ||
247
            (($type instanceof WrappingTypeInterface) && $this->isInputType($type->getOfType()));
248
    }
249
250
    /**
251
     * Returns either a value which is valid for the provided type or a list of
252
     * encountered coercion errors.
253
     *
254
     * @param mixed|array   $value
255
     * @param mixed         $type
256
     * @param NodeInterface $blameNode
257
     * @param Path|null     $path
258
     * @return CoercedValue
259
     * @throws GraphQLException
260
     * @throws InvariantException
261
     */
262
    private function coerceValue($value, $type, $blameNode, ?Path $path = null): CoercedValue
263
    {
264
        if ($type instanceof NonNullType) {
265
            return $this->coerceValueForNonNullType($value, $type, $blameNode, $path);
266
        }
267
268
        if (null === $value) {
269
            return new CoercedValue(null);
270
        }
271
272
        if ($type instanceof ScalarType) {
273
            return $this->coerceValueForScalarType($value, $type, $blameNode, $path);
274
        }
275
276
        if ($type instanceof EnumType) {
277
            return $this->coerceValueForEnumType($value, $type, $blameNode, $path);
278
        }
279
280
        if ($type instanceof ListType) {
281
            return $this->coerceValueForListType($value, $type, $blameNode, $path);
282
        }
283
284
        if ($type instanceof InputObjectType) {
285
            return $this->coerceValueForInputObjectType($value, $type, $blameNode, $path);
286
        }
287
288
        throw new GraphQLException('Unexpected type.');
289
    }
290
291
    /**
292
     * @param mixed         $value
293
     * @param NonNullType   $type
294
     * @param NodeInterface $blameNode
295
     * @param Path|null     $path
296
     * @return CoercedValue
297
     * @throws GraphQLException
298
     * @throws InvariantException
299
     */
300
    protected function coerceValueForNonNullType(
301
        $value,
302
        NonNullType $type,
303
        NodeInterface $blameNode,
304
        ?Path $path
305
    ): CoercedValue {
306
        if (null === $value) {
307
            return new CoercedValue(null, [
308
                $this->buildCoerceException(
309
                    sprintf('Expected non-nullable type %s not to be null', (string)$type),
310
                    $blameNode,
311
                    $path
312
                )
313
            ]);
314
        }
315
        return $this->coerceValue($value, $type->getOfType(), $blameNode, $path);
316
    }
317
318
    /**
319
     * Scalars determine if a value is valid via parseValue(), which can
320
     * throw to indicate failure. If it throws, maintain a reference to
321
     * the original error.
322
     *
323
     * @param mixed         $value
324
     * @param ScalarType    $type
325
     * @param NodeInterface $blameNode
326
     * @param Path|null     $path
327
     * @return CoercedValue
328
     */
329
    protected function coerceValueForScalarType(
330
        $value,
331
        ScalarType $type,
332
        NodeInterface $blameNode,
333
        ?Path $path
334
    ): CoercedValue {
335
        try {
336
            $parseResult = $type->parseValue($value);
337
            if (null === $parseResult) {
338
                return new CoercedValue(null, [
339
                    new GraphQLException(sprintf('Expected type %s', (string)$type))
340
                ]);
341
            }
342
            return new CoercedValue($parseResult);
343
        } catch (InvalidTypeException|CoercingException $ex) {
344
            return new CoercedValue(null, [
345
                $this->buildCoerceException(
346
                    sprintf('Expected type %s', (string)$type),
347
                    $blameNode,
348
                    $path,
349
                    $ex->getMessage(),
350
                    $ex
351
                )
352
            ]);
353
        }
354
    }
355
356
    /**
357
     * @param mixed         $value
358
     * @param EnumType      $type
359
     * @param NodeInterface $blameNode
360
     * @param Path|null     $path
361
     * @return CoercedValue
362
     * @throws InvariantException
363
     */
364
    protected function coerceValueForEnumType(
365
        $value,
366
        EnumType $type,
367
        NodeInterface $blameNode,
368
        ?Path $path
369
    ): CoercedValue {
370
        if (\is_string($value) && null !== ($enumValue = $type->getValue($value))) {
371
            return new CoercedValue($enumValue);
372
        }
373
374
        $suggestions = suggestionList((string)$value, \array_map(function (EnumValue $enumValue) {
375
            return $enumValue->getName();
376
        }, $type->getValues()));
377
378
        $didYouMean = (!empty($suggestions))
379
            ? 'did you mean' . \implode(',', $suggestions)
380
            : null;
381
382
        return new CoercedValue(null, [
383
            $this->buildCoerceException(\sprintf('Expected type %s', $type->getName()), $blameNode, $path, $didYouMean)
384
        ]);
385
    }
386
387
    /**
388
     * @param mixed           $value
389
     * @param InputObjectType $type
390
     * @param NodeInterface   $blameNode
391
     * @param Path|null       $path
392
     * @return CoercedValue
393
     * @throws GraphQLException
394
     * @throws InvariantException
395
     */
396
    protected function coerceValueForInputObjectType(
397
        $value,
398
        InputObjectType $type,
399
        NodeInterface $blameNode,
400
        ?Path $path
401
    ): CoercedValue {
402
        $errors        = [];
403
        $coercedValues = [];
404
        $fields        = $type->getFields();
405
406
        // Ensure every defined field is valid.
407
        foreach ($fields as $field) {
408
            $fieldType = $field->getType();
409
410
            if (!isset($value[$field->getName()])) {
411
                if (!empty($field->getDefaultValue())) {
412
                    $coercedValue[$field->getName()] = $field->getDefaultValue();
413
                } elseif ($fieldType instanceof NonNullType) {
414
                    $errors[] = new GraphQLException(
415
                        \sprintf(
416
                            "Field %s of required type %s! was not provided.",
417
                            $this->printPath(new Path($path, $field->getName())),
418
                            (string)$fieldType->getOfType()
419
                        )
420
                    );
421
                }
422
            } else {
423
                $fieldValue   = $value[$field->getName()];
424
                $coercedValue = $this->coerceValue(
425
                    $fieldValue,
426
                    $fieldType,
427
                    $blameNode,
428
                    new Path($path, $field->getName())
429
                );
430
431
                if ($coercedValue->hasErrors()) {
432
                    $errors = \array_merge($errors, $coercedValue->getErrors());
433
                } elseif (empty($errors)) {
434
                    $coercedValues[$field->getName()] = $coercedValue->getValue();
435
                }
436
            }
437
        }
438
439
        // Ensure every provided field is defined.
440
        foreach ($value as $fieldName => $fieldValue) {
441
            if (!isset($fields[$fieldName])) {
442
                $suggestions = suggestionList($fieldName, \array_keys($fields));
443
                $didYouMean  = (!empty($suggestions))
444
                    ? 'did you mean' . \implode(',', $suggestions)
445
                    : null;
446
447
                $errors[] = $this->buildCoerceException(
448
                    \sprintf('Field "%s" is not defined by type %s', $fieldName, $type->getName()),
449
                    $blameNode,
450
                    $path,
451
                    $didYouMean
452
                );
453
            }
454
        }
455
456
        return new CoercedValue($coercedValues, $errors);
457
    }
458
459
    /**
460
     * @param mixed         $value
461
     * @param ListType      $type
462
     * @param NodeInterface $blameNode
463
     * @param Path|null     $path
464
     * @return CoercedValue
465
     * @throws GraphQLException
466
     * @throws InvariantException
467
     */
468
    protected function coerceValueForListType(
469
        $value,
470
        ListType $type,
471
        NodeInterface $blameNode,
472
        ?Path $path
473
    ): CoercedValue {
474
        $itemType = $type->getOfType();
475
476
        if (\is_array($value) || $value instanceof \Traversable) {
477
            $errors        = [];
478
            $coercedValues = [];
479
            foreach ($value as $index => $itemValue) {
480
                $coercedValue = $this->coerceValue($itemValue, $itemType, $blameNode, new Path($path, $index));
481
482
                if ($coercedValue->hasErrors()) {
483
                    $errors = \array_merge($errors, $coercedValue->getErrors());
484
                } else {
485
                    $coercedValues[] = $coercedValue->getValue();
486
                }
487
            }
488
489
            return new CoercedValue($coercedValues, $errors);
490
        }
491
492
        // Lists accept a non-list value as a list of one.
493
        $coercedValue = $this->coerceValue($value, $itemType, $blameNode);
494
495
        return new CoercedValue([$coercedValue->getValue()], $coercedValue->getErrors());
496
    }
497
498
    /**
499
     * @param string                $message
500
     * @param NodeInterface         $blameNode
501
     * @param Path|null             $path
502
     * @param null|string           $subMessage
503
     * @param GraphQLException|null $originalException
504
     * @return GraphQLException
505
     */
506
    protected function buildCoerceException(
507
        string $message,
508
        NodeInterface $blameNode,
509
        ?Path $path,
510
        ?string $subMessage = null,
511
        ?GraphQLException $originalException = null
512
    ) {
513
        $stringPath = $this->printPath($path);
514
515
        return new CoercingException(
516
            $message .
517
            (($stringPath !== '') ? ' at ' . $stringPath : $stringPath) .
518
            (($subMessage !== null) ? '; ' . $subMessage : '.'),
519
            [$blameNode],
520
            null,
521
            null,
522
            // TODO: Change this to null
523
            [],
524
            null,
525
            $originalException
526
        );
527
    }
528
529
    /**
530
     * @param Path|null $path
531
     * @return string
532
     */
533
    protected function printPath(?Path $path)
534
    {
535
        $stringPath  = '';
536
        $currentPath = $path;
537
538
        while ($currentPath !== null) {
539
            $stringPath = \is_string($currentPath->getKey())
540
                ? '.' . $currentPath->getKey() . $stringPath
541
                : '[' . (string)$currentPath->getKey() . ']' . $stringPath;
542
543
            $currentPath = $currentPath->getPrevious();
544
        }
545
546
        return !empty($stringPath) ? 'value' . $stringPath : '';
547
    }
548
549
    /**
550
     * @param VariableNode  $variableNode
551
     * @param TypeInterface $argumentType
552
     * @param string        $argumentName
553
     * @param array         $variableValues
554
     * @param mixed         $defaultValue
555
     * @return mixed
556
     * @throws ExecutionException
557
     */
558
    protected function coerceValueForVariableNode(
559
        VariableNode $variableNode,
560
        TypeInterface $argumentType,
561
        string $argumentName,
562
        array $variableValues,
563
        $defaultValue
564
    ) {
565
        $variableName = $variableNode->getNameValue();
566
567
        if (!empty($variableValues) && isset($variableValues[$variableName])) {
568
            // Note: this does not check that this variable value is correct.
569
            // This assumes that this query has been validated and the variable
570
            // usage here is of the correct type.
571
            return $variableValues[$variableName];
572
        }
573
574
        if (null !== $defaultValue) {
575
            return $defaultValue;
576
        }
577
578
        if ($argumentType instanceof NonNullType) {
579
            throw new ExecutionException(
580
                \sprintf(
581
                    'Argument "%s" of required type "%s" was provided the variable "$%s" which was not provided a runtime value.',
582
                    $argumentName,
583
                    $argumentType,
584
                    $variableName
585
                ),
586
                [$variableNode]
587
            );
588
        }
589
    }
590
}
591