Passed
Push — master ( 85f4c7...6bdead )
by Vladimir
03:17
created

FormattedError::setInternalErrorMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Error;
6
7
use GraphQL\Language\AST\Node;
8
use GraphQL\Language\Source;
9
use GraphQL\Language\SourceLocation;
10
use GraphQL\Type\Definition\Type;
11
use GraphQL\Type\Definition\WrappingType;
12
use GraphQL\Utils\Utils;
13
use function addcslashes;
14
use function array_filter;
15
use function array_intersect_key;
16
use function array_map;
17
use function array_merge;
18
use function array_shift;
19
use function count;
20
use function get_class;
21
use function gettype;
22
use function implode;
23
use function is_array;
24
use function is_bool;
25
use function is_object;
26
use function is_scalar;
27
use function is_string;
28
use function mb_strlen;
29
use function preg_split;
30
use function sprintf;
31
use function str_repeat;
32
use function strlen;
33
34
/**
35
 * This class is used for [default error formatting](error-handling.md).
36
 * It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors)
37
 * and provides tools for error debugging.
38
 */
39
class FormattedError
40
{
41
    /** @var string */
42
    private static $internalErrorMessage = 'Internal server error';
43
44
    /**
45
     * Set default error message for internal errors formatted using createFormattedError().
46
     * This value can be overridden by passing 3rd argument to `createFormattedError()`.
47
     *
48
     * @api
49
     * @param string $msg
50
     */
51
    public static function setInternalErrorMessage($msg)
52
    {
53
        self::$internalErrorMessage = $msg;
54
    }
55
56
    /**
57
     * Prints a GraphQLError to a string, representing useful location information
58
     * about the error's position in the source.
59
     *
60
     * @return string
61
     */
62 12
    public static function printError(Error $error)
63
    {
64 12
        $printedLocations = [];
65 12
        if ($error->nodes) {
66
            /** @var Node $node */
67 1
            foreach ($error->nodes as $node) {
68 1
                if (! $node->loc) {
69
                    continue;
70
                }
71
72 1
                if ($node->loc->source === null) {
73
                    continue;
74
                }
75
76 1
                $printedLocations[] = self::highlightSourceAtLocation(
77 1
                    $node->loc->source,
78 1
                    $node->loc->source->getLocation($node->loc->start)
79
                );
80
            }
81 11
        } elseif ($error->getSource() && $error->getLocations()) {
82 10
            $source = $error->getSource();
83 10
            foreach ($error->getLocations() as $location) {
84 10
                $printedLocations[] = self::highlightSourceAtLocation($source, $location);
85
            }
86
        }
87
88 12
        return ! $printedLocations
89 1
            ? $error->getMessage()
90 12
            : implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
91
    }
92
93
    /**
94
     * Render a helpful description of the location of the error in the GraphQL
95
     * Source document.
96
     *
97
     * @return string
98
     */
99 11
    private static function highlightSourceAtLocation(Source $source, SourceLocation $location)
100
    {
101 11
        $line          = $location->line;
102 11
        $lineOffset    = $source->locationOffset->line - 1;
103 11
        $columnOffset  = self::getColumnOffset($source, $location);
104 11
        $contextLine   = $line + $lineOffset;
105 11
        $contextColumn = $location->column + $columnOffset;
106 11
        $prevLineNum   = (string) ($contextLine - 1);
107 11
        $lineNum       = (string) $contextLine;
108 11
        $nextLineNum   = (string) ($contextLine + 1);
109 11
        $padLen        = strlen($nextLineNum);
110 11
        $lines         = preg_split('/\r\n|[\n\r]/', $source->body);
111
112 11
        $lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0];
113
114
        $outputLines = [
115 11
            sprintf('%s (%s:%s)', $source->name, $contextLine, $contextColumn),
116 11
            $line >= 2 ? (self::lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
117 11
            self::lpad($padLen, $lineNum) . ': ' . $lines[$line - 1],
118 11
            self::whitespace(2 + $padLen + $contextColumn - 1) . '^',
119 11
            $line < count($lines) ? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
0 ignored issues
show
Bug introduced by
It seems like $lines can also be of type false; however, parameter $var of count() does only seem to accept Countable|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 ignore-type  annotation

119
            $line < count(/** @scrutinizer ignore-type */ $lines) ? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
Loading history...
120
        ];
121
122 11
        return implode("\n", array_filter($outputLines));
123
    }
124
125
    /**
126
     * @return int
127
     */
128 11
    private static function getColumnOffset(Source $source, SourceLocation $location)
129
    {
130 11
        return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
131
    }
132
133
    /**
134
     * @param int $len
135
     * @return string
136
     */
137 11
    private static function whitespace($len)
138
    {
139 11
        return str_repeat(' ', $len);
140
    }
141
142
    /**
143
     * @param int $len
144
     * @return string
145
     */
146 11
    private static function lpad($len, $str)
147
    {
148 11
        return self::whitespace($len - mb_strlen($str)) . $str;
149
    }
150
151
    /**
152
     * Standard GraphQL error formatter. Converts any exception to array
153
     * conforming to GraphQL spec.
154
     *
155
     * This method only exposes exception message when exception implements ClientAware interface
156
     * (or when debug flags are passed).
157
     *
158
     * For a list of available debug flags see GraphQL\Error\Debug constants.
159
     *
160
     * @api
161
     * @param \Throwable $e
162
     * @param bool|int   $debug
163
     * @param string     $internalErrorMessage
164
     * @return mixed[]
165
     * @throws \Throwable
166
     */
167 70
    public static function createFromException($e, $debug = false, $internalErrorMessage = null)
168
    {
169 70
        Utils::invariant(
170 70
            $e instanceof \Exception || $e instanceof \Throwable,
171 70
            'Expected exception, got %s',
172 70
            Utils::getVariableType($e)
173
        );
174
175 70
        $internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage;
176
177 70
        if ($e instanceof ClientAware) {
178
            $formattedError = [
179 70
                'message'  => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage,
0 ignored issues
show
Bug introduced by
The method getMessage() does not exist on GraphQL\Error\ClientAware. Since it exists in all sub-types, consider adding an abstract or default implementation to GraphQL\Error\ClientAware. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

179
                'message'  => $e->isClientSafe() ? $e->/** @scrutinizer ignore-call */ getMessage() : $internalErrorMessage,
Loading history...
180 70
                'category' => $e->getCategory(),
181
            ];
182
        } else {
183
            $formattedError = [
184
                'message'  => $internalErrorMessage,
185
                'category' => Error::CATEGORY_INTERNAL,
186
            ];
187
        }
188
189 70
        if ($e instanceof Error) {
190 70
            $locations = Utils::map(
191 70
                $e->getLocations(),
192
                function (SourceLocation $loc) {
193 62
                    return $loc->toSerializableArray();
194 70
                }
195
            );
196 70
            if (! empty($locations)) {
197 62
                $formattedError['locations'] = $locations;
198
            }
199 70
            if (! empty($e->path)) {
200 46
                $formattedError['path'] = $e->path;
201
            }
202 70
            if (! empty($e->getExtensions())) {
203
                $formattedError['extensions'] = $e->getExtensions();
204
            }
205
        }
206
207 70
        if ($debug) {
208
            $formattedError = self::addDebugEntries($formattedError, $e, $debug);
0 ignored issues
show
Bug introduced by
It seems like $debug can also be of type integer; however, parameter $debug of GraphQL\Error\FormattedError::addDebugEntries() does only seem to accept boolean, 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 ignore-type  annotation

208
            $formattedError = self::addDebugEntries($formattedError, $e, /** @scrutinizer ignore-type */ $debug);
Loading history...
209
        }
210
211 70
        return $formattedError;
212
    }
213
214
    /**
215
     * Decorates spec-compliant $formattedError with debug entries according to $debug flags
216
     * (see GraphQL\Error\Debug for available flags)
217
     *
218
     * @param mixed[]    $formattedError
219
     * @param \Throwable $e
220
     * @param bool       $debug
221
     * @return mixed[]
222
     * @throws \Throwable
223
     */
224 27
    public static function addDebugEntries(array $formattedError, $e, $debug)
225
    {
226 27
        if (! $debug) {
227
            return $formattedError;
228
        }
229
230 27
        Utils::invariant(
231 27
            $e instanceof \Exception || $e instanceof \Throwable,
232 27
            'Expected exception, got %s',
233 27
            Utils::getVariableType($e)
234
        );
235
236 27
        $debug = (int) $debug;
237
238 27
        if ($debug & Debug::RETHROW_INTERNAL_EXCEPTIONS) {
239
            if (! $e instanceof Error) {
240
                throw $e;
241
            }
242
243
            if ($e->getPrevious()) {
244
                throw $e->getPrevious();
245
            }
246
        }
247
248 27
        $isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe();
249
250 27
        if (($debug & Debug::RETHROW_UNSAFE_EXCEPTIONS) && $isUnsafe) {
251 1
            if ($e->getPrevious()) {
252 1
                throw $e->getPrevious();
253
            }
254
        }
255
256 26
        if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isUnsafe) {
257
            // Displaying debugMessage as a first entry:
258 22
            $formattedError = ['debugMessage' => $e->getMessage()] + $formattedError;
259
        }
260
261 26
        if ($debug & Debug::INCLUDE_TRACE) {
262 2
            if ($e instanceof \ErrorException || $e instanceof \Error) {
263
                $formattedError += [
264
                    'file' => $e->getFile(),
265
                    'line' => $e->getLine(),
266
                ];
267
            }
268
269 2
            $isTrivial = $e instanceof Error && ! $e->getPrevious();
270
271 2
            if (! $isTrivial) {
272 2
                $debugging               = $e->getPrevious() ?: $e;
273 2
                $formattedError['trace'] = static::toSafeTrace($debugging);
274
            }
275
        }
276
277 26
        return $formattedError;
278
    }
279
280
    /**
281
     * Prepares final error formatter taking in account $debug flags.
282
     * If initial formatter is not set, FormattedError::createFromException is used
283
     *
284
     * @param  bool $debug
285
     * @return callable|\Closure
286
     */
287 72
    public static function prepareFormatter(?callable $formatter = null, $debug)
288
    {
289
        $formatter = $formatter ?: function ($e) {
290 70
            return FormattedError::createFromException($e);
291 72
        };
292 72
        if ($debug) {
293
            $formatter = function ($e) use ($formatter, $debug) {
294 27
                return FormattedError::addDebugEntries($formatter($e), $e, $debug);
295 27
            };
296
        }
297
298 72
        return $formatter;
299
    }
300
301
    /**
302
     * Returns error trace as serializable array
303
     *
304
     * @api
305
     * @param \Throwable $error
306
     * @return mixed[]
307
     */
308 2
    public static function toSafeTrace($error)
309
    {
310 2
        $trace = $error->getTrace();
311
312 2
        if (isset($trace[0]['function']) && isset($trace[0]['class']) &&
313
            // Remove invariant entries as they don't provide much value:
314 2
            ($trace[0]['class'] . '::' . $trace[0]['function'] === 'GraphQL\Utils\Utils::invariant')) {
315
            array_shift($trace);
316 2
        } elseif (! isset($trace[0]['file'])) {
317
            // Remove root call as it's likely error handler trace:
318
            array_shift($trace);
319
        }
320
321 2
        return array_map(
322
            function ($err) {
323 2
                $safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);
324
325 2
                if (isset($err['function'])) {
326 2
                    $func    = $err['function'];
327 2
                    $args    = ! empty($err['args']) ? array_map([__CLASS__, 'printVar'], $err['args']) : [];
328 2
                    $funcStr = $func . '(' . implode(', ', $args) . ')';
329
330 2
                    if (isset($err['class'])) {
331 2
                        $safeErr['call'] = $err['class'] . '::' . $funcStr;
332
                    } else {
333
                        $safeErr['function'] = $funcStr;
334
                    }
335
                }
336
337 2
                return $safeErr;
338 2
            },
339 2
            $trace
340
        );
341
    }
342
343
    /**
344
     * @param mixed $var
345
     * @return string
346
     */
347 2
    public static function printVar($var)
348
    {
349 2
        if ($var instanceof Type) {
350
            // FIXME: Replace with schema printer call
351 2
            if ($var instanceof WrappingType) {
352
                $var = $var->getWrappedType(true);
353
            }
354
355 2
            return 'GraphQLType: ' . $var->name;
356
        }
357
358 2
        if (is_object($var)) {
359 2
            return 'instance of ' . get_class($var) . ($var instanceof \Countable ? '(' . count($var) . ')' : '');
360
        }
361 2
        if (is_array($var)) {
362 2
            return 'array(' . count($var) . ')';
363
        }
364 2
        if ($var === '') {
365
            return '(empty string)';
366
        }
367 2
        if (is_string($var)) {
368 2
            return "'" . addcslashes($var, "'") . "'";
369
        }
370 2
        if (is_bool($var)) {
371 2
            return $var ? 'true' : 'false';
372
        }
373 2
        if (is_scalar($var)) {
374
            return $var;
375
        }
376 2
        if ($var === null) {
377 2
            return 'null';
378
        }
379
380
        return gettype($var);
381
    }
382
383
    /**
384
     * @deprecated as of v0.8.0
385
     * @param string           $error
386
     * @param SourceLocation[] $locations
387
     * @return mixed[]
388
     */
389 194
    public static function create($error, array $locations = [])
390
    {
391 194
        $formatted = ['message' => $error];
392
393 194
        if (! empty($locations)) {
394 180
            $formatted['locations'] = array_map(
395
                function ($loc) {
396 180
                    return $loc->toArray();
397 180
                },
398 180
                $locations
399
            );
400
        }
401
402 194
        return $formatted;
403
    }
404
405
    /**
406
     * @deprecated as of v0.10.0, use general purpose method createFromException() instead
407
     * @return mixed[]
408
     */
409
    public static function createFromPHPError(\ErrorException $e)
410
    {
411
        return [
412
            'message'  => $e->getMessage(),
413
            'severity' => $e->getSeverity(),
414
            'trace'    => self::toSafeTrace($e),
415
        ];
416
    }
417
}
418