Passed
Pull Request — master (#182)
by Arman
05:01 queued 02:11
created

ErrorHandler::handleException()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 12
nc 2
nop 1
dl 0
loc 20
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 2.9.5
13
 */
14
15
namespace Quantum\Tracer;
16
17
use Quantum\Exceptions\StopExecutionException;
18
use Quantum\Libraries\Storage\FileSystem;
19
use Quantum\Exceptions\ViewException;
20
use Quantum\Exceptions\DiException;
21
use Quantum\Factory\ViewFactory;
22
use Twig\Error\RuntimeError;
23
use Twig\Error\SyntaxError;
24
use Twig\Error\LoaderError;
25
use Quantum\Http\Response;
26
use Quantum\Logger\Logger;
27
use ReflectionException;
28
use Psr\Log\LogLevel;
29
use ErrorException;
30
use Quantum\Di\Di;
31
use ParseError;
32
use Throwable;
33
34
/**
35
 * Class ErrorHandler
36
 * @package Quantum\Tracer
37
 */
38
class ErrorHandler
39
{
40
    /**
41
     * Number of lines to be returned
42
     */
43
    const NUM_LINES = 10;
44
45
    /**
46
     * @var string[][]
47
     */
48
    const ERROR_TYPES = [
49
        E_ERROR => 'error',
50
        E_WARNING => 'warning',
51
        E_PARSE => 'error',
52
        E_NOTICE => 'notice',
53
        E_CORE_ERROR => 'error',
54
        E_CORE_WARNING => 'warning',
55
        E_COMPILE_ERROR => 'error',
56
        E_COMPILE_WARNING => 'warning',
57
        E_USER_ERROR => 'error',
58
        E_USER_WARNING => 'warning',
59
        E_USER_NOTICE => 'notice',
60
        E_STRICT => 'notice',
61
        E_RECOVERABLE_ERROR => 'error',
62
    ];
63
64
    /**
65
     * @var Logger
66
     */
67
    private $logger;
68
69
    /**
70
     * @var array
71
     */
72
    private $trace = [];
73
74
    private static $instance;
75
76
    private function __construct()
77
    {
78
        // Prevent direct instantiation
79
    }
80
81
    private function __clone()
82
    {
83
        // Prevent cloning
84
    }
85
86
    /**
87
     * @return ErrorHandler
88
     */
89
    public static function getInstance(): ErrorHandler
90
    {
91
        if (self::$instance === null) {
92
            self::$instance = new self();
93
        }
94
95
        return self::$instance;
96
    }
97
98
    /**
99
     * @return void
100
     */
101
    public function setup(Logger $logger)
102
    {
103
        $this->logger = $logger;
104
105
        set_error_handler([$this, 'handleError']);
106
        set_exception_handler([$this, 'handleException']);
107
    }
108
109
    /**
110
     * @param $severity
111
     * @param $message
112
     * @param $file
113
     * @param $line
114
     * @throws ErrorException
115
     */
116
    public function handleError($severity, $message, $file, $line)
117
    {
118
        if (!(error_reporting() & $severity)) {
119
            return;
120
        }
121
122
        throw new ErrorException($message, 0, $severity, $file, $line);
123
    }
124
125
    /**
126
     * @param Throwable $e
127
     * @return void
128
     * @throws DiException
129
     * @throws LoaderError
130
     * @throws ReflectionException
131
     * @throws RuntimeError
132
     * @throws SyntaxError
133
     * @throws ViewException
134
     * @throws StopExecutionException
135
     */
136
    public function handleException(Throwable $e): void
137
    {
138
        $this->composeStackTrace($e);
139
140
        $view = ViewFactory::getInstance();
141
142
        $errorType = $this->getErrorType($e);
143
144
        if (is_debug_mode()) {
145
            Response::html($view->renderPartial('errors' . DS . 'trace', [
146
                'stackTrace' => $this->trace,
147
                'errorMessage' => $e->getMessage(),
148
                'severity' => ucfirst($errorType),
149
            ]));
150
        } else {
151
            $this->logError($e, $errorType);
152
            Response::html($view->renderPartial('errors' . DS . '500'));
153
        }
154
155
        Response::send();
156
    }
157
158
    /**
159
     * @param Throwable $e
160
     * @param string $errorType
161
     * @return void
162
     */
163
    private function logError(Throwable $e, string $errorType): void
164
    {
165
        if (method_exists($this->logger, $errorType)) {
166
            $this->logger->$errorType($e->getMessage(), ['trace' => $e->getTraceAsString()]);
167
        } else {
168
            $this->logger->error($e->getMessage(), ['trace' => $e->getTraceAsString()]);
169
        }
170
    }
171
172
    /**
173
     * Composes the stack trace
174
     * @param Throwable $e
175
     * @throws DiException
176
     * @throws ReflectionException
177
     */
178
    private function composeStackTrace(Throwable $e)
179
    {
180
        $this->trace[] = [
181
            'file' => $e->getFile(),
182
            'code' => $this->getSourceCode($e->getFile(), $e->getLine(), 'error-line'),
183
        ];
184
185
        foreach ($e->getTrace() as $item) {
186
            if (($item['class'] ?? null) === __CLASS__) {
187
                continue;
188
            }
189
190
            if (isset($item['file'])) {
191
                $this->trace[] = [
192
                    'file' => $item['file'],
193
                    'code' => $this->getSourceCode($item['file'], $item['line'] ?? 1, 'switch-line'),
194
                ];
195
            }
196
        }
197
    }
198
199
    /**
200
     * Gets the source code where the error happens
201
     * @param string $filename
202
     * @param int $lineNumber
203
     * @param string $className
204
     * @return string
205
     * @throws DiException
206
     * @throws ReflectionException
207
     */
208
    private function getSourceCode(string $filename, int $lineNumber, string $className): string
209
    {
210
        $fs = Di::get(FileSystem::class);
211
212
        $start = max($lineNumber - floor(self::NUM_LINES / 2), 1);
213
214
        $lines = $fs->getLines($filename, $start, self::NUM_LINES);
215
216
        $code = '<ol start="' . key($lines) . '">';
217
        foreach ($lines as $currentLineNumber => $line) {
218
            $highlight = $currentLineNumber === $lineNumber ? ' class="' . $className . '"' : '';
219
            $code .= '<li' . $highlight . '><pre>' . htmlspecialchars($line, ENT_QUOTES) . '</pre></li>';
220
        }
221
        $code .= '</ol>';
222
223
        return $code;
224
    }
225
226
    /**
227
     * Gets the error type based on the exception class
228
     * @param Throwable $e
229
     * @return string
230
     */
231
    private function getErrorType(Throwable $e): string
232
    {
233
        if ($e instanceof ErrorException) {
234
            return self::ERROR_TYPES[$e->getSeverity()] ?? LogLevel::ERROR;
235
        }
236
237
        if ($e instanceof ParseError) {
238
            return LogLevel::CRITICAL;
239
        }
240
241
        if ($e instanceof ReflectionException) {
242
            return LogLevel::WARNING;
243
        }
244
245
        return LogLevel::ERROR;
246
    }
247
}