Passed
Pull Request — master (#96)
by Sergei
02:52
created

ErrorException::getBacktrace()   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
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ErrorHandler\Exception;
6
7
use Exception;
8
use ReflectionProperty;
9
use Yiisoft\FriendlyException\FriendlyExceptionInterface;
10
11
use function array_slice;
12
use function in_array;
13
use function function_exists;
14
15
/**
16
 * `ErrorException` represents a PHP error.
17
 *
18
 * @psalm-type DebugBacktraceType = list<array{
19
 *     args?: list<mixed>,
20
 *     class?: class-string,
21
 *     file?: string,
22
 *     function: string,
23
 *     line?: int,
24
 *     object?: object,
25
 *     type?: string
26
 * }>
27
 */
28
class ErrorException extends \ErrorException implements FriendlyExceptionInterface
29
{
30
    private const ERROR_NAMES = [
31
        E_ERROR => 'PHP Fatal Error',
32
        E_WARNING => 'PHP Warning',
33
        E_PARSE => 'PHP Parse Error',
34
        E_NOTICE => 'PHP Notice',
35
        E_CORE_ERROR => 'PHP Core Error',
36
        E_CORE_WARNING => 'PHP Core Warning',
37
        E_COMPILE_ERROR => 'PHP Compile Error',
38
        E_COMPILE_WARNING => 'PHP Compile Warning',
39
        E_USER_ERROR => 'PHP User Error',
40
        E_USER_WARNING => 'PHP User Warning',
41
        E_USER_NOTICE => 'PHP User Notice',
42
        E_STRICT => 'PHP Strict Warning',
43
        E_RECOVERABLE_ERROR => 'PHP Recoverable Error',
44
        E_DEPRECATED => 'PHP Deprecated Warning',
45
        E_USER_DEPRECATED => 'PHP User Deprecated Warning',
46
    ];
47
48
    /**
49
     * @psalm-param DebugBacktraceType $backtrace
50
     */
51 3
    public function __construct(
52
        string $message = '',
53
        int $code = 0,
54
        int $severity = 1,
55
        string $filename = __FILE__,
56
        int $line = __LINE__,
57
        private array $backtrace = [],
58
        Exception $previous = null
59
    ) {
60 3
        parent::__construct($message, $code, $severity, $filename, $line, $previous);
61 3
        $this->addXDebugTraceToFatalIfAvailable();
62
    }
63
64
    /**
65
     * Returns if error is one of fatal type.
66
     *
67
     * @param array $error error got from error_get_last()
68
     *
69
     * @return bool If error is one of fatal type.
70
     */
71 1
    public static function isFatalError(array $error): bool
72
    {
73 1
        return isset($error['type']) && in_array(
74 1
            $error['type'],
75 1
            [E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING],
76 1
            true,
77 1
        );
78
    }
79
80
    /**
81
     * @return string The user-friendly name of this exception.
82
     */
83 1
    public function getName(): string
84
    {
85 1
        return self::ERROR_NAMES[$this->getCode()] ?? 'Error';
86
    }
87
88 1
    public function getSolution(): ?string
89
    {
90 1
        return null;
91
    }
92
93
    /**
94
     * @psalm-return DebugBacktraceType
95
     */
96
    public function getBacktrace(): array
97
    {
98
        return $this->backtrace;
99
    }
100
101
    /**
102
     * Fatal errors normally do not provide any trace making it harder to debug. In case XDebug is installed, we
103
     * can get a trace using `xdebug_get_function_stack()`.
104
     */
105 3
    private function addXDebugTraceToFatalIfAvailable(): void
106
    {
107 3
        if ($this->isXdebugStackAvailable()) {
108
            /**
109
             * XDebug trace can't be modified and used directly with PHP 7
110
             *
111
             * @see https://github.com/yiisoft/yii2/pull/11723
112
             *
113
             * @psalm-var array<int,array>
114
             */
115
            $xDebugTrace = array_slice(array_reverse(xdebug_get_function_stack()), 1, -1);
116
            $trace = [];
117
118
            foreach ($xDebugTrace as $frame) {
119
                if (!isset($frame['function'])) {
120
                    $frame['function'] = 'unknown';
121
                }
122
123
                // XDebug < 2.1.1: https://bugs.xdebug.org/view.php?id=695
124
                if (!isset($frame['type']) || $frame['type'] === 'static') {
125
                    $frame['type'] = '::';
126
                } elseif ($frame['type'] === 'dynamic') {
127
                    $frame['type'] = '->';
128
                }
129
130
                // XDebug has a different key name
131
                if (isset($frame['params']) && !isset($frame['args'])) {
132
                    /** @var mixed */
133
                    $frame['args'] = $frame['params'];
134
                }
135
                $trace[] = $frame;
136
            }
137
138
            $ref = new ReflectionProperty(Exception::class, 'trace');
139
            $ref->setAccessible(true);
140
            $ref->setValue($this, $trace);
141
        }
142
    }
143
144
    /**
145
     * Ensures that Xdebug stack trace is available based on Xdebug version.
146
     * Idea taken from developer bishopb at https://github.com/rollbar/rollbar-php
147
     */
148 3
    private function isXdebugStackAvailable(): bool
149
    {
150 3
        if (!function_exists('\xdebug_get_function_stack')) {
151
            return false;
152
        }
153
154
        // check for Xdebug being installed to ensure origin of xdebug_get_function_stack()
155 3
        $version = phpversion('xdebug');
156
157 3
        if ($version === false) {
158
            return false;
159
        }
160
161
        // Xdebug 2 and prior
162 3
        if (version_compare($version, '3.0.0', '<')) {
163
            return true;
164
        }
165
166
        // Xdebug 3 and later, proper mode is required
167 3
        return str_contains(ini_get('xdebug.mode'), 'develop');
168
    }
169
}
170