Passed
Pull Request — master (#321)
by Christoffer
04:21 queued 01:06
created

ValuesResolver::coerceVariableValues()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 68
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 47
nc 9
nop 3
dl 0
loc 68
rs 7.6008
c 0
b 0
f 0

How to fix   Long Method   

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\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
/**
34
 * TODO: Make this class static
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 InvalidTypeException
50
     * @throws InvariantException
51
     */
52
    public function coerceArgumentValues($definition, ArgumentsAwareInterface $node, array $variableValues = []): array
53
    {
54
        $coercedValues       = [];
55
        $argumentDefinitions = $definition->getArguments();
56
        $argumentNodes       = $node->getArguments();
57
58
        if (empty($argumentDefinitions)) {
59
            return $coercedValues;
60
        }
61
62
        /** @var ArgumentNode[] $argumentNodeMap */
63
        $argumentNodeMap = keyMap($argumentNodes, function (ArgumentNode $value) {
64
            return $value->getNameValue();
65
        });
66
67
        foreach ($argumentDefinitions as $argumentDefinition) {
68
            $argumentName  = $argumentDefinition->getName();
69
            $argumentType  = $argumentDefinition->getType();
70
            $argumentNode  = $argumentNodeMap[$argumentName] ?? null;
71
            $defaultValue  = $argumentDefinition->getDefaultValue();
72
            $argumentValue = null !== $argumentNode ? $argumentNode->getValue() : null;
73
74
            if (null === $argumentNode) {
75
                if (null !== $defaultValue) {
76
                    $coercedValues[$argumentName] = $defaultValue;
77
                } elseif ($argumentType instanceof NonNullType) {
78
                    throw new ExecutionException(
79
                        sprintf(
80
                            'Argument "%s" of required type "%s" was not provided.',
81
                            $argumentName,
82
                            $argumentType
83
                        ),
84
                        [$node]
85
                    );
86
                }
87
            } elseif ($argumentValue instanceof VariableNode) {
88
                $coercedValues[$argumentName] = $this->coerceValueForVariableNode(
89
                    $argumentValue,
90
                    $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

90
                    /** @scrutinizer ignore-type */ $argumentType,
Loading history...
91
                    $argumentName,
92
                    $variableValues,
93
                    $defaultValue
94
                );
95
            } else {
96
                try {
97
                    $coercedValues[$argumentName] = ValueASTConverter::convert(
98
                        $argumentNode->getValue(),
99
                        $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

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