Value::coerceValue()   F
last analyzed

Complexity

Conditions 31
Paths 116

Size

Total Lines 178
Code Lines 113

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 109
CRAP Score 31.0056

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 113
c 2
b 0
f 0
dl 0
loc 178
ccs 109
cts 111
cp 0.982
rs 3.2266
cc 31
nc 116
nop 4
crap 31.0056

How to fix   Long Method    Complexity   

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
declare(strict_types=1);
4
5
namespace GraphQL\Utils;
6
7
use Exception;
8
use GraphQL\Error\Error;
9
use GraphQL\Language\AST\Node;
10
use GraphQL\Type\Definition\EnumType;
11
use GraphQL\Type\Definition\InputObjectType;
12
use GraphQL\Type\Definition\InputType;
13
use GraphQL\Type\Definition\ListOfType;
14
use GraphQL\Type\Definition\NonNull;
15
use GraphQL\Type\Definition\ScalarType;
16
use stdClass;
17
use Throwable;
18
use Traversable;
19
use function array_key_exists;
20
use function array_keys;
21
use function array_map;
22
use function array_merge;
23
use function is_array;
24
use function is_object;
25
use function is_string;
26
use function sprintf;
27
28
/**
29
 * Coerces a PHP value given a GraphQL Type.
30
 *
31
 * Returns either a value which is valid for the provided type or a list of
32
 * encountered coercion errors.
33
 */
34
class Value
35
{
36
    /**
37
     * Given a type and any value, return a runtime value coerced to match the type.
38
     *
39
     * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type
40
     * @param mixed[]                                                $path
41
     */
42 81
    public static function coerceValue($value, InputType $type, $blameNode = null, ?array $path = null)
43
    {
44 81
        if ($type instanceof NonNull) {
45 33
            if ($value === null) {
46 3
                return self::ofErrors([
47 3
                    self::coercionError(
48 3
                        sprintf('Expected non-nullable type %s not to be null', $type),
49 3
                        $blameNode,
50 3
                        $path
51
                    ),
52
                ]);
53
            }
54
55 33
            return self::coerceValue($value, $type->getWrappedType(), $blameNode, $path);
0 ignored issues
show
Bug introduced by
$type->getWrappedType() of type GraphQL\Type\Definition\Type is incompatible with the type GraphQL\Type\Definition\InputType expected by parameter $type of GraphQL\Utils\Value::coerceValue(). ( Ignorable by Annotation )

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

55
            return self::coerceValue($value, /** @scrutinizer ignore-type */ $type->getWrappedType(), $blameNode, $path);
Loading history...
56
        }
57
58 81
        if ($value === null) {
59
            // Explicitly return the value null.
60 4
            return self::ofValue(null);
61
        }
62
63 79
        if ($type instanceof ScalarType) {
64
            // Scalars determine if a value is valid via parseValue(), which can
65
            // throw to indicate failure. If it throws, maintain a reference to
66
            // the original error.
67
            try {
68 70
                return self::ofValue($type->parseValue($value));
69 27
            } catch (Throwable $error) {
70 27
                return self::ofErrors([
71 27
                    self::coercionError(
72 27
                        sprintf('Expected type %s', $type->name),
73 27
                        $blameNode,
74 27
                        $path,
75 27
                        $error->getMessage(),
76 27
                        $error
77
                    ),
78
                ]);
79
            }
80
        }
81
82 27
        if ($type instanceof EnumType) {
83 7
            if (is_string($value)) {
84 5
                $enumValue = $type->getValue($value);
85 5
                if ($enumValue) {
86 4
                    return self::ofValue($enumValue->value);
87
                }
88
            }
89
90 3
            $suggestions = Utils::suggestionList(
91 3
                Utils::printSafe($value),
92 3
                array_map(
93
                    static function ($enumValue) {
94 3
                        return $enumValue->name;
95 3
                    },
96 3
                    $type->getValues()
97
                )
98
            );
99
100 3
            $didYouMean = $suggestions
101 1
                ? 'did you mean ' . Utils::orList($suggestions) . '?'
102 3
                : null;
103
104 3
            return self::ofErrors([
105 3
                self::coercionError(
106 3
                    sprintf('Expected type %s', $type->name),
107 3
                    $blameNode,
108 3
                    $path,
109 3
                    $didYouMean
110
                ),
111
            ]);
112
        }
113
114 20
        if ($type instanceof ListOfType) {
115 11
            $itemType = $type->getWrappedType();
116 11
            if (is_array($value) || $value instanceof Traversable) {
117 10
                $errors       = [];
118 10
                $coercedValue = [];
119 10
                foreach ($value as $index => $itemValue) {
120 10
                    $coercedItem = self::coerceValue(
121 10
                        $itemValue,
122 10
                        $itemType,
123 10
                        $blameNode,
124 10
                        self::atPath($path, $index)
125
                    );
126 10
                    if ($coercedItem['errors']) {
127 2
                        $errors = self::add($errors, $coercedItem['errors']);
128
                    } else {
129 10
                        $coercedValue[] = $coercedItem['value'];
130
                    }
131
                }
132
133 10
                return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
134
            }
135
            // Lists accept a non-list value as a list of one.
136 2
            $coercedItem = self::coerceValue($value, $itemType, $blameNode);
137
138 2
            return $coercedItem['errors'] ? $coercedItem : self::ofValue([$coercedItem['value']]);
139
        }
140
141 12
        if ($type instanceof InputObjectType) {
142 12
            if (! is_object($value) && ! is_array($value) && ! $value instanceof Traversable) {
143 2
                return self::ofErrors([
144 2
                    self::coercionError(
145 2
                        sprintf('Expected type %s to be an object', $type->name),
146 2
                        $blameNode,
147 2
                        $path
148
                    ),
149
                ]);
150
            }
151
152
            // Cast \stdClass to associative array before checking the fields. Note that the coerced value will be an array.
153 11
            if ($value instanceof stdClass) {
154 2
                $value = (array) $value;
155
            }
156
157 11
            $errors       = [];
158 11
            $coercedValue = [];
159 11
            $fields       = $type->getFields();
160 11
            foreach ($fields as $fieldName => $field) {
161 11
                if (array_key_exists($fieldName, $value)) {
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type Traversable and object; 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 10
                    $fieldValue   = $value[$fieldName];
163 10
                    $coercedField = self::coerceValue(
164 10
                        $fieldValue,
165 10
                        $field->getType(),
166 10
                        $blameNode,
167 10
                        self::atPath($path, $fieldName)
168
                    );
169 10
                    if ($coercedField['errors']) {
170 3
                        $errors = self::add($errors, $coercedField['errors']);
171
                    } else {
172 10
                        $coercedValue[$fieldName] = $coercedField['value'];
173
                    }
174 10
                } elseif ($field->defaultValueExists()) {
175
                    $coercedValue[$fieldName] = $field->defaultValue;
176 10
                } 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()
185
                            ),
186 11
                            $blameNode
187
                        )
188
                    );
189
                }
190
            }
191
192
            // Ensure every provided field is defined.
193 11
            foreach ($value as $fieldName => $field) {
194 11
                if (array_key_exists($fieldName, $fields)) {
195 10
                    continue;
196
                }
197
198 4
                $suggestions = Utils::suggestionList(
199 4
                    (string) $fieldName,
200 4
                    array_keys($fields)
201
                );
202 4
                $didYouMean  = $suggestions
203 1
                    ? 'did you mean ' . Utils::orList($suggestions) . '?'
204 4
                    : null;
205 4
                $errors      = self::add(
206 4
                    $errors,
207 4
                    self::coercionError(
208 4
                        sprintf('Field "%s" is not defined by type %s', $fieldName, $type->name),
209 4
                        $blameNode,
210 4
                        $path,
211 4
                        $didYouMean
212
                    )
213
                );
214
            }
215
216 11
            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 38
    private static function ofErrors($errors)
223
    {
224 38
        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
     *
234
     * @return Error
235
     */
236 38
    private static function coercionError(
237
        $message,
238
        $blameNode,
239
        ?array $path = null,
240
        $subMessage = null,
241
        $originalError = null
242
    ) {
243 38
        $pathStr = self::printPath($path);
244
245
        // Return a GraphQLError instance
246 38
        return new Error(
247
            $message .
248 38
            ($pathStr ? ' at ' . $pathStr : '') .
249 38
            ($subMessage ? '; ' . $subMessage : '.'),
250 38
            $blameNode,
251 38
            null,
252 38
            null,
253 38
            null,
254 38
            $originalError
255
        );
256
    }
257
258
    /**
259
     * Build a string describing the path into the value where the error was found
260
     *
261
     * @param mixed[]|null $path
262
     *
263
     * @return string
264
     */
265 38
    private static function printPath(?array $path = null)
266
    {
267 38
        $pathStr     = '';
268 38
        $currentPath = $path;
269 38
        while ($currentPath) {
270
            $pathStr     =
271 6
                (is_string($currentPath['key'])
272 4
                    ? '.' . $currentPath['key']
273 6
                    : '[' . $currentPath['key'] . ']') . $pathStr;
274 6
            $currentPath = $currentPath['prev'];
275
        }
276
277 38
        return $pathStr ? 'value' . $pathStr : '';
278
    }
279
280
    /**
281
     * @param mixed $value
282
     *
283
     * @return (mixed|null)[]
284
     */
285 49
    private static function ofValue($value)
286
    {
287 49
        return ['errors' => null, 'value' => $value];
288
    }
289
290
    /**
291
     * @param mixed|null $prev
292
     * @param mixed|null $key
293
     *
294
     * @return (mixed|null)[]
295
     */
296 18
    private static function atPath($prev, $key)
297
    {
298 18
        return ['prev' => $prev, 'key' => $key];
299
    }
300
301
    /**
302
     * @param Error[]       $errors
303
     * @param Error|Error[] $moreErrors
304
     *
305
     * @return Error[]
306
     */
307 9
    private static function add($errors, $moreErrors)
308
    {
309 9
        return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]);
310
    }
311
}
312