Failed Conditions
Push — master ( 1ef376...9977a5 )
by Arnold
03:50
created

ErrorHandler::handleException()   C

Complexity

Conditions 7
Paths 24

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 31
ccs 21
cts 21
cp 1
rs 6.7272
cc 7
eloc 18
nc 24
nop 1
crap 7
1
<?php
2
3
namespace Jasny;
4
5
use Jasny\ErrorHandler\ErrorCodes;
6
use Jasny\ErrorHandler\Logging;
7
use Jasny\ErrorHandler\Middleware;
8
use Psr\Log\LoggerAwareInterface;
9
10
/**
11
 * Handle error in following middlewares/app actions
12
 */
13
class ErrorHandler implements LoggerAwareInterface
14
{
15
    use Logging;
16
    use ErrorCodes;
17
    
18
    /**
19
     * @var \Exception|\Error
20
     */
21
    protected $error;
22
    
23
    /**
24
     * @var callable|false
25
     */
26
    protected $chainedErrorHandler;
27
28
    /**
29
     * @var callable|false
30
     */
31
    protected $chainedExceptionHandler;
32
33
    /**
34
     * @var boolean
35
     */
36
    protected $registeredShutdown = false;
37
    
38
    /**
39
     * Convert fatal errors to exceptions
40
     * @var boolean
41
     */
42
    protected $convertFatalErrors = false;
43
    
44
    /**
45
     * Log the following error types (in addition to caugth errors)
46
     * @var int
47
     */
48
    protected $logErrorTypes = 0;
49
    
50
    /**
51
     * Log the following exception classes (and subclasses)
52
     * @var array
53
     */
54
    protected $logExceptionClasses = [];
55
    
56
    /**
57
     * A string which reserves memory that can be used to log the error in case of an out of memory fatal error
58
     * @var string
59
     */
60
    protected $reservedMemory;
61
    
62
    /**
63
     * @var callback
64
     */
65
    protected $onFatalError;
66
67
    
68
    /**
69
     * Set the caught error
70
     * 
71
     * @param \Throwable|\Exception|\Error
72
     */
73 4
    public function setError($error)
74
    {
75 4
        if (isset($error) && !$error instanceof \Error && !$error instanceof \Exception) {
76 2
            $type = (is_object($error) ? get_class($error) . ' ' : '') . gettype($error);
77 2
            trigger_error("Excpeted an Error or Exception, got a $type", E_USER_WARNING);
78 2
            return;
79
        }
80
        
81 2
        $this->error = $error;
82 2
    }
83
    
84
    /**
85
     * Get the caught error
86
     * 
87
     * @return \Throwable|\Exception|\Error
88
     */
89 4
    public function getError()
90
    {
91 4
        return $this->error;
92
    }
93
    
94
    
95
    /**
96
     * Get the error handler that has been replaced.
97
     * 
98
     * @return callable|false|null
99
     */
100 2
    public function getChainedErrorHandler()
101
    {
102 2
        return $this->chainedErrorHandler;
103
    }
104
    
105
    /**
106
     * Get the error handler that has been replaced.
107
     * 
108
     * @return callable|false|null
109
     */
110 2
    public function getChainedExceptionHandler()
111
    {
112 2
        return $this->chainedExceptionHandler;
113
    }
114
    
115
    /**
116
     * Get the types of errors that will be logged
117
     * 
118
     * @return int  Binary set of E_* constants
119
     */
120 22
    public function getLoggedErrorTypes()
121
    {
122 22
        return $this->logErrorTypes;
123
    }
124
    
125
    /**
126
     * Get a list of Exception and other Throwable classes that will be logged
127
     * @return array
128
     */
129 4
    public function getLoggedExceptionClasses()
130
    {
131 4
        return $this->logExceptionClasses;
132
    }
133
    
134
    
135
    /**
136
     * Use the global error handler to convert E_USER_ERROR and E_RECOVERABLE_ERROR to an ErrorException
137
     */
138 28
    public function converErrorsToExceptions()
139
    {
140 28
        $this->convertFatalErrors = true;
141 28
        $this->initErrorHandler();
142 28
    }
143
    
144
    /**
145
     * Log these types of errors or exceptions
146
     * 
147
     * @param int|string $type  E_* contants as binary set OR Exception class name
148
     */
149 84
    public function logUncaught($type)
150
    {
151 84
        if (is_int($type)) {
152 66
            $this->logUncaughtErrors($type);
153 52
        } elseif (is_string($type)) {
154 18
            $this->logUncaughtException($type);
155 9
        } else {
156 2
            throw new \InvalidArgumentException("Type should be an error code (int) or Exception class (string)");
157
        }
158 82
    }
159
    
160
    /**
161
     * Log these types of errors or exceptions
162
     * 
163
     * @param string $class  Exception class name
164
     */
165 18
    protected function logUncaughtException($class)
166
    {
167 18
        if (!in_array($class, $this->logExceptionClasses)) {
168 18
            $this->logExceptionClasses[] = $class;
169 9
        }
170
171 18
        $this->initExceptionHandler();
172 18
    }
173
    
174
    /**
175
     * Log these types of errors or exceptions
176
     * 
177
     * @param int $type  E_* contants as binary set
178
     */
179 66
    protected function logUncaughtErrors($type)
180
    {
181 66
        $this->logErrorTypes |= $type;
182
183 66
        $unhandled = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR;
184
185 66
        if ($type & ~$unhandled) {
186 56
            $this->initErrorHandler();
187 28
        }
188
189 66
        if ($type & $unhandled) {
190 32
            $this->initShutdownFunction();
191 16
        }
192 66
    }
193
194
    
195
    /**
196
     * Set a callback for when the script dies because of a fatal, non-catchable error.
197
     * The callback should have an `ErrorException` as only argument.
198
     * 
199
     * @param callable $callback
200
     * @param boolean  $clearOutput  Clear the output buffer before calling the callback
201
     */
202 8
    public function onFatalError($callback, $clearOutput = false)
203
    {
204 8
        if (!$clearOutput) {
205 4
            $this->onFatalError = $callback;
206 2
        } else {
207
            $this->onFatalError = function ($error) use ($callback) {
208 4
                $this->clearOutputBuffer();
209 4
                $callback($error);
210 4
            };
211
        }
212 8
    }
213
    
214
    /**
215
     * Use this error handler as middleware
216
     */
217 2
    public function asMiddleware()
218
    {
219 2
        return new Middleware($this);
220
    }
221
    
222
    
223
    /**
224
     * Use the global error handler
225
     */
226 84
    protected function initErrorHandler()
227
    {
228 84
        if (!isset($this->chainedErrorHandler)) {
229 84
            $this->chainedErrorHandler = $this->setErrorHandler([$this, 'handleError']) ?: false;
230 42
        }
231 84
    }
232
    
233
    /**
234
     * Uncaught error handler
235
     * @ignore
236
     * 
237
     * @param int    $type
238
     * @param string $message
239
     * @param string $file
240
     * @param int    $line
241
     * @param array  $context
242
     */
243 52
    public function handleError($type, $message, $file, $line, $context)
244
    {
245 52
        if ($this->errorReporting() & $type) {
246 52
            $error = new \ErrorException($message, 0, $type, $file, $line);
247
248 52
            if ($this->convertFatalErrors && ($type & (E_RECOVERABLE_ERROR | E_USER_ERROR))) {
249 8
                throw $error;
250
            }
251
252 44
            if ($this->logErrorTypes & $type) {
253 12
                $this->log($error);
254 6
            }
255 22
        }
256
        
257 44
        return $this->chainedErrorHandler
258 22
            ? call_user_func($this->chainedErrorHandler, $type, $message, $file, $line, $context)
259 44
            : false;
260
    }
261
262
    
263
    /**
264
     * Use the global error handler
265
     */
266 18
    protected function initExceptionHandler()
267
    {
268 18
        if (!isset($this->chainedExceptionHandler)) {
269 18
            $this->chainedExceptionHandler = $this->setExceptionHandler([$this, 'handleException']) ?: false;
270 9
        }
271 18
    }
272
    
273
    /**
274
     * Uncaught exception handler
275
     * @ignore
276
     * 
277
     * @param \Exception|\Error $exception
278
     */
279 18
    public function handleException($exception)
280
    {
281 18
        $this->setExceptionHandler(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
282 18
        $this->setErrorHandler(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a callable.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
283
        
284 18
        $isInstanceOf = array_map(function($class) use ($exception) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
285 12
            return is_a($exception, $class);
286 18
        }, $this->logExceptionClasses);
287
        
288 18
        if ($exception instanceof \Error || $exception instanceof \ErrorException) {
289 4
            $type = $exception instanceof \Error ? $exception->getCode() : $exception->getSeverity();
290 4
            $shouldLog = $this->logErrorTypes & $type;
291 2
        } else {
292 14
            $shouldLog = array_sum($isInstanceOf) > 0;
293
        }
294
        
295 18
        if ($shouldLog) {
296 12
            $this->log($exception);
297 6
        }
298
        
299 18
        if ($this->onFatalError) {
300 4
            call_user_func($this->onFatalError, $exception);
301 2
        }
302
        
303 18
        if ($this->chainedExceptionHandler) {
304 6
            call_user_func($this->chainedExceptionHandler, $exception);
305 3
        }
306
        
307
        
308 18
        throw $exception; // This is now handled by the default exception and error handler
309
    }
310
311
    
312
    /**
313
     * Reserve memory for shutdown function in case of out of memory
314
     */
315 32
    protected function reserveMemory()
316
    {
317 32
        $this->reservedMemory = str_repeat(' ', 10 * 1024);
318 32
    }
319
    
320
    /**
321
     * Register a shutdown function
322
     */
323 32
    protected function initShutdownFunction()
324
    {
325 32
        if (!$this->registeredShutdown) {
326 32
            $this->registerShutdownFunction([$this, 'shutdownFunction']) ?: false;
327 32
            $this->registeredShutdown = true;
328
            
329 32
            $this->reserveMemory();
330 16
        }
331 32
    }
332
    
333
    /**
334
     * Called when the script has ends
335
     * @ignore
336
     */
337 16
    public function shutdownFunction()
338
    {
339 16
        $this->reservedMemory = null;
340
        
341 16
        $err = $this->errorGetLast();
342 16
        $unhandled = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR;
343
        
344 16
        if (!$err || !($err['type'] & $unhandled)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $err of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
345 6
            return;
346
        }
347
        
348 10
        $error = new \ErrorException($err['message'], 0, $err['type'], $err['file'], $err['line']);
349
        
350 10
        if ($err['type'] & $this->logErrorTypes) {
351 4
            $this->log($error);
352 2
        }
353
        
354 10
        if ($this->onFatalError) {
355 4
            call_user_func($this->onFatalError, $error);
356 2
        }
357 10
    }
358
    
359
   
360
    /**
361
     * Clear and destroy all the output buffers
362
     * @codeCoverageIgnore
363
     */
364
    protected function clearOutputBuffer()
365
    {
366
        while (ob_get_level() > 0) {
367
            ob_end_clean();
368
        }
369
    }
370
    
371
    /**
372
     * Wrapper method for `error_reporting`
373
     * @codeCoverageIgnore
374
     * 
375
     * @return int
376
     */
377
    protected function errorReporting()
378
    {
379
        return error_reporting();
380
    }
381
382
    /**
383
     * Wrapper method for `error_get_last`
384
     * @codeCoverageIgnore
385
     * 
386
     * @return array
387
     */
388
    protected function errorGetLast()
389
    {
390
        return error_get_last();
391
    }
392
    
393
    /**
394
     * Wrapper method for `set_error_handler`
395
     * @codeCoverageIgnore
396
     * 
397
     * @param callable $callback
398
     * @param int      $error_types
399
     * @return callable|null
400
     */
401
    protected function setErrorHandler($callback, $error_types = E_ALL)
402
    {
403
        return set_error_handler($callback, $error_types);
404
    }
405
    
406
    /**
407
     * Wrapper method for `set_exception_handler`
408
     * @codeCoverageIgnore
409
     * 
410
     * @param callable $callback
411
     * @return callable|null
412
     */
413
    protected function setExceptionHandler($callback)
414
    {
415
        return set_exception_handler($callback);
416
    }
417
    
418
    /**
419
     * Wrapper method for `register_shutdown_function`
420
     * @codeCoverageIgnore
421
     * 
422
     * @param callable $callback
423
     */
424
    protected function registerShutdownFunction($callback)
425
    {
426
        register_shutdown_function($callback);
427
    }
428
}
429