Failed Conditions
Branch master (8bf861)
by Arnold
02:57
created

ErrorHandler   C

Complexity

Total Complexity 79

Size/Duplication

Total Lines 476
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 79
lcom 1
cbo 4
dl 0
loc 476
ccs 172
cts 172
cp 1
rs 5.442
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A setLogger() 0 4 1
A getLogger() 0 8 2
B log() 0 13 5
A logError() 0 18 2
A logException() 0 11 1
A getError() 0 4 1
A getChainedErrorHandler() 0 4 1
A getLoggedErrorTypes() 0 4 1
B __invoke() 0 22 5
A errorResponse() 0 7 1
A converErrorsToExceptions() 0 5 1
A alsoLog() 0 15 3
A onFatalError() 0 11 2
A initErrorHandler() 0 6 3
B handleError() 0 18 6
A reserveMemory() 0 4 1
A initShutdownFunction() 0 9 3
B shutdownFunction() 0 21 5
C getLogLevel() 0 27 13
C codeToString() 0 32 16
A clearOutputBuffer() 0 6 2
A errorReporting() 0 4 1
A errorGetLast() 0 4 1
A setErrorHandler() 0 4 1
A registerShutdownFunction() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ErrorHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ErrorHandler, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Jasny;
4
5
use Psr\Http\Message\ServerRequestInterface;
6
use Psr\Http\Message\ResponseInterface;
7
8
use Psr\Log\LoggerInterface;
9
use Psr\Log\LoggerAwareInterface;
10
use Psr\Log\LogLevel;
11
use Psr\Log\NullLogger;
12
13
/**
14
 * Handle error in following middlewares/app actions
15
 */
16
class ErrorHandler implements LoggerAwareInterface
17
{
18
    /**
19
     * @var LoggerInterface
20
     */
21
    protected $logger;
22
23
    /**
24
     * @var \Exception|\Error
25
     */
26
    protected $error;
27
    
28
    /**
29
     * @var callable|false
30
     */
31
    protected $chainedErrorHandler;
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
     * A string which reserves memory that can be used to log the error in case of an out of memory fatal error
52
     * @var string
53
     */
54
    protected $reservedMemory;
55
    
56
    /**
57
     * @var callback
58
     */
59
    protected $onFatalError;
60
61
    
62
    /**
63
     * Set the logger for logging errors
64
     * 
65
     * @param LoggerInterface $logger
66
     */
67 106
    public function setLogger(LoggerInterface $logger)
68
    {
69 106
        $this->logger = $logger;
70 106
    }
71
    
72
    /**
73
     * Set the logger for logging errors
74
     * 
75
     * @return LoggerInterface
76
     */
77 61
    public function getLogger()
78
    {
79 61
        if (!isset($this->logger)) {
80 3
            $this->logger = new NullLogger();
81 1
        }
82
        
83 61
        return $this->logger;
84
    }
85
    
86
    /**
87
     * Log an error or exception
88
     * 
89
     * @param \Exception|\Error $error
90
     */
91 59
    public function log($error)
92
    {
93 59
        if ($error instanceof \Error || $error instanceof \ErrorException) {
1 ignored issue
show
Bug introduced by
The class Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
94 49
            return $this->logError($error);
95
        }
96
        
97 10
        if ($error instanceof \Exception) {
98 6
            return $this->logException($error);
99
        }
100
        
101 4
        $message = "Unable to log a " . (is_object($error) ? get_class($error) . ' ' : '') . gettype($error);
102 4
        $this->getLogger()->log(LogLevel::WARNING, $message);
103 4
    }
104
    
105
    /**
106
     * Log an error
107
     * 
108
     * @param \Error|\ErrorException $error
109
     */
110 49
    protected function logError($error)
111
    {
112 49
        $code = $error instanceof \ErrorException ? $error->getSeverity() : E_ERROR;
113 49
        $level = $this->getLogLevel($code);
114
        
115 49
        $message = sprintf('%s: %s at %s line %s', $this->codeToString($code), $error->getMessage(),
116 49
            $error->getFile(), $error->getLine());
117
118
        $context = [
119 49
            'error' => $error,
120 49
            'code' => $code,
121 49
            'message' => $error->getMessage(),
122 49
            'file' => $error->getFile(),
123 49
            'line' => $error->getLine()
124 24
        ];
125
126 49
        $this->getLogger()->log($level, $message, $context);
127 49
    }
128
    
129
    /**
130
     * Log an exception
131
     * 
132
     * @param \Exception $error
133
     */
134 6
    protected function logException(\Exception $error)
135
    {
136 6
        $level = $this->getLogLevel();
137
        
138 6
        $message = sprintf('Uncaught Exception %s: "%s" at %s line %s', get_class($error), $error->getMessage(),
139 6
            $error->getFile(), $error->getLine());
140
        
141 6
        $context = ['exception' => $error];
142
143 6
        $this->getLogger()->log($level, $message, $context);
144 6
    }
145
    
146
    
147
    /**
148
     * Get the caught error
149
     * 
150
     * @return \Throwable|\Exception|\Error
151
     */
152 3
    public function getError()
153
    {
154 3
        return $this->error;
155
    }
156
    
157
    /**
158
     * Get the error handler that has been replaced.
159
     * 
160
     * @return callable|false|null
161
     */
162 2
    public function getChainedErrorHandler()
163
    {
164 2
        return $this->chainedErrorHandler;
165
    }
166
    
167
    /**
168
     * Get the types of errors that will be logged
169
     * 
170
     * @return int  Binary set of E_* constants
171
     */
172 22
    public function getLoggedErrorTypes()
173
    {
174 22
        return $this->logErrorTypes;
175
    }
176
    
177
    
178
    /**
179
     * Run middleware action
180
     *
181
     * @param ServerRequestInterface $request
182
     * @param ResponseInterface      $response
183
     * @param callback               $next
184
     * @return ResponseInterface
185
     */
186 9
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
187
    {
188 9
        if (!is_callable($next)) {
189 2
            throw new \InvalidArgumentException("'next' should be a callback");            
190
        }
191
192
        try {
193 7
            $this->error = null;
194 7
            $nextResponse = $next($request, $response);
195 6
        } catch(\Error $e) {
1 ignored issue
show
Bug introduced by
The class Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
Coding Style introduced by
Expected 1 space after CATCH keyword; 0 found
Loading history...
196 1
            $this->error = $e;
197 4
        } catch(\Exception $e) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after CATCH keyword; 0 found
Loading history...
198 4
            $this->error = $e;
199
        }
200
        
201 7
        if ($this->error) {
202 5
            $this->log($this->error);
203 5
            $nextResponse = $this->errorResponse($request, $response);
204 2
        }
205
        
206 7
        return $nextResponse;
207
    }
208
209
    /**
210
     * Handle caught error
211
     *
212
     * @param ServerRequestInterface $request
213
     * @param ResponseInterface      $response
214
     * @return ResponseInterface
215
     */
216 5
    protected function errorResponse(ServerRequestInterface $request, ResponseInterface $response)
1 ignored issue
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
217
    {
218 5
        $errorResponse = $response->withStatus(500);
219 5
        $errorResponse->getBody()->write('Unexpected error');
220
221 5
        return $errorResponse;
222
    }
223
    
224
    
225
    /**
226
     * Use the global error handler to convert E_USER_ERROR and E_RECOVERABLE_ERROR to an ErrorException
227
     */
228 28
    public function converErrorsToExceptions()
229
    {
230 28
        $this->convertFatalErrors = true;
231 28
        $this->initErrorHandler();
232 28
    }
233
    
234
    /**
235
     * Also log these types of errors in addition to caught errors and exceptions
236
     * 
237
     * @param int $errorTypes  E_* contants as binary set
238
     */
239 62
    public function alsoLog($errorTypes)
240
    {
241 62
        $this->logErrorTypes |= $errorTypes;
242
        
243 62
        $nonFatal = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_STRICT | E_DEPRECATED | E_USER_DEPRECATED;
244 62
        $unhandled = E_ERROR|E_PARSE|E_CORE_ERROR|E_COMPILE_ERROR;
245
            
246 62
        if ($this->logErrorTypes & $nonFatal) {
247 42
            $this->initErrorHandler();
248 21
        }
249
        
250 62
        if ($this->logErrorTypes & $unhandled) {
251 30
            $this->initShutdownFunction();
252 15
        }
253 62
    }
254
    
255
    /**
256
     * Set a callback for when the script dies because of a fatal, non-catchable error.
257
     * The callback should have an `ErrorException` as only argument.
258
     * 
259
     * @param callable $callback
260
     * @param boolean  $clearOutput  Clear the output buffer before calling the callback
261
     */
262 4
    public function onFatalError($callback, $clearOutput = false)
263
    {
264 4
        if (!$clearOutput) {
265 2
            $this->onFatalError = $callback;
266 1
        } else {
267 2
            $this->onFatalError = function($error) use ($callback) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
268 2
                $this->clearOutputBuffer();
269 2
                $callback($error);
270 2
            };
271
        }
272 4
    }
273
    
274
    /**
275
     * Use the global error handler
276
     */
277 70
    protected function initErrorHandler()
278
    {
279 70
        if (!isset($this->chainedErrorHandler)) {
280 70
            $this->chainedErrorHandler = $this->setErrorHandler([$this, 'handleError']) ?: false;
281 35
        }
282 70
    }
283
    
284
    /**
285
     * Uncaught error handler
286
     * @ignore
287
     * 
288
     * @param int    $type
289
     * @param string $message
290
     * @param string $file
291
     * @param int    $line
292
     * @param array  $context
293
     */
294 52
    public function handleError($type, $message, $file, $line, $context)
295
    {
296 52
        if ($this->errorReporting() & $type) {
297 52
            $error = new \ErrorException($message, 0, $type, $file, $line);
298
299 52
            if ($this->convertFatalErrors && ($type & (E_RECOVERABLE_ERROR | E_USER_ERROR))) {
300 8
                throw $error;
301
            }
302
303 44
            if ($this->logErrorTypes & $type) {
304 12
                $this->log($error);
305 6
            }
306 22
        }
307
        
308 44
        return $this->chainedErrorHandler
309 22
            ? call_user_func($this->chainedErrorHandler, $type, $message, $file, $line, $context)
310 44
            : false;
311
    }
312
313
    /**
314
     * Reserve memory for shutdown function in case of out of memory
315
     */
316 30
    protected function reserveMemory()
317
    {
318 30
        $this->reservedMemory = str_repeat(' ', 10 * 1024);
319 30
    }
320
    
321
    /**
322
     * Register a shutdown function
323
     */
324 30
    protected function initShutdownFunction()
325
    {
326 30
        if (!$this->registeredShutdown) {
327 30
            $this->registerShutdownFunction([$this, 'shutdownFunction']) ?: false;
328 30
            $this->registeredShutdown = true;
329
            
330 30
            $this->reserveMemory();
331 15
        }
332 30
    }
333
    
334
    /**
335
     * Called when the script has ends
336
     * @ignore
337
     */
338 16
    public function shutdownFunction()
339
    {
340 16
        $this->reservedMemory = null;
341
        
342 16
        $err = $this->errorGetLast();
343 16
        $unhandled = E_ERROR|E_PARSE|E_CORE_ERROR|E_COMPILE_ERROR;
344
        
345 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...
346 6
            return;
347
        }
348
        
349 10
        $error = new \ErrorException($err['message'], 0, $err['type'], $err['file'], $err['line']);
350
        
351 10
        if ($err['type'] & $this->logErrorTypes) {
352 4
            $this->log($error);
353 2
        }
354
        
355 10
        if ($this->onFatalError) {
356 4
            call_user_func($this->onFatalError, $error);
357 2
        }
358 10
    }
359
    
360
    
361
    /**
362
     * Get the log level for an error code
363
     * 
364
     * @param int $code  E_* error code
365
     * @return string
366
     */
367 55
    protected function getLogLevel($code = null)
368
    {
369
        switch ($code) {
370 55
            case E_STRICT:
371 53
            case E_DEPRECATED:
372 52
            case E_USER_DEPRECATED:
373 8
                return LogLevel::INFO;
374
            
375 47
            case E_NOTICE:
376 45
            case E_USER_NOTICE:
377 6
                return LogLevel::NOTICE;
378
                
379 41
            case E_WARNING:
380 38
            case E_CORE_WARNING:
381 37
            case E_COMPILE_WARNING:
382 36
            case E_USER_WARNING:
383 12
                return LogLevel::WARNING;
384
            
385 29
            case E_PARSE:
386 27
            case E_CORE_ERROR:
387 26
            case E_COMPILE_ERROR:
388 8
                return LogLevel::CRITICAL;
389
            
390 10
            default:
391 21
                return LogLevel::ERROR;
392 10
        }
393
    }
394
    
395
    /**
396
     * Turn an error code into a string
397
     * 
398
     * @param int $code
399
     * @return string
400
     */
401 49
    protected function codeToString($code)
402
    {
403
        switch ($code) {
404 49
            case E_ERROR:
405 46
            case E_USER_ERROR:
406 45
            case E_RECOVERABLE_ERROR:
407 13
                return 'Fatal error';
408 36
            case E_WARNING:
409 33
            case E_USER_WARNING:
410 8
                return 'Warning';
411 28
            case E_PARSE:
412 4
                return 'Parse error';
413 24
            case E_NOTICE:
414 22
            case E_USER_NOTICE:
415 6
                return 'Notice';
416 18
            case E_CORE_ERROR:
417 2
                return 'Core error';
418 16
            case E_CORE_WARNING:
419 2
                return 'Core warning';
420 14
            case E_COMPILE_ERROR:
421 2
                return 'Compile error';
422 12
            case E_COMPILE_WARNING:
423 2
                return 'Compile warning';
424 10
            case E_STRICT:
425 4
                return 'Strict standards';
426 6
            case E_DEPRECATED:
427 5
            case E_USER_DEPRECATED:
428 4
                return 'Deprecated';
429
        }
430
        
431 2
        return 'Unknown error';
432
    }
433
    
434
    
435
    /**
436
     * Clear and destroy all the output buffers
437
     * @codeCoverageIgnore
438
     */
439
    protected function clearOutputBuffer()
440
    {
441
        while (ob_get_level() > 0) {
442
            ob_end_clean();
443
        }
444
    }
445
    
446
    /**
447
     * Wrapper method for `error_reporting`
448
     * @codeCoverageIgnore
449
     * 
450
     * @return int
451
     */
452
    protected function errorReporting()
453
    {
454
        return error_reporting();
455
    }
456
457
    /**
458
     * Wrapper method for `error_get_last`
459
     * @codeCoverageIgnore
460
     * 
461
     * @return array
462
     */
463
    protected function errorGetLast()
464
    {
465
        return error_get_last();
466
    }
467
    
468
    /**
469
     * Wrapper method for `set_error_handler`
470
     * @codeCoverageIgnore
471
     * 
472
     * @param callable $callback
473
     * @param int      $error_types
474
     * @return callable|null
475
     */
476
    protected function setErrorHandler($callback, $error_types = E_ALL)
477
    {
478
        return set_error_handler($callback, $error_types);
479
    }
480
    
481
    /**
482
     * Wrapper method for `register_shutdown_function`
483
     * @codeCoverageIgnore
484
     * 
485
     * @param callable $callback
486
     */
487
    protected function registerShutdownFunction($callback)
488
    {
489
        register_shutdown_function($callback);
490
    }
491
}
492