Completed
Push — master ( d1be6d...8440b6 )
by Andrey
07:58
created

ErrorHandler::throwAt()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Debug;
13
14
use Psr\Log\LogLevel;
15
use Psr\Log\LoggerInterface;
16
use Symfony\Component\Debug\Exception\ContextErrorException;
17
use Symfony\Component\Debug\Exception\FatalErrorException;
18
use Symfony\Component\Debug\Exception\FatalThrowableError;
19
use Symfony\Component\Debug\Exception\OutOfMemoryException;
20
use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler;
21
use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler;
22
use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler;
23
use Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface;
24
25
/**
26
 * A generic ErrorHandler for the PHP engine.
27
 *
28
 * Provides five bit fields that control how errors are handled:
29
 * - thrownErrors: errors thrown as \ErrorException
30
 * - loggedErrors: logged errors, when not @-silenced
31
 * - scopedErrors: errors thrown or logged with their local context
32
 * - tracedErrors: errors logged with their stack trace, only once for repeated errors
33
 * - screamedErrors: never @-silenced errors
34
 *
35
 * Each error level can be logged by a dedicated PSR-3 logger object.
36
 * Screaming only applies to logging.
37
 * Throwing takes precedence over logging.
38
 * Uncaught exceptions are logged as E_ERROR.
39
 * E_DEPRECATED and E_USER_DEPRECATED levels never throw.
40
 * E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw.
41
 * Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so.
42
 * As errors have a performance cost, repeated errors are all logged, so that the developer
43
 * can see them and weight them as more important to fix than others of the same level.
44
 *
45
 * @author Nicolas Grekas <[email protected]>
46
 */
