Passed
Pull Request — master (#169)
by Quang
02:45
created

ValuesHelper   D

Complexity

Total Complexity 61

Size/Duplication

Total Lines 499
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 61
dl 0
loc 499
rs 4.054
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A coerceDirectiveValues() 0 15 3
C coerceArgumentValues() 0 63 10
C coerceValue() 0 27 7
A coerceValueForNonNullType() 0 16 2
B coerceValueForListType() 0 28 5
A buildCoerceException() 0 18 3
B coerceValueForEnumType() 0 23 4
B coerceValueForVariableNode() 0 29 5
C coerceVariableValues() 0 59 9
A coerceValueForScalarType() 0 22 3
C coerceValueForInputObjectType() 0 59 10

How to fix   Complexity   

Complex Class

Complex classes like ValuesHelper 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 ValuesHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Digia\GraphQL\Execution;
4
5
use Digia\GraphQL\Error\CoercingException;
6
use Digia\GraphQL\Error\ExecutionException;
7
use Digia\GraphQL\Error\GraphQLException;
8
use Digia\GraphQL\Error\InvalidTypeException;
9
use Digia\GraphQL\Error\InvariantException;
10
use Digia\GraphQL\Language\Node\ArgumentNode;
11
use Digia\GraphQL\Language\Node\ArgumentsAwareInterface;
12
use Digia\GraphQL\Language\Node\NameAwareInterface;
13
use Digia\GraphQL\Language\Node\NodeInterface;
14
use Digia\GraphQL\Language\Node\VariableDefinitionNode;
15
use Digia\GraphQL\Language\Node\VariableNode;
16
use Digia\GraphQL\Schema\Schema;
17
use Digia\GraphQL\Type\Definition\Directive;
18
use Digia\GraphQL\Type\Definition\EnumType;
19
use Digia\GraphQL\Type\Definition\EnumValue;
20
use Digia\GraphQL\Type\Definition\Field;
21
use Digia\GraphQL\Type\Definition\InputObjectType;
22
use Digia\GraphQL\Type\Definition\InputTypeInterface;
23
use Digia\GraphQL\Type\Definition\ListType;
24
use Digia\GraphQL\Type\Definition\NonNullType;
25
use Digia\GraphQL\Type\Definition\ScalarType;
26
use Digia\GraphQL\Type\Definition\TypeInterface;
27
use Digia\GraphQL\Type\Definition\WrappingTypeInterface;
28
use function Digia\GraphQL\Util\find;
29
use function Digia\GraphQL\Util\keyMap;
30
use function Digia\GraphQL\Util\suggestionList;
31
use function Digia\GraphQL\Util\typeFromAST;
32
use function Digia\GraphQL\Util\valueFromAST;
33
34
class ValuesHelper
35
{
36
    /**
37
     * Prepares an object map of argument values given a list of argument
38
     * definitions and list of argument AST nodes.
39
     *
40
     * @see http://facebook.github.io/graphql/October2016/#CoerceArgumentValues()
41
     *
42
     * @param Field|Directive         $definition
43
     * @param ArgumentsAwareInterface $node
44
     * @param array                   $variableValues
45
     * @return array
46
     * @throws ExecutionException
47
     * @throws InvalidTypeException
48
     * @throws InvariantException
49
     */
50
    public function coerceArgumentValues($definition, ArgumentsAwareInterface $node, array $variableValues = []): array
51
    {
52
        $coercedValues       = [];
53
        $argumentDefinitions = $definition->getArguments();
54
        $argumentNodes       = $node->getArguments();
55
56
        if (empty($argumentDefinitions) || empty($argumentNodes)) {
57
            return $coercedValues;
58
        }
59
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
            /** @var ArgumentNode $argumentNode */
68
            $argumentNode = $argumentNodeMap[$argumentName];
69
            $defaultValue = $argumentDefinition->getDefaultValue();
70
71
            if (null === $argumentNode) {
72
                if (null === $defaultValue) {
73
                    $coercedValues[$argumentName] = $defaultValue;
74
                } elseif (!$argumentType instanceof NonNullType) {
75
                    throw new ExecutionException(
76
                        sprintf('Argument "%s" of required type "%s" was not provided.', $argumentName, $argumentType),
77
                        [$node]
78
                    );
79
                }
80
            } elseif ($argumentNode instanceof VariableNode) {
81
                $coercedValues[$argumentName] = $this->coerceValueForVariableNode(
82
                    $argumentNode,
83
                    $argumentType,
84
                    $argumentName,
85
                    $variableValues,
86
                    $defaultValue
87
                );
88
            } else {
89
                $coercedValue = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $coercedValue is dead and can be removed.
Loading history...
90
91
                try {
92
                    $coercedValue = valueFromAST($argumentNode->getValue(), $argumentType, $variableValues);
93
                } catch (CoercingException $ex) {
94
                    // Value nodes that cannot be resolved should be treated as invalid values
95
                    // therefore we catch the exception and leave the `$coercedValue` as `null`.
96
                }
97
98
                if (null === $coercedValue) {
99
                    // Note: ValuesOfCorrectType validation should catch this before
100
                    // execution. This is a runtime check to ensure execution does not
101
                    // continue with an invalid argument value.
102
                    throw new ExecutionException(
103
                        sprintf('Argument "%s" has invalid value %s.', $argumentName, $argumentNode),
104
                        [$argumentNode->getValue()]
105
                    );
106
                }
107
108
                $coercedValues[$argumentName] = $coercedValue;
109
            }
110
        }
111
112
        return $coercedValues;
113
    }
114
115
    /**
116
     * Prepares an object map of argument values given a directive definition
117
     * and a AST node which may contain directives. Optionally also accepts a map
118
     * of variable values.
119
     *
120
     * If the directive does not exist on the node, returns null.
121
     *
122
     * @param Directive $directive
123
     * @param mixed     $node
124
     * @param array     $variableValues
125
     * @return array|null
126
     * @throws ExecutionException
127
     * @throws InvalidTypeException
128
     * @throws InvariantException
129
     */
130
    public function coerceDirectiveValues(
131
        Directive $directive,
132
        $node,
133
        array $variableValues = []
134
    ): ?array {
135
        $directiveNode = $node->hasDirectives()
136
            ? find($node->getDirectives(), function (NameAwareInterface $value) use ($directive) {
137
                return $value->getNameValue() === $directive->getName();
138
            }) : null;
139
140
        if (null !== $directiveNode) {
141
            return $this->coerceArgumentValues($directive, $directiveNode, $variableValues);
142
        }
143
144
        return null;
145
    }
146
147
    /**
148
     * Prepares an object map of variableValues of the correct type based on the
149
     * provided variable definitions and arbitrary input. If the input cannot be
150
     * parsed to match the variable definitions, a GraphQLError will be thrown.
151
     *
152
     * @param Schema                         $schema
153
     * @param array|VariableDefinitionNode[] $variableDefinitionNodes
154
     * @param                                $input
155
     * @return CoercedValue
156
     * @throws \Exception
157
     */
158
    public function coerceVariableValues(Schema $schema, array $variableDefinitionNodes, array $inputs): CoercedValue
159
    {
160
        $coercedValues = [];
161
        $errors        = [];
162
163
        foreach ($variableDefinitionNodes as $variableDefinitionNode) {
164
            $variableName = $variableDefinitionNode->getVariable()->getNameValue();
165
            $variableType = typeFromAST($schema, $variableDefinitionNode->getType());
166
167
            $type = $variableType;
168
            if ($variableType instanceof WrappingTypeInterface) {
169
                $type = $variableType->getOfType();
170
            }
171
172
            if (!$type instanceof InputTypeInterface) {
173
                $errors[] = new GraphQLException(
174
                    sprintf(
175
                        'Variable "%s" expected value of type %s which cannot be used as an input type.',
176
                        $variableName,
177
                        $variableDefinitionNode->getType()->getName()
178
                    ),
179
                    [$variableDefinitionNode->getType()]
180
                );
181
            } else {
182
                if (!isset($inputs[$variableName])) {
183
                    if ($variableType instanceof NonNullType) {
184
                        throw new GraphQLException('NonNullType');
185
                    } elseif ($variableDefinitionNode->getDefaultValue() !== null) {
186
                        $coercedValues[$variableName] = valueFromAST(
187
                            $variableDefinitionNode->getDefaultValue(),
188
                            $variableType
189
                        );
190
                    }
191
                } else {
192
                    $value        = $inputs[$variableName];
193
                    $coercedValue = $this->coerceValue($value, $variableType, $variableDefinitionNode);
194
                    if ($coercedValue->hasErrors()) {
195
                        $messagePrelude = sprintf(
196
                            'Variable "$%s" got invalid value %s',
197
                            $variableName, json_encode
198
                            ($value)
199
                        );
200
                        foreach ($coercedValue->getErrors() as $error) {
201
                            $errors[] = $this->buildCoerceException(
202
                                $messagePrelude,
203
                                $variableDefinitionNode,
204
                                [],
205
                                $error->getMessage(),
206
                                $error
207
                            );
208
                        }
209
                    } else {
210
                        $coercedValues[$variableName] = $coercedValue->getValue();
211
                    }
212
                }
213
            }
214
        }
215
216
        return new CoercedValue($coercedValues, $errors);
217
    }
218
219
    /**
220
     * Returns either a value which is valid for the provided type or a list of
221
     * encountered coercion errors.
222
     *
223
     * @param  mixed|array $value
224
     * @param              $type
225
     * @param              $blameNode
226
     * @param array        $path
227
     * @return CoercedValue
228
     * @throws InvariantException
229
     * @throws GraphQLException
230
     */
231
    private function coerceValue($value, $type, $blameNode, ?array $path = null): CoercedValue
232
    {
233
        if ($type instanceof NonNullType) {
234
            return $this->coerceValueForNonNullType($value, $type, $blameNode, $path);
235
        }
236
237
        if (empty($value)) {
238
            return new CoercedValue(null, null);
239
        }
240
241
        if ($type instanceof ScalarType) {
242
            return $this->coerceValueForScalarType($value, $type, $blameNode, $path);
243
        }
244
245
        if ($type instanceof EnumType) {
246
            return $this->coerceValueForEnumType($value, $type, $blameNode, $path);
247
        }
248
249
        if ($type instanceof ListType) {
250
            return $this->coerceValueForListType($value, $type, $blameNode, $path);
251
        }
252
253
        if ($type instanceof InputObjectType) {
254
            return $this->coerceValueForInputObjectType($value, $type, $blameNode, $path);
255
        }
256
257
        throw new GraphQLException('Unexpected type.');
258
    }
259
260
    /**
261
     * @param               $value
262
     * @param NonNullType   $type
263
     * @param NodeInterface $blameNode
264
     * @param array|null    $path
265
     * @return CoercedValue
266
     * @throws GraphQLException
267
     * @throws InvariantException
268
     */
269
    protected function coerceValueForNonNullType(
270
        $value,
271
        NonNullType $type,
272
        NodeInterface $blameNode,
273
        ?array $path
274
    ): CoercedValue {
275
        if (empty($value)) {
276
            return new CoercedValue(null, [
277
                $this->buildCoerceException(
278
                    sprintf('Expected non-nullable type %s! not to be null', $type->getOfType()->getName()),
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Definition\TypeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Type\Defin...n\AbstractTypeInterface or Digia\GraphQL\Type\Defin...n\WrappingTypeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

278
                    sprintf('Expected non-nullable type %s! not to be null', $type->getOfType()->/** @scrutinizer ignore-call */ getName()),
Loading history...
279
                    $blameNode,
280
                    $path
281
                )
282
            ]);
283
        }
284
        return $this->coerceValue($value, $type->getOfType(), $blameNode, $path);
285
    }
286
287
    /**
288
     * Scalars determine if a value is valid via parseValue(), which can
289
     * throw to indicate failure. If it throws, maintain a reference to
290
     * the original error.
291
     *
292
     * @param               $value
293
     * @param ScalarType    $type
294
     * @param NodeInterface $blameNode
295
     * @param array|null    $path
296
     * @return CoercedValue
297
     */
298
    protected function coerceValueForScalarType(
299
        $value,
300
        ScalarType $type,
301
        NodeInterface $blameNode,
302
        ?array $path
303
    ): CoercedValue {
304
        try {
305
            $parseResult = $type->parseValue($value);
306
            if (empty($parseResult)) {
307
                return new CoercedValue(null, [
308
                    new GraphQLException(sprintf('Expected type %s', $type->getName()))
309
                ]);
310
            }
311
            return new CoercedValue($parseResult, null);
312
        } catch (\Exception $ex) {
313
            return new CoercedValue(null, [
314
                $this->buildCoerceException(
315
                    sprintf('Expected type %s', $type->getName()),
316
                    $blameNode,
317
                    $path,
318
                    $ex->getMessage(),
319
                    $ex
320
                )
321
            ]);
322
        }
323
    }
324
325
    /**
326
     * @param               $value
327
     * @param EnumType      $type
328
     * @param NodeInterface $blameNode
329
     * @param array|null    $path
330
     * @return CoercedValue
331
     * @throws InvariantException
332
     */
333
    protected function coerceValueForEnumType(
334
        $value,
335
        EnumType $type,
336
        NodeInterface $blameNode,
337
        ?array $path
338
    ): CoercedValue {
339
        if (is_string($value)) {
340
            $enumValue = $type->getValue($value);
341
            if ($enumValue !== null) {
342
                return new CoercedValue($enumValue, null);
343
            }
344
        }
345
346
        $suggestions = suggestionList((string)$value, array_map(function (EnumValue $enumValue) {
347
            return $enumValue->getName();
348
        }, $type->getValues()));
349
350
        $didYouMean = (!empty($suggestions))
351
            ? 'did you mean' . implode(',', $suggestions)
352
            : null;
353
354
        return new CoercedValue(null, [
355
            $this->buildCoerceException(sprintf('Expected type %s', $type->getName()), $blameNode, $path, $didYouMean)
356
        ]);
357
    }
358
359
    /**
360
     * @param               $value
361
     * @param ListType      $type
362
     * @param NodeInterface $blameNode
363
     * @param array|null    $path
364
     * @return CoercedValue
365
     * @throws GraphQLException
366
     * @throws InvariantException
367
     */
368
    protected function coerceValueForInputObjectType(
369
        $value,
370
        InputObjectType $type,
371
        NodeInterface $blameNode,
372
        ?array $path
373
    ): CoercedValue {
374
        $errors        = [];
375
        $coercedValues = [];
376
        $fields        = $type->getFields();
377
378
        // Ensure every defined field is valid.
379
        foreach ($fields as $field) {
380
            if (!array_key_exists($field->getName(), $value)) {
381
                if (!empty($field->getDefaultValue())) {
382
                    $coercedValue[$field->getName()] = $field->getDefaultValue();
383
                } elseif ($type instanceof NonNullType) {
384
                    $errors[] = new GraphQLException(
385
                        sprintf(
386
                            "Field %s of required type %s was not provided",
387
                            implode(",", array_merge($path, [$field->getName()])),
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $array1 of array_merge() does only seem to accept array, 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

387
                            implode(",", array_merge(/** @scrutinizer ignore-type */ $path, [$field->getName()])),
Loading history...
388
                            $type->getName()
389
                        )
390
                    );
391
                }
392
            } else {
393
                $fieldValue   = $value[$field->getName()];
394
                $coercedValue = $this->coerceValue(
395
                    $fieldValue,
396
                    $field->getType(),
397
                    $blameNode,
398
                    [$path, $field->getName()] // new path
399
                );
400
401
                if ($coercedValue->hasErrors()) {
402
                    $errors = array_merge($errors, $coercedValue->getErrors());
403
                } elseif (empty($errors)) {
404
                    $coercedValues[$field->getName()] = $coercedValue->getValue();
405
                }
406
            }
407
        }
408
409
        // Ensure every provided field is defined.
410
        foreach ($value as $fieldName => $fieldValue) {
411
            if ($fields[$fieldName] === null) {
412
                $suggestions = suggestionList($fieldName, array_keys($fields));
413
                $didYouMean  = (!empty($suggestions))
414
                    ? 'did you mean' . implode(',', $suggestions)
415
                    : null;
416
417
                $errors[] = $this->buildCoerceException(
418
                    sprintf('Field "%s" is not defined by type %s', $fieldName, $type->getName()),
419
                    $blameNode,
420
                    $path,
421
                    $didYouMean
422
                );
423
            }
424
        }
425
426
        return new CoercedValue($coercedValues, $errors);
427
    }
428
429
    /**
430
     * @param $value
431
     * @param $type
432
     * @return CoercedValue
433
     * @throws GraphQLException
434
     * @throws InvariantException
435
     */
436
    protected function coerceValueForListType(
437
        $value,
438
        ListType $type,
439
        NodeInterface $blameNode,
440
        ?array $path
441
    ): CoercedValue {
442
        $itemType = $type->getOfType();
443
444
        if (is_array($value) || $value instanceof \Traversable) {
445
            $errors        = [];
446
            $coercedValues = [];
447
            foreach ($value as $index => $itemValue) {
448
                $coercedValue = $this->coerceValue($itemValue, $itemType, $blameNode, [$path, $index]);
449
450
                if ($coercedValue->hasErrors()) {
451
                    $errors = array_merge($errors, $coercedValue->getErrors());
452
                } else {
453
                    $coercedValues[] = $coercedValue->getValue();
454
                }
455
            }
456
457
            return new CoercedValue($coercedValues, $errors);
458
        }
459
460
        // Lists accept a non-list value as a list of one.
461
        $coercedValue = $this->coerceValue($value, $itemType, $blameNode);
462
463
        return new CoercedValue([$coercedValue->getValue()], $coercedValue->getErrors());
464
    }
465
466
    /**
467
     * @param string                $message
468
     * @param NodeInterface         $blameNode
469
     * @param array|null            $path
470
     * @param null|string           $subMessage
471
     * @param GraphQLException|null $origin$originalException
472
     * @return GraphQLException
473
     */
474
    protected function buildCoerceException(
475
        string $message,
476
        NodeInterface $blameNode,
477
        ?array $path,
478
        ?string $subMessage = null,
479
        ?GraphQLException $originalException = null
480
    ) {
481
        $stringPath = implode(".", $path);
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $pieces of implode() does only seem to accept array, 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

481
        $stringPath = implode(".", /** @scrutinizer ignore-type */ $path);
Loading history...
482
483
        return new CoercingException(
484
            $message .
485
            (($stringPath !== '') ? ' at value' . $stringPath : $stringPath) .
486
            (($subMessage !== null) ? '; ' . $subMessage : '.'),
487
            [$blameNode],
488
            null,
489
            null,
490
            [],
491
            $originalException
492
        );
493
    }
494
495
    /**
496
     * @param VariableNode  $variableNode
497
     * @param TypeInterface $argumentType
498
     * @param string        $argumentName
499
     * @param array         $variableValues
500
     * @param mixed         $defaultValue
501
     * @return mixed
502
     * @throws ExecutionException
503
     */
504
    protected function coerceValueForVariableNode(
505
        VariableNode $variableNode,
506
        TypeInterface $argumentType,
507
        string $argumentName,
508
        array $variableValues,
509
        $defaultValue
510
    ) {
511
        $variableName = $variableNode->getNameValue();
512
513
        if (!empty($variableValues) && isset($variableValues[$variableName])) {
514
            // Note: this does not check that this variable value is correct.
515
            // This assumes that this query has been validated and the variable
516
            // usage here is of the correct type.
517
            return $variableValues[$variableName];
518
        }
519
520
        if (null !== $defaultValue) {
521
            return $defaultValue;
522
        }
523
524
        if ($argumentType instanceof NonNullType) {
525
            throw new ExecutionException(
526
                \sprintf(
527
                    'Argument "%s" of required type "%s" was provided the variable "%s" which was not provided a runtime value.',
528
                    $argumentName,
529
                    $argumentType,
530
                    $variableName
531
                ),
532
                [$variableNode->getValue()]
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on Digia\GraphQL\Language\Node\VariableNode. ( Ignorable by Annotation )

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

532
                [$variableNode->/** @scrutinizer ignore-call */ 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...
533
            );
534
        }
535
    }
536
}
537