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

ValuesHelper::coerceVariableValues()   C

Complexity

Conditions 8
Paths 13

Size

Total Lines 46
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 46
rs 5.5555
c 0
b 0
f 0
cc 8
eloc 31
nc 13
nop 3
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\VariableDefinitionNode;
14
use Digia\GraphQL\Language\Node\VariableNode;
15
use Digia\GraphQL\Schema\Schema;
16
use Digia\GraphQL\Type\Definition\Directive;
17
use Digia\GraphQL\Type\Definition\EnumType;
18
use Digia\GraphQL\Type\Definition\EnumValue;
19
use Digia\GraphQL\Type\Definition\Field;
20
use Digia\GraphQL\Type\Definition\InputObjectType;
21
use Digia\GraphQL\Type\Definition\InputTypeInterface;
22
use Digia\GraphQL\Type\Definition\ListType;
23
use Digia\GraphQL\Type\Definition\NonNullType;
24
use Digia\GraphQL\Type\Definition\ScalarType;
25
use Digia\GraphQL\Type\Definition\TypeInterface;
26
use Digia\GraphQL\Type\Definition\WrappingTypeInterface;
27
use function Digia\GraphQL\Util\find;
28
use function Digia\GraphQL\Util\keyMap;
29
use function Digia\GraphQL\Util\suggestionList;
30
use function Digia\GraphQL\Util\typeFromAST;
31
use function Digia\GraphQL\Util\valueFromAST;
32
33
class ValuesHelper
34
{
35
    /**
36
     * Prepares an object map of argument values given a list of argument
37
     * definitions and list of argument AST nodes.
38
     *
39
     * Note: The returned value is a plain Object with a prototype, since it is
40
     * exposed to user code. Care should be taken to not pull values from the
41
     * Object prototype.
42
     *
43
     * @see http://facebook.github.io/graphql/October2016/#CoerceArgumentValues()
44
     *
45
     * @param Field|Directive         $definition
46
     * @param ArgumentsAwareInterface $node
47
     * @param array                   $variableValues
48
     * @return array
49
     * @throws ExecutionException
50
     * @throws InvalidTypeException
51
     * @throws InvariantException
52
     */
53
    public function coerceArgumentValues($definition, ArgumentsAwareInterface $node, array $variableValues = []): array
54
    {
55
        $coercedValues       = [];
56
        $argumentDefinitions = $definition->getArguments();
57
        $argumentNodes       = $node->getArguments();
58
59
        if (empty($argumentDefinitions) || empty($argumentNodes)) {
60
            return $coercedValues;
61
        }
62
63
        $argumentNodeMap = keyMap($argumentNodes, function (ArgumentNode $value) {
64
            return $value->getNameValue();
65
        });
66
67
        foreach ($argumentDefinitions as $argumentDefinition) {
68
            $argumentName = $argumentDefinition->getName();
69
            $argumentType = $argumentDefinition->getType();
70
            /** @var ArgumentNode $argumentNode */
71
            $argumentNode = $argumentNodeMap[$argumentName];
72
            $defaultValue = $argumentDefinition->getDefaultValue();
73
74
            if (null === $argumentNode) {
75
                if (null === $defaultValue) {
76
                    $coercedValues[$argumentName] = $defaultValue;
77
                } elseif (!$argumentType instanceof NonNullType) {
78
                    throw new ExecutionException(
79
                        sprintf('Argument "%s" of required type "%s" was not provided.', $argumentName, $argumentType),
80
                        [$node]
81
                    );
82
                }
83
            } elseif ($argumentNode instanceof VariableNode) {
84
                $coercedValues[$argumentName] = $this->coerceValueForVariableNode(
85
                    $argumentNode,
86
                    $argumentType,
87
                    $argumentName,
88
                    $variableValues,
89
                    $defaultValue
90
                );
91
            } else {
92
                $coercedValue = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $coercedValue is dead and can be removed.
Loading history...
93
94
                try {
95
                    $coercedValue = valueFromAST($argumentNode->getValue(), $argumentType, $variableValues);
96
                } catch (CoercingException $ex) {
97
                    // Value nodes that cannot be resolved should be treated as invalid values
98
                    // therefore we catch the exception and leave the `$coercedValue` as `null`.
99
                }
100
101
                if (null === $coercedValue) {
102
                    // Note: ValuesOfCorrectType validation should catch this before
103
                    // execution. This is a runtime check to ensure execution does not
104
                    // continue with an invalid argument value.
105
                    throw new ExecutionException(
106
                        sprintf('Argument "%s" has invalid value %s.', $argumentName, $argumentNode),
107
                        [$argumentNode->getValue()]
108
                    );
109
                }
110
111
                $coercedValues[$argumentName] = $coercedValue;
112
            }
113
        }
114
115
        return $coercedValues;
116
    }
117
118
    /**
119
     * Prepares an object map of argument values given a directive definition
120
     * and a AST node which may contain directives. Optionally also accepts a map
121
     * of variable values.
122
     *
123
     * If the directive does not exist on the node, returns undefined.
124
     *
125
     * Note: The returned value is a plain Object with a prototype, since it is
126
     * exposed to user code. Care should be taken to not pull values from the
127
     * Object prototype.
128
     *
129
     * @param Directive $directive
130
     * @param mixed     $node
131
     * @param array     $variableValues
132
     * @return array|null
133
     * @throws ExecutionException
134
     * @throws InvalidTypeException
135
     * @throws InvariantException
136
     */
137
    public function coerceDirectiveValues(
138
        Directive $directive,
139
        $node,
140
        array $variableValues = []
141
    ): ?array {
142
        $directiveNode = $node->hasDirectives()
143
            ? find($node->getDirectives(), function (NameAwareInterface $value) use ($directive) {
144
                return $value->getNameValue() === $directive->getName();
145
            }) : null;
146
147
        if (null !== $directiveNode) {
148
            return $this->coerceArgumentValues($directive, $directiveNode, $variableValues);
149
        }
150
151
        return null;
152
    }
153
154
    /**
155
     * @param Schema                         $schema
156
     * @param array|VariableDefinitionNode[] $variableDefinitionNodes
157
     * @param                                $input
158
     * @return array
159
     * @throws \Exception
160
     */
161
    public function coerceVariableValues(Schema $schema, array $variableDefinitionNodes, array $inputs): array
162
    {
163
        $coercedValues = [];
164
        $errors        = [];
165
166
        foreach ($variableDefinitionNodes as $variableDefinitionNode) {
167
            $variableName = $variableDefinitionNode->getVariable()->getNameValue();
168
            $variableType = typeFromAST($schema, $variableDefinitionNode->getType());
169
170
            $type = $variableType;
171
            if ($variableType instanceof WrappingTypeInterface) {
172
                $type = $variableType->getOfType();
173
            }
174
175
            //!isInputType(varType)
176
            if (!$type instanceof InputTypeInterface) {
177
                throw new GraphQLException('InputTypeInterface');
178
            } else {
179
                if (!isset($inputs[$variableName])) {
180
                    if ($variableType instanceof NonNullType) {
181
                        throw new GraphQLException('NonNullType');
182
                    } elseif ($variableDefinitionNode->getDefaultValue() !== null) {
183
                        $coercedValues[$variableName] = valueFromAST(
184
                            $variableDefinitionNode->getDefaultValue(),
185
                            $variableType
186
                        );
187
                    }
188
                } else {
189
                    $value   = $inputs[$variableName];
190
                    $coerced = $this->coerceValue($value, $variableType, $variableDefinitionNode);
191
                    if (!empty($coerced['errors'])) {
192
                        $messagePrelude = sprintf(
0 ignored issues
show
Unused Code introduced by
The assignment to $messagePrelude is dead and can be removed.
Loading history...
193
                            'Variable "%s" got invalid value %s',
194
                            $variableName, json_encode
195
                            ($value)
196
                        );
197
                    } else {
198
                        $coercedValues[$variableName] = $coerced['coerced'];
199
                    }
200
                }
201
            }
202
        }
203
204
        return [
205
            'errors'  => $errors ?? [],
206
            'coerced' => $coercedValues ?? []
207
        ];
208
    }
209
210
    /**
211
     * @param       $value
212
     * @param       $type
213
     * @param       $blameNode
214
     * @param array $path
215
     * @return array
216
     * @throws GraphQLException
217
     * @throws InvariantException
218
     */
219
    private function coerceValue($value, $type, $blameNode, ?array $path = [])
220
    {
221
        if ($type instanceof NonNullType) {
222
            if (empty($value)) {
223
                throw new GraphQLException(
224
                    sprintf('Expected non-nullable type %s not to be null', (string)$type),
225
                    $blameNode,
226
                    $path
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type array; however, parameter $source of Digia\GraphQL\Error\Grap...xception::__construct() does only seem to accept null|Digia\GraphQL\Language\Source, 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

226
                    /** @scrutinizer ignore-type */ $path
Loading history...
227
                );
228
            }
229
            return $this->coerceValue($value, $type->getOfType(), $blameNode, $path);
230
        }
231
232
        if (empty($value)) {
233
            return ['value' => null, 'errors' => null];
234
        }
235
236
        if ($type instanceof ScalarType) {
237
            try {
238
                $parseResult = $type->parseValue($value);
239
                if (empty($parseResult)) {
240
                    return [
241
                        'errors'  => new GraphQLException(sprintf('Expected type %s', $type->getName())),
242
                        'coerced' => null
243
                    ];
244
                }
245
                return [
246
                    'errors'  => null,
247
                    'coerced' => $parseResult
248
                ];
249
            } catch (\Exception $ex) {
250
                return [
251
                    'errors'  => new GraphQLException(sprintf('Expected type %s', $type->getName())),
252
                    'coerced' => null
253
                ];
254
            }
255
        }
256
257
        if ($type instanceof EnumType) {
258
            if (gettype($value) === 'string') {
259
                $enumValue = $type->getValue($value);
260
                if ($enumValue !== null) {
261
                    return [
262
                        'value'  => $enumValue,
263
                        'errors' => null
264
                    ];
265
                }
266
            }
267
            $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...
268
                return $enumValue->getName();
269
            }, $type->getValues()));
270
        }
271
272
        if ($type instanceof ListType) {
273
            $itemType = $type->getOfType();
274
            if (is_array($value) || $value instanceof \Traversable) {
275
                $errors       = [];
276
                $coercedValue = [];
277
                foreach ($value as $index => $itemValue) {
278
                    $coercedItem = $this->coerceValue(
279
                        $itemValue,
280
                        $itemType,
281
                        $blameNode,
282
                        [$path, $index]
283
                    );
284
285
                    if (!empty($coercedItem['errors'])) {
286
                        $errors = array_merge($errors, $coercedItem['errors']);
287
                    } else {
288
                        $coercedValue[] = $coercedItem['coerced'];
289
                    }
290
                }
291
292
                return [
293
                    'errors'  => $errors ?? [],
294
                    'coerced' => $coercedValue ?? []
295
                ];
296
            }
297
        }
298
299
        if ($type instanceof InputObjectType) {
300
            $errors       = [];
301
            $coercedValue = [];
302
            $fields       = $type->getFields();
303
304
            // Ensure every defined field is valid.
305
            foreach ($fields as $field) {
306
                if (!isset($value[$field->getName()])) {
307
                    if (!empty($field->getDefaultValue())) {
308
                        $coercedValue[$field->getName()] = $field->getDefaultValue();
309
                    } elseif ($type instanceof NonNullType) {
310
                        $errors[] = new GraphQLException(
311
                            sprintf(
312
                                "Field %s of required type %s was not provided",
313
                                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

313
                                implode(",", array_merge(/** @scrutinizer ignore-type */ $path, [$field->getName()])),
Loading history...
314
                                $type->getName()
315
                            )
316
                        );
317
                    }
318
                } else {
319
                    $fieldValue   = $value[$field->getName()];
320
                    $coercedField = $this->coerceValue(
321
                        $fieldValue,
322
                        $field->getType(),
323
                        $blameNode,
324
                        [$path, $field->getName()] // new path
325
                    );
326
327
                    if ($coercedField['errors']) {
328
                        $errors = array_merge($errors, $coercedField['errors']);
329
                    } elseif (empty($errors)) {
330
                        $coercedValue[$field->getName()] = $coercedField['coerced'];
331
                    }
332
                }
333
            }
334
335
            // Ensure every provided field is defined.
336
            foreach ($value as $fieldName => $fieldValue) {
337
                if ($fields[$fieldName] === null) {
338
                    $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...
339
                    $errors[]   = new GraphQLException(
340
                        sprintf('Field "%s" is not defined by type %s', $fieldName, $type->getName())
341
                    );
342
                }
343
            }
344
345
            return [
346
                'errors'  => $errors ?? [],
347
                'coerced' => $coercedValue ?? []
348
            ];
349
        }
350
351
        throw new GraphQLException('Unexpected type.');
352
    }
353
354
    /**
355
     * @param VariableNode  $variableNode
356
     * @param TypeInterface $argumentType
357
     * @param string        $argumentName
358
     * @param array         $variableValues
359
     * @param mixed         $defaultValue
360
     * @return mixed
361
     * @throws ExecutionException
362
     */
363
    protected function coerceValueForVariableNode(
364
        VariableNode $variableNode,
365
        TypeInterface $argumentType,
366
        string $argumentName,
367
        array $variableValues,
368
        $defaultValue
369
    ) {
370
        $variableName = $variableNode->getNameValue();
371
372
        if (!empty($variableValues) && isset($variableValues[$variableName])) {
373
            // Note: this does not check that this variable value is correct.
374
            // This assumes that this query has been validated and the variable
375
            // usage here is of the correct type.
376
            return $variableValues[$variableName];
377
        }
378
379
        if (null !== $defaultValue) {
380
            return $defaultValue;
381
        }
382
383
        if ($argumentType instanceof NonNullType) {
384
            throw new ExecutionException(
385
                \sprintf(
386
                    'Argument "%s" of required type "%s" was provided the variable "%s" which was not provided a runtime value.',
387
                    $argumentName,
388
                    $argumentType,
389
                    $variableName
390
                ),
391
                [$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

391
                [$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...
392
            );
393
        }
394
    }
395
}
396