47
class ErrorHandler
48
{
49
    /**
50
     * @deprecated since version 2.6, to be removed in 3.0.
51
     */
52
    const TYPE_DEPRECATION = -100;
53
54
    private $levels = array(
55
        E_DEPRECATED => 'Deprecated',
56
        E_USER_DEPRECATED => 'User Deprecated',
57
        E_NOTICE => 'Notice',
58
        E_USER_NOTICE => 'User Notice',
59
        E_STRICT => 'Runtime Notice',
60
        E_WARNING => 'Warning',
61
        E_USER_WARNING => 'User Warning',
62
        E_COMPILE_WARNING => 'Compile Warning',
63
        E_CORE_WARNING => 'Core Warning',
64
        E_USER_ERROR => 'User Error',
65
        E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
66
        E_COMPILE_ERROR => 'Compile Error',
67
        E_PARSE => 'Parse Error',
68
        E_ERROR => 'Error',
69
        E_CORE_ERROR => 'Core Error',
70
    );
71
72
    private $loggers = array(
73
        E_DEPRECATED => array(null, LogLevel::INFO),
74
        E_USER_DEPRECATED => array(null, LogLevel::INFO),
75
        E_NOTICE => array(null, LogLevel::WARNING),
76
        E_USER_NOTICE => array(null, LogLevel::WARNING),
77
        E_STRICT => array(null, LogLevel::WARNING),
78
        E_WARNING => array(null, LogLevel::WARNING),
79
        E_USER_WARNING => array(null, LogLevel::WARNING),
80
        E_COMPILE_WARNING => array(null, LogLevel::WARNING),
81
        E_CORE_WARNING => array(null, LogLevel::WARNING),
82
        E_USER_ERROR => array(null, LogLevel::CRITICAL),
83
        E_RECOVERABLE_ERROR => array(null, LogLevel::CRITICAL),
84
        E_COMPILE_ERROR => array(null, LogLevel::CRITICAL),
85
        E_PARSE => array(null, LogLevel::CRITICAL),
86
        E_ERROR => array(null, LogLevel::CRITICAL),
87
        E_CORE_ERROR => array(null, LogLevel::CRITICAL),
88
    );
89
90
    private $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED
91
    private $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED
92
    private $tracedErrors = 0x77FB; // E_ALL - E_STRICT - E_PARSE
93
    private $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE
94
    private $loggedErrors = 0;
95
96
    private $loggedTraces = array();
97
    private $isRecursive = 0;
98
    private $isRoot = false;
99
    private $exceptionHandler;
100
    private $bootstrappingLogger;
101
102
    private static $reservedMemory;
103
    private static $stackedErrors = array();
104
    private static $stackedErrorLevels = array();
105
    private static $toStringException = null;
106
    private static $exitCode = 0;
107
108
    /**
109
     * Same init value as thrownErrors.
110
     *
111
     * @deprecated since version 2.6, to be removed in 3.0.
112
     */
113
    private $displayErrors = 0x1FFF;
114
115
    /**
116
     * Registers the error handler.
117
     *
118
     * @param self|null|int $handler The handler to register, or @deprecated (since version 2.6, to be removed in 3.0) bit field of thrown levels
119
     * @param bool          $replace Whether to replace or not any existing handler
120
     *
121
     * @return self The registered error handler
122
     */
123
    public static function register($handler = null, $replace = true)
124
    {
125
        if (null === self::$reservedMemory) {
126
            self::$reservedMemory = str_repeat('x', 10240);
127
            register_shutdown_function(__CLASS__.'::handleFatalError');
128
        }
129
130
        $levels = -1;
131
132
        if ($handlerIsNew = !$handler instanceof self) {
133
            // @deprecated polymorphism, to be removed in 3.0
134
            if (null !== $handler) {
135
                $levels = $replace ? $handler : 0;
136
                $replace = true;
137
            }
138
            $handler = new static();
139
        }
140
141
        if (null === $prev = set_error_handler(array($handler, 'handleError'))) {
142
            restore_error_handler();
143
            // Specifying the error types earlier would expose us to https://bugs.php.net/63206
144
            set_error_handler(array($handler, 'handleError'), $handler->thrownErrors | $handler->loggedErrors);
145
            $handler->isRoot = true;
146
        }
147
148
        if ($handlerIsNew && is_array($prev) && $prev[0] instanceof self) {
149
            $handler = $prev[0];
150
            $replace = false;
151
        }
152
        if ($replace || !$prev) {
153
            $handler->setExceptionHandler(set_exception_handler(array($handler, 'handleException')));
154
        } else {
155
            restore_error_handler();
156
        }
157
158
        $handler->throwAt($levels & $handler->thrownErrors, true);
159
160
        return $handler;
161
    }
162
163
    public function __construct(BufferingLogger $bootstrappingLogger = null)
164
    {
165
        if ($bootstrappingLogger) {
166
            $this->bootstrappingLogger = $bootstrappingLogger;
167
            $this->setDefaultLogger($bootstrappingLogger);
168
        }
169
    }
170
171
    /**
172
     * Sets a logger to non assigned errors levels.
173
     *
174
     * @param LoggerInterface $logger  A PSR-3 logger to put as default for the given levels
175
     * @param array|int       $levels  An array map of E_* to LogLevel::* or an integer bit field of E_* constants
176
     * @param bool            $replace Whether to replace or not any existing logger
177
     */
178
    public function setDefaultLogger(LoggerInterface $logger, $levels = null, $replace = false)
179
    {
180
        $loggers = array();
181
182
        if (is_array($levels)) {
183
            foreach ($levels as $type => $logLevel) {
184
                if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) {
185
                    $loggers[$type] = array($logger, $logLevel);
186
                }
187
            }
188
        } else {
189
            if (null === $levels) {
190
                $levels = E_ALL | E_STRICT;
191
            }
192
            foreach ($this->loggers as $type => $log) {
193
                if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) {
194
                    $log[0] = $logger;
195
                    $loggers[$type] = $log;
196
                }
197
            }
198
        }
199
200
        $this->setLoggers($loggers);
201
    }
202
203
    /**
204
     * Sets a logger for each error level.
205
     *
206
     * @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map
207
     *
208
     * @return array The previous map
209
     *
210
     * @throws \InvalidArgumentException
211
     */
