Passed
Push — master ( d62051...e99676 )
by Arman
03:00 queued 14s
created

ErrorHandler   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 74
dl 0
loc 228
rs 10
c 4
b 0
f 0
wmc 23

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getInstance() 0 7 2
A handleError() 0 7 2
A setup() 0 6 1
A __construct() 0 2 1
A __clone() 0 2 1
A getErrorType() 0 15 4
A formatLineItem() 0 10 2
A getSourceCode() 0 19 2
A composeStackTrace() 0 21 4
A logError() 0 5 2
A handleException() 0 18 2
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\Libraries\Storage\FileSystem;
18
use Quantum\Exceptions\ViewException;
19
use Quantum\Exceptions\DiException;
20
use Quantum\Factory\ViewFactory;
21
use Twig\Error\RuntimeError;
22
use Twig\Error\SyntaxError;
23
use Twig\Error\LoaderError;
24
use Quantum\Http\Response;
25
use Quantum\Logger\Logger;
26
use ReflectionException;
27
use Psr\Log\LogLevel;
28
use ErrorException;
29
use Quantum\Di\Di;
30
use ParseError;
31
use Throwable;
32
33
/**
34
 * Class ErrorHandler
35
 * @package Quantum\Tracer
36
 */
37
class ErrorHandler
38
{
39
    /**
40
     * Number of lines to be returned
41
     */
42
    const NUM_LINES = 10;
43
44
    /**
45
     * @var string[][]
46
     */
47
    const ERROR_TYPES = [
48
        E_ERROR => 'error',
49
        E_WARNING => 'warning',
50
        E_PARSE => 'error',
51
        E_NOTICE => 'notice',
52
        E_CORE_ERROR => 'error',
53
        E_CORE_WARNING => 'warning',
54
        E_COMPILE_ERROR => 'error',
55
        E_COMPILE_WARNING => 'warning',
56
        E_USER_ERROR => 'error',
57
        E_USER_WARNING => 'warning',
58
        E_USER_NOTICE => 'notice',
59
        E_STRICT => 'notice',
60
        E_RECOVERABLE_ERROR => 'error',
61
    ];
62
63
    /**
64
     * @var Logger
65
     */
66
    private $logger;
67
68
    /**
69
     * @var ErrorHandler|null
70
     */
71
    private static $instance = null;
72
73
    private function __construct()
74
    {
75
        // Prevent direct instantiation
76
    }
77
78
    private function __clone()
79
    {
80
        // Prevent cloning
81
    }
82
83
    /**
84
     * @return ErrorHandler
85
     */
86
    public static function getInstance(): ?ErrorHandler
87
    {
88
        if (self::$instance === null) {
89
            self::$instance = new self();
90
        }
91
92
        return self::$instance;
93
    }
94
95
    /**
96
     * @return void
97
     */
98
    public function setup(Logger $logger)
99
    {
100
        $this->logger = $logger;
101
102
        set_error_handler([$this, 'handleError']);
103
        set_exception_handler([$this, 'handleException']);
104
    }
105
106
    /**
107
     * @param $severity
108
     * @param $message
109
     * @param $file
110
     * @param $line
111
     * @throws ErrorException
112
     */
113
    public function handleError($severity, $message, $file, $line)
114
    {
115
        if (!(error_reporting() & $severity)) {
116
            return;
117
        }
118
119
        throw new ErrorException($message, 0, $severity, $file, $line);
120
    }
121
122
    /**
123
     * @param Throwable $e
124
     * @return void
125
     * @throws DiException
126
     * @throws LoaderError
127
     * @throws ReflectionException
128
     * @throws RuntimeError
129
     * @throws SyntaxError
130
     * @throws ViewException
131
     */
132
    public function handleException(Throwable $e): void
133
    {
134
        $view = ViewFactory::getInstance();
135
        $errorType = $this->getErrorType($e);
136
137
        if (is_debug_mode()) {
138
            $errorPage = $view->renderPartial('errors' . DS . 'trace', [
139
                'stackTrace' => $this->composeStackTrace($e),
140
                'errorMessage' => $e->getMessage(),
141
                'severity' => ucfirst($errorType),
142
            ]);
143
        } else {
144
            $errorPage = $view->renderPartial('errors' . DS . '500');
145
            $this->logError($e, $errorType);
146
        }
147
148
        Response::html($errorPage);
149
        Response::send();
150
    }
151
152
    /**
153
     * @param Throwable $e
154
     * @param string $errorType
155
     * @return void
156
     */
157
    private function logError(Throwable $e, string $errorType): void
158
    {
159
        $logMethod = method_exists($this->logger, $errorType) ? $errorType : 'error';
160
161
        $this->logger->$logMethod($e->getMessage(), ['trace' => $e->getTraceAsString()]);
162
    }
163
164
    /**
165
     * Composes the stack trace
166
     * @param Throwable $e
167
     * @return array
168
     * @throws DiException
169
     * @throws ReflectionException
170
     */
171
    private function composeStackTrace(Throwable $e): array
172
    {
173
        $trace[] = [
0 ignored issues
show
Comprehensibility Best Practice introduced by
$trace was never initialized. Although not strictly required by PHP, it is generally a good practice to add $trace = array(); before regardless.
Loading history...
174
            'file' => $e->getFile(),
175
            'code' => $this->getSourceCode($e->getFile(), $e->getLine(), 'error-line'),
176
        ];
177
178
        foreach ($e->getTrace() as $item) {
179
            if (($item['class'] ?? null) === __CLASS__) {
180
                continue;
181
            }
182
183
            if (isset($item['file'])) {
184
                $trace[] = [
185
                    'file' => $item['file'],
186
                    'code' => $this->getSourceCode($item['file'], $item['line'] ?? 1, 'switch-line'),
187
                ];
188
            }
189
        }
190
191
        return $trace;
192
    }
193
194
    /**
195
     * Gets the source code where the error happens
196
     * @param string $filename
197
     * @param int $lineNumber
198
     * @param string $className
199
     * @return string
200
     * @throws DiException
201
     * @throws ReflectionException
202
     */
203
    private function getSourceCode(string $filename, int $lineNumber, string $className): string
204
    {
205
        $fs = Di::get(FileSystem::class);
206
207
        $lineNumber--;
208
209
        $start = max($lineNumber - floor(self::NUM_LINES / 2), 1);
210
211
        $lines = $fs->getLines($filename, $start, self::NUM_LINES);
212
213
        $code = '<ol start="' . key($lines) . '">';
214
215
        foreach ($lines as $currentLineNumber => $line) {
216
            $code .= $this->formatLineItem($currentLineNumber, $line, $lineNumber, $className);
217
        }
218
219
        $code .= '</ol>';
220
221
        return $code;
222
    }
223
224
    /**
225
     * Formats the line item
226
     * @param int $currentLineNumber
227
     * @param string $line
228
     * @param int $lineNumber
229
     * @param string $className
230
     * @return string
231
     */
232
    private function formatLineItem(int $currentLineNumber, string $line, int $lineNumber, string $className): string
233
    {
234
        $highlightClass = $currentLineNumber === $lineNumber ? " class=\"{$className}\"" : '';
235
236
        $encodedLine = htmlspecialchars($line, ENT_QUOTES);
237
238
        return sprintf(
239
            '<li%s><pre>%s</pre></li>',
240
            $highlightClass,
241
            $encodedLine
242
        );
243
    }
244
245
    /**
246
     * Gets the error type based on the exception class
247
     * @param Throwable $e
248
     * @return string
249
     */
250
    private function getErrorType(Throwable $e): string
251
    {
252
        if ($e instanceof ErrorException) {
253
            return self::ERROR_TYPES[$e->getSeverity()] ?? LogLevel::ERROR;
254
        }
255
256
        if ($e instanceof ParseError) {
257
            return LogLevel::CRITICAL;
258
        }
259
260
        if ($e instanceof ReflectionException) {
261
            return LogLevel::WARNING;
262
        }
263
264
        return LogLevel::ERROR;
265
    }
266
}