ValuesResolver::coerceArgumentValues()   D
last analyzed

Complexity

Conditions 21
Paths 110

Size

Total Lines 114
Code Lines 72

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 21
eloc 72
c 2
b 0
f 0
nc 110
nop 3
dl 0
loc 114
rs 4.0833

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\NullValueNode;
13
use Digia\GraphQL\Language\Node\VariableDefinitionNode;
14
use Digia\GraphQL\Language\Node\VariableNode;
15
use Digia\GraphQL\Schema\Schema;
16
use Digia\GraphQL\Type\Coercer\CoercingException;
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 Digia\GraphQL\Util\ConversionException;
28
use Digia\GraphQL\Util\TypeASTConverter;
29
use Digia\GraphQL\Util\ValueASTConverter;
30
use function Digia\GraphQL\Test\jsonEncode;
31
use function Digia\GraphQL\Util\find;
32
use function Digia\GraphQL\Util\invariant;
33
use function Digia\GraphQL\Util\keyMap;
34
use function Digia\GraphQL\Util\suggestionList;
35
36
class ValuesResolver
37
{
38
    /**
39
     * Prepares an object map of argument values given a list of argument
40
     * definitions and list of argument AST nodes.
41
     *
42
     * @see https://facebook.github.io/graphql/October2016/#CoerceArgumentValues()
43
     *
44
     * @param Field|Directive         $definition
45
     * @param ArgumentsAwareInterface $node
46
     * @param array                   $variableValues
47
     * @return array
48
     * @throws ExecutionException
49
     * @throws InvariantException
50
     */
51
    public static function coerceArgumentValues(
52
        $definition,
53
        ArgumentsAwareInterface $node,
54
        array $variableValues = []
55
    ): array {
56
        $coercedValues       = [];
57
        $argumentDefinitions = $definition->getArguments();
58
        $argumentNodes       = $node->getArguments();
59
60
        if (empty($argumentDefinitions)) {
61
            return $coercedValues;
62
        }
63
64
        /** @var ArgumentNode[] $argumentNodeMap */
65
        $argumentNodeMap = keyMap($argumentNodes, function (ArgumentNode $value) {
66
            return $value->getNameValue();
67
        });
68
69
        foreach ($argumentDefinitions as $argumentDefinition) {
70
            $argumentName  = $argumentDefinition->getName();
71
            $argumentType  = $argumentDefinition->getType();
72
            $argumentNode  = $argumentNodeMap[$argumentName] ?? null;
73
            $defaultValue  = $argumentDefinition->getDefaultValue();
74
            $argumentValue = null !== $argumentNode ? $argumentNode->getValue() : null;
75
76
            if (null !== $argumentNode && $argumentValue instanceof VariableNode) {
77
                $variableName = $argumentValue->getNameValue();
78
                $hasValue     = !empty($variableValues) && \array_key_exists($variableName, $variableValues);
79
                $isNull       = $hasValue && null === $variableValues[$variableName];
80
            } else {
81
                $hasValue = null !== $argumentNode;
82
                $isNull   = $hasValue && $argumentValue instanceof NullValueNode;
83
            }
84
85
            if (!$hasValue && null !== $defaultValue) {
86
                // If no argument was provided where the definition has a default value,
87
                // use the default value.
88
                $coercedValues[$argumentName] = $defaultValue;
89
            } elseif ((!$hasValue || $isNull) && $argumentType instanceof NonNullType) {
90
                // If no argument or a null value was provided to an argument with a
91
                // non-null type (required), produce a field error.
92
                if ($isNull) {
93
                    throw new ExecutionException(
94
                        \sprintf(
95
                            'Argument "%s" of non-null type "%s" must not be null.',
96
                            $argumentName,
97
                            $argumentType
98
                        ),
99
                        [$argumentValue]
100
                    );
101
                } elseif (null !== $argumentNode && $argumentValue instanceof VariableNode) {
102
                    $variableName = $argumentValue->getNameValue();
103
                    throw new ExecutionException(
104
                        \sprintf(
105
                            'Argument "%s" of required type "%s" was provided the variable "$%s" '
106
                            . 'which was not provided a runtime value.',
107
                            $argumentName,
108
                            $argumentType,
109
                            $variableName
110
                        ),
111
                        [$argumentValue]
112
                    );
113
                } else {
114
                    throw new ExecutionException(
115
                        \sprintf(
116
                            'Argument "%s" of required type "%s" was not provided.',
117
                            $argumentName,
118
                            $argumentType
119
                        ),
120
                        [$node]
121
                    );
122
                }
123
            } elseif ($hasValue) {
124
                if ($argumentValue instanceof NullValueNode) {
125
                    // If the explicit value `null` was provided, an entry in the coerced
126
                    // values must exist as the value `null`.
127
                    $coercedValues[$argumentName] = null;
128
                } elseif ($argumentValue instanceof VariableNode) {
129
                    $variableName = $argumentValue->getNameValue();
130
                    invariant(!empty($variableValues), 'Must exist for hasValue to be true.');
131
                    // Note: This does no further checking that this variable is correct.
132
                    // This assumes that this query has been validated and the variable
133
                    // usage here is of the correct type.
134
                    $coercedValues[$argumentName] = $variableValues[$variableName];
135
                } else {
136
                    $valueNode = $argumentNode->getValue();
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on null. ( Ignorable by Annotation )

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

136
                    /** @scrutinizer ignore-call */ 
137
                    $valueNode = $argumentNode->getValue();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
137
                    try {
138
                        // Value nodes that cannot be resolved should be treated as invalid values
139
                        // because there is no undefined value in PHP so that we throw an exception
140
                        $coercedValue = ValueASTConverter::convert($valueNode, $argumentType, $variableValues);
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

140
                        $coercedValue = ValueASTConverter::convert($valueNode, /** @scrutinizer ignore-type */ $argumentType, $variableValues);
Loading history...
Bug introduced by
It seems like $valueNode can also be of type null; however, parameter $node of Digia\GraphQL\Util\ValueASTConverter::convert() does only seem to accept Digia\GraphQL\Language\Node\NodeInterface, 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

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