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

ValuesHelper::coerceVariableValues()   C

Complexity

Conditions 9
Paths 15

Size

Total Lines 52
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 52
rs 6.5703
c 0
b 0
f 0
cc 9
eloc 35
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
     * Note: The returned value is a plain Object with a prototype, since it is
41
     * exposed to user code. Care should be taken to not pull values from the
42
     * Object prototype.
43
     *
44
     * @see http://facebook.github.io/graphql/October2016/#CoerceArgumentValues()
45
     *
46
     * @param Field|Directive         $definition
47
     * @param ArgumentsAwareInterface $node
48
     * @param array                   $variableValues
49
     * @return array
50
     * @throws ExecutionException
51
     * @throws InvalidTypeException
52
     * @throws InvariantException
53
     */
54
    public function coerceArgumentValues($definition, ArgumentsAwareInterface $node, array $variableValues = []): array
55
    {
56
        $coercedValues       = [];
57
        $argumentDefinitions = $definition->getArguments();
58
        $argumentNodes       = $node->getArguments();
59
60
        if (empty($argumentDefinitions) || empty($argumentNodes)) {
61
            return $coercedValues;
62
        }
63
64
        $argumentNodeMap = keyMap($argumentNodes, function (ArgumentNode $value) {
65
            return $value->getNameValue();
66
        });
67
68
        foreach ($argumentDefinitions as $argumentDefinition) {
69
            $argumentName = $argumentDefinition->getName();
70
            $argumentType = $argumentDefinition->getType();
71
            /** @var ArgumentNode $argumentNode */
72
            $argumentNode = $argumentNodeMap[$argumentName];
73
            $defaultValue = $argumentDefinition->getDefaultValue();
74
75
            if (null === $argumentNode) {
76
                if (null === $defaultValue) {
77
                    $coercedValues[$argumentName] = $defaultValue;
78
                } elseif (!$argumentType instanceof NonNullType) {
79
                    throw new ExecutionException(
80
                        sprintf('Argument "%s" of required type "%s" was not provided.', $argumentName, $argumentType),
81
                        [$node]
82
                    );
83
                }
84
            } elseif ($argumentNode instanceof VariableNode) {
85
                $coercedValues[$argumentName] = $this->coerceValueForVariableNode(
86
                    $argumentNode,
87
                    $argumentType,
88
                    $argumentName,
89
                    $variableValues,
90
                    $defaultValue
91
                );
92
            } else {
93
                $coercedValue = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $coercedValue is dead and can be removed.
Loading history...
94
95
                try {
96
                    $coercedValue = valueFromAST($argumentNode->getValue(), $argumentType, $variableValues);
97
                } catch (CoercingException $ex) {
98
                    // Value nodes that cannot be resolved should be treated as invalid values
99
                    // therefore we catch the exception and leave the `$coercedValue` as `null`.
100
                }
101
102
                if (null === $coercedValue) {
103
                    // Note: ValuesOfCorrectType validation should catch this before
104
                    // execution. This is a runtime check to ensure execution does not
105
                    // continue with an invalid argument value.
106
                    throw new ExecutionException(
107
                        sprintf('Argument "%s" has invalid value %s.', $argumentName, $argumentNode),
108
                        [$argumentNode->getValue()]
109
                    );
110
                }
111
112
                $coercedValues[$argumentName] = $coercedValue;
113
            }
114
        }
115
116
        return $coercedValues;
117
    }
118
119
    /**
120
     * Prepares an object map of argument values given a directive definition
121
     * and a AST node which may contain directives. Optionally also accepts a map
122
     * of variable values.
123
     *
124
     * If the directive does not exist on the node, returns undefined.
125
     *
126
     * Note: The returned value is a plain Object with a prototype, since it is
127
     * exposed to user code. Care should be taken to not pull values from the
128
     * Object prototype.
129
     *
130
     * @param Directive $directive
131
     * @param mixed     $node
132
     * @param array     $variableValues
133
     * @return array|null
134
     * @throws ExecutionException
135
     * @throws InvalidTypeException
136
     * @throws InvariantException
137
     */
138
    public function coerceDirectiveValues(
139
        Directive $directive,
140
        $node,
141
        array $variableValues = []
142
    ): ?array {
143
        $directiveNode = $node->hasDirectives()
144
            ? find($node->getDirectives(), function (NameAwareInterface $value) use ($directive) {
145
                return $value->getNameValue() === $directive->getName();
146
            }) : null;
147
148
        if (null !== $directiveNode) {
149
            return $this->coerceArgumentValues($directive, $directiveNode, $variableValues);
150
        }
151
152
        return null;
153
    }
154
155
    /**
156
     * @param Schema                         $schema
157
     * @param array|VariableDefinitionNode[] $variableDefinitionNodes
158
     * @param                                $input
159
     * @return CoercedValue
160
     * @throws \Exception
161
     */
162
    public function coerceVariableValues(Schema $schema, array $variableDefinitionNodes, array $inputs): CoercedValue
163
    {
164
        $coercedValues = [];
165
        $errors        = [];
166
167
        foreach ($variableDefinitionNodes as $variableDefinitionNode) {
168
            $variableName = $variableDefinitionNode->getVariable()->getNameValue();
169
            $variableType = typeFromAST($schema, $variableDefinitionNode->getType());
170
171
            $type = $variableType;
172
            if ($variableType instanceof WrappingTypeInterface) {
173
                $type = $variableType->getOfType();
174
            }
175
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
                    $coercedValue = $this->coerceValue($value, $variableType, $variableDefinitionNode);
191
                    if ($coercedValue->hasErrors()) {
192
                        $messagePrelude = sprintf(
193
                            'Variable "$%s" got invalid value %s',
194
                            $variableName, json_encode
195
                            ($value)
196
                        );
197
                        foreach ($coercedValue->getErrors() as $error) {
198
                            $errors[] = $this->buildCoerceException(
199
                                $messagePrelude,
200
                                $variableDefinitionNode,
201
                                [],
202
                                $error->getMessage(),
203
                                $error
204
                            );
205
                        }
206
                    } else {
207
                        $coercedValues[$variableName] = $coercedValue->getValue();
208
                    }
209
                }
210
            }
211
        }
212
213
        return new CoercedValue($coercedValues, $errors);
214
    }
215
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
            try {
246
                $parseResult = $type->parseValue($value);
247
                if (empty($parseResult)) {
248
                    return new CoercedValue(null, [
249
                        new GraphQLException(sprintf('Expected type %s', $type->getName()))
250
                    ]);
251
                }
252
                return new CoercedValue($parseResult, null);
253
            } catch (\Exception $ex) {
254
                //@TODO Check exception message
255
                return new CoercedValue(null, [
256
                    new GraphQLException(sprintf('Expected type %s', $type->getName()))
257
                ]);
258
            }
259
        }
260
261
        if ($type instanceof EnumType) {
262
            if (is_string($value)) {
263
                $enumValue = $type->getValue($value);
264
                if ($enumValue !== null) {
265
                    return new CoercedValue($enumValue, null);
266
                }
267
            }
268
            $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...
269
                return $enumValue->getName();
270
            }, $type->getValues()));
271
272
            //@TODO throw proper error
273
        }
274
275
        if ($type instanceof ListType) {
276
            $itemType = $type->getOfType();
277
            if (is_array($value) || $value instanceof \Traversable) {
278
                $errors        = [];
279
                $coercedValues = [];
280
                foreach ($value as $index => $itemValue) {
281
                    $coercedValue = $this->coerceValue(
282
                        $itemValue,
283
                        $itemType,
284
                        $blameNode,
285
                        [$path, $index]
286
                    );
287
288
                    if ($coercedValue->hasErrors()) {
289
                        $errors = array_merge($errors, $coercedValue->getErrors());
290
                    } else {
291
                        $coercedValues[] = $coercedValue->getValue();
292
                    }
293
                }
294
295
                return new CoercedValue($coercedValues, $errors);
296
            }
297
298
            // Lists accept a non-list value as a list of one.
299
            $coercedValue = $this->coerceValue($value, $itemType, $blameNode);
300
            return new CoercedValue([$coercedValue->getValue()], $coercedValue->getErrors());
301
        }
302
303
        if ($type instanceof InputObjectType) {
304
            $errors        = [];
305
            $coercedValues = [];
306
            $fields        = $type->getFields();
307
308
            // Ensure every defined field is valid.
309
            foreach ($fields as $field) {
310
                if (!array_key_exists($field->getName(), $value)) {
311
                    if (!empty($field->getDefaultValue())) {
312
                        $coercedValue[$field->getName()] = $field->getDefaultValue();
313
                    } elseif ($type instanceof NonNullType) {
314
                        $errors[] = new GraphQLException(
315
                            sprintf(
316
                                "Field %s of required type %s was not provided",
317
                                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

317
                                implode(",", array_merge(/** @scrutinizer ignore-type */ $path, [$field->getName()])),
Loading history...
318
                                $type->getName()
319
                            )
320
                        );
321
                    }
322
                } else {
323
                    $fieldValue   = $value[$field->getName()];
324
                    $coercedValue = $this->coerceValue(
325
                        $fieldValue,
326
                        $field->getType(),
327
                        $blameNode,
328
                        [$path, $field->getName()] // new path
329
                    );
330
331
                    if ($coercedValue->hasErrors()) {
332
                        $errors = array_merge($errors, $coercedValue->getErrors());
333
                    } elseif (empty($errors)) {
334
                        $coercedValues[$field->getName()] = $coercedValue->getValue();
335
                    }
336
                }
337
            }
338
339
            // Ensure every provided field is defined.
340
            foreach ($value as $fieldName => $fieldValue) {
341
                if ($fields[$fieldName] === null) {
342
                    $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...
343
                    $errors[]   = new GraphQLException(
344
                        sprintf('Field "%s" is not defined by type %s', $fieldName, $type->getName())
345
                    );
346
                }
347
            }
348
349
            return new CoercedValue($coercedValues, $errors);
350
        }
351
352
        throw new GraphQLException('Unexpected type.');
353
    }
354
355
    /**
356
     * @param string                $message
357
     * @param NodeInterface         $blameNode
358
     * @param array|null            $path
359
     * @param null|string           $subMessage
360
     * @param GraphQLException|null $originalError
361
     * @return GraphQLException
362
     */
363
    protected function buildCoerceException(
364
        string $message,
365
        NodeInterface $blameNode,
366
        ?array $path,
367
        ?string $subMessage = null,
368
        ?GraphQLException $originalError = null
369
    ) {
370
        $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

370
        $stringPath = implode(".", /** @scrutinizer ignore-type */ $path);
Loading history...
371
372
        return new CoercingException(
373
            $message .
374
            (($stringPath !== '') ? ' at value' . $stringPath : $stringPath) .
375
            (($subMessage !== null) ? '; ' . $subMessage : '.'),
376
            [$blameNode],
377
            null,
378
            null,
379
            [],
380
            $originalError
381
        );
382
    }
383
384
    /**
385
     * @param VariableNode  $variableNode
386
     * @param TypeInterface $argumentType
387
     * @param string        $argumentName
388
     * @param array         $variableValues
389
     * @param mixed         $defaultValue
390
     * @return mixed
391
     * @throws ExecutionException
392
     */
393
    protected function coerceValueForVariableNode(
394
        VariableNode $variableNode,
395
        TypeInterface $argumentType,
396
        string $argumentName,
397
        array $variableValues,
398
        $defaultValue
399
    ) {
400
        $variableName = $variableNode->getNameValue();
401
402
        if (!empty($variableValues) && isset($variableValues[$variableName])) {
403
            // Note: this does not check that this variable value is correct.
404
            // This assumes that this query has been validated and the variable
405
            // usage here is of the correct type.
406
            return $variableValues[$variableName];
407
        }
408
409
        if (null !== $defaultValue) {
410
            return $defaultValue;
411
        }
412
413
        if ($argumentType instanceof NonNullType) {
414
            throw new ExecutionException(
415
                \sprintf(
416
                    'Argument "%s" of required type "%s" was provided the variable "%s" which was not provided a runtime value.',
417
                    $argumentName,
418
                    $argumentType,
419
                    $variableName
420
                ),
421
                [$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

421
                [$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...
422
            );
423
        }
424
    }
425
}
426