212
    public function setLoggers(array $loggers)
213
    {
214
        $prevLogged = $this->loggedErrors;
215
        $prev = $this->loggers;
216
        $flush = array();
217
218
        foreach ($loggers as $type => $log) {
219
            if (!isset($prev[$type])) {
220
                throw new \InvalidArgumentException('Unknown error type: '.$type);
221
            }
222
            if (!is_array($log)) {
223
                $log = array($log);
224
            } elseif (!array_key_exists(0, $log)) {
225
                throw new \InvalidArgumentException('No logger provided');
226
            }
227
            if (null === $log[0]) {
228
                $this->loggedErrors &= ~$type;
229
            } elseif ($log[0] instanceof LoggerInterface) {
230
                $this->loggedErrors |= $type;
231
            } else {
232
                throw new \InvalidArgumentException('Invalid logger provided');
233
            }
234
            $this->loggers[$type] = $log + $prev[$type];
235
236
            if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) {
237
                $flush[$type] = $type;
238
            }
239
        }
240
        $this->reRegister($prevLogged | $this->thrownErrors);
241
242
        if ($flush) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $flush 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...
243
            foreach ($this->bootstrappingLogger->cleanLogs() as $log) {
244
                $type = $log[2]['type'];
245
                if (!isset($flush[$type])) {
246
                    $this->bootstrappingLogger->log($log[0], $log[1], $log[2]);
247
                } elseif ($this->loggers[$type][0]) {
248
                    $this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]);
249
                }
250
            }
251
        }
252
253
        return $prev;
254
    }
255
256
    /**
257
     * Sets a user exception handler.
258
     *
259
     * @param callable $handler A handler that will be called on Exception
260
     *
261
     * @return callable|null The previous exception handler
262
     *
263
     * @throws \InvalidArgumentException
264
     */
265 View Code Duplication
    public function setExceptionHandler($handler)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
266
    {
267
        if (null !== $handler && !is_callable($handler)) {
268
            throw new \LogicException('The exception handler must be a valid PHP callable.');
269
        }
270
        $prev = $this->exceptionHandler;
271
        $this->exceptionHandler = $handler;
272
273
        return $prev;
274
    }
275
276
    /**
277
     * Sets the PHP error levels that throw an exception when a PHP error occurs.
278
     *
279
     * @param int  $levels  A bit field of E_* constants for thrown errors
280
     * @param bool $replace Replace or amend the previous value
281
     *
282
     * @return int The previous value
283
     */
284
    public function throwAt($levels, $replace = false)
285
    {
286
        $prev = $this->thrownErrors;
287
        $this->thrownErrors = ($levels | E_RECOVERABLE_ERROR | E_USER_ERROR) & ~E_USER_DEPRECATED & ~E_DEPRECATED;
288
        if (!$replace) {
289
            $this->thrownErrors |= $prev;
290
        }
291
        $this->reRegister($prev | $this->loggedErrors);
292
293
        // $this->displayErrors is @deprecated since version 2.6
294
        $this->displayErrors = $this->thrownErrors;
0 ignored issues
show
Deprecated Code introduced by
The property Symfony\Component\Debug\...Handler::$displayErrors has been deprecated with message: since version 2.6, to be removed in 3.0.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
295
296
        return $prev;
297
    }
298
299
    /**
300
     * Sets the PHP error levels for which local variables are preserved.
301
     *
302
     * @param int  $levels  A bit field of E_* constants for scoped errors
303
     * @param bool $replace Replace or amend the previous value
304
     *
305
     * @return int The previous value
306
     */
307
    public function scopeAt($levels, $replace = false)
308
    {
309
        $prev = $this->scopedErrors;
310
        $this->scopedErrors = (int) $levels;
311
        if (!$replace) {
312
            $this->scopedErrors |= $prev;
313
        }
314
315
        return $prev;
316
    }
317
318
    /**
319
     * Sets the PHP error levels for which the stack trace is preserved.
320
     *
321
     * @param int  $levels  A bit field of E_* constants for traced errors
322
     * @param bool $replace Replace or amend the previous value
323
     *
324
     * @return int The previous value
325
     */
