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();