ErrorException::isFatalError()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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