326
    public function traceAt($levels, $replace = false)
327
    {
328
        $prev = $this->tracedErrors;
329
        $this->tracedErrors = (int) $levels;
330
        if (!$replace) {
331
            $this->tracedErrors |= $prev;
332
        }
333
334
        return $prev;
335
    }
336
337
    /**
338
     * Sets the error levels where the @-operator is ignored.
339
     *
340
     * @param int  $levels  A bit field of E_* constants for screamed errors
341
     * @param bool $replace Replace or amend the previous value
342
     *
343
     * @return int The previous value
344
     */
345
    public function screamAt($levels, $replace = false)
346
    {
347
        $prev = $this->screamedErrors;
348
        $this->screamedErrors = (int) $levels;
349
        if (!$replace) {
350
            $this->screamedErrors |= $prev;
351
        }
352
353
        return $prev;
354
    }
355
356
    /**
357
     * Re-registers as a PHP error handler if levels changed.
358
     */
359
    private function reRegister($prev)
360
    {
361
        if ($prev !== $this->thrownErrors | $this->loggedErrors) {
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: ($prev !== $this->thrown...) | $this->loggedErrors, Probably Intended Meaning: $prev !== ($this->thrown... | $this->loggedErrors)

When comparing the result of a bit operation, we suggest to add explicit parenthesis and not to rely on PHP’s built-in operator precedence to ensure the code behaves as intended and to make it more readable.

Let’s take a look at these examples:

// Returns always int(0).
return 0 === $foo & 4;
return (0 === $foo) & 4;

// More likely intended return: true/false
return 0 === ($foo & 4);
Loading history...
362
            $handler = set_error_handler('var_dump');
363
            $handler = is_array($handler) ? $handler[0] : null;
364
            restore_error_handler();
365
            if ($handler === $this) {
366
                restore_error_handler();
367
                if ($this->isRoot) {
368
                    set_error_handler(array($this, 'handleError'), $this->thrownErrors | $this->loggedErrors);
369
                } else {
370
                    set_error_handler(array($this, 'handleError'));
371
                }
372
            }
373
        }
374
    }
375
376
    /**
377
     * Handles errors by filtering then logging them according to the configured bit fields.
378
     *
379
     * @param int    $type    One of the E_* constants
380
     * @param string $message
381
     * @param string $file
382
     * @param int    $line
383
     *
384
     * @return bool Returns false when no handling happens so that the PHP engine can handle the error itself
385
     *
386
     * @throws \ErrorException When $this->thrownErrors requests so
387
     *
388
     * @internal
389
     */
390
    public function handleError($type, $message, $file, $line)
391
    {
392
        $level = error_reporting() | E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED;
393
        $log = $this->loggedErrors & $type;
394
        $throw = $this->thrownErrors & $type & $level;
395
        $type &= $level | $this->screamedErrors;
396
397
        if (!$type || (!$log && !$throw)) {
398
            return $type && $log;
399
        }
400
        $scope = $this->scopedErrors & $type;
401
402
        if (4 < $numArgs = func_num_args()) {
403
            $context = $scope ? (func_get_arg(4) ?: array()) : array();
404
            $backtrace = 5 < $numArgs ? func_get_arg(5) : null; // defined on HHVM
405
        } else {
406
            $context = array();
407
            $backtrace = null;
408
        }
409
410
        if (isset($context['GLOBALS']) && $scope) {
411
            $e = $context;                  // Whatever the signature of the method,
412
            unset($e['GLOBALS'], $context); // $context is always a reference in 5.3
413
            $context = $e;
414
        }
415
416
        if (null !== $backtrace && $type & E_ERROR) {
417
            // E_ERROR fatal errors are triggered on HHVM when
418
            // hhvm.error_handling.call_user_handler_on_fatals=1
419
            // which is the way to get their backtrace.
420
            $this->handleFatalError(compact('type', 'message', 'file', 'line', 'backtrace'));
421
422
            return true;
423
        }
424
425
        if ($throw) {
426
            if (null !== self::$toStringException) {
427
                $throw = self::$toStringException;
428
                self::$toStringException = null;
429
            } elseif ($scope && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
430
                // Checking for class existence is a work around for https://bugs.php.net/42098
431
                $throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context);
432
            } else {
433
                $throw = new \ErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line);
434
            }
