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
![]() |
|||||
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
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
![]() |
|||||
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
|
|||||
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 |