Passed
Pull Request — master (#169)
by Quang
03:21
created

ValuesHelper::coerceVariableValues()   C

Complexity

Conditions 9
Paths 15

Size

Total Lines 59
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 59
rs 6.9133
c 0
b 0
f 0
cc 9
eloc 40
nc 15
nop 3

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\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
                    // because there is no undefined value in PHP so that we throw an exception
96
97
                    throw new ExecutionException(
98
                        sprintf('Argument "%s" has invalid value %s.', $argumentName, $argumentNode),
99
                        [$argumentNode->getValue()],
100
                        null, null, null, $ex
101
                    );
102
                }
103
104
                $coercedValues[$argumentName] = $coercedValue;
105
            }
106
        }
107
108
        return $coercedValues;
109
    }
110
111
    /**
112
     * Prepares an object map of argument values given a directive definition
113
     * and a AST node which may contain directives. Optionally also accepts a map
114
     * of variable values.
115
     *
116
     * If the directive does not exist on the node, returns null.
117
     *
118
     * @param Directive $directive
119
     * @param mixed     $node
120
     * @param array     $variableValues
121
     * @return array|null
122
     * @throws ExecutionException
123
     * @throws InvalidTypeException
124
     * @throws InvariantException
125
     */
126
    public function coerceDirectiveValues(
127
        Directive $directive,
128
        $node,
129
        array $variableValues = []
130
    ): ?array {
131
        $directiveNode = $node->hasDirectives()
132
            ? find($node->getDirectives(), function (NameAwareInterface $value) use ($directive) {
133
                return $value->getNameValue() === $directive->getName();
134
            }) : null;
135
136
        if (null !== $directiveNode) {
137
            return $this->coerceArgumentValues($directive, $directiveNode, $variableValues);
138
        }
139
140
        return null;
141
    }
142
143
    /**
144
     * Prepares an object map of variableValues of the correct type based on the
145
     * provided variable definitions and arbitrary input. If the input cannot be
146
     * parsed to match the variable definitions, a GraphQLError will be thrown.
147
     *
148
     * @param Schema                         $schema
149
     * @param array|VariableDefinitionNode[] $variableDefinitionNodes
150
     * @param                                $input
151
     * @return CoercedValue
152
     * @throws \Exception
153
     */
154
    public function coerceVariableValues(Schema $schema, array $variableDefinitionNodes, array $inputs): CoercedValue
155
    {
156
        $coercedValues = [];
157
        $errors        = [];
158
159
        foreach ($variableDefinitionNodes as $variableDefinitionNode) {
160
            $variableName = $variableDefinitionNode->getVariable()->getNameValue();
161
            $variableType = typeFromAST($schema, $variableDefinitionNode->getType());
162
163
            $type = $variableType;
164
            if ($variableType instanceof WrappingTypeInterface) {
165
                $type = $variableType->getOfType();
166
            }
167
168
            if (!$type instanceof InputTypeInterface) {
169
                $errors[] = new GraphQLException(
170
                    sprintf(
171
                        'Variable "%s" expected value of type %s which cannot be used as an input type.',
172
                        $variableName,
173
                        $variableDefinitionNode->getType()->getName()
174
                    ),
175
                    [$variableDefinitionNode->getType()]
176
                );
177
            } else {
178
                if (!isset($inputs[$variableName])) {
179
                    if ($variableType instanceof NonNullType) {
180
                        throw new GraphQLException('NonNullType');
181
                    } elseif ($variableDefinitionNode->getDefaultValue() !== null) {
182
                        $coercedValues[$variableName] = valueFromAST(
183
                            $variableDefinitionNode->getDefaultValue(),
184
                            $variableType
185
                        );
186
                    }
187
                } else {
188
                    $value        = $inputs[$variableName];
189
                    $coercedValue = $this->coerceValue($value, $variableType, $variableDefinitionNode);
190
                    if ($coercedValue->hasErrors()) {
191
                        $messagePrelude = sprintf(
192
                            'Variable "$%s" got invalid value %s',
193
                            $variableName, json_encode
194
                            ($value)
195
                        );
196
                        foreach ($coercedValue->getErrors() as $error) {
197
                            $errors[] = $this->buildCoerceException(
198
                                $messagePrelude,
199
                                $variableDefinitionNode,
200
                                [],
201
                                $error->getMessage(),
202
                                $error
203
                            );
204
                        }
205
                    } else {
206
                        $coercedValues[$variableName] = $coercedValue->getValue();
207
                    }
208
                }
209
            }
210
        }
211
212
        return new CoercedValue($coercedValues, $errors);
213
    }