435
436
            if (\PHP_VERSION_ID <= 50407 && (\PHP_VERSION_ID >= 50400 || \PHP_VERSION_ID <= 50317)) {
437
                // Exceptions thrown from error handlers are sometimes not caught by the exception
438
                // handler and shutdown handlers are bypassed before 5.4.8/5.3.18.
439
                // We temporarily re-enable display_errors to prevent any blank page related to this bug.
440
441
                $throw->errorHandlerCanary = new ErrorHandlerCanary();
442
            }
443
444
            if (E_USER_ERROR & $type) {
445
                $backtrace = $backtrace ?: $throw->getTrace();
446
447
                for ($i = 1; isset($backtrace[$i]); ++$i) {
448
                    if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function'])
449
                        && '__toString' === $backtrace[$i]['function']
450
                        && '->' === $backtrace[$i]['type']
451
                        && !isset($backtrace[$i - 1]['class'])
452
                        && ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function'])
453
                    ) {
454
                        // Here, we know trigger_error() has been called from __toString().
455
                        // HHVM is fine with throwing from __toString() but PHP triggers a fatal error instead.
456
                        // A small convention allows working around the limitation:
457
                        // given a caught $e exception in __toString(), quitting the method with
458
                        // `return trigger_error($e, E_USER_ERROR);` allows this error handler
459
                        // to make $e get through the __toString() barrier.
460
461
                        foreach ($context as $e) {
462
                            if (($e instanceof \Exception || $e instanceof \Throwable) && $e->__toString() === $message) {
463
                                if (1 === $i) {
464
                                    // On HHVM
465
                                    $throw = $e;
466
                                    break;
467
                                }
468
                                self::$toStringException = $e;
469
470
                                return true;
471
                            }
472
                        }
473
474
                        if (1 < $i) {
475
                            // On PHP (not on HHVM), display the original error message instead of the default one.
476
                            $this->handleException($throw);
477
478
                            // Stop the process by giving back the error to the native handler.
479
                            return false;
480
                        }
481
                    }
482
                }
483
            }
484
485
            throw $throw;
486
        }
487
488
        // For duplicated errors, log the trace only once
489
        $e = md5("{$type}/{$line}/{$file}\x00{$message}", true);
490
        $trace = true;
491
492
        if (!($this->tracedErrors & $type) || isset($this->loggedTraces[$e])) {
493
            $trace = false;
494
        } else {
495
            $this->loggedTraces[$e] = 1;
496
        }
497
498
        $e = compact('type', 'file', 'line', 'level');
499
500
        if ($type & $level) {
501
            if ($scope) {
502
                $e['scope_vars'] = $context;
503
                if ($trace) {
504
                    $e['stack'] = $backtrace ?: debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
505
                }
506
            } elseif ($trace) {
507
                if (null === $backtrace) {
508
                    $e['stack'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
509
                } else {
510
                    foreach ($backtrace as &$frame) {
511
                        unset($frame['args'], $frame);
512
                    }
513
                    $e['stack'] = $backtrace;
514
                }
515
            }
516
        }
517
518
        if ($this->isRecursive) {
519
            $log = 0;
520
        } elseif (self::$stackedErrorLevels) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$stackedErrorLevels 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...
521
            self::$stackedErrors[] = array($this->loggers[$type][0], ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG, $message, $e);
522
        } else {
523
            try {
524
                $this->isRecursive = true;
0 ignored issues
show
Documentation Bug introduced by
The property $isRecursive was declared of type integer, but true is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
525
                $this->loggers[$type][0]->log(($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG, $message, $e);
526
                $this->isRecursive = false;
0 ignored issues
show
Documentation Bug introduced by
The property $isRecursive was declared of type integer, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
527
            } catch (\Exception $e) {
528
                $this->isRecursive = false;
529
530
                throw $e;
531
            } catch (\Throwable $e) {
532
                $this->isRecursive = false;
533
534
                throw $e;
535
            }
536
        }
537
538
        return $type && $log;
539
    }
