Completed
Pull Request — master (#349)
by Christoffer
02:36
created

ValuesResolver   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 570
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 238
c 3
b 0
f 0
dl 0
loc 570
rs 2.4
wmc 75

13 Methods

Rating   Name   Duplication   Size   Complexity  
A printPath() 0 14 4
A isInputType() 0 6 5
A coerceDirectiveValues() 0 15 3
A coerceValueForVariableNode() 0 29 5
A coerceValueForEnumType() 0 20 4
A coerceValueForNonNullType() 0 16 2
A coerceValueForListType() 0 28 5
A buildCoerceException() 0 20 3
C coerceVariableValues() 0 84 15
A coerceValueForScalarType() 0 22 3
B coerceValueForInputObjectType() 0 61 10
B coerceArgumentValues() 0 71 9
B coerceValue() 0 27 7

How to fix   Complexity   

Complex Class

Complex classes like ValuesResolver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ValuesResolver, and based on these observations, apply Extract Interface, too.

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\Test\jsonEncode;
30
use function Digia\GraphQL\Util\find;
31
use function Digia\GraphQL\Util\keyMap;
32
use function Digia\GraphQL\Util\suggestionList;
33
use function Digia\GraphQL\Util\toString;
34
35
/**
36
 * TODO: Make this class static
37
 */
38
class ValuesResolver
39
{
40
    /**
41
     * Prepares an object map of argument values given a list of argument
42
     * definitions and list of argument AST nodes.
43
     *
44
     * @see https://facebook.github.io/graphql/October2016/#CoerceArgumentValues()
45
     *
46
     * @param Field|Directive         $definition
47
     * @param ArgumentsAwareInterface $node
48
     * @param array                   $variableValues
49
     * @return array
50
     * @throws ExecutionException
51
     * @throws InvalidTypeException
52
     * @throws InvariantException
53
     */
54
    public function coerceArgumentValues($definition, ArgumentsAwareInterface $node, array $variableValues = []): array
55
    {
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) {
77
                if (null !== $defaultValue) {
78
                    $coercedValues[$argumentName] = $defaultValue;
79
                } elseif ($argumentType instanceof NonNullType) {
80
                    throw new ExecutionException(
81
                        \sprintf(
82
                            'Argument "%s" of required type "%s" was not provided.',
83
                            $argumentName,
84
                            $argumentType
85
                        ),
86
                        [$node]
87
                    );
88
                }
89
            } elseif ($argumentValue instanceof VariableNode) {
90
                $coercedValues[$argumentName] = $this->coerceValueForVariableNode(
91
                    $argumentValue,
92
                    $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

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

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