Compare commits

...

7 Commits

Author SHA1 Message Date
Fabien Potencier 7fd8006e46 minor #10988 [Debug] preserve modified error level (nicolas-grekas)
This PR was merged into the 2.5 branch.

Discussion
----------

[Debug] preserve modified error level

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | none
| License       | MIT
| Doc PR        | none

Minor edge case, but still a bug fix.
Replaces https://github.com/symfony/symfony/pull/10978

Commits
-------

e40b717 [Debug] preserve modified error level
2014-05-25 16:44:21 +02:00
Nicolas Grekas 3002c206bc [Debug] throw even in stacking mode to preserve code paths 2014-05-25 12:15:31 +02:00
Nicolas Grekas 4a463783a3 [Debug] preserve modified error level 2014-05-25 11:36:54 +02:00
Nicolas Grekas 560cda449b [Debug] better ouf of memory error handling 2014-05-21 14:34:28 +02:00
Nicolas Grekas 2e8dc2e20c [Debug] cleanup interfaces before 2.5-final 2014-05-20 09:56:04 +02:00
Nicolas Grekas b293ddb67a [Debug] enhance perf of DebugClassLoader 2014-05-08 10:31:54 +02:00
Nicolas Grekas e25750cda2 [Debug] fix handling deprecated warnings and stacked errors turned into exceptions 2014-05-05 07:50:37 +00:00
9 changed files with 221 additions and 190 deletions
+1 -2
View File
@@ -4,9 +4,8 @@ CHANGELOG
2.5.0
-----
* added ErrorHandler::setFatalErrorExceptionHandler()
* added ExceptionHandler::setHandler()
* added UndefinedMethodFatalErrorHandler
* deprecated ExceptionHandlerInterface
* deprecated DummyException
2.4.0
+31 -25
View File
@@ -29,6 +29,7 @@ class DebugClassLoader
private $classLoader;
private $isFinder;
private $wasFinder;
private static $caseCheck;
/**
* Constructor.
@@ -49,6 +50,10 @@ class DebugClassLoader
$this->classLoader = $classLoader;
$this->isFinder = is_array($classLoader) && method_exists($classLoader[0], 'findFile');
}
if (!isset(self::$caseCheck)) {
self::$caseCheck = false !== stripos(PHP_OS, 'win') ? (false !== stripos(PHP_OS, 'darwin') ? 2 : 1) : 0;
}
}
/**
@@ -162,9 +167,13 @@ class DebugClassLoader
$exists = class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false));
if ('\\' === $class[0]) {
$class = substr($class, 1);
}
if ($exists) {
$name = new \ReflectionClass($class);
$name = $name->getName();
$refl = new \ReflectionClass($class);
$name = $refl->getName();
if ($name !== $class) {
throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: %s vs %s', $class, $name));
@@ -172,30 +181,35 @@ class DebugClassLoader
}
if ($file) {
if ('\\' == $class[0]) {
$class = substr($class, 1);
if (!$exists) {
if (false !== strpos($class, '/')) {
throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class));
}
throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file));
}
if (preg_match('#([/\\\\][a-zA-Z_\x7F-\xFF][a-zA-Z0-9_\x7F-\xFF]*)+\.(php|hh)$#', $file, $tail)) {
if (self::$caseCheck && preg_match('#([/\\\\][a-zA-Z_\x7F-\xFF][a-zA-Z0-9_\x7F-\xFF]*)+\.(php|hh)$#D', $file, $tail)) {
$tail = $tail[0];
$real = realpath($file);
$real = $refl->getFilename();
if (false !== stripos(PHP_OS, 'darwin')) {
// realpath() on MacOSX doesn't normalize the case of characters,
// let's do it ourselves. This is tricky.
if (2 === self::$caseCheck) {
// realpath() on MacOSX doesn't normalize the case of characters
$cwd = getcwd();
$basename = strrpos($real, '/');
chdir(substr($real, 0, $basename));
$basename = substr($real, $basename + 1);
$real = getcwd().'/';
$h = opendir('.');
while (false !== $f = readdir($h)) {
if (0 === strcasecmp($f, $basename)) {
$real .= $f;
break;
// glob() patterns are case-sensitive even if the underlying fs is not
if (!in_array($basename, glob($basename.'*', GLOB_NOSORT), true)) {
$real = getcwd().'/';
$h = opendir('.');
while (false !== $f = readdir($h)) {
if (0 === strcasecmp($f, $basename)) {
$real .= $f;
break;
}
}
closedir($h);
}
closedir($h);
chdir($cwd);
}
@@ -206,14 +220,6 @@ class DebugClassLoader
}
}
if (!$exists) {
if (false !== strpos($class, '/')) {
throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class));
}
throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file));
}
return true;
}
}
+54 -114
View File
@@ -15,6 +15,7 @@ use Psr\Log\LogLevel;
use Psr\Log\LoggerInterface;
use Symfony\Component\Debug\Exception\ContextErrorException;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\Exception\OutOfMemoryException;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler;
use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler;
@@ -53,8 +54,6 @@ class ErrorHandler
private $displayErrors;
private $caughtOutput = 0;
/**
* @var LoggerInterface[] Loggers for channels
*/
@@ -64,8 +63,6 @@ class ErrorHandler
private static $stackedErrorLevels = array();
private static $fatalHandler = false;
/**
* Registers the error handler.
*
@@ -120,17 +117,7 @@ class ErrorHandler
}
/**
* Sets a fatal error exception handler.
*
* @param callable $handler An handler that will be called on FatalErrorException
*/
public static function setFatalErrorExceptionHandler($handler)
{
self::$fatalHandler = $handler;
}
/**
* @throws ContextErrorException When error_reporting returns error
* @throws \ErrorException When error_reporting returns error
*/
public function handle($level, $message, $file = 'unknown', $line = 0, $context = array())
{
@@ -154,24 +141,23 @@ class ErrorHandler
self::$loggers['deprecation']->warning($message, array('type' => self::TYPE_DEPRECATION, 'stack' => $stack));
}
}
return true;
}
if ($this->displayErrors && error_reporting() & $level && $this->level & $level) {
if (self::$stackedErrorLevels) {
self::$stackedErrors[] = func_get_args();
return true;
}
} elseif ($this->displayErrors && error_reporting() & $level && $this->level & $level) {
if (PHP_VERSION_ID < 50400 && isset($context['GLOBALS']) && is_array($context)) {
unset($context['GLOBALS']);
$c = $context; // Whatever the signature of the method,
unset($c['GLOBALS'], $context); // $context is always a reference in 5.3
$context = $c;
}
$exception = sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line);
$exception = new ContextErrorException($exception, 0, $level, $file, $line, $context);
if ($context && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
// Checking for class existence is a work around for https://bugs.php.net/42098
$exception = new ContextErrorException($exception, 0, $level, $file, $line, $context);
} else {
$exception = new \ErrorException($exception, 0, $level, $file, $line);
}
if (PHP_VERSION_ID <= 50407 && (PHP_VERSION_ID >= 50400 || PHP_VERSION_ID <= 50317)) {
// Exceptions thrown from error handlers are sometimes not caught by the exception
@@ -240,7 +226,11 @@ class ErrorHandler
$level = array_pop(self::$stackedErrorLevels);
if (null !== $level) {
error_reporting($level);
$e = error_reporting($level);
if ($e !== ($level | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR)) {
// If the user changed the error level, do not overwrite it
error_reporting($e);
}
}
if (empty(self::$stackedErrorLevels)) {
@@ -264,22 +254,35 @@ class ErrorHandler
gc_collect_cycles();
$error = error_get_last();
while (self::$stackedErrorLevels) {
static::unstackErrors();
// get current exception handler
$exceptionHandler = set_exception_handler('var_dump');
restore_exception_handler();
try {
while (self::$stackedErrorLevels) {
static::unstackErrors();
}
} catch (\Exception $exception) {
if ($exceptionHandler) {
call_user_func($exceptionHandler, $exception);
return;
}
if ($this->displayErrors) {
ini_set('display_errors', 1);
}
throw $exception;
}
if (null === $error) {
return;
}
$type = $error['type'];
if (0 === $this->level || !in_array($type, array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) {
if (!$error || !$this->level || !($error['type'] & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_PARSE))) {
return;
}
if (isset(self::$loggers['emergency'])) {
$fatal = array(
'type' => $type,
'type' => $error['type'],
'file' => $error['file'],
'line' => $error['line'],
);
@@ -287,14 +290,8 @@ class ErrorHandler
self::$loggers['emergency']->emergency($error['message'], $fatal);
}
if ($this->displayErrors) {
// get current exception handler
$exceptionHandler = set_exception_handler('var_dump');
restore_exception_handler();
if ($exceptionHandler || self::$fatalHandler) {
$this->handleFatalError($exceptionHandler, $error);
}
if ($this->displayErrors && $exceptionHandler) {
$this->handleFatalError($exceptionHandler, $error);
}
}
@@ -322,82 +319,25 @@ class ErrorHandler
$level = isset($this->levels[$error['type']]) ? $this->levels[$error['type']] : $error['type'];
$message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']);
$exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line'], 3);
if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) {
$exception = new OutOfMemoryException($message, 0, $error['type'], $error['file'], $error['line'], 3, false);
} else {
$exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line'], 3, true);
foreach ($this->getFatalErrorHandlers() as $handler) {
if ($e = $handler->handleError($error, $exception)) {
$exception = $e;
break;
}
}
// To be as fail-safe as possible, the FatalErrorException is first handled
// by the exception handler, then by the fatal error handler. The latter takes
// precedence and any output from the former is cancelled, if and only if
// nothing bad happens in this handling path.
$caughtOutput = 0;
if ($exceptionHandler) {
$this->caughtOutput = false;
ob_start(array($this, 'catchOutput'));
try {
call_user_func($exceptionHandler, $exception);
} catch (\Exception $e) {
// Ignore this exception, we have to deal with the fatal error
}
if (false === $this->caughtOutput) {
ob_end_clean();
}
if (isset($this->caughtOutput[0])) {
ob_start(array($this, 'cleanOutput'));
echo $this->caughtOutput;
$caughtOutput = ob_get_length();
}
$this->caughtOutput = 0;
}
if (self::$fatalHandler) {
try {
call_user_func(self::$fatalHandler, $exception);
if ($caughtOutput) {
$this->caughtOutput = $caughtOutput;
}
} catch (\Exception $e) {
if (!$caughtOutput) {
// Neither the exception nor the fatal handler succeeded.
// Let PHP handle that now.
throw $exception;
foreach ($this->getFatalErrorHandlers() as $handler) {
if ($e = $handler->handleError($error, $exception)) {
$exception = $e;
break;
}
}
}
}
/**
* @internal
*/
public function catchOutput($buffer)
{
$this->caughtOutput = $buffer;
return '';
}
/**
* @internal
*/
public function cleanOutput($buffer)
{
if ($this->caughtOutput) {
// use substr_replace() instead of substr() for mbstring overloading resistance
$cleanBuffer = substr_replace($buffer, '', 0, $this->caughtOutput);
if (isset($cleanBuffer[0])) {
$buffer = $cleanBuffer;
}
try {
call_user_func($exceptionHandler, $exception);
} catch (\Exception $e) {
// The handler failed. Let PHP handle that now.
throw $exception;
}
return $buffer;
}
}
+14 -10
View File
@@ -20,7 +20,7 @@ namespace Symfony\Component\Debug\Exception;
*/
class FatalErrorException extends \ErrorException
{
public function __construct($message, $code, $severity, $filename, $lineno, $traceOffset = null)
public function __construct($message, $code, $severity, $filename, $lineno, $traceOffset = null, $traceArgs = true)
{
parent::__construct($message, $code, $severity, $filename, $lineno);
@@ -28,28 +28,32 @@ class FatalErrorException extends \ErrorException
if (function_exists('xdebug_get_function_stack')) {
$trace = xdebug_get_function_stack();
if (0 < $traceOffset) {
$trace = array_slice($trace, 0, -$traceOffset);
array_splice($trace, -$traceOffset);
}
$trace = array_reverse($trace);
foreach ($trace as $i => $frame) {
foreach ($trace as &$frame) {
if (!isset($frame['type'])) {
// XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695
if (isset($frame['class'])) {
$trace[$i]['type'] = '::';
$frame['type'] = '::';
}
} elseif ('dynamic' === $frame['type']) {
$trace[$i]['type'] = '->';
$frame['type'] = '->';
} elseif ('static' === $frame['type']) {
$trace[$i]['type'] = '::';
$frame['type'] = '::';
}
// XDebug also has a different name for the parameters array
if (isset($frame['params']) && !isset($frame['args'])) {
$trace[$i]['args'] = $frame['params'];
unset($trace[$i]['params']);
if (!$traceArgs) {
unset($frame['params'], $frame['args']);
} elseif (isset($frame['params']) && !isset($frame['args'])) {
$frame['args'] = $frame['params'];
unset($frame['params']);
}
}
unset($frame);
$trace = array_reverse($trace);
} else {
$trace = array();
}
+21
View File
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Exception;
/**
* Out of memory exception.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class OutOfMemoryException extends FatalErrorException
{
}
+96 -6
View File
@@ -13,6 +13,7 @@ namespace Symfony\Component\Debug;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\Debug\Exception\OutOfMemoryException;
if (!defined('ENT_SUBSTITUTE')) {
define('ENT_SUBSTITUTE', 8);
@@ -29,10 +30,12 @@ if (!defined('ENT_SUBSTITUTE')) {
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ExceptionHandler implements ExceptionHandlerInterface
class ExceptionHandler
{
private $debug;
private $charset;
private $handler;
private $caughtOutput = 0;
public function __construct($debug = true, $charset = 'UTF-8')
{
@@ -56,6 +59,24 @@ class ExceptionHandler implements ExceptionHandlerInterface
return $handler;
}
/**
* Sets a user exception handler.
*
* @param callable $handler An handler that will be called on Exception
*
* @return callable|null The previous exception handler if any
*/
public function setHandler($handler)
{
if (isset($handler) && !is_callable($handler)) {
throw new \LogicException('The exception handler must be a valid PHP callable.');
}
$old = $this->handler;
$this->handler = $handler;
return $old;
}
/**
* {@inheritdoc}
*
@@ -70,12 +91,55 @@ class ExceptionHandler implements ExceptionHandlerInterface
*/
public function handle(\Exception $exception)
{
if (class_exists('Symfony\Component\HttpFoundation\Response')) {
$response = $this->createResponse($exception);
$response->sendHeaders();
$response->sendContent();
} else {
if ($exception instanceof OutOfMemoryException) {
$this->sendPhpResponse($exception);
return;
}
// To be as fail-safe as possible, the exception is first handled
// by our simple exception handler, then by the user exception handler.
// The latter takes precedence and any output from the former is cancelled,
// if and only if nothing bad happens in this handling path.
$caughtOutput = 0;
$this->caughtOutput = false;
ob_start(array($this, 'catchOutput'));
try {
if (class_exists('Symfony\Component\HttpFoundation\Response')) {
$response = $this->createResponse($exception);
$response->sendHeaders();
$response->sendContent();
} else {
$this->sendPhpResponse($exception);
}
} catch (\Exception $e) {
// Ignore this $e exception, we have to deal with $exception
}
if (false === $this->caughtOutput) {
ob_end_clean();
}
if (isset($this->caughtOutput[0])) {
ob_start(array($this, 'cleanOutput'));
echo $this->caughtOutput;
$caughtOutput = ob_get_length();
}
$this->caughtOutput = 0;
if (!empty($this->handler)) {
try {
call_user_func($this->handler, $exception);
if ($caughtOutput) {
$this->caughtOutput = $caughtOutput;
}
} catch (\Exception $e) {
if (!$caughtOutput) {
// All handlers failed. Let PHP handle that now.
throw $exception;
}
}
}
}
@@ -317,4 +381,30 @@ EOF;
return implode(', ', $result);
}
/**
* @internal
*/
public function catchOutput($buffer)
{
$this->caughtOutput = $buffer;
return '';
}
/**
* @internal
*/
public function cleanOutput($buffer)
{
if ($this->caughtOutput) {
// use substr_replace() instead of substr() for mbstring overloading resistance
$cleanBuffer = substr_replace($buffer, '', 0, $this->caughtOutput);
if (isset($cleanBuffer[0])) {
$buffer = $cleanBuffer;
}
}
return $buffer;
}
}
-29
View File
@@ -1,29 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug;
/**
* An ExceptionHandler does something useful with an exception.
*
* @author Andrew Moore <me@andrewmoore.ca>
*
* @deprecated since version 2.5, to be removed in 3.0.
*/
interface ExceptionHandlerInterface
{
/**
* Handles an exception.
*
* @param \Exception $exception An \Exception instance
*/
public function handle(\Exception $exception);
}
+1 -1
View File
@@ -99,7 +99,7 @@ class DebugClassLoaderTest extends \PHPUnit_Framework_TestCase
class ChildTestingStacking extends TestingStacking { function foo($bar) {} }
');
$this->fail('ContextErrorException expected');
} catch (ContextErrorException $exception) {
} catch (\ErrorException $exception) {
// if an exception is thrown, the test passed
restore_error_handler();
restore_exception_handler();
+3 -3
View File
@@ -134,12 +134,12 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase
restore_error_handler();
$handler = ErrorHandler::register(E_USER_DEPRECATED);
$this->assertTrue($handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, array()));
$this->assertFalse($handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, array()));
restore_error_handler();
$handler = ErrorHandler::register(E_DEPRECATED);
$this->assertTrue($handler->handle(E_DEPRECATED, 'foo', 'foo.php', 12, array()));
$this->assertFalse($handler->handle(E_DEPRECATED, 'foo', 'foo.php', 12, array()));
restore_error_handler();
@@ -162,7 +162,7 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase
$handler = ErrorHandler::register(E_USER_DEPRECATED);
$handler->setLogger($logger);
$handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, array());
$this->assertTrue($handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, array()));
restore_error_handler();