540
541
    /**
542
     * Handles an exception by logging then forwarding it to another handler.
543
     *
544
     * @param \Exception|\Throwable $exception An exception to handle
545
     * @param array                 $error     An array as returned by error_get_last()
546
     *
547
     * @internal
548
     */
549
    public function handleException($exception, array $error = null)
550
    {
551
        if (null === $error) {
552
            self::$exitCode = 255;
553
        }
554
        if (!$exception instanceof \Exception) {
555
            $exception = new FatalThrowableError($exception);
556
        }
557
        $type = $exception instanceof FatalErrorException ? $exception->getSeverity() : E_ERROR;
558
559
        if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) {
560
            $e = array(
561
                'type' => $type,
562
                'file' => $exception->getFile(),
0 ignored issues
show
Bug introduced by
The method getFile does only exist in Exception, but not in Symfony\Component\Debug\...ion\FatalErrorException.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
563
                'line' => $exception->getLine(),
0 ignored issues
show
Bug introduced by
The method getLine does only exist in Exception, but not in Symfony\Component\Debug\...ion\FatalErrorException.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
564
                'level' => error_reporting(),
565
                'stack' => $exception->getTrace(),
0 ignored issues
show
Bug introduced by
The method getTrace does only exist in Exception, but not in Symfony\Component\Debug\...ion\FatalErrorException.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
566
            );
567
            if ($exception instanceof FatalErrorException) {
568
                if ($exception instanceof FatalThrowableError) {
569
                    $error = array(
570
                        'type' => $type,
571
                        'message' => $message = $exception->getMessage(),
572
                        'file' => $e['file'],
573
                        'line' => $e['line'],
574
                    );
575
                } else {
576
                    $message = 'Fatal '.$exception->getMessage();
577
                }
578
            } elseif ($exception instanceof \ErrorException) {
0 ignored issues
show
Bug introduced by
The class ErrorException 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...
579
                $message = 'Uncaught '.$exception->getMessage();
580
                if ($exception instanceof ContextErrorException) {
581
                    $e['context'] = $exception->getContext();
582
                }
583
            } else {
584
                $message = 'Uncaught Exception: '.$exception->getMessage();
585
            }
586
        }
587
        if ($this->loggedErrors & $type) {
588
            try {
589
                $this->loggers[$type][0]->log($this->loggers[$type][1], $message, $e);
0 ignored issues
show
Bug introduced by
The variable $message does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $e does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
590
            } catch (\Exception $handlerException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
591
            } catch (\Throwable $handlerException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
592
            }
593
        }
594
        if ($exception instanceof FatalErrorException && !$exception instanceof OutOfMemoryException && $error) {
595
            foreach ($this->getFatalErrorHandlers() as $handler) {
596
                if ($e = $handler->handleError($error, $exception)) {
597
                    $exception = $e;
598
                    break;
599
                }
600
            }
601
        }
602
        if (empty($this->exceptionHandler)) {
603
            throw $exception; // Give back $exception to the native handler
604
        }
605
        try {
606
            call_user_func($this->exceptionHandler, $exception);
607
        } catch (\Exception $handlerException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
608
        } catch (\Throwable $handlerException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
609
        }
610
        if (isset($handlerException)) {
611
            $this->exceptionHandler = null;
612
            $this->handleException($handlerException);
613
        }
614
    }
615
616
    /**
617
     * Shutdown registered function for handling PHP fatal errors.
618
     *
619
     * @param array $error An array as returned by error_get_last()
620
     *
621
     * @internal
622
     */
623
    public static function handleFatalError(array $error = null)
