FormattedError   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 397
Duplicated Lines 0 %

Test Coverage

Coverage 85.53%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 69
eloc 152
c 2
b 0
f 0
dl 0
loc 397
ccs 130
cts 152
cp 0.8553
rs 2.88

13 Methods

Rating   Name   Duplication   Size   Complexity  
D addDebugEntries() 0 54 17
A create() 0 14 2
A prepareFormatter() 0 12 3
C printVar() 0 34 12
A highlightSourceAtLocation() 0 24 3
B toSafeTrace() 0 32 8
B createFromException() 0 49 9
A lpad() 0 3 1
A setInternalErrorMessage() 0 3 1
A whitespace() 0 3 1
A getColumnOffset() 0 3 2
B printError() 0 29 9
A createFromPHPError() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like FormattedError often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FormattedError, and based on these observations, apply Extract Interface, too.

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

123
            $line < count(/** @scrutinizer ignore-type */ $lines) ? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
Loading history...
124
        ];
125
126 11
        return implode("\n", array_filter($outputLines));
127
    }
128
129
    /**
130
     * @return int
131
     */
132 11
    private static function getColumnOffset(Source $source, SourceLocation $location)
133
    {
134 11
        return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
135
    }
136
137
    /**
138
     * @param int $len
139
     *
140
     * @return string
141
     */
142 11
    private static function whitespace($len)
143
    {
144 11
        return str_repeat(' ', $len);
145
    }
146
147
    /**
148
     * @param int $len
149
     *
150
     * @return string
151
     */
152 11
    private static function lpad($len, $str)
153
    {
154 11
        return self::whitespace($len - mb_strlen($str)) . $str;
155
    }
156
157
    /**
158
     * Standard GraphQL error formatter. Converts any exception to array
159
     * conforming to GraphQL spec.
160
     *
161
     * This method only exposes exception message when exception implements ClientAware interface
162
     * (or when debug flags are passed).
163
     *
164
     * For a list of available debug flags see GraphQL\Error\Debug constants.
165
     *
166
     * @param Throwable $e
167
     * @param bool|int  $debug
168
     * @param string    $internalErrorMessage
169
     *
170
     * @return mixed[]
171
     *
172
     * @throws Throwable
173
     *
174
     * @api
175
     */
176 74
    public static function createFromException($e, $debug = false, $internalErrorMessage = null)
177
    {
178 74
        Utils::invariant(
179 74
            $e instanceof Throwable,
180 74
            'Expected exception, got %s',
181 74
            Utils::getVariableType($e)
182
        );
183
184 74
        $internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage;
185
186 74
        if ($e instanceof ClientAware) {
187
            $formattedError = [
188 74
                'message'  => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage,
189
                'extensions' => [
190 74
                    'category' => $e->getCategory(),
191
                ],
192
            ];
193
        } else {
194
            $formattedError = [
195
                'message'  => $internalErrorMessage,
196
                'extensions' => [
197
                    'category' => Error::CATEGORY_INTERNAL,
198
                ],
199
            ];
200
        }
201
202 74
        if ($e instanceof Error) {
203 74
            $locations = Utils::map(
204 74
                $e->getLocations(),
205
                static function (SourceLocation $loc) {
206 66
                    return $loc->toSerializableArray();
207 74
                }
208
            );
209 74
            if (! empty($locations)) {
210 66
                $formattedError['locations'] = $locations;
211
            }
212 74
            if (! empty($e->path)) {
213 50
                $formattedError['path'] = $e->path;
214
            }
215 74
            if (! empty($e->getExtensions())) {
216 1
                $formattedError['extensions'] = $e->getExtensions() + $formattedError['extensions'];
217
            }
218
        }
219
220 74
        if ($debug) {
221
            $formattedError = self::addDebugEntries($formattedError, $e, $debug);
222
        }
223
224 74
        return $formattedError;
225
    }
226
227
    /**
228
     * Decorates spec-compliant $formattedError with debug entries according to $debug flags
229
     * (see GraphQL\Error\Debug for available flags)
230
     *
231
     * @param mixed[]   $formattedError
232
     * @param Throwable $e
233
     * @param bool|int  $debug
234
     *
235
     * @return mixed[]
236
     *
237
     * @throws Throwable
238
     */
239 27
    public static function addDebugEntries(array $formattedError, $e, $debug)
240
    {
241 27
        if (! $debug) {
242
            return $formattedError;
243
        }
244
245 27
        Utils::invariant(
246 27
            $e instanceof Throwable,
247 27
            'Expected exception, got %s',
248 27
            Utils::getVariableType($e)
249
        );
250
251 27
        $debug = (int) $debug;
252
253 27
        if ($debug & Debug::RETHROW_INTERNAL_EXCEPTIONS) {
254
            if (! $e instanceof Error) {
255
                throw $e;
256
            }
257
258
            if ($e->getPrevious()) {
259
                throw $e->getPrevious();
260
            }
261
        }
262
263 27
        $isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe();
264
265 27
        if (($debug & Debug::RETHROW_UNSAFE_EXCEPTIONS) && $isUnsafe) {
266 1
            if ($e->getPrevious()) {
267 1
                throw $e->getPrevious();
268
            }
269
        }
270
271 26
        if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isUnsafe) {
272
            // Displaying debugMessage as a first entry:
273 22
            $formattedError = ['debugMessage' => $e->getMessage()] + $formattedError;
274
        }
275
276 26
        if ($debug & Debug::INCLUDE_TRACE) {
277 2
            if ($e instanceof ErrorException || $e instanceof \Error) {
278
                $formattedError += [
279
                    'file' => $e->getFile(),
280
                    'line' => $e->getLine(),
281
                ];
282
            }
283
284 2
            $isTrivial = $e instanceof Error && ! $e->getPrevious();
285
286 2
            if (! $isTrivial) {
287 2
                $debugging               = $e->getPrevious() ?: $e;
288 2
                $formattedError['trace'] = static::toSafeTrace($debugging);
289
            }
290
        }
291
292 26
        return $formattedError;
293
    }
294
295
    /**
296
     * Prepares final error formatter taking in account $debug flags.
297
     * If initial formatter is not set, FormattedError::createFromException is used
298
     *
299
     * @param  bool|int $debug
300
     *
301
     * @return callable|callable
302
     */
303 76
    public static function prepareFormatter(?callable $formatter = null, $debug)
304
    {
305
        $formatter = $formatter ?: static function ($e) {
306 74
            return FormattedError::createFromException($e);
307 76
        };
308 76
        if ($debug) {
309
            $formatter = static function ($e) use ($formatter, $debug) {
310 27
                return FormattedError::addDebugEntries($formatter($e), $e, $debug);
311 27
            };
312
        }
313
314 76
        return $formatter;
315
    }
316
317
    /**
318
     * Returns error trace as serializable array
319
     *
320
     * @param Throwable $error
321
     *
322
     * @return mixed[]
323
     *
324
     * @api
325
     */
326 2
    public static function toSafeTrace($error)
327
    {
328 2
        $trace = $error->getTrace();
329
330 2
        if (isset($trace[0]['function']) && isset($trace[0]['class']) &&
331
            // Remove invariant entries as they don't provide much value:
332 2
            ($trace[0]['class'] . '::' . $trace[0]['function'] === 'GraphQL\Utils\Utils::invariant')) {
333
            array_shift($trace);
334 2
        } elseif (! isset($trace[0]['file'])) {
335
            // Remove root call as it's likely error handler trace:
336
            array_shift($trace);
337
        }
338
339 2
        return array_map(
340
            static function ($err) {
341 2
                $safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);
342
343 2
                if (isset($err['function'])) {
344 2
                    $func    = $err['function'];
345 2
                    $args    = ! empty($err['args']) ? array_map([self::class, 'printVar'], $err['args']) : [];
346 2
                    $funcStr = $func . '(' . implode(', ', $args) . ')';
347
348 2
                    if (isset($err['class'])) {
349 2
                        $safeErr['call'] = $err['class'] . '::' . $funcStr;
350
                    } else {
351
                        $safeErr['function'] = $funcStr;
352
                    }
353
                }
354
355 2
                return $safeErr;
356 2
            },
357 2
            $trace
358
        );
359
    }
360
361
    /**
362
     * @param mixed $var
363
     *
364
     * @return string
365
     */
366 2
    public static function printVar($var)
367
    {
368 2
        if ($var instanceof Type) {
369
            // FIXME: Replace with schema printer call
370 2
            if ($var instanceof WrappingType) {
371
                $var = $var->getWrappedType(true);
372
            }
373
374 2
            return 'GraphQLType: ' . $var->name;
375
        }
376
377 2
        if (is_object($var)) {
378 2
            return 'instance of ' . get_class($var) . ($var instanceof Countable ? '(' . count($var) . ')' : '');
379
        }
380 2
        if (is_array($var)) {
381 2
            return 'array(' . count($var) . ')';
382
        }
383 2
        if ($var === '') {
384
            return '(empty string)';
385
        }
386 2
        if (is_string($var)) {
387 2
            return "'" . addcslashes($var, "'") . "'";
388
        }
389 2
        if (is_bool($var)) {
390 2
            return $var ? 'true' : 'false';
391
        }
392 2
        if (is_scalar($var)) {
393
            return $var;
394
        }
395 2
        if ($var === null) {
396 2
            return 'null';
397
        }
398
399
        return gettype($var);
400
    }
401
402
    /**
403
     * @deprecated as of v0.8.0
404
     *
405
     * @param string           $error
406
     * @param SourceLocation[] $locations
407
     *
408
     * @return mixed[]
409
     */
410 200
    public static function create($error, array $locations = [])
411
    {
412 200
        $formatted = ['message' => $error];
413
414 200
        if (! empty($locations)) {
415 185
            $formatted['locations'] = array_map(
416
                static function ($loc) {
417 185
                    return $loc->toArray();
418 185
                },
419 185
                $locations
420
            );
421
        }
422
423 200
        return $formatted;
424
    }
425
426
    /**
427
     * @deprecated as of v0.10.0, use general purpose method createFromException() instead
428
     *
429
     * @return mixed[]
430
     *
431
     * @codeCoverageIgnore
432
     */
433
    public static function createFromPHPError(ErrorException $e)
434
    {
435
        return [
436
            'message'  => $e->getMessage(),
437
            'severity' => $e->getSeverity(),
438
            'trace'    => self::toSafeTrace($e),
439
        ];
440
    }
441
}
442