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

ValuesHelper::coerceValue()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 19
nc 8
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) || 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
                //@TODO proper message
174
                throw new GraphQLException('InputTypeInterface');
175
            } else {
176
                if (!isset($inputs[$variableName])) {
177
                    if ($variableType instanceof NonNullType) {
178
                        throw new GraphQLException('NonNullType');
179
                    } elseif ($variableDefinitionNode->getDefaultValue() !== null) {
180
                        $coercedValues[$variableName] = valueFromAST(
181
                            $variableDefinitionNode->getDefaultValue(),
182
                            $variableType
183
                        );
184
                    }
185
                } else {
186
                    $value        = $inputs[$variableName];
187
                    $coercedValue = $this->coerceValue($value, $variableType, $variableDefinitionNode);
188
                    if ($coercedValue->hasErrors()) {
189
                        $messagePrelude = sprintf(
190
                            'Variable "$%s" got invalid value %s',
191
                            $variableName, json_encode
192
                            ($value)
193
                        );
194
                        foreach ($coercedValue->getErrors() as $error) {
195
                            $errors[] = $this->buildCoerceException(
196
                                $messagePrelude,
197
                                $variableDefinitionNode,
198
                                [],
199
                                $error->getMessage(),
200
                                $error
201
                            );
202
                        }
203
                    } else {
204
                        $coercedValues[$variableName] = $coercedValue->getValue();
205
                    }
206
                }
207
            }
208
        }
209
210
        return new CoercedValue($coercedValues, $errors);
211
    }
212
213
    /**
214
     * Returns either a value which is valid for the provided type or a list of
215
     * encountered coercion errors.
216
     *
217
     * @param  mixed|array $value
218
     * @param              $type
219
     * @param              $blameNode
220
     * @param array        $path
221
     * @return CoercedValue
222
     * @throws InvariantException
223
     * @throws GraphQLException
224
     */
225
    private function coerceValue($value, $type, $blameNode, ?array $path = null): CoercedValue
226
    {
227
        if ($type instanceof NonNullType) {
228
            if (empty($value)) {
229
                return new CoercedValue(null, [
230
                    $this->buildCoerceException(
231
                        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

231
                        sprintf('Expected non-nullable type %s! not to be null', $type->getOfType()->/** @scrutinizer ignore-call */ getName()),
Loading history...
232
                        $blameNode,
233
                        $path
234
                    )
235
                ]);
236
            }
237
            return $this->coerceValue($value, $type->getOfType(), $blameNode, $path);
238
        }
239
240
        if (empty($value)) {
241
            return new CoercedValue(null, null);
242
        }
243
244
        if ($type instanceof ScalarType) {
245
            return $this->coerceValueForScalarype($value, $type, $blameNode, $path);
246
        }
247
248
        if ($type instanceof EnumType) {
249
            return $this->coerceValueForEnumType($value, $type, $blameNode, $path);
250
        }
251
252
        if ($type instanceof ListType) {
253
            return $this->coerceValueForListType($value, $type, $blameNode, $path);
254
        }
255
256
        if ($type instanceof InputObjectType) {
257
            return $this->coerceValueForInputObjectType($value, $type, $blameNode, $path);
258
        }
259
260
        throw new GraphQLException('Unexpected type.');
261
    }
262
263
    /**
264
     * @param               $value
265
     * @param ScalarType    $type
266
     * @param NodeInterface $blameNode
267
     * @param array|null    $path
268
     * @return CoercedValue
269
     */