214
215
    /**
216
     * Returns either a value which is valid for the provided type or a list of
217
     * encountered coercion errors.
218
     *
219
     * @param  mixed|array $value
220
     * @param              $type
221
     * @param              $blameNode
222
     * @param array        $path
223
     * @return CoercedValue
224
     * @throws InvariantException
225
     * @throws GraphQLException
226
     */
227
    private function coerceValue($value, $type, $blameNode, ?array $path = null): CoercedValue
228
    {
229
        if ($type instanceof NonNullType) {
230
            return $this->coerceValueForNonNullType($value, $type, $blameNode, $path);
231
        }
232
233
        if (empty($value)) {
234
            return new CoercedValue(null, null);
235
        }
236
237
        if ($type instanceof ScalarType) {
238
            return $this->coerceValueForScalarType($value, $type, $blameNode, $path);
239
        }
240
241
        if ($type instanceof EnumType) {
242
            return $this->coerceValueForEnumType($value, $type, $blameNode, $path);
243
        }
244
245
        if ($type instanceof ListType) {
246
            return $this->coerceValueForListType($value, $type, $blameNode, $path);
247
        }
248
249
        if ($type instanceof InputObjectType) {
250
            return $this->coerceValueForInputObjectType($value, $type, $blameNode, $path);
251
        }
252
253
        throw new GraphQLException('Unexpected type.');
254
    }
255
256
    /**
257
     * @param               $value
258
     * @param NonNullType   $type
259
     * @param NodeInterface $blameNode
260
     * @param array|null    $path
261
     * @return CoercedValue
262
     * @throws GraphQLException
263
     * @throws InvariantException
264
     */
265
    protected function coerceValueForNonNullType(
266
        $value,
267
        NonNullType $type,
268
        NodeInterface $blameNode,
269
        ?array $path
270
    ): CoercedValue {
271
        if (empty($value)) {
272
            return new CoercedValue(null, [
273
                $this->buildCoerceException(
274
                    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

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

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

384
                            /** @scrutinizer ignore-type */ $field->getType()->getOfType()
Loading history...
385
                        )
386
                    );
387
                }
388
            } else {
389
                $fieldValue   = $value[$field->getName()];
390
                $coercedValue = $this->coerceValue(
391
                    $fieldValue,
392
                    $field->getType(),
393
                    $blameNode,
394
                    [$path, $field->getName()] // new path
395
                );
396
397
                if ($coercedValue->hasErrors()) {
398
                    $errors = array_merge($errors, $coercedValue->getErrors());
399
                } elseif (empty($errors)) {
400
                    $coercedValues[$field->getName()] = $coercedValue->getValue();
401
                }
402
            }
403
        }
404
405
        // Ensure every provided field is defined.
406
        foreach ($value as $fieldName => $fieldValue) {
407
            if (!isset($fields[$fieldName])) {
408
                $suggestions = suggestionList($fieldName, array_keys($fields));
409
                $didYouMean  = (!empty($suggestions))
410
                    ? 'did you mean' . implode(',', $suggestions)
411
                    : null;
412
413
                $errors[] = $this->buildCoerceException(
414
                    sprintf('Field "%s" is not defined by type %s', $fieldName, $type->getName()),
415
                    $blameNode,
416
                    $path,
417
                    $didYouMean
418
                );
419
            }
420
        }
421
422
        return new CoercedValue($coercedValues, $errors);
423
    }
424
425
    /**
426
     * @param $value
427
     * @param $type
428
     * @return CoercedValue
429
     * @throws GraphQLException
430
     * @throws InvariantException
431
     */
432
    protected function coerceValueForListType(
433
        $value,
434
        ListType $type,
435
        NodeInterface $blameNode,
436
        ?array $path
437
    ): CoercedValue {
438
        $itemType = $type->getOfType();
439
440
        if (is_array($value) || $value instanceof \Traversable) {
441
            $errors        = [];
442
            $coercedValues = [];
443
            foreach ($value as $index => $itemValue) {
444
                $coercedValue = $this->coerceValue($itemValue, $itemType, $blameNode, [$path, $index]);
445
446
                if ($coercedValue->hasErrors()) {
447
                    $errors = array_merge($errors, $coercedValue->getErrors());
448
                } else {
449
                    $coercedValues[] = $coercedValue->getValue();
450
                }
451
            }
452
453
            return new CoercedValue($coercedValues, $errors);
454
        }
455
456
        // Lists accept a non-list value as a list of one.
457
        $coercedValue = $this->coerceValue($value, $itemType, $blameNode);
458
459
        return new CoercedValue([$coercedValue->getValue()], $coercedValue->getErrors());
460
    }
461
462
    /**
463
     * @param string                $message
464
     * @param NodeInterface         $blameNode
465
     * @param array|null            $path
466
     * @param null|string           $subMessage
467
     * @param GraphQLException|null $origin $originalException
468
     * @return GraphQLException
469
     */
470
    protected function buildCoerceException(
471
        string $message,
472
        NodeInterface $blameNode,
473
        ?array $path,
474
        ?string $subMessage = null,
475
        ?GraphQLException $originalException = null
476
    ) {
477
        $stringPath = $this->printPath($path);
478
479
        return new CoercingException(
480
            $message .
481
            (($stringPath !== '') ? ' at ' . $stringPath : $stringPath) .
482
            (($subMessage !== null) ? '; ' . $subMessage : '.'),
483
            [$blameNode],
484
            null,
485
            null,
486
            [],
487
            $originalException
488
        );
489
    }
490
491
    /**
492
     * @param array|null $path
493
     * @return string
494
     */
495
    protected function printPath(?array $path)
496
    {
497
        $stringPath = ltrim(implode(".", $path ?? []), '.');
498
499
        return ($stringPath !== '')
500
            ? $stringPath = 'value.' . $stringPath
0 ignored issues
show
Unused Code introduced by
The assignment to $stringPath is dead and can be removed.
Loading history...
501
            : $stringPath;
502
    }
503
504
    /**
505
     * @param VariableNode  $variableNode
506
     * @param TypeInterface $argumentType
507
     * @param string        $argumentName
508
     * @param array         $variableValues
509
     * @param mixed         $defaultValue
510
     * @return mixed
511
     * @throws ExecutionException
512
     */
513
    protected function coerceValueForVariableNode(
514
        VariableNode $variableNode,
515
        TypeInterface $argumentType,
516
        string $argumentName,
517
        array $variableValues,
518
        $defaultValue
519
    ) {
520
        $variableName = $variableNode->getNameValue();
521
522
        if (!empty($variableValues) && isset($variableValues[$variableName])) {
523
            // Note: this does not check that this variable value is correct.
524
            // This assumes that this query has been validated and the variable
525
            // usage here is of the correct type.
526
            return $variableValues[$variableName];
527
        }
528
529
        if (null !== $defaultValue) {
530
            return $defaultValue;
531
        }
532
533
        if ($argumentType instanceof NonNullType) {
534
            throw new ExecutionException(
535
                \sprintf(
536
                    'Argument "%s" of required type "%s" was provided the variable "%s" which was not provided a runtime value.',
537
                    $argumentName,
538
                    $argumentType,
539
                    $variableName
540
                ),
541
                [$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

541
                [$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...
542
            );
543
        }
544
    }
545
}
546