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\Type\Definition\Type; |
9
|
|
|
use GraphQL\Type\Definition\WrappingType; |
10
|
|
|
use GraphQL\Utils\Utils; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* This class is used for [default error formatting](error-handling.md). |
14
|
|
|
* It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors) |
15
|
|
|
* and provides tools for error debugging. |
16
|
|
|
*/ |
17
|
|
|
class FormattedError |
18
|
|
|
{ |
19
|
|
|
private static $internalErrorMessage = 'Internal server error'; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Set default error message for internal errors formatted using createFormattedError(). |
23
|
|
|
* This value can be overridden by passing 3rd argument to `createFormattedError()`. |
24
|
|
|
* |
25
|
|
|
* @api |
26
|
|
|
* @param string $msg |
27
|
|
|
*/ |
28
|
|
|
public static function setInternalErrorMessage($msg) |
29
|
|
|
{ |
30
|
|
|
self::$internalErrorMessage = $msg; |
31
|
|
|
} |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Prints a GraphQLError to a string, representing useful location information |
35
|
|
|
* about the error's position in the source. |
36
|
|
|
* |
37
|
|
|
* @param Error $error |
38
|
|
|
* @return string |
39
|
|
|
*/ |
40
|
11 |
|
public static function printError(Error $error) |
41
|
|
|
{ |
42
|
11 |
|
$printedLocations = []; |
43
|
11 |
|
if ($error->nodes) { |
|
|
|
|
44
|
|
|
/** @var Node $node */ |
45
|
1 |
|
foreach ($error->nodes as $node) { |
46
|
1 |
|
if ($node->loc) { |
47
|
1 |
|
$printedLocations[] = self::highlightSourceAtLocation( |
48
|
1 |
|
$node->loc->source, |
49
|
1 |
|
$node->loc->source->getLocation($node->loc->start) |
50
|
|
|
); |
51
|
|
|
} |
52
|
|
|
} |
53
|
10 |
|
} elseif ($error->getSource() && $error->getLocations()) { |
54
|
9 |
|
$source = $error->getSource(); |
55
|
9 |
|
foreach ($error->getLocations() as $location) { |
56
|
9 |
|
$printedLocations[] = self::highlightSourceAtLocation($source, $location); |
57
|
|
|
} |
58
|
|
|
} |
59
|
|
|
|
60
|
11 |
|
return ! $printedLocations |
61
|
1 |
|
? $error->getMessage() |
62
|
11 |
|
: join("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n"; |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Render a helpful description of the location of the error in the GraphQL |
67
|
|
|
* Source document. |
68
|
|
|
* |
69
|
|
|
* @param Source $source |
70
|
|
|
* @param SourceLocation $location |
71
|
|
|
* @return string |
72
|
|
|
*/ |
73
|
10 |
|
private static function highlightSourceAtLocation(Source $source, SourceLocation $location) |
74
|
|
|
{ |
75
|
10 |
|
$line = $location->line; |
76
|
10 |
|
$lineOffset = $source->locationOffset->line - 1; |
77
|
10 |
|
$columnOffset = self::getColumnOffset($source, $location); |
78
|
10 |
|
$contextLine = $line + $lineOffset; |
79
|
10 |
|
$contextColumn = $location->column + $columnOffset; |
80
|
10 |
|
$prevLineNum = (string) ($contextLine - 1); |
81
|
10 |
|
$lineNum = (string) $contextLine; |
82
|
10 |
|
$nextLineNum = (string) ($contextLine + 1); |
83
|
10 |
|
$padLen = strlen($nextLineNum); |
84
|
10 |
|
$lines = preg_split('/\r\n|[\n\r]/', $source->body); |
85
|
|
|
|
86
|
10 |
|
$lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0]; |
87
|
|
|
|
88
|
|
|
$outputLines = [ |
89
|
10 |
|
"{$source->name} ($contextLine:$contextColumn)", |
90
|
10 |
|
$line >= 2 ? (self::lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null, |
91
|
10 |
|
self::lpad($padLen, $lineNum) . ': ' . $lines[$line - 1], |
92
|
10 |
|
self::whitespace(2 + $padLen + $contextColumn - 1) . '^', |
93
|
10 |
|
$line < count($lines) ? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null, |
|
|
|
|
94
|
|
|
]; |
95
|
|
|
|
96
|
10 |
|
return join("\n", array_filter($outputLines)); |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* @param Source $source |
101
|
|
|
* @param SourceLocation $location |
102
|
|
|
* @return int |
103
|
|
|
*/ |
104
|
10 |
|
private static function getColumnOffset(Source $source, SourceLocation $location) |
105
|
|
|
{ |
106
|
10 |
|
return $location->line === 1 ? $source->locationOffset->column - 1 : 0; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* @param int $len |
111
|
|
|
* @return string |
112
|
|
|
*/ |
113
|
10 |
|
private static function whitespace($len) |
114
|
|
|
{ |
115
|
10 |
|
return str_repeat(' ', $len); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* @param int $len |
120
|
|
|
* @return string |
121
|
|
|
*/ |
122
|
10 |
|
private static function lpad($len, $str) |
123
|
|
|
{ |
124
|
10 |
|
return self::whitespace($len - mb_strlen($str)) . $str; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Standard GraphQL error formatter. Converts any exception to array |
129
|
|
|
* conforming to GraphQL spec. |
130
|
|
|
* |
131
|
|
|
* This method only exposes exception message when exception implements ClientAware interface |
132
|
|
|
* (or when debug flags are passed). |
133
|
|
|
* |
134
|
|
|
* For a list of available debug flags see GraphQL\Error\Debug constants. |
135
|
|
|
* |
136
|
|
|
* @api |
137
|
|
|
* @param \Throwable $e |
138
|
|
|
* @param bool|int $debug |
139
|
|
|
* @param string $internalErrorMessage |
140
|
|
|
* @return array |
141
|
|
|
* @throws \Throwable |
142
|
|
|
*/ |
143
|
65 |
|
public static function createFromException($e, $debug = false, $internalErrorMessage = null) |
144
|
|
|
{ |
145
|
65 |
|
Utils::invariant( |
146
|
65 |
|
$e instanceof \Exception || $e instanceof \Throwable, |
147
|
65 |
|
"Expected exception, got %s", |
148
|
65 |
|
Utils::getVariableType($e) |
149
|
|
|
); |
150
|
|
|
|
151
|
65 |
|
$internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage; |
152
|
|
|
|
153
|
65 |
|
if ($e instanceof ClientAware) { |
154
|
|
|
$formattedError = [ |
155
|
65 |
|
'message' => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage, |
|
|
|
|
156
|
65 |
|
'category' => $e->getCategory(), |
157
|
|
|
]; |
158
|
|
|
} else { |
159
|
|
|
$formattedError = [ |
160
|
|
|
'message' => $internalErrorMessage, |
161
|
|
|
'category' => Error::CATEGORY_INTERNAL, |
162
|
|
|
]; |
163
|
|
|
} |
164
|
|
|
|
165
|
65 |
|
if ($e instanceof Error) { |
166
|
65 |
|
if ($e->getExtensions()) { |
167
|
|
|
$formattedError = array_merge($e->getExtensions(), $formattedError); |
168
|
|
|
} |
169
|
|
|
|
170
|
65 |
|
$locations = Utils::map($e->getLocations(), |
171
|
|
|
function (SourceLocation $loc) { |
172
|
58 |
|
return $loc->toSerializableArray(); |
173
|
65 |
|
}); |
174
|
|
|
|
175
|
65 |
|
if (! empty($locations)) { |
176
|
58 |
|
$formattedError['locations'] = $locations; |
177
|
|
|
} |
178
|
65 |
|
if (! empty($e->path)) { |
179
|
44 |
|
$formattedError['path'] = $e->path; |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
|
183
|
65 |
|
if ($debug) { |
184
|
|
|
$formattedError = self::addDebugEntries($formattedError, $e, $debug); |
|
|
|
|
185
|
|
|
} |
186
|
|
|
|
187
|
65 |
|
return $formattedError; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Decorates spec-compliant $formattedError with debug entries according to $debug flags |
192
|
|
|
* (see GraphQL\Error\Debug for available flags) |
193
|
|
|
* |
194
|
|
|
* @param array $formattedError |
195
|
|
|
* @param \Throwable $e |
196
|
|
|
* @param bool $debug |
197
|
|
|
* @return array |
198
|
|
|
* @throws \Throwable |
199
|
|
|
*/ |
200
|
26 |
|
public static function addDebugEntries(array $formattedError, $e, $debug) |
201
|
|
|
{ |
202
|
26 |
|
if (! $debug) { |
203
|
|
|
return $formattedError; |
204
|
|
|
} |
205
|
|
|
|
206
|
26 |
|
Utils::invariant( |
207
|
26 |
|
$e instanceof \Exception || $e instanceof \Throwable, |
208
|
26 |
|
"Expected exception, got %s", |
209
|
26 |
|
Utils::getVariableType($e) |
210
|
|
|
); |
211
|
|
|
|
212
|
26 |
|
$debug = (int) $debug; |
213
|
|
|
|
214
|
26 |
|
if ($debug & Debug::RETHROW_INTERNAL_EXCEPTIONS) { |
215
|
|
|
if (! $e instanceof Error) { |
216
|
|
|
throw $e; |
217
|
|
|
} elseif ($e->getPrevious()) { |
218
|
|
|
throw $e->getPrevious(); |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
|
222
|
26 |
|
$isInternal = ! $e instanceof ClientAware || ! $e->isClientSafe(); |
223
|
|
|
|
224
|
26 |
|
if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isInternal) { |
225
|
|
|
// Displaying debugMessage as a first entry: |
226
|
21 |
|
$formattedError = ['debugMessage' => $e->getMessage()] + $formattedError; |
227
|
|
|
} |
228
|
|
|
|
229
|
26 |
|
if ($debug & Debug::INCLUDE_TRACE) { |
230
|
2 |
|
if ($e instanceof \ErrorException || $e instanceof \Error) { |
231
|
|
|
$formattedError += [ |
232
|
|
|
'file' => $e->getFile(), |
233
|
|
|
'line' => $e->getLine(), |
234
|
|
|
]; |
235
|
|
|
} |
236
|
|
|
|
237
|
2 |
|
$isTrivial = $e instanceof Error && ! $e->getPrevious(); |
238
|
|
|
|
239
|
2 |
|
if (! $isTrivial) { |
240
|
2 |
|
$debugging = $e->getPrevious() ?: $e; |
241
|
2 |
|
$formattedError['trace'] = static::toSafeTrace($debugging); |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
|
245
|
26 |
|
return $formattedError; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Prepares final error formatter taking in account $debug flags. |
250
|
|
|
* If initial formatter is not set, FormattedError::createFromException is used |
251
|
|
|
* |
252
|
|
|
* @param callable|null $formatter |
253
|
|
|
* @param $debug |
254
|
|
|
* @return callable|\Closure |
255
|
|
|
*/ |
256
|
67 |
|
public static function prepareFormatter(callable $formatter = null, $debug) |
257
|
|
|
{ |
258
|
|
|
$formatter = $formatter ?: function ($e) { |
259
|
65 |
|
return FormattedError::createFromException($e); |
260
|
67 |
|
}; |
261
|
67 |
|
if ($debug) { |
262
|
|
|
$formatter = function ($e) use ($formatter, $debug) { |
263
|
26 |
|
return FormattedError::addDebugEntries($formatter($e), $e, $debug); |
264
|
26 |
|
}; |
265
|
|
|
} |
266
|
|
|
|
267
|
67 |
|
return $formatter; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* Returns error trace as serializable array |
272
|
|
|
* |
273
|
|
|
* @api |
274
|
|
|
* @param \Throwable $error |
275
|
|
|
* @return array |
276
|
|
|
*/ |
277
|
2 |
|
public static function toSafeTrace($error) |
278
|
|
|
{ |
279
|
2 |
|
$trace = $error->getTrace(); |
280
|
|
|
|
281
|
|
|
// Remove invariant entries as they don't provide much value: |
282
|
|
|
if ( |
283
|
2 |
|
isset($trace[0]['function']) && isset($trace[0]['class']) && |
284
|
2 |
|
('GraphQL\Utils\Utils::invariant' === $trace[0]['class'] . '::' . $trace[0]['function'])) { |
285
|
|
|
array_shift($trace); |
286
|
|
|
} // Remove root call as it's likely error handler trace: |
287
|
2 |
|
elseif (! isset($trace[0]['file'])) { |
288
|
|
|
array_shift($trace); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
return array_map(function ($err) { |
292
|
2 |
|
$safeErr = array_intersect_key($err, ['file' => true, 'line' => true]); |
293
|
|
|
|
294
|
2 |
|
if (isset($err['function'])) { |
295
|
2 |
|
$func = $err['function']; |
296
|
2 |
|
$args = ! empty($err['args']) ? array_map([__CLASS__, 'printVar'], $err['args']) : []; |
297
|
2 |
|
$funcStr = $func . '(' . implode(", ", $args) . ')'; |
298
|
|
|
|
299
|
2 |
|
if (isset($err['class'])) { |
300
|
2 |
|
$safeErr['call'] = $err['class'] . '::' . $funcStr; |
301
|
|
|
} else { |
302
|
|
|
$safeErr['function'] = $funcStr; |
303
|
|
|
} |
304
|
|
|
} |
305
|
|
|
|
306
|
2 |
|
return $safeErr; |
307
|
2 |
|
}, |
308
|
2 |
|
$trace); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* @param $var |
313
|
|
|
* @return string |
314
|
|
|
*/ |
315
|
2 |
|
public static function printVar($var) |
316
|
|
|
{ |
317
|
2 |
|
if ($var instanceof Type) { |
318
|
|
|
// FIXME: Replace with schema printer call |
319
|
2 |
|
if ($var instanceof WrappingType) { |
320
|
|
|
$var = $var->getWrappedType(true); |
321
|
|
|
} |
322
|
|
|
|
323
|
2 |
|
return 'GraphQLType: ' . $var->name; |
324
|
|
|
} |
325
|
|
|
|
326
|
2 |
|
if (is_object($var)) { |
327
|
2 |
|
return 'instance of ' . get_class($var) . ($var instanceof \Countable ? '(' . count($var) . ')' : ''); |
328
|
|
|
} |
329
|
2 |
|
if (is_array($var)) { |
330
|
2 |
|
return 'array(' . count($var) . ')'; |
331
|
|
|
} |
332
|
2 |
|
if ('' === $var) { |
333
|
|
|
return '(empty string)'; |
334
|
|
|
} |
335
|
2 |
|
if (is_string($var)) { |
336
|
2 |
|
return "'" . addcslashes($var, "'") . "'"; |
337
|
|
|
} |
338
|
2 |
|
if (is_bool($var)) { |
339
|
2 |
|
return $var ? 'true' : 'false'; |
340
|
|
|
} |
341
|
2 |
|
if (is_scalar($var)) { |
342
|
|
|
return $var; |
343
|
|
|
} |
344
|
2 |
|
if (null === $var) { |
345
|
2 |
|
return 'null'; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
return gettype($var); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* @deprecated as of v0.8.0 |
353
|
|
|
* @param $error |
354
|
|
|
* @param SourceLocation[] $locations |
355
|
|
|
* @return array |
356
|
|
|
*/ |
357
|
|
|
public static function create($error, array $locations = []) |
358
|
|
|
{ |
359
|
|
|
$formatted = [ |
360
|
|
|
'message' => $error, |
361
|
|
|
]; |
362
|
|
|
|
363
|
|
|
if (! empty($locations)) { |
364
|
|
|
$formatted['locations'] = array_map(function ($loc) { |
365
|
|
|
return $loc->toArray(); |
366
|
|
|
}, |
367
|
|
|
$locations); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
return $formatted; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* @param \ErrorException $e |
375
|
|
|
* @deprecated as of v0.10.0, use general purpose method createFromException() instead |
376
|
|
|
* @return array |
377
|
|
|
*/ |
378
|
|
|
public static function createFromPHPError(\ErrorException $e) |
379
|
|
|
{ |
380
|
|
|
return [ |
381
|
|
|
'message' => $e->getMessage(), |
382
|
|
|
'severity' => $e->getSeverity(), |
383
|
|
|
'trace' => self::toSafeTrace($e), |
384
|
|
|
]; |
385
|
|
|
} |
386
|
|
|
} |
387
|
|
|
|
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.