624
    {
625
        if (null === self::$reservedMemory) {
626
            return;
627
        }
628
629
        self::$reservedMemory = null;
630
631
        $handler = set_error_handler('var_dump');
632
        $handler = is_array($handler) ? $handler[0] : null;
633
        restore_error_handler();
634
635
        if (!$handler instanceof self) {
636
            return;
637
        }
638
639
        if ($exit = null === $error) {
640
            $error = error_get_last();
641
        }
642
643
        try {
644
            while (self::$stackedErrorLevels) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$stackedErrorLevels 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...
645
                static::unstackErrors();
646
            }
647
        } catch (\Exception $exception) {
648
            // Handled below
649
        } catch (\Throwable $exception) {
650
            // Handled below
651
        }
652
653
        if ($error && $error['type'] &= E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $error 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...
654
            // Let's not throw anymore but keep logging
655
            $handler->throwAt(0, true);
656
            $trace = isset($error['backtrace']) ? $error['backtrace'] : null;
657
658
            if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) {
659
                $exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false, $trace);
660
            } else {
661
                $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace);
662
            }
663
        }
664
665
        try {
666
            if (isset($exception)) {
667
                self::$exitCode = 255;
668
                $handler->handleException($exception, $error);
669
            }
670
        } catch (FatalErrorException $e) {
671
            // Ignore this re-throw
672
        }
673
674
        if ($exit && self::$exitCode) {
675
            $exitCode = self::$exitCode;
676
            register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); });
677
        }
678
    }
679
680
    /**
681
     * Configures the error handler for delayed handling.
682
     * Ensures also that non-catchable fatal errors are never silenced.
683
     *
684
     * As shown by http://bugs.php.net/42098 and http://bugs.php.net/60724
685
     * PHP has a compile stage where it behaves unusually. To workaround it,
686
     * we plug an error handler that only stacks errors for later.
687
     *
688
     * The most important feature of this is to prevent
689
     * autoloading until unstackErrors() is called.
690
     */
691
    public static function stackErrors()
692
    {
693
        self::$stackedErrorLevels[] = error_reporting(error_reporting() | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR);
694
    }
695
696
    /**
697
     * Unstacks stacked errors and forwards to the logger.
698
     */
699
    public static function unstackErrors()
700
    {
701
        $level = array_pop(self::$stackedErrorLevels);
702
703
        if (null !== $level) {
704
            $e = error_reporting($level);
705
            if ($e !== ($level | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR)) {
706
                // If the user changed the error level, do not overwrite it
707
                error_reporting($e);
708
            }
709
        }
710
711
        if (empty(self::$stackedErrorLevels)) {
712
            $errors = self::$stackedErrors;
713
            self::$stackedErrors = array();
714
715
            foreach ($errors as $e) {
716
                $e[0]->log($e[1], $e[2], $e[3]);
717
            }
718
        }
719
    }
720
721
    /**
722
     * Gets the fatal error handlers.
723
     *
724
     * Override this method if you want to define more fatal error handlers.
725
     *
726
     * @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface
727
     */
728
    protected function getFatalErrorHandlers()
729
    {
730
        return array(
731
            new UndefinedFunctionFatalErrorHandler(),
732
            new UndefinedMethodFatalErrorHandler(),
733
            new ClassNotFoundFatalErrorHandler(),
734
        );
735
    }
736
737
    /**
738
     * Sets the level at which the conversion to Exception is done.
739
     *
740
     * @param int|null $level The level (null to use the error_reporting() value and 0 to disable)
741
     *
742
     * @deprecated since version 2.6, to be removed in 3.0. Use throwAt() instead.
743
     */
744
    public function setLevel($level)
745
    {
746
        @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the throwAt() method instead.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
747
748
        $level = null === $level ? error_reporting() : $level;
749
        $this->throwAt($level, true);
750
    }
751
752
    /**
753
     * Sets the display_errors flag value.
754
     *
755
     * @param int $displayErrors The display_errors flag value
756
     *
757
     * @deprecated since version 2.6, to be removed in 3.0. Use throwAt() instead.
758
     */
