Passed
Pull Request — master (#182)
by Arman
03:14
created

ErrorHandler::getSourceCode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 3
dl 0
loc 16
rs 9.9666
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\Libraries\Storage\FileSystem;
18
use Quantum\Exceptions\ViewException;
19
use Quantum\Exceptions\DiException;
20
use Quantum\Factory\ViewFactory;
21
use Quantum\Logger\LoggerConfig;
22
use Quantum\Http\Response;
23
use Quantum\Logger\Logger;
24
use ReflectionException;
25
use Psr\Log\LogLevel;
26
use ErrorException;
27
use Quantum\Di\Di;
28
use ParseError;
29
use Throwable;
30
31
/**
32
 * Class ErrorHandler
33
 * @package Quantum\Tracer
34
 */
35
class ErrorHandler
36
{
37
    /**
38
     * Number of lines to be returned
39
     */
40
    const NUM_LINES = 10;
41
42
    /**
43
     * @var string[][]
44
     */
45
    const ERROR_TYPES = [
46
        E_ERROR => 'error',
47
        E_WARNING => 'warning',
48
        E_PARSE => 'error',
49
        E_NOTICE => 'notice',
50
        E_CORE_ERROR => 'error',
51
        E_CORE_WARNING => 'warning',
52
        E_COMPILE_ERROR => 'error',
53
        E_COMPILE_WARNING => 'warning',
54
        E_USER_ERROR => 'error',
55
        E_USER_WARNING => 'warning',
56
        E_USER_NOTICE => 'notice',
57
        E_STRICT => 'notice',
58
        E_RECOVERABLE_ERROR => 'error',
59
    ];
60
61
    /**
62
     * @var Logger
63
     */
64
    private $logger;
65
66
    /**
67
     * @var array
68
     */
69
    private $trace = [];
70
71
    private static $instance;
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 ReflectionException
127
     * @throws ViewException
128
     * @throws \Twig\Error\LoaderError
129
     * @throws \Twig\Error\RuntimeError
130
     * @throws \Twig\Error\SyntaxError
131
     */
132
    public function handleException(Throwable $e): void
133
    {
134
        $this->composeStackTrace($e);
135
136
        $view = ViewFactory::getInstance();
137
138
        $errorType = $this->getErrorType($e);
139
140
        if (is_debug_mode()) {
141
            Response::html($view->renderPartial('errors' . DS . 'trace', [
142
                'stackTrace' => $this->trace,
143
                'errorMessage' => $e->getMessage(),
144
                'severity' => ucfirst($errorType),
145
            ]));
146
        } else {
147
            $this->logError($e, $errorType);
148
            Response::html($view->renderPartial('errors' . DS . '500'));
149
        }
150
151
        Response::send();
152
    }
153
154
    /**
155
     * @param Throwable $e
156
     * @param string $errorType
157
     * @return void
158
     */
159
    private function logError(Throwable $e, string $errorType): void
160
    {
161
        if (LoggerConfig::getLogLevel($errorType) >= LoggerConfig::getAppLogLevel()) {
162
            if (method_exists($this->logger, $errorType)) {
163
                $this->logger->$errorType($e->getMessage(), ['trace' => $e->getTraceAsString()]);
164
            } else {
165
                $this->logger->error($e->getMessage(), ['trace' => $e->getTraceAsString()]);
166
            }
167
        }
168
    }
169
170
    /**
171
     * Composes the stack trace
172
     * @param Throwable $e
173
     * @throws DiException
174
     * @throws ReflectionException
175
     */
176
    protected function composeStackTrace(Throwable $e)
177
    {
178
        $this->trace[] = [
179
            'file' => $e->getFile(),
180
            'code' => $this->getSourceCode($e->getFile(), $e->getLine(), 'error-line'),
181
        ];
182
183
        foreach ($e->getTrace() as $item) {
184
            if (($item['class'] ?? null) === __CLASS__) {
185
                continue;
186
            }
187
188
            if (isset($item['file'])) {
189
                $this->trace[] = [
190
                    'file' => $item['file'],
191
                    'code' => $this->getSourceCode($item['file'], $item['line'] ?? 1, 'switch-line'),
192
                ];
193
            }
194
        }
195
    }
196
197
    /**
198
     * Gets the source code where the error happens
199
     * @param string $filename
200
     * @param int $lineNumber
201
     * @param string $className
202
     * @return string
203
     * @throws DiException
204
     * @throws ReflectionException
205
     */
206
    protected function getSourceCode(string $filename, int $lineNumber, string $className): string
207
    {
208
        $fs = Di::get(FileSystem::class);
209
210
        $start = max($lineNumber - floor(self::NUM_LINES / 2), 1);
211
212
        $lines = $fs->getLines($filename, $start, self::NUM_LINES);
213
214
        $code = '<ol start="' . key($lines) . '">';
215
        foreach ($lines as $currentLineNumber => $line) {
216
            $highlight = $currentLineNumber === $lineNumber ? ' class="' . $className . '"' : '';
217
            $code .= '<li' . $highlight . '><pre>' . htmlspecialchars($line, ENT_QUOTES) . '</pre></li>';
218
        }
219
        $code .= '</ol>';
220
221
        return $code;
222
    }
223
224
    /**
225
     * Gets the error type based on the exception class
226
     * @param Throwable $e
227
     * @return string
228
     */
229
    private function getErrorType(Throwable $e): string
230
    {
231
        if ($e instanceof ErrorException) {
232
            return self::ERROR_TYPES[$e->getSeverity()] ?? LogLevel::ERROR;
233
        }
234
235
        if ($e instanceof ParseError) {
236
            return LogLevel::CRITICAL;
237
        }
238
239
        if ($e instanceof ReflectionException) {
240
            return LogLevel::WARNING;
241
        }
242
243
        return LogLevel::ERROR;
244
    }
245
}