diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml index 2366ac1f0604e..c457e4f903a36 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml @@ -9,7 +9,7 @@ Symfony\Component\Stopwatch\Stopwatch %kernel.cache_dir%/%kernel.container_class%.xml Symfony\Component\HttpKernel\Controller\TraceableControllerResolver - Symfony\Component\HttpKernel\EventListener\FatalErrorExceptionsListener + Symfony\Component\HttpKernel\EventListener\DebugHandlersListener @@ -41,11 +41,11 @@ - + - handleFatalErrorException + terminateWithException diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index b128efaaa8913..776468fb7a59e 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -4,9 +4,8 @@ CHANGELOG 2.5.0 ----- -* added ErrorHandler::setFatalErrorExceptionHandler() +* added ExceptionHandler::setHandler() * added UndefinedMethodFatalErrorHandler -* deprecated ExceptionHandlerInterface * deprecated DummyException 2.4.0 diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index b62e5b752150f..850f7d9c55376 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -15,6 +15,7 @@ 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. * @@ -119,16 +116,6 @@ public static function setLogger(LoggerInterface $logger, $channel = 'deprecatio self::$loggers[$channel] = $logger; } - /** - * 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 */ @@ -284,7 +271,7 @@ public function handleFatal() throw $exception; } - if (!$error || !$this->level || !in_array($error['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; } @@ -298,7 +285,7 @@ public function handleFatal() self::$loggers['emergency']->emergency($error['message'], $fatal); } - if ($this->displayErrors && ($exceptionHandler || self::$fatalHandler)) { + if ($this->displayErrors && $exceptionHandler) { $this->handleFatalError($exceptionHandler, $error); } } @@ -327,82 +314,25 @@ private function handleFatalError($exceptionHandler, array $error) $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); - - 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; + 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; } } } - } - - /** - * @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; } } diff --git a/src/Symfony/Component/Debug/Exception/FatalErrorException.php b/src/Symfony/Component/Debug/Exception/FatalErrorException.php index 4e29495f302cb..d5b58468c9c6d 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -20,7 +20,7 @@ */ 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 @@ public function __construct($message, $code, $severity, $filename, $lineno, $tra 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(); } diff --git a/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php new file mode 100644 index 0000000000000..fec1979836450 --- /dev/null +++ b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php @@ -0,0 +1,21 @@ + + * + * 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 + */ +class OutOfMemoryException extends FatalErrorException +{ +} diff --git a/src/Symfony/Component/Debug/ExceptionHandler.php b/src/Symfony/Component/Debug/ExceptionHandler.php index 91e904fbe25ce..bfbd78313fb2f 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -13,6 +13,7 @@ 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 @@ * * @author Fabien Potencier */ -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 @@ public static function register($debug = true) 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 @@ public static function register($debug = true) */ 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 @@ private function formatArgs(array $args) 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; + } } diff --git a/src/Symfony/Component/Debug/ExceptionHandlerInterface.php b/src/Symfony/Component/Debug/ExceptionHandlerInterface.php deleted file mode 100644 index f1740184c6dfe..0000000000000 --- a/src/Symfony/Component/Debug/ExceptionHandlerInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * 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 - * - * @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); -} diff --git a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php new file mode 100644 index 0000000000000..f46ef71208bde --- /dev/null +++ b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\EventListener; + +use Symfony\Component\Debug\ExceptionHandler; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\KernelEvents; + +/** + * Configures the ExceptionHandler. + * + * @author Nicolas Grekas + */ +class DebugHandlersListener implements EventSubscriberInterface +{ + private $exceptionHandler; + + public function __construct($exceptionHandler) + { + if (is_callable($exceptionHandler)) { + $this->exceptionHandler = $exceptionHandler; + } + } + + public function configure() + { + if ($this->exceptionHandler) { + $mainHandler = set_exception_handler('var_dump'); + restore_exception_handler(); + if ($mainHandler instanceof ExceptionHandler) { + $mainHandler->setHandler($this->exceptionHandler); + } + $this->exceptionHandler = null; + } + } + + public static function getSubscribedEvents() + { + return array(KernelEvents::REQUEST => array('configure', 2048)); + } +} diff --git a/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php b/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php deleted file mode 100644 index 0677682810ea8..0000000000000 --- a/src/Symfony/Component/HttpKernel/EventListener/FatalErrorExceptionsListener.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpKernel\EventListener; - -use Symfony\Component\Debug\ErrorHandler; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpKernel\KernelEvents; - -/** - * Injects a fatal error exceptions handler into the ErrorHandler. - * - * @author Nicolas Grekas - */ -class FatalErrorExceptionsListener implements EventSubscriberInterface -{ - private $handler = null; - - public function __construct($handler) - { - if (is_callable($handler)) { - $this->handler = $handler; - } - } - - public function injectHandler() - { - if ($this->handler) { - ErrorHandler::setFatalErrorExceptionHandler($this->handler); - $this->handler = null; - } - } - - public static function getSubscribedEvents() - { - // Don't register early as e.g. the Router is generally required by the handler - return array(KernelEvents::REQUEST => array('injectHandler', 8)); - } -} diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index c3556972e97d1..68d89c94e9be3 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -25,7 +25,6 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\Debug\Exception\FatalErrorException; /** * HttpKernel notifies events to convert a Request object to a Response one. @@ -87,11 +86,16 @@ public function terminate(Request $request, Response $response) } /** + * @throws \LogicException If the request stack is empty + * * @internal */ - public function handleFatalErrorException(FatalErrorException $exception) + public function terminateWithException(\Exception $exception) { - $request = $this->requestStack->getMasterRequest(); + if (!$request = $this->requestStack->getMasterRequest()) { + throw new \LogicException('Request stack is empty', 0, $exception); + } + $response = $this->handleException($exception, $request, self::MASTER_REQUEST); $response->sendHeaders();