feature #12081 [FrameworkBundle] enable ErrorHandler in prod (nicolas-grekas)

This PR was merged into the 2.6-dev branch.

Discussion
----------

[FrameworkBundle] enable ErrorHandler in prod

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

-  a new debug.error_handler service is the registered PHP error handler, with ErrorHandler::register() as factory
- ErrorHandler::register() is patched so that it checks if the currently registered error handler is an instance of ErrorHandler - in which case it returns this instance and don't create a new one.
- DebugHandlersListener now listen to ConsoleEvents and re-injects fatal errors within the $app->renderException code path
- DebugHandlersListener also has a new $scream parameter to control is silenced errors are logged or not

Commits
-------

fac3cc4 [FrameworkBundle] register ErrorHandler at boot time
4acf5d3 [Debug] make screaming configurable
4d0ab7d [FrameworkBundle] enable ErrorHandler in prod
This commit is contained in:
Fabien Potencier 2014-10-03 11:43:16 +02:00
commit 3da6fc22c6
8 changed files with 159 additions and 25 deletions

View File

@ -127,14 +127,12 @@ class FrameworkExtension extends Extension
$definition = $container->findDefinition('debug.debug_handlers_listener');
if ($container->hasParameter('templating.helper.code.file_link_format')) {
$definition->replaceArgument(4, '%templating.helper.code.file_link_format%');
$definition->replaceArgument(5, '%templating.helper.code.file_link_format%');
}
if ($container->getParameter('kernel.debug')) {
$loader->load('debug.xml');
$definition->replaceArgument(0, array(new Reference('http_kernel', ContainerInterface::NULL_ON_INVALID_REFERENCE), 'terminateWithException'));
$definition = $container->findDefinition('http_kernel');
$definition->replaceArgument(2, new Reference('debug.controller_resolver'));
@ -150,6 +148,8 @@ class FrameworkExtension extends Extension
$this->addClassesToCompile(array(
'Symfony\\Component\\Config\\FileLocator',
'Symfony\\Component\\Debug\\ErrorHandler',
'Symfony\\Component\\EventDispatcher\\Event',
'Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher',

View File

@ -30,6 +30,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationExtra
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationDumperPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FragmentRendererPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SerializerPass;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\Scope;
@ -46,6 +47,8 @@ class FrameworkBundle extends Bundle
{
public function boot()
{
ErrorHandler::register($this->container->getParameter('debug.error_handler.throw_at'));
if ($trustedProxies = $this->container->getParameter('kernel.trusted_proxies')) {
Request::setTrustedProxies($trustedProxies);
}

View File

@ -8,6 +8,7 @@
<parameter key="debug.event_dispatcher.class">Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher</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.error_handler.throw_at">-1</parameter>
</parameters>
<services>

View File

@ -7,6 +7,7 @@
<parameters>
<parameter key="debug.debug_handlers_listener.class">Symfony\Component\HttpKernel\EventListener\DebugHandlersListener</parameter>
<parameter key="debug.stopwatch.class">Symfony\Component\Stopwatch\Stopwatch</parameter>
<parameter key="debug.error_handler.throw_at">0</parameter>
</parameters>
<services>
@ -16,7 +17,8 @@
<argument /><!-- Exception handler -->
<argument type="service" id="logger" on-invalid="null" />
<argument /><!-- Log levels map for enabled error levels -->
<argument>%kernel.debug%</argument>
<argument>null</argument>
<argument>true</argument>
<argument>null</argument><!-- %templating.helper.code.file_link_format% -->
</service>

View File

@ -124,9 +124,15 @@ class ErrorHandler
$handler = new static();
$levels &= $handler->thrownErrors;
set_error_handler(array($handler, 'handleError'), $levels);
$prev = set_error_handler(array($handler, 'handleError'), $levels);
$prev = is_array($prev) ? $prev[0] : null;
if ($prev instanceof self) {
restore_error_handler();
$handler = $prev;
} else {
$handler->setExceptionHandler(set_exception_handler(array($handler, 'handleException')));
}
$handler->throwAt($throw ? $levels : 0, true);
$handler->setExceptionHandler(set_exception_handler(array($handler, 'handleException')));
return $handler;
}

View File

@ -46,6 +46,30 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase
error_reporting($this->errorReporting);
}
public function testRegister()
{
$handler = ErrorHandler::register();
try {
$this->assertInstanceOf('Symfony\Component\Debug\ErrorHandler', $handler);
try {
$this->assertSame($handler, ErrorHandler::register());
} catch (\Exception $e) {
restore_error_handler();
restore_exception_handler();
}
} catch (\Exception $e) {
}
restore_error_handler();
restore_exception_handler();
if (isset($e)) {
throw $e;
}
}
public function testNotice()
{
ErrorHandler::register();

View File

@ -14,8 +14,14 @@ namespace Symfony\Component\HttpKernel\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\ExceptionHandler;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\KernelEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
/**
* Configures errors and exceptions handlers.
@ -27,45 +33,77 @@ class DebugHandlersListener implements EventSubscriberInterface
private $exceptionHandler;
private $logger;
private $levels;
private $debug;
private $throwAt;
private $scream;
private $fileLinkFormat;
/**
* @param callable $exceptionHandler A handler that will be called on Exception
* @param callable|null $exceptionHandler A handler that will be called on Exception
* @param LoggerInterface|null $logger A PSR-3 logger
* @param array|int $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants
* @param bool $debug Enables/disables debug mode
* @param int|null $throwAt Thrown errors in a bit field of E_* constants, or null to keep the current value
* @param bool $scream Enables/disables screaming mode, where even silenced errors are logged
* @param string $fileLinkFormat The format for links to source files
*/
public function __construct($exceptionHandler, LoggerInterface $logger = null, $levels = null, $debug = true, $fileLinkFormat = null)
public function __construct($exceptionHandler, LoggerInterface $logger = null, $levels = null, $throwAt = -1, $scream = true, $fileLinkFormat = null)
{
$this->exceptionHandler = $exceptionHandler;
$this->logger = $logger;
$this->levels = $levels;
$this->debug = $debug;
$this->throwAt = is_numeric($throwAt) ? (int) $throwAt : (null === $throwAt ? null : ($throwAt ? -1 : null));
$this->scream = (bool) $scream;
$this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
}
public function configure()
/**
* Configures the error handler.
*
* @param Event|null $event The triggering event
* @param string|null $eventName The triggering event name
* @param EventDispatcherInterface|null $eventDispatcher The dispatcher used to trigger $event
*/
public function configure(Event $event = null, $eventName = null, EventDispatcherInterface $eventDispatcher = null)
{
if ($this->logger) {
$handler = set_error_handler('var_dump', 0);
$handler = is_array($handler) ? $handler[0] : null;
restore_error_handler();
if ($handler instanceof ErrorHandler) {
if ($this->debug) {
$handler->throwAt(-1);
}
if (null !== $eventDispatcher) {
foreach (array_keys(static::getSubscribedEvents()) as $name) {
$eventDispatcher->removeListener($name, array($this, 'configure'));
}
}
$handler = set_error_handler('var_dump', 0);
$handler = is_array($handler) ? $handler[0] : null;
restore_error_handler();
if ($handler instanceof ErrorHandler) {
if ($this->logger) {
$handler->setDefaultLogger($this->logger, $this->levels);
if (is_array($this->levels)) {
$scream = 0;
foreach ($this->levels as $type => $log) {
$scream |= $type;
}
$this->levels = $scream;
} else {
$scream = null === $this->levels ? E_ALL | E_STRICT : $this->levels;
}
$handler->screamAt($this->levels);
if ($this->scream) {
$handler->screamAt($scream);
}
$this->logger = $this->levels = null;
}
if (null !== $this->throwAt) {
$handler->throwAt($this->throwAt, true);
}
}
if (!$this->exceptionHandler) {
if ($event instanceof KernelEvent) {
$this->exceptionHandler = array($event->getKernel(), 'terminateWithException');
} elseif ($event instanceof ConsoleEvent && $app = $event->getCommand()->getApplication()) {
$output = $event->getOutput();
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
}
$this->exceptionHandler = function ($e) use ($app, $output) {
$app->renderException($e, $output);
};
}
$this->logger = $this->levels = null;
}
if ($this->exceptionHandler) {
$handler = set_exception_handler('var_dump');
@ -78,7 +116,9 @@ class DebugHandlersListener implements EventSubscriberInterface
}
if ($handler instanceof ExceptionHandler) {
$handler->setHandler($this->exceptionHandler);
$handler->setFileLinkFormat($this->fileLinkFormat);
if (null !== $this->fileLinkFormat) {
$handler->setFileLinkFormat($this->fileLinkFormat);
}
}
$this->exceptionHandler = null;
}
@ -86,6 +126,12 @@ class DebugHandlersListener implements EventSubscriberInterface
public static function getSubscribedEvents()
{
return array(KernelEvents::REQUEST => array('configure', 2048));
$events = array(KernelEvents::REQUEST => array('configure', 2048));
if (defined('Symfony\Component\Console\ConsoleEvents::COMMAND')) {
$events[ConsoleEvents::COMMAND] = array('configure', 2048);
}
return $events;
}
}

View File

@ -12,9 +12,17 @@
namespace Symfony\Component\HttpKernel\Tests\EventListener;
use Psr\Log\LogLevel;
use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\ExceptionHandler;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\EventListener\DebugHandlersListener;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* DebugHandlersListenerTest
@ -53,4 +61,48 @@ class DebugHandlersListenerTest extends \PHPUnit_Framework_TestCase
$this->assertArrayHasKey(E_DEPRECATED, $loggers);
$this->assertSame(array($logger, LogLevel::INFO), $loggers[E_DEPRECATED]);
}
public function testConsoleEvent()
{
$dispatcher = new EventDispatcher();
$listener = new DebugHandlersListener(null);
$app = $this->getMock('Symfony\Component\Console\Application');
$app->expects($this->once())->method('getHelperSet')->will($this->returnValue(new HelperSet()));
$command = new Command(__FUNCTION__);
$command->setApplication($app);
$event = new ConsoleEvent($command, new ArgvInput(), new ConsoleOutput());
$dispatcher->addSubscriber($listener);
$xListeners = array(
KernelEvents::REQUEST => array(array($listener, 'configure')),
ConsoleEvents::COMMAND => array(array($listener, 'configure')),
);
$this->assertSame($xListeners, $dispatcher->getListeners());
$exception = null;
$eHandler = new ErrorHandler();
set_error_handler(array($eHandler, 'handleError'));
set_exception_handler(array($eHandler, 'handleException'));
try {
$dispatcher->dispatch(ConsoleEvents::COMMAND, $event);
} catch (\Exception $exception) {
}
restore_exception_handler();
restore_error_handler();
if (null !== $exception) {
throw $exception;
}
$this->assertSame(array(), $dispatcher->getListeners());
$xHandler = $eHandler->setExceptionHandler('var_dump');
$this->assertInstanceOf('Closure', $xHandler);
$app->expects($this->once())
->method('renderException');
$xHandler(new \Exception());
}
}