Completed
Pull Request — master (#169)
by Quang
04:44
created

ValuesHelper::coerceValue()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 13
nc 7
nop 4
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) || $argumentNodes === null) {
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] ?? null;
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,
77
                            $argumentType),
78
                        [$node]
79
                    );
80
                }
81
            } elseif ($argumentNode instanceof VariableNode) {
82
                $coercedValues[$argumentName] = $this->coerceValueForVariableNode(
83
                    $argumentNode,
84
                    $argumentType,
85
                    $argumentName,
86
                    $variableValues,
87
                    $defaultValue
88
                );
89
            } else {
90
                $coercedValue = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $coercedValue is dead and can be removed.
Loading history...
91
92
                try {
93
                    $coercedValue = valueFromAST($argumentNode->getValue(), $argumentType, $variableValues);
94
                } catch (CoercingException $ex) {
95
                    // Value nodes that cannot be resolved should be treated as invalid values
96
                    // because there is no undefined value in PHP so that we throw an exception
97
98
                    throw new ExecutionException(
99
                        sprintf('Argument "%s" has invalid value %s.', $argumentName, $argumentNode),
100
                        [$argumentNode->getValue()],
101
                        null, null, null, $ex
102
                    );
103
                }
104
105
                $coercedValues[$argumentName] = $coercedValue;
106
            }
107
        }
108
109
        return $coercedValues;
110
    }
111
112
    /**
113
     * Prepares an object map of argument values given a directive definition
114
     * and a AST node which may contain directives. Optionally also accepts a map
115
     * of variable values.
116
     *
117
     * If the directive does not exist on the node, returns null.
118
     *
119
     * @param Directive $directive
120
     * @param mixed     $node
121
     * @param array     $variableValues
122
     * @return array|null
123
     * @throws ExecutionException
124
     * @throws InvalidTypeException
125
     * @throws InvariantException
126
     */
127
    public function coerceDirectiveValues(
128
        Directive $directive,
129
        $node,
130
        array $variableValues = []
131
    ): ?array {
132
        $directiveNode = $node->hasDirectives()
133
            ? find($node->getDirectives(), function (NameAwareInterface $value) use ($directive) {
134
                return $value->getNameValue() === $directive->getName();
135
            }) : null;
136
137
        if (null !== $directiveNode) {
138
            return $this->coerceArgumentValues($directive, $directiveNode, $variableValues);
139
        }
140
141
        return null;
142
    }
143
144
    /**
145
     * Prepares an object map of variableValues of the correct type based on the
146
     * provided variable definitions and arbitrary input. If the input cannot be
147
     * parsed to match the variable definitions, a GraphQLError will be thrown.
148
     *
149
     * @param Schema                         $schema
150
     * @param array|VariableDefinitionNode[] $variableDefinitionNodes
151
     * @param                                $input
152
     * @return CoercedValue
153
     * @throws \Exception
154
     */
155
    public function coerceVariableValues(Schema $schema, array $variableDefinitionNodes, array $inputs): CoercedValue
156
    {
157
        $coercedValues = [];
158
        $errors        = [];
159
160
        foreach ($variableDefinitionNodes as $variableDefinitionNode) {
161
            $variableName = $variableDefinitionNode->getVariable()->getNameValue();
162
            $variableType = typeFromAST($schema, $variableDefinitionNode->getType());
163
164
            $type = $variableType;
165
            if ($variableType instanceof WrappingTypeInterface) {
166
                $type = $variableType->getOfType();
167
            }
168
169
            if (!$type instanceof InputTypeInterface) {
170
                $errors[] = $this->buildCoerceException(
171
                    sprintf(
172
                        'Variable "$%s" expected value of type "%s!" which cannot be used as an input type',
173
                        $variableName,
174
                        $type->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

174
                        $type->/** @scrutinizer ignore-call */ 
175
                               getName()
Loading history...
175
                    ),
176
                    $variableDefinitionNode,
177
                    []
178
                );
179
            } else {
180
                if (!array_key_exists($variableName, $inputs)) {
181
                    if ($variableType instanceof NonNullType) {
182
                        $errors[] = $this->buildCoerceException(
183
                            sprintf(
184
                                'Variable "$%s" of required type "%s!" was not provided',
185
                                $variableName,
186
                                $type->getName()
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Digia\GraphQL\Type\Definition\InputTypeInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Digia\GraphQL\Type\Definition\InputTypeInterface. ( Ignorable by Annotation )

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

186
                                $type->/** @scrutinizer ignore-call */ 
187
                                       getName()
Loading history...
187
                            ),
188
                            $variableDefinitionNode,
189
                            []
190
                        );
191
                    } elseif ($variableDefinitionNode->getDefaultValue() !== null) {
192
                        $coercedValues[$variableName] = valueFromAST(
193
                            $variableDefinitionNode->getDefaultValue(),
194
                            $variableType
195
                        );
196
                    }
197
                } else {
198
                    $value        = $inputs[$variableName];
199
                    $coercedValue = $this->coerceValue($value, $variableType, $variableDefinitionNode);
200
                    if ($coercedValue->hasErrors()) {
201
                        $messagePrelude = sprintf(
202
                            'Variable "$%s" got invalid value %s',
203
                            $variableName, json_encode($value)
204
                        );
205
                        foreach ($coercedValue->getErrors() as $error) {
206
                            $errors[] = $this->buildCoerceException(
207
                                $messagePrelude,
208
                                $variableDefinitionNode,
209
                                [],
210
                                $error->getMessage(),
211
                                $error
212
                            );
213
                        }
214
                    } else {
215
                        $coercedValues[$variableName] = $coercedValue->getValue();
216
                    }
217
                }
218
            }
219
        }
220
221
        return new CoercedValue($coercedValues, $errors);
222
    }
223
224
    /**
225
     * Returns either a value which is valid for the provided type or a list of
226
     * encountered coercion errors.
227
     *
228
     * @param  mixed|array $value
229
     * @param              $type
230
     * @param              $blameNode
231
     * @param array        $path
232
     * @return CoercedValue
233
     * @throws InvariantException
234
     * @throws GraphQLException
235
     */
236
    private function coerceValue($value, $type, $blameNode, ?array $path = null): CoercedValue
237
    {
238
        if ($type instanceof NonNullType) {
239
            return $this->coerceValueForNonNullType($value, $type, $blameNode, $path);
240
        }
241
242
        if (empty($value)) {
243
            return new CoercedValue(null, null);
244
        }
245
246
        if ($type instanceof ScalarType) {
247
            return $this->coerceValueForScalarType($value, $type, $blameNode, $path);
248
        }
249
250
        if ($type instanceof EnumType) {
251
            return $this->coerceValueForEnumType($value, $type, $blameNode, $path);
252
        }
253
254
        if ($type instanceof ListType) {
255
            return $this->coerceValueForListType($value, $type, $blameNode, $path);
256
        }
257
258
        if ($type instanceof InputObjectType) {
259
            return $this->coerceValueForInputObjectType($value, $type, $blameNode, $path);
260
        }
261
262
        throw new GraphQLException('Unexpected type.');
263
    }
264
265
    /**
266
     * @param               $value
267
     * @param NonNullType   $type
268
     * @param NodeInterface $blameNode
269
     * @param array|null    $path
270
     * @return CoercedValue
271
     * @throws GraphQLException
272
     * @throws InvariantException
273
     */
274
    protected function coerceValueForNonNullType(
275
        $value,
276
        NonNullType $type,
277
        NodeInterface $blameNode,
278
        ?array $path
279
    ): CoercedValue {
280
        if (empty($value)) {
281
            return new CoercedValue(null, [
282
                $this->buildCoerceException(
283
                    sprintf('Expected non-nullable type %s! not to be null', $type->getOfType()->getName()),
284
                    $blameNode,
285
                    $path
286
                )
287
            ]);
288
        }
289
        return $this->coerceValue($value, $type->getOfType(), $blameNode, $path);
290
    }
291
292
    /**
293
     * Scalars determine if a value is valid via parseValue(), which can
294
     * throw to indicate failure. If it throws, maintain a reference to
295
     * the original error.
296
     *
297
     * @param               $value
298
     * @param ScalarType    $type
299
     * @param NodeInterface $blameNode
300
     * @param array|null    $path
301
     * @return CoercedValue
302
     */
303
    protected function coerceValueForScalarType(
304
        $value,
305
        ScalarType $type,
306
        NodeInterface $blameNode,
307
        ?array $path
308
    ): CoercedValue {
309
        try {
310
            $parseResult = $type->parseValue($value);
311
            if (empty($parseResult)) {
312
                return new CoercedValue(null, [
313
                    new GraphQLException(sprintf('Expected type %s', $type->getName()))
314
                ]);
315
            }
316
            return new CoercedValue($parseResult, null);
317
        } catch (\Exception $ex) {
318
            return new CoercedValue(null, [
319
                $this->buildCoerceException(
320
                    sprintf('Expected type %s', $type->getName()),
321
                    $blameNode,
322
                    $path,
323
                    $ex->getMessage(),
324
                    $ex
325
                )
326
            ]);
327
        }
328
    }
329
330
    /**
331
     * @param               $value
332
     * @param EnumType      $type
333
     * @param NodeInterface $blameNode
334
     * @param array|null    $path
335
     * @return CoercedValue
336
     * @throws InvariantException
337
     */
338
    protected function coerceValueForEnumType(
339
        $value,
340
        EnumType $type,
341
        NodeInterface $blameNode,
342
        ?array $path
343
    ): CoercedValue {
344
        if (is_string($value)) {
345
            $enumValue = $type->getValue($value);
346
            if ($enumValue !== null) {
347
                return new CoercedValue($enumValue, null);
348
            }
349
        }
350
351
        $suggestions = suggestionList((string)$value, array_map(function (EnumValue $enumValue) {
352
            return $enumValue->getName();
353
        }, $type->getValues()));
354
355
        $didYouMean = (!empty($suggestions))
356
            ? 'did you mean' . implode(',', $suggestions)
357
            : null;
358
359
        return new CoercedValue(null, [
360
            $this->buildCoerceException(sprintf('Expected type %s', $type->getName()), $blameNode, $path, $didYouMean)
361
        ]);
362
    }
363
364
    /**
365
     * @param               $value
366
     * @param ListType      $type
367
     * @param NodeInterface $blameNode
368
     * @param array|null    $path
369
     * @return CoercedValue
370
     * @throws GraphQLException
371
     * @throws InvariantException
372
     */
373
    protected function coerceValueForInputObjectType(
374
        $value,
375
        InputObjectType $type,
376
        NodeInterface $blameNode,
377
        ?array $path
378
    ): CoercedValue {
379
        $errors        = [];
380
        $coercedValues = [];
381
        $fields        = $type->getFields();
382
383
        // Ensure every defined field is valid.
384
        foreach ($fields as $field) {
385
            if (empty($value[$field->getName()])) {
386
                if (!empty($field->getDefaultValue())) {
387
                    $coercedValue[$field->getName()] = $field->getDefaultValue();
388
                } elseif ($field->getType() instanceof NonNullType) {
389
                    $errors[] = new GraphQLException(
390
                        sprintf(
391
                            "Field %s of required type %s! was not provided.",
392
                            $this->printPath(array_merge($path ?? [], [$field->getName()])),
393
                            $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

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

550
                [$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...
551
            );
552
        }
553
    }
554
}
555