270
    protected function coerceValueForScalarype(
271
        $value,
272
        ScalarType $type,
273
        NodeInterface $blameNode,
0 ignored issues
show
Unused Code introduced by
The parameter $blameNode is not used and could be removed. ( Ignorable by Annotation )

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

273
        /** @scrutinizer ignore-unused */ NodeInterface $blameNode,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
274
        ?array $path
0 ignored issues
show
Unused Code introduced by
The parameter $path is not used and could be removed. ( Ignorable by Annotation )

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

274
        /** @scrutinizer ignore-unused */ ?array $path

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
275
    ): CoercedValue {
276
        try {
277
            $parseResult = $type->parseValue($value);
278
            if (empty($parseResult)) {
279
                return new CoercedValue(null, [
280
                    new GraphQLException(sprintf('Expected type %s', $type->getName()))
281
                ]);
282
            }
283
            return new CoercedValue($parseResult, null);
284
        } catch (\Exception $ex) {
285
            //@TODO Check exception message
286
            return new CoercedValue(null, [
287
                new GraphQLException(sprintf('Expected type %s', $type->getName()))
288
            ]);
289
        }
290
    }
291
292
    /**
293
     * @param               $value
294
     * @param EnumType      $type
295
     * @param NodeInterface $blameNode
296
     * @param array|null    $path
297
     * @return CoercedValue
298
     * @throws InvariantException
299
     */
300
    protected function coerceValueForEnumType(
301
        $value,
302
        EnumType $type,
303
        NodeInterface $blameNode,
0 ignored issues
show
Unused Code introduced by
The parameter $blameNode is not used and could be removed. ( Ignorable by Annotation )

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

303
        /** @scrutinizer ignore-unused */ NodeInterface $blameNode,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
304
        ?array $path
0 ignored issues
show
Unused Code introduced by
The parameter $path is not used and could be removed. ( Ignorable by Annotation )

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

304
        /** @scrutinizer ignore-unused */ ?array $path

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
305
    ): CoercedValue {
306
        if (is_string($value)) {
307
            $enumValue = $type->getValue($value);
308
            if ($enumValue !== null) {
309
                return new CoercedValue($enumValue, null);
310
            }
311
        }
312
313
        $suggestions = suggestionList((string)$value, array_map(function (EnumValue $enumValue) {
0 ignored issues
show
Unused Code introduced by
The assignment to $suggestions is dead and can be removed.
Loading history...
314
            return $enumValue->getName();
315
        }, $type->getValues()));
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return Digia\GraphQL\Execution\CoercedValue. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
316
317
        //@TODO throw proper error
318
    }
319
320
    /**
321
     * @param               $value
322
     * @param ListType      $type
323
     * @param NodeInterface $blameNode
324
     * @param array|null    $path
325
     * @return CoercedValue
326
     * @throws GraphQLException
327
     * @throws InvariantException
328
     */
329
    protected function coerceValueForInputObjectType(
330
        $value,
331
        InputObjectType $type,
332
        NodeInterface $blameNode,
333
        ?array $path
334
    ): CoercedValue {
335
        $errors        = [];
336
        $coercedValues = [];
337
        $fields        = $type->getFields();
338
339
        // Ensure every defined field is valid.
340
        foreach ($fields as $field) {
341
            if (!array_key_exists($field->getName(), $value)) {
342
                if (!empty($field->getDefaultValue())) {
343
                    $coercedValue[$field->getName()] = $field->getDefaultValue();
344
                } elseif ($type instanceof NonNullType) {
345
                    $errors[] = new GraphQLException(
346
                        sprintf(
347
                            "Field %s of required type %s was not provided",
348
                            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

348
                            implode(",", array_merge(/** @scrutinizer ignore-type */ $path, [$field->getName()])),
Loading history...
349
                            $type->getName()
350
                        )
351
                    );
352
                }
353
            } else {
354
                $fieldValue   = $value[$field->getName()];
355
                $coercedValue = $this->coerceValue(
356
                    $fieldValue,
357
                    $field->getType(),
358
                    $blameNode,
359
                    [$path, $field->getName()] // new path
360
                );
361
362
                if ($coercedValue->hasErrors()) {
363
                    $errors = array_merge($errors, $coercedValue->getErrors());
364
                } elseif (empty($errors)) {
365
                    $coercedValues[$field->getName()] = $coercedValue->getValue();
366
                }
367
            }
368
        }
369
370
        // Ensure every provided field is defined.
371
        foreach ($value as $fieldName => $fieldValue) {
372
            if ($fields[$fieldName] === null) {
373
                $suggestion = suggestionList($fieldName, array_keys($fields));
0 ignored issues
show
Unused Code introduced by
The assignment to $suggestion is dead and can be removed.
Loading history...
374
                $errors[]   = new GraphQLException(
375
                    sprintf('Field "%s" is not defined by type %s', $fieldName, $type->getName())
376
                );
377
            }
378
        }
379
380
        return new CoercedValue($coercedValues, $errors);
381
    }
382
383
    /**
384
     * @param $value
385
     * @param $type
386
     * @return CoercedValue
387
     * @throws GraphQLException
388
     * @throws InvariantException
389
     */
390
    protected function coerceValueForListType(
391
        $value,
392
        ListType $type,
393
        NodeInterface $blameNode,
394
        ?array $path
395
    ): CoercedValue {
396
        $itemType = $type->getOfType();
397
398
        if (is_array($value) || $value instanceof \Traversable) {
399
            $errors        = [];
400
            $coercedValues = [];
401
            foreach ($value as $index => $itemValue) {
402
                $coercedValue = $this->coerceValue(
403
                    $itemValue,
404
                    $itemType,
405
                    $blameNode,
406
                    [$path, $index]
407
                );
408
409
                if ($coercedValue->hasErrors()) {
410
                    $errors = array_merge($errors, $coercedValue->getErrors());
411
                } else {
412
                    $coercedValues[] = $coercedValue->getValue();
413
                }
414
            }
415
416
            return new CoercedValue($coercedValues, $errors);
417
        }
418
419
        // Lists accept a non-list value as a list of one.
420
        $coercedValue = $this->coerceValue($value, $itemType, $blameNode);
421
422
        return new CoercedValue([$coercedValue->getValue()], $coercedValue->getErrors());
423
    }
424
425
    /**
426
     * @param string                $message
427
     * @param NodeInterface         $blameNode
428
     * @param array|null            $path
429
     * @param null|string           $subMessage
430
     * @param GraphQLException|null $originalError
431
     * @return GraphQLException
432
     */
433
    protected function buildCoerceException(
434
        string $message,
435
        NodeInterface $blameNode,
436
        ?array $path,
437
        ?string $subMessage = null,
438
        ?GraphQLException $originalError = null
439
    ) {
440
        $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

440
        $stringPath = implode(".", /** @scrutinizer ignore-type */ $path);
Loading history...
441
442
        return new CoercingException(
443
            $message .
444
            (($stringPath !== '') ? ' at value' . $stringPath : $stringPath) .
445
            (($subMessage !== null) ? '; ' . $subMessage : '.'),
446
            [$blameNode],
447
            null,
448
            null,
449
            [],
450
            $originalError
451
        );
452
    }
453
454
    /**
455
     * @param VariableNode  $variableNode
456
     * @param TypeInterface $argumentType
457
     * @param string        $argumentName
458
     * @param array         $variableValues
459
     * @param mixed         $defaultValue
460
     * @return mixed
461
     * @throws ExecutionException
462
     */
463
    protected function coerceValueForVariableNode(
464
        VariableNode $variableNode,
465
        TypeInterface $argumentType,
466
        string $argumentName,
467
        array $variableValues,
468
        $defaultValue
469
    ) {
470
        $variableName = $variableNode->getNameValue();
471
472
        if (!empty($variableValues) && isset($variableValues[$variableName])) {
473
            // Note: this does not check that this variable value is correct.
474
            // This assumes that this query has been validated and the variable
475
            // usage here is of the correct type.
476
            return $variableValues[$variableName];
477
        }
478
479
        if (null !== $defaultValue) {
480
            return $defaultValue;
481
        }
482
483
        if ($argumentType instanceof NonNullType) {
484
            throw new ExecutionException(
485
                \sprintf(
486
                    'Argument "%s" of required type "%s" was provided the variable "%s" which was not provided a runtime value.',
487
                    $argumentName,
488
                    $argumentType,
489
                    $variableName
490
                ),
491
                [$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

491
                [$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...
492
            );
493
        }
494
    }
495
}
496