Passed
Push — master ( d8d032...bdbb30 )
by Vladimir
05:55
created

FormattedError::printVar()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 13.152

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 34
c 0
b 0
f 0
rs 6.9666
ccs 16
cts 20
cp 0.8
cc 12
nc 12
nop 1
crap 13.152

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

124
            $line < count(/** @scrutinizer ignore-type */ $lines) ? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
Loading history...
125
        ];
126
127 11
        return implode("\n", array_filter($outputLines));
128
    }
129
130
    /**
131
     * @return int
132
     */
133 11
    private static function getColumnOffset(Source $source, SourceLocation $location)
134
    {
135 11
        return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
136
    }
137
138
    /**
139
     * @param int $len
140
     *
141
     * @return string
142
     */
143 11
    private static function whitespace($len)
144
    {
145 11
        return str_repeat(' ', $len);
146
    }
147
148
    /**
149
     * @param int $len
150
     *
151
     * @return string
152
     */
153 11
    private static function lpad($len, $str)
154
    {
155 11
        return self::whitespace($len - mb_strlen($str)) . $str;
156
    }
157
158
    /**
159
     * Standard GraphQL error formatter. Converts any exception to array
160
     * conforming to GraphQL spec.
161
     *
162
     * This method only exposes exception message when exception implements ClientAware interface
163
     * (or when debug flags are passed).
164
     *
165
     * For a list of available debug flags see GraphQL\Error\Debug constants.
166
     *
167
     * @param Throwable $e
168
     * @param bool|int  $debug
169
     * @param string    $internalErrorMessage
170
     *
171
     * @return mixed[]
172
     *
173
     * @throws Throwable
174
     *
175
     * @api
176
     */
177 70
    public static function createFromException($e, $debug = false, $internalErrorMessage = null)
178
    {
179 70
        Utils::invariant(
180 70
            $e instanceof Exception || $e instanceof Throwable,
181 70
            'Expected exception, got %s',
182 70
            Utils::getVariableType($e)
183
        );
184
185 70
        $internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage;
186
187 70
        if ($e instanceof ClientAware) {
188
            $formattedError = [
189 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

189
                'message'  => $e->isClientSafe() ? $e->/** @scrutinizer ignore-call */ getMessage() : $internalErrorMessage,
Loading history...
190 70
                'category' => $e->getCategory(),
191
            ];
192
        } else {
193
            $formattedError = [
194
                'message'  => $internalErrorMessage,
195
                'category' => Error::CATEGORY_INTERNAL,
196
            ];
197
        }
198
199 70
        if ($e instanceof Error) {
200 70
            $locations = Utils::map(
201 70
                $e->getLocations(),
202
                static function (SourceLocation $loc) {
203 62
                    return $loc->toSerializableArray();
204 70
                }
205
            );
206 70
            if (! empty($locations)) {
207 62
                $formattedError['locations'] = $locations;
208
            }
209 70
            if (! empty($e->path)) {
210 46
                $formattedError['path'] = $e->path;
211
            }
212 70
            if (! empty($e->getExtensions())) {
213 1
                $formattedError['extensions'] = $e->getExtensions();
214
            }
215
        }
216
217 70
        if ($debug) {
218
            $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

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