feature #10941 [Debug] cleanup interfaces before 2.5-final (nicolas-grekas)
This PR was merged into the 2.4-dev branch. Discussion ---------- [Debug] cleanup interfaces before 2.5-final | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | none | License | MIT | Doc PR | none This PR is targeted at cleaning up interfaces before 2.5 final: - ExceptionHandlerInterface has never been released in a stable Symfony, lets drop it, not deprecate it, - generalize a little bit how fatal errors are handled and make them take the same path as uncaught exceptions, - enhance handling of out of memory situations. Commits -------e3255bf
[Debug] better ouf of memory error handlingdfa8ff8
[Debug] cleanup interfaces before 2.5-final
This commit is contained in:
commit
06adb24367
@ -9,7 +9,7 @@
|
||||
<parameter key="debug.stopwatch.class">Symfony\Component\Stopwatch\Stopwatch</parameter>
|
||||
<parameter key="debug.container.dump">%kernel.cache_dir%/%kernel.container_class%.xml</parameter>
|
||||
<parameter key="debug.controller_resolver.class">Symfony\Component\HttpKernel\Controller\TraceableControllerResolver</parameter>
|
||||
<parameter key="debug.fatal_error_exceptions_listener.class">Symfony\Component\HttpKernel\EventListener\FatalErrorExceptionsListener</parameter>
|
||||
<parameter key="debug.debug_handlers_listener.class">Symfony\Component\HttpKernel\EventListener\DebugHandlersListener</parameter>
|
||||
</parameters>
|
||||
|
||||
<services>
|
||||
@ -41,11 +41,11 @@
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
</service>
|
||||
|
||||
<service id="debug.fatal_error_exceptions_listener" class="%debug.fatal_error_exceptions_listener.class%">
|
||||
<service id="debug.debug_handlers_listener" class="%debug.debug_handlers_listener.class%">
|
||||
<tag name="kernel.event_subscriber" />
|
||||
<argument type="collection">
|
||||
<argument type="service" id="http_kernel" on-invalid="null" />
|
||||
<argument>handleFatalErrorException</argument>
|
||||
<argument>terminateWithException</argument>
|
||||
</argument>
|
||||
</service>
|
||||
</services>
|
||||
|
@ -4,9 +4,8 @@ CHANGELOG
|
||||
2.5.0
|
||||
-----
|
||||
|
||||
* added ErrorHandler::setFatalErrorExceptionHandler()
|
||||
* added ExceptionHandler::setHandler()
|
||||
* added UndefinedMethodFatalErrorHandler
|
||||
* deprecated ExceptionHandlerInterface
|
||||
* deprecated DummyException
|
||||
|
||||
2.4.0
|
||||
|
@ -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.
|
||||
*
|
||||
@ -119,16 +116,6 @@ class ErrorHandler
|
||||
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 @@ class ErrorHandler
|
||||
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 @@ class ErrorHandler
|
||||
self::$loggers['emergency']->emergency($error['message'], $fatal);
|
||||
}
|
||||
|
||||
if ($this->displayErrors && ($exceptionHandler || self::$fatalHandler)) {
|
||||
if ($this->displayErrors && $exceptionHandler) {
|
||||
$this->handleFatalError($exceptionHandler, $error);
|
||||
}
|
||||
}
|
||||
@ -327,7 +314,10 @@ 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)) {
|
||||
@ -335,76 +325,16 @@ class ErrorHandler
|
||||
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.
|
||||
// The handler failed. Let PHP handle that now.
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private class used to work around https://bugs.php.net/54275
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
}
|
@ -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,6 +91,22 @@ class ExceptionHandler implements ExceptionHandlerInterface
|
||||
*/
|
||||
public function handle(\Exception $exception)
|
||||
{
|
||||
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();
|
||||
@ -77,6 +114,33 @@ class ExceptionHandler implements ExceptionHandlerInterface
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?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\HttpKernel\EventListener;
|
||||
|
||||
use Symfony\Component\Debug\ExceptionHandler;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
/**
|
||||
* Configures the ExceptionHandler.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
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));
|
||||
}
|
||||
}
|
@ -1,47 +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\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 <p@tchwork.com>
|
||||
*/
|
||||
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));
|
||||
}
|
||||
}
|
@ -25,7 +25,6 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
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 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
|
Reference in New Issue
Block a user