759
    public function setDisplayErrors($displayErrors)
760
    {
761
        @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the throwAt() method instead.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
762
763
        if ($displayErrors) {
764
            $this->throwAt($this->displayErrors, true);
0 ignored issues
show
Deprecated Code introduced by
The property Symfony\Component\Debug\...Handler::$displayErrors has been deprecated with message: since version 2.6, to be removed in 3.0.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
765
        } else {
766
            $displayErrors = $this->displayErrors;
0 ignored issues
show
Deprecated Code introduced by
The property Symfony\Component\Debug\...Handler::$displayErrors has been deprecated with message: since version 2.6, to be removed in 3.0.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
767
            $this->throwAt(0, true);
768
            $this->displayErrors = $displayErrors;
0 ignored issues
show
Deprecated Code introduced by
The property Symfony\Component\Debug\...Handler::$displayErrors has been deprecated with message: since version 2.6, to be removed in 3.0.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
769
        }
770
    }
771
772
    /**
773
     * Sets a logger for the given channel.
774
     *
775
     * @param LoggerInterface $logger  A logger interface
776
     * @param string          $channel The channel associated with the logger (deprecation, emergency or scream)
777
     *
778
     * @deprecated since version 2.6, to be removed in 3.0. Use setLoggers() or setDefaultLogger() instead.
779
     */
780
    public static function setLogger(LoggerInterface $logger, $channel = 'deprecation')
781
    {
782
        @trigger_error('The '.__METHOD__.' static method is deprecated since version 2.6 and will be removed in 3.0. Use the setLoggers() or setDefaultLogger() methods instead.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
783
784
        $handler = set_error_handler('var_dump');
785
        $handler = is_array($handler) ? $handler[0] : null;
786
        restore_error_handler();
787
        if (!$handler instanceof self) {
788
            return;
789
        }
790
        if ('deprecation' === $channel) {
791
            $handler->setDefaultLogger($logger, E_DEPRECATED | E_USER_DEPRECATED, true);
792
            $handler->screamAt(E_DEPRECATED | E_USER_DEPRECATED);
793
        } elseif ('scream' === $channel) {
794
            $handler->setDefaultLogger($logger, E_ALL | E_STRICT, false);
795
            $handler->screamAt(E_ALL | E_STRICT);
796
        } elseif ('emergency' === $channel) {
797
            $handler->setDefaultLogger($logger, E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR, true);
798
            $handler->screamAt(E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR);
799
        }
800
    }
801
802
    /**
803
     * @deprecated since version 2.6, to be removed in 3.0. Use handleError() instead.
804
     */
805
    public function handle($level, $message, $file = 'unknown', $line = 0, $context = array())
806
    {
807
        $this->handleError(E_USER_DEPRECATED, 'The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the handleError() method instead.', __FILE__, __LINE__, array());
808
809
        return $this->handleError($level, $message, $file, $line, (array) $context);
810
    }
811
812
    /**
813
     * Handles PHP fatal errors.
814
     *
815
     * @deprecated since version 2.6, to be removed in 3.0. Use handleFatalError() instead.
816
     */
817
    public function handleFatal()
818
    {
819
        @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the handleFatalError() method instead.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
820
821
        static::handleFatalError();
822
    }
823
}
824
825
/**
826
 * Private class used to work around https://bugs.php.net/54275.
827
 *
828
 * @author Nicolas Grekas <[email protected]>
829
 *
830
 * @internal
831
 */
832
class ErrorHandlerCanary
833
{
834
    private static $displayErrors = null;
835
836
    public function __construct()
837
    {
838
        if (null === self::$displayErrors) {
839
            self::$displayErrors = ini_set('display_errors', 1);
840
        }
841
    }
842
843
    public function __destruct()
844
    {
845
        if (null !== self::$displayErrors) {
846
            ini_set('display_errors', self::$displayErrors);
847
            self::$displayErrors = null;
848
        }
849
    }
850
}
851