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