Passed
Pull Request — master (#182)
by Arman
12:44 queued 09:39
created

ErrorHandler::getInstance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
rs 10
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 (method_exists($this->logger, $errorType)) {
162
            $this->logger->$errorType($e->getMessage(), ['trace' => $e->getTraceAsString()]);
163
        } else {
164
            $this->logger->error($e->getMessage(), ['trace' => $e->getTraceAsString()]);
165
        }
166
    }
167
168
    /**
169
     * Composes the stack trace
170
     * @param Throwable $e
171
     * @throws DiException
172
     * @throws ReflectionException
173
     */
174
    protected function composeStackTrace(Throwable $e)
175
    {
176
        $this->trace[] = [
177
            'file' => $e->getFile(),
178
            'code' => $this->getSourceCode($e->getFile(), $e->getLine(), 'error-line'),
179
        ];
180
181
        foreach ($e->getTrace() as $item) {
182
            if (($item['class'] ?? null) === __CLASS__) {
183
                continue;
184
            }
185
186
            if (isset($item['file'])) {
187
                $this->trace[] = [
188
                    'file' => $item['file'],
189
                    'code' => $this->getSourceCode($item['file'], $item['line'] ?? 1, 'switch-line'),
190
                ];
191
            }
192
        }
193
    }
194
195
    /**
196
     * Gets the source code where the error happens
197
     * @param string $filename
198
     * @param int $lineNumber
199
     * @param string $className
200
     * @return string
201
     * @throws DiException
202
     * @throws ReflectionException
203
     */
204
    protected function getSourceCode(string $filename, int $lineNumber, string $className): string
205
    {
206
        $fs = Di::get(FileSystem::class);
207
208
        $start = max($lineNumber - floor(self::NUM_LINES / 2), 1);
209
210
        $lines = $fs->getLines($filename, $start, self::NUM_LINES);
211
212
        $code = '<ol start="' . key($lines) . '">';
213
        foreach ($lines as $currentLineNumber => $line) {
214
            $highlight = $currentLineNumber === $lineNumber ? ' class="' . $className . '"' : '';
215
            $code .= '<li' . $highlight . '><pre>' . htmlspecialchars($line, ENT_QUOTES) . '</pre></li>';
216
        }
217
        $code .= '</ol>';
218
219
        return $code;
220
    }
221
222
    /**
223
     * Gets the error type based on the exception class
224
     * @param Throwable $e
225
     * @return string
226
     */
227
    private function getErrorType(Throwable $e): string
228
    {
229
        if ($e instanceof ErrorException) {
230
            return self::ERROR_TYPES[$e->getSeverity()] ?? LogLevel::ERROR;
231
        }
232
233
        if ($e instanceof ParseError) {
234
            return LogLevel::CRITICAL;
235
        }
236
237
        if ($e instanceof ReflectionException) {
238
            return LogLevel::WARNING;
239
        }
240
241
        return LogLevel::ERROR;
242
    }
243
}