1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace GraphQL\Error; |
6
|
|
|
|
7
|
|
|
use Exception; |
8
|
|
|
use GraphQL\Language\AST\Node; |
9
|
|
|
use GraphQL\Language\Source; |
10
|
|
|
use GraphQL\Language\SourceLocation; |
11
|
|
|
use GraphQL\Utils\Utils; |
12
|
|
|
use JsonSerializable; |
13
|
|
|
use Throwable; |
14
|
|
|
use Traversable; |
15
|
|
|
use function array_filter; |
16
|
|
|
use function array_map; |
17
|
|
|
use function array_values; |
18
|
|
|
use function is_array; |
19
|
|
|
use function iterator_to_array; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Describes an Error found during the parse, validate, or |
23
|
|
|
* execute phases of performing a GraphQL operation. In addition to a message |
24
|
|
|
* and stack trace, it also includes information about the locations in a |
25
|
|
|
* GraphQL document and/or execution result that correspond to the Error. |
26
|
|
|
* |
27
|
|
|
* When the error was caused by an exception thrown in resolver, original exception |
28
|
|
|
* is available via `getPrevious()`. |
29
|
|
|
* |
30
|
|
|
* Also read related docs on [error handling](error-handling.md) |
31
|
|
|
* |
32
|
|
|
* Class extends standard PHP `\Exception`, so all standard methods of base `\Exception` class |
33
|
|
|
* are available in addition to those listed below. |
34
|
|
|
*/ |
35
|
|
|
class Error extends Exception implements JsonSerializable, ClientAware |
36
|
|
|
{ |
37
|
|
|
const CATEGORY_GRAPHQL = 'graphql'; |
38
|
|
|
const CATEGORY_INTERNAL = 'internal'; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* A message describing the Error for debugging purposes. |
42
|
|
|
* |
43
|
|
|
* @var string |
44
|
|
|
*/ |
45
|
|
|
public $message; |
46
|
|
|
|
47
|
|
|
/** @var SourceLocation[] */ |
48
|
|
|
private $locations; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* An array describing the JSON-path into the execution response which |
52
|
|
|
* corresponds to this error. Only included for errors during execution. |
53
|
|
|
* |
54
|
|
|
* @var mixed[]|null |
55
|
|
|
*/ |
56
|
|
|
public $path; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* An array of GraphQL AST Nodes corresponding to this error. |
60
|
|
|
* |
61
|
|
|
* @var Node[]|null |
62
|
|
|
*/ |
63
|
|
|
public $nodes; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* The source GraphQL document for the first location of this error. |
67
|
|
|
* |
68
|
|
|
* Note that if this Error represents more than one node, the source may not |
69
|
|
|
* represent nodes after the first node. |
70
|
|
|
* |
71
|
|
|
* @var Source|null |
72
|
|
|
*/ |
73
|
|
|
private $source; |
74
|
|
|
|
75
|
|
|
/** @var int[]|null */ |
76
|
|
|
private $positions; |
77
|
|
|
|
78
|
|
|
/** @var bool */ |
79
|
|
|
private $isClientSafe; |
80
|
|
|
|
81
|
|
|
/** @var string */ |
82
|
|
|
protected $category; |
83
|
|
|
|
84
|
|
|
/** @var mixed[]|null */ |
85
|
|
|
protected $extensions; |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* @param string $message |
89
|
|
|
* @param Node|Node[]|Traversable|null $nodes |
90
|
|
|
* @param mixed[]|null $positions |
91
|
|
|
* @param mixed[]|null $path |
92
|
|
|
* @param Throwable $previous |
93
|
|
|
* @param mixed[] $extensions |
94
|
|
|
*/ |
95
|
539 |
|
public function __construct( |
96
|
|
|
$message, |
97
|
|
|
$nodes = null, |
98
|
|
|
?Source $source = null, |
99
|
|
|
$positions = null, |
100
|
|
|
$path = null, |
101
|
|
|
$previous = null, |
102
|
|
|
array $extensions = [] |
103
|
|
|
) { |
104
|
539 |
|
parent::__construct($message, 0, $previous); |
105
|
|
|
|
106
|
|
|
// Compute list of blame nodes. |
107
|
539 |
|
if ($nodes instanceof Traversable) { |
108
|
49 |
|
$nodes = iterator_to_array($nodes); |
109
|
539 |
|
} elseif ($nodes && ! is_array($nodes)) { |
110
|
67 |
|
$nodes = [$nodes]; |
111
|
|
|
} |
112
|
|
|
|
113
|
539 |
|
$this->nodes = $nodes; |
|
|
|
|
114
|
539 |
|
$this->source = $source; |
115
|
539 |
|
$this->positions = $positions; |
116
|
539 |
|
$this->path = $path; |
117
|
539 |
|
$this->extensions = $extensions ?: ( |
118
|
538 |
|
$previous && $previous instanceof self |
119
|
41 |
|
? $previous->extensions |
120
|
538 |
|
: [] |
121
|
|
|
); |
122
|
|
|
|
123
|
539 |
|
if ($previous instanceof ClientAware) { |
124
|
71 |
|
$this->isClientSafe = $previous->isClientSafe(); |
125
|
71 |
|
$this->category = $previous->getCategory() ?: self::CATEGORY_INTERNAL; |
126
|
516 |
|
} elseif ($previous) { |
127
|
28 |
|
$this->isClientSafe = false; |
128
|
28 |
|
$this->category = self::CATEGORY_INTERNAL; |
129
|
|
|
} else { |
130
|
492 |
|
$this->isClientSafe = true; |
131
|
492 |
|
$this->category = self::CATEGORY_GRAPHQL; |
132
|
|
|
} |
133
|
539 |
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Given an arbitrary Error, presumably thrown while attempting to execute a |
137
|
|
|
* GraphQL operation, produce a new GraphQLError aware of the location in the |
138
|
|
|
* document responsible for the original Error. |
139
|
|
|
* |
140
|
|
|
* @param mixed $error |
141
|
|
|
* @param Node[]|null $nodes |
142
|
|
|
* @param mixed[]|null $path |
143
|
|
|
* |
144
|
|
|
* @return Error |
145
|
|
|
*/ |
146
|
61 |
|
public static function createLocatedError($error, $nodes = null, $path = null) |
147
|
|
|
{ |
148
|
61 |
|
if ($error instanceof self) { |
149
|
28 |
|
if ($error->path && $error->nodes) { |
150
|
18 |
|
return $error; |
151
|
|
|
} |
152
|
|
|
|
153
|
10 |
|
$nodes = $nodes ?: $error->nodes; |
154
|
10 |
|
$path = $path ?: $error->path; |
155
|
|
|
} |
156
|
|
|
|
157
|
61 |
|
$source = $positions = $originalError = null; |
158
|
61 |
|
$extensions = []; |
159
|
|
|
|
160
|
61 |
|
if ($error instanceof self) { |
161
|
10 |
|
$message = $error->getMessage(); |
162
|
10 |
|
$originalError = $error; |
163
|
10 |
|
$nodes = $error->nodes ?: $nodes; |
164
|
10 |
|
$source = $error->source; |
165
|
10 |
|
$positions = $error->positions; |
166
|
10 |
|
$extensions = $error->extensions; |
167
|
53 |
|
} elseif ($error instanceof Exception || $error instanceof Throwable) { |
168
|
52 |
|
$message = $error->getMessage(); |
169
|
52 |
|
$originalError = $error; |
170
|
|
|
} else { |
171
|
1 |
|
$message = (string) $error; |
172
|
|
|
} |
173
|
|
|
|
174
|
61 |
|
return new static( |
175
|
61 |
|
$message ?: 'An unknown error occurred.', |
176
|
61 |
|
$nodes, |
177
|
61 |
|
$source, |
178
|
61 |
|
$positions, |
179
|
61 |
|
$path, |
180
|
61 |
|
$originalError, |
181
|
61 |
|
$extensions |
|
|
|
|
182
|
|
|
); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* @return mixed[] |
187
|
|
|
*/ |
188
|
195 |
|
public static function formatError(Error $error) |
189
|
|
|
{ |
190
|
195 |
|
return $error->toSerializableArray(); |
|
|
|
|
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* @inheritdoc |
195
|
|
|
*/ |
196
|
104 |
|
public function isClientSafe() |
197
|
|
|
{ |
198
|
104 |
|
return $this->isClientSafe; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* @inheritdoc |
203
|
|
|
*/ |
204
|
103 |
|
public function getCategory() |
205
|
|
|
{ |
206
|
103 |
|
return $this->category; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* @return Source|null |
211
|
|
|
*/ |
212
|
408 |
|
public function getSource() |
213
|
|
|
{ |
214
|
408 |
|
if ($this->source === null) { |
215
|
349 |
|
if (! empty($this->nodes[0]) && ! empty($this->nodes[0]->loc)) { |
216
|
287 |
|
$this->source = $this->nodes[0]->loc->source; |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
408 |
|
return $this->source; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* @return int[] |
225
|
|
|
*/ |
226
|
407 |
|
public function getPositions() |
227
|
|
|
{ |
228
|
407 |
|
if ($this->positions === null && ! empty($this->nodes)) { |
229
|
295 |
|
$positions = array_map( |
230
|
|
|
static function ($node) { |
231
|
295 |
|
return isset($node->loc) ? $node->loc->start : null; |
232
|
295 |
|
}, |
233
|
295 |
|
$this->nodes |
234
|
|
|
); |
235
|
|
|
|
236
|
295 |
|
$positions = array_filter( |
237
|
295 |
|
$positions, |
238
|
|
|
static function ($p) { |
239
|
295 |
|
return $p !== null; |
240
|
295 |
|
} |
241
|
|
|
); |
242
|
|
|
|
243
|
295 |
|
$this->positions = array_values($positions); |
244
|
|
|
} |
245
|
|
|
|
246
|
407 |
|
return $this->positions; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* An array of locations within the source GraphQL document which correspond to this error. |
251
|
|
|
* |
252
|
|
|
* Each entry has information about `line` and `column` within source GraphQL document: |
253
|
|
|
* $location->line; |
254
|
|
|
* $location->column; |
255
|
|
|
* |
256
|
|
|
* Errors during validation often contain multiple locations, for example to |
257
|
|
|
* point out to field mentioned in multiple fragments. Errors during execution include a |
258
|
|
|
* single location, the field which produced the error. |
259
|
|
|
* |
260
|
|
|
* @return SourceLocation[] |
261
|
|
|
* |
262
|
|
|
* @api |
263
|
|
|
*/ |
264
|
397 |
|
public function getLocations() |
265
|
|
|
{ |
266
|
397 |
|
if ($this->locations === null) { |
267
|
397 |
|
$positions = $this->getPositions(); |
268
|
397 |
|
$source = $this->getSource(); |
269
|
397 |
|
$nodes = $this->nodes; |
270
|
|
|
|
271
|
397 |
|
if ($positions && $source) { |
|
|
|
|
272
|
344 |
|
$this->locations = array_map( |
273
|
|
|
static function ($pos) use ($source) { |
274
|
344 |
|
return $source->getLocation($pos); |
275
|
344 |
|
}, |
276
|
344 |
|
$positions |
277
|
|
|
); |
278
|
56 |
|
} elseif ($nodes) { |
279
|
|
|
$locations = array_filter( |
280
|
|
|
array_map( |
281
|
|
|
static function ($node) { |
282
|
|
|
if ($node->loc && $node->loc->source) { |
283
|
|
|
return $node->loc->source->getLocation($node->loc->start); |
284
|
|
|
} |
285
|
|
|
}, |
286
|
|
|
$nodes |
287
|
|
|
) |
288
|
|
|
); |
289
|
|
|
$this->locations = array_values($locations); |
290
|
|
|
} else { |
291
|
56 |
|
$this->locations = []; |
292
|
|
|
} |
293
|
|
|
} |
294
|
|
|
|
295
|
397 |
|
return $this->locations; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* @return Node[]|null |
300
|
|
|
*/ |
301
|
14 |
|
public function getNodes() |
302
|
|
|
{ |
303
|
14 |
|
return $this->nodes; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Returns an array describing the path from the root value to the field which produced this error. |
308
|
|
|
* Only included for execution errors. |
309
|
|
|
* |
310
|
|
|
* @return mixed[]|null |
311
|
|
|
* |
312
|
|
|
* @api |
313
|
|
|
*/ |
314
|
15 |
|
public function getPath() |
315
|
|
|
{ |
316
|
15 |
|
return $this->path; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* @return mixed[] |
321
|
|
|
*/ |
322
|
85 |
|
public function getExtensions() |
323
|
|
|
{ |
324
|
85 |
|
return $this->extensions; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Returns array representation of error suitable for serialization |
329
|
|
|
* |
330
|
|
|
* @deprecated Use FormattedError::createFromException() instead |
331
|
|
|
* |
332
|
|
|
* @return mixed[] |
333
|
|
|
*/ |
334
|
200 |
|
public function toSerializableArray() |
335
|
|
|
{ |
336
|
|
|
$arr = [ |
337
|
200 |
|
'message' => $this->getMessage(), |
338
|
|
|
]; |
339
|
|
|
|
340
|
200 |
|
$locations = Utils::map( |
341
|
200 |
|
$this->getLocations(), |
342
|
|
|
static function (SourceLocation $loc) { |
343
|
177 |
|
return $loc->toSerializableArray(); |
344
|
200 |
|
} |
345
|
|
|
); |
346
|
|
|
|
347
|
200 |
|
if (! empty($locations)) { |
348
|
177 |
|
$arr['locations'] = $locations; |
349
|
|
|
} |
350
|
200 |
|
if (! empty($this->path)) { |
351
|
2 |
|
$arr['path'] = $this->path; |
352
|
|
|
} |
353
|
200 |
|
if (! empty($this->extensions)) { |
354
|
1 |
|
$arr['extensions'] = $this->extensions; |
355
|
|
|
} |
356
|
|
|
|
357
|
200 |
|
return $arr; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* Specify data which should be serialized to JSON |
362
|
|
|
* |
363
|
|
|
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php |
364
|
|
|
* |
365
|
|
|
* @return mixed data which can be serialized by <b>json_encode</b>, |
366
|
|
|
* which is a value of any type other than a resource. |
367
|
|
|
*/ |
368
|
|
|
public function jsonSerialize() |
369
|
|
|
{ |
370
|
|
|
return $this->toSerializableArray(); |
|
|
|
|
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* @return string |
375
|
|
|
*/ |
376
|
10 |
|
public function __toString() |
377
|
|
|
{ |
378
|
10 |
|
return FormattedError::printError($this); |
379
|
|
|
} |
380
|
|
|
} |
381
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.