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); |
|
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)) { |
|
162 | 7 | $fieldValue = $value[$fieldName]; |
|
163 | 7 | $coercedField = self::coerceValue( |
|
164 | 7 | $fieldValue, |
|
165 | 7 | $field->getType(), |
|
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
![]() |
|||
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
|
|||
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 |