Failed Conditions
Pull Request — master (#328)
by Šimon
04:02
created

Value::coercionError()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 19
ccs 10
cts 10
cp 1
rs 9.9332
c 0
b 0
f 0
cc 3
nc 1
nop 5
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Utils;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Language\AST\Node;
9
use GraphQL\Type\Definition\EnumType;
10
use GraphQL\Type\Definition\InputObjectType;
11
use GraphQL\Type\Definition\InputType;
12
use GraphQL\Type\Definition\ListOfType;
13
use GraphQL\Type\Definition\NonNull;
14
use GraphQL\Type\Definition\ScalarType;
15
use function array_key_exists;
16
use function array_keys;
17
use function array_map;
18
use function array_merge;
19
use function is_array;
20
use function is_object;
21
use function is_string;
22
use function sprintf;
23
24
/**
25
 * Coerces a PHP value given a GraphQL Type.
26
 *
27
 * Returns either a value which is valid for the provided type or a list of
28
 * encountered coercion errors.
29
 */
30
class Value
31
{
32
    /**
33
     * Given a type and any value, return a runtime value coerced to match the type.
34
     *
35
     * @param mixed[] $path
36
     */
37 71
    public static function coerceValue($value, InputType $type, $blameNode = null, ?array $path = null)
38
    {
39 71
        if ($type instanceof NonNull) {
40 32
            if ($value === null) {
41 6
                return self::ofErrors([
42 6
                    self::coercionError(
43 6
                        sprintf('Expected non-nullable type %s not to be null', $type),
44 6
                        $blameNode,
45 6
                        $path
46
                    ),
47
                ]);
48
            }
49
50 29
            return self::coerceValue($value, $type->getWrappedType(), $blameNode, $path);
0 ignored issues
show
Bug introduced by
It seems like $type->getWrappedType() can also be of type GraphQL\Type\Definition\UnionType and GraphQL\Type\Definition\ObjectType and GraphQL\Type\Definition\InterfaceType; however, parameter $type of GraphQL\Utils\Value::coerceValue() does only seem to accept GraphQL\Type\Definition\InputType, 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

50
            return self::coerceValue($value, /** @scrutinizer ignore-type */ $type->getWrappedType(), $blameNode, $path);
Loading history...
51
        }
52
53 68
        if ($value === null) {
54
            // Explicitly return the value null.
55 4
            return self::ofValue(null);
56
        }
57
58 66
        if ($type instanceof ScalarType) {
59
            // Scalars determine if a value is valid via parseValue(), which can
60
            // throw to indicate failure. If it throws, maintain a reference to
61
            // the original error.
62
            try {
63 58
                return self::ofValue($type->parseValue($value));
64 19
            } catch (\Exception $error) {
65 19
                return self::ofErrors([
66 19
                    self::coercionError(
67 19
                        sprintf('Expected type %s', $type->name),
68 19
                        $blameNode,
69 19
                        $path,
70 19
                        $error->getMessage(),
71 19
                        $error
72
                    ),
73
                ]);
74
            } catch (\Throwable $error) {
75
                return self::ofErrors([
76
                    self::coercionError(
77
                        sprintf('Expected type %s', $type->name),
78
                        $blameNode,
79
                        $path,
80
                        $error->getMessage(),
81
                        $error
82
                    ),
83
                ]);
84
            }
85
        }
86
87 23
        if ($type instanceof EnumType) {
88 7
            if (is_string($value)) {
89 5
                $enumValue = $type->getValue($value);
90 5
                if ($enumValue) {
91 4
                    return self::ofValue($enumValue->value);
92
                }
93
            }
94
95 3
            $suggestions = Utils::suggestionList(
96 3
                Utils::printSafe($value),
97 3
                array_map(
98
                    function ($enumValue) {
99 3
                        return $enumValue->name;
100 3
                    },
101 3
                    $type->getValues()
102
                )
103
            );
104
105 3
            $didYouMean = $suggestions
106 1
                ? 'did you mean ' . Utils::orList($suggestions) . '?'
107 3
                : null;
108
109 3
            return self::ofErrors([
110 3
                self::coercionError(
111 3
                    sprintf('Expected type %s', $type->name),
112 3
                    $blameNode,
113 3
                    $path,
114 3
                    $didYouMean
115
                ),
116
            ]);
117
        }
118
119 16
        if ($type instanceof ListOfType) {
120 9
            $itemType = $type->getWrappedType();
121 9
            if (is_array($value) || $value instanceof \Traversable) {
122 9
                $errors       = [];
123 9
                $coercedValue = [];
124 9
                foreach ($value as $index => $itemValue) {
125 9
                    $coercedItem = self::coerceValue(
126 9
                        $itemValue,
127 9
                        $itemType,
128 9
                        $blameNode,
129 9
                        self::atPath($path, $index)
130
                    );
131 9
                    if ($coercedItem['errors']) {
132 2
                        $errors = self::add($errors, $coercedItem['errors']);
133
                    } else {
134 9
                        $coercedValue[] = $coercedItem['value'];
135
                    }
136
                }
137
138 9
                return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
139
            }
140
            // Lists accept a non-list value as a list of one.
141 1
            $coercedItem = self::coerceValue($value, $itemType, $blameNode);
142
143 1
            return $coercedItem['errors'] ? $coercedItem : self::ofValue([$coercedItem['value']]);
144
        }
145
146 8
        if ($type instanceof InputObjectType) {
147 8
            if (! is_object($value) && ! is_array($value) && ! $value instanceof \Traversable) {
148 2
                return self::ofErrors([
149 2
                    self::coercionError(
150 2
                        sprintf('Expected type %s to be an object', $type->name),
151 2
                        $blameNode,
152 2
                        $path
153
                    ),
154
                ]);
155
            }
156
157 7
            $errors       = [];
158 7
            $coercedValue = [];
159 7
            $fields       = $type->getFields();
160 7
            foreach ($fields as $fieldName => $field) {
161 7
                if (array_key_exists($fieldName, $value)) {
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type object and Traversable; however, parameter $search of array_key_exists() 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

161
                if (array_key_exists($fieldName, /** @scrutinizer ignore-type */ $value)) {
Loading history...
162 7
                    $fieldValue   = $value[$fieldName];
163 7
                    $coercedField = self::coerceValue(
164 7
                        $fieldValue,
165 7
                        $field->getType(),
0 ignored issues
show
Bug introduced by
It seems like $field->getType() can also be of type callable; however, parameter $type of GraphQL\Utils\Value::coerceValue() does only seem to accept GraphQL\Type\Definition\InputType, 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

165
                        /** @scrutinizer ignore-type */ $field->getType(),
Loading history...
166 7
                        $blameNode,
167 7
                        self::atPath($path, $fieldName)
168
                    );
169 7
                    if ($coercedField['errors']) {
170 3
                        $errors = self::add($errors, $coercedField['errors']);
171
                    } else {
172 7
                        $coercedValue[$fieldName] = $coercedField['value'];
173
                    }
174 6
                } elseif ($field->defaultValueExists()) {
175
                    $coercedValue[$fieldName] = $field->defaultValue;
176 6
                } elseif ($field->getType() instanceof NonNull) {
177 2
                    $fieldPath = self::printPath(self::atPath($path, $fieldName));
178 2
                    $errors    = self::add(
179 2
                        $errors,
180 2
                        self::coercionError(
181 2
                            sprintf(
182 2
                                'Field %s of required type %s was not provided',
183 2
                                $fieldPath,
184 2
                                $field->type->toString()
0 ignored issues
show
Bug introduced by
The method toString() does not exist on GraphQL\Type\Definition\InputType. Since it exists in all sub-types, consider adding an abstract or default implementation to GraphQL\Type\Definition\InputType. ( Ignorable by Annotation )

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

184
                                $field->type->/** @scrutinizer ignore-call */ 
185
                                              toString()
Loading history...
185
                            ),
186 7
                            $blameNode
187
                        )
188
                    );
189
                }
190
            }
191
192
            // Ensure every provided field is defined.
193 7
            foreach ($value as $fieldName => $field) {
194 7
                if (array_key_exists($fieldName, $fields)) {
195 7
                    continue;
196
                }
197
198 3
                $suggestions = Utils::suggestionList(
199 3
                    $fieldName,
200 3
                    array_keys($fields)
201
                );
202 3
                $didYouMean  = $suggestions
203 1
                    ? 'did you mean ' . Utils::orList($suggestions) . '?'
204 3
                    : null;
205 3
                $errors      = self::add(
206 3
                    $errors,
207 3
                    self::coercionError(
208 3
                        sprintf('Field "%s" is not defined by type %s', $fieldName, $type->name),
209 3
                        $blameNode,
210 3
                        $path,
211 3
                        $didYouMean
212
                    )
213
                );
214
            }
215
216 7
            return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
217
        }
218
219
        throw new Error(sprintf('Unexpected type %s', $type->name));
0 ignored issues
show
Bug introduced by
Accessing name on the interface GraphQL\Type\Definition\InputType suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
220
    }
221
222 32
    private static function ofErrors($errors)
223
    {
224 32
        return ['errors' => $errors, 'value' => Utils::undefined()];
225
    }
226
227
    /**
228
     * @param string                     $message
229
     * @param Node                       $blameNode
230
     * @param mixed[]|null               $path
231
     * @param string                     $subMessage
232
     * @param \Exception|\Throwable|null $originalError
233
     * @return Error
234
     */
235 32
    private static function coercionError(
236
        $message,
237
        $blameNode,
238
        ?array $path = null,
239
        $subMessage = null,
240
        $originalError = null
241
    ) {
242 32
        $pathStr = self::printPath($path);
243
244
        // Return a GraphQLError instance
245 32
        return new Error(
246
            $message .
247 32
            ($pathStr ? ' at ' . $pathStr : '') .
248 32
            ($subMessage ? '; ' . $subMessage : '.'),
249 32
            $blameNode,
250 32
            null,
251 32
            null,
252 32
            null,
253 32
            $originalError
254
        );
255
    }
256
257
    /**
258
     * Build a string describing the path into the value where the error was found
259
     *
260
     * @param mixed[]|null $path
261
     * @return string
262
     */
263 32
    private static function printPath(?array $path = null)
264
    {
265 32
        $pathStr     = '';
266 32
        $currentPath = $path;
267 32
        while ($currentPath) {
268
            $pathStr     =
269 6
                (is_string($currentPath['key'])
270 4
                    ? '.' . $currentPath['key']
271 6
                    : '[' . $currentPath['key'] . ']') . $pathStr;
272 6
            $currentPath = $currentPath['prev'];
273
        }
274
275 32
        return $pathStr ? 'value' . $pathStr : '';
276
    }
277
278
    /**
279
     * @param mixed $value
280
     * @return (mixed|null)[]
281
     */
282 45
    private static function ofValue($value)
283
    {
284 45
        return ['errors' => null, 'value' => $value];
285
    }
286
287
    /**
288
     * @param mixed|null $prev
289
     * @param mixed|null $key
290
     * @return (mixed|null)[]
291
     */
292 15
    private static function atPath($prev, $key)
293
    {
294 15
        return ['prev' => $prev, 'key' => $key];
295
    }
296
297
    /**
298
     * @param Error[]       $errors
299
     * @param Error|Error[] $moreErrors
300
     * @return Error[]
301
     */
302 8
    private static function add($errors, $moreErrors)
303
    {
304 8
        return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]);
305
    }
306
}
307