Completed
Push — master ( 9e787e...177394 )
by Vladimir
25s queued 13s
created

Value   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 286
Duplicated Lines 0 %

Test Coverage

Coverage 93.15%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 44
eloc 144
c 2
b 0
f 0
dl 0
loc 286
rs 8.8798
ccs 136
cts 146
cp 0.9315

7 Methods

Rating   Name   Duplication   Size   Complexity  
A add() 0 3 2
A ofValue() 0 3 1
A coercionError() 0 19 3
A ofErrors() 0 3 1
A printPath() 0 13 4
A atPath() 0 3 1
F coerceValue() 0 188 32

How to fix   Complexity   

Complex Class

Complex classes like Value often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Value, and based on these observations, apply Extract Interface, too.

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 76
    public static function coerceValue($value, InputType $type, $blameNode = null, ?array $path = null)
43
    {
44 76
        if ($type instanceof NonNull) {
45 34
            if ($value === null) {
46 6
                return self::ofErrors([
47 6
                    self::coercionError(
48 6
                        sprintf('Expected non-nullable type %s not to be null', $type),
49 6
                        $blameNode,
50 6
                        $path
51
                    ),
52
                ]);
53
            }
54
55 31
            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 73
        if ($value === null) {
59
            // Explicitly return the value null.
60 6
            return self::ofValue(null);
61
        }
62
63 69
        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 60
                return self::ofValue($type->parseValue($value));
69 19
            } catch (Exception $error) {
70 19
                return self::ofErrors([
71 19
                    self::coercionError(
72 19
                        sprintf('Expected type %s', $type->name),
73 19
                        $blameNode,
74 19
                        $path,
75 19
                        $error->getMessage(),
76 19
                        $error
77
                    ),
78
                ]);
79
            } catch (Throwable $error) {
80
                return self::ofErrors([
81
                    self::coercionError(
82
                        sprintf('Expected type %s', $type->name),
83
                        $blameNode,
84
                        $path,
85
                        $error->getMessage(),
86
                        $error
87
                    ),
88
                ]);
89
            }
90
        }
91
92 26
        if ($type instanceof EnumType) {
93 7
            if (is_string($value)) {
94 5
                $enumValue = $type->getValue($value);
95 5
                if ($enumValue) {
96 4
                    return self::ofValue($enumValue->value);
97
                }
98
            }
99
100 3
            $suggestions = Utils::suggestionList(
101 3
                Utils::printSafe($value),
102 3
                array_map(
103
                    static function ($enumValue) {
104 3
                        return $enumValue->name;
105 3
                    },
106 3
                    $type->getValues()
107
                )
108
            );
109
110 3
            $didYouMean = $suggestions
111 1
                ? 'did you mean ' . Utils::orList($suggestions) . '?'
112 3
                : null;
113
114 3
            return self::ofErrors([
115 3
                self::coercionError(
116 3
                    sprintf('Expected type %s', $type->name),
117 3
                    $blameNode,
118 3
                    $path,
119 3
                    $didYouMean
120
                ),
121
            ]);
122
        }
123
124 19
        if ($type instanceof ListOfType) {
125 10
            $itemType = $type->getWrappedType();
126 10
            if (is_array($value) || $value instanceof Traversable) {
127 10
                $errors       = [];
128 10
                $coercedValue = [];
129 10
                foreach ($value as $index => $itemValue) {
130 10
                    $coercedItem = self::coerceValue(
131 10
                        $itemValue,
132 10
                        $itemType,
0 ignored issues
show
Bug introduced by
It seems like $itemType can also be of type GraphQL\Type\Definition\InterfaceType and GraphQL\Type\Definition\ObjectType and GraphQL\Type\Definition\UnionType; 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

132
                        /** @scrutinizer ignore-type */ $itemType,
Loading history...
133 10
                        $blameNode,
134 10
                        self::atPath($path, $index)
135
                    );
136 10
                    if ($coercedItem['errors']) {
137 2
                        $errors = self::add($errors, $coercedItem['errors']);
138
                    } else {
139 10
                        $coercedValue[] = $coercedItem['value'];
140
                    }
141
                }
142
143 10
                return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
144
            }
145
            // Lists accept a non-list value as a list of one.
146 1
            $coercedItem = self::coerceValue($value, $itemType, $blameNode);
147
148 1
            return $coercedItem['errors'] ? $coercedItem : self::ofValue([$coercedItem['value']]);
149
        }
150
151 11
        if ($type instanceof InputObjectType) {
152 11
            if (! is_object($value) && ! is_array($value) && ! $value instanceof Traversable) {
153 2
                return self::ofErrors([
154 2
                    self::coercionError(
155 2
                        sprintf('Expected type %s to be an object', $type->name),
156 2
                        $blameNode,
157 2
                        $path
158
                    ),
159
                ]);
160
            }
161
162
            // Cast \stdClass to associative array before checking the fields. Note that the coerced value will be an array.
163 10
            if ($value instanceof stdClass) {
164 2
                $value = (array) $value;
165
            }
166
167 10
            $errors       = [];
168 10
            $coercedValue = [];
169 10
            $fields       = $type->getFields();
170 10
            foreach ($fields as $fieldName => $field) {
171 10
                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

171
                if (array_key_exists($fieldName, /** @scrutinizer ignore-type */ $value)) {
Loading history...
172 9
                    $fieldValue   = $value[$fieldName];
173 9
                    $coercedField = self::coerceValue(
174 9
                        $fieldValue,
175 9
                        $field->getType(),
176 9
                        $blameNode,
177 9
                        self::atPath($path, $fieldName)
178
                    );
179 9
                    if ($coercedField['errors']) {
180 3
                        $errors = self::add($errors, $coercedField['errors']);
181
                    } else {
182 9
                        $coercedValue[$fieldName] = $coercedField['value'];
183
                    }
184 9
                } elseif ($field->defaultValueExists()) {
185
                    $coercedValue[$fieldName] = $field->defaultValue;
186 9
                } elseif ($field->getType() instanceof NonNull) {
187 2
                    $fieldPath = self::printPath(self::atPath($path, $fieldName));
188 2
                    $errors    = self::add(
189 2
                        $errors,
190 2
                        self::coercionError(
191 2
                            sprintf(
192 2
                                'Field %s of required type %s was not provided',
193 2
                                $fieldPath,
194 2
                                $field->type->toString()
195
                            ),
196 10
                            $blameNode
197
                        )
198
                    );
199
                }
200
            }
201
202
            // Ensure every provided field is defined.
203 10
            foreach ($value as $fieldName => $field) {
204 10
                if (array_key_exists($fieldName, $fields)) {
205 9
                    continue;
206
                }
207
208 4
                $suggestions = Utils::suggestionList(
209 4
                    (string) $fieldName,
210 4
                    array_keys($fields)
211
                );
212 4
                $didYouMean  = $suggestions
213 1
                    ? 'did you mean ' . Utils::orList($suggestions) . '?'
214 4
                    : null;
215 4
                $errors      = self::add(
216 4
                    $errors,
217 4
                    self::coercionError(
218 4
                        sprintf('Field "%s" is not defined by type %s', $fieldName, $type->name),
219 4
                        $blameNode,
220 4
                        $path,
221 4
                        $didYouMean
222
                    )
223
                );
224
            }
225
226 10
            return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
227
        }
228
229
        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...
230
    }
231
232 33
    private static function ofErrors($errors)
233
    {
234 33
        return ['errors' => $errors, 'value' => Utils::undefined()];
235
    }
236
237
    /**
238
     * @param string                   $message
239
     * @param Node                     $blameNode
240
     * @param mixed[]|null             $path
241
     * @param string                   $subMessage
242
     * @param Exception|Throwable|null $originalError
243
     *
244
     * @return Error
245
     */
246 33
    private static function coercionError(
247
        $message,
248
        $blameNode,
249
        ?array $path = null,
250
        $subMessage = null,
251
        $originalError = null
252
    ) {
253 33
        $pathStr = self::printPath($path);
254
255
        // Return a GraphQLError instance
256 33
        return new Error(
257
            $message .
258 33
            ($pathStr ? ' at ' . $pathStr : '') .
259 33
            ($subMessage ? '; ' . $subMessage : '.'),
260 33
            $blameNode,
261 33
            null,
262 33
            null,
263 33
            null,
264 33
            $originalError
265
        );
266
    }
267
268
    /**
269
     * Build a string describing the path into the value where the error was found
270
     *
271
     * @param mixed[]|null $path
272
     *
273
     * @return string
274
     */
275 33
    private static function printPath(?array $path = null)
276
    {
277 33
        $pathStr     = '';
278 33
        $currentPath = $path;
279 33
        while ($currentPath) {
280
            $pathStr     =
281 6
                (is_string($currentPath['key'])
282 4
                    ? '.' . $currentPath['key']
283 6
                    : '[' . $currentPath['key'] . ']') . $pathStr;
284 6
            $currentPath = $currentPath['prev'];
285
        }
286
287 33
        return $pathStr ? 'value' . $pathStr : '';
288
    }
289
290
    /**
291
     * @param mixed $value
292
     *
293
     * @return (mixed|null)[]
294
     */
295 49
    private static function ofValue($value)
296
    {
297 49
        return ['errors' => null, 'value' => $value];
298
    }
299
300
    /**
301
     * @param mixed|null $prev
302
     * @param mixed|null $key
303
     *
304
     * @return (mixed|null)[]
305
     */
306 17
    private static function atPath($prev, $key)
307
    {
308 17
        return ['prev' => $prev, 'key' => $key];
309
    }
310
311
    /**
312
     * @param Error[]       $errors
313
     * @param Error|Error[] $moreErrors
314
     *
315
     * @return Error[]
316
     */
317 9
    private static function add($errors, $moreErrors)
318
    {
319 9
        return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]);
320
    }
321
}
322