From e191df5597bb8e0b8966ff72b36b511cce7a4b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Haso=C5=88?= Date: Thu, 30 Oct 2014 22:32:13 +0100 Subject: [PATCH 1/6] [Debug] Added ExceptionFlattener --- src/Symfony/Component/Debug/CHANGELOG.md | 1 + .../Debug/Exception/FlattenException.php | 53 ++++++- .../Component/Debug/ExceptionFlattener.php | 88 ++++++++++++ .../FlattenExceptionProcessorInterface.php | 31 ++++ .../Tests/Exception/FlattenExceptionTest.php | 6 +- .../Debug/Tests/ExceptionFlattenerTest.php | 134 ++++++++++++++++++ 6 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 src/Symfony/Component/Debug/ExceptionFlattener.php create mode 100644 src/Symfony/Component/Debug/FlattenExceptionProcessorInterface.php create mode 100644 src/Symfony/Component/Debug/Tests/ExceptionFlattenerTest.php diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index ab07458a94a84..142e9dcd0e514 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * added BufferingLogger for errors that happen before a proper logger is configured * allow throwing from `__toString()` with `return trigger_error($e, E_USER_ERROR);` * deprecate ExceptionHandler::createResponse +* added ExceptionFlattener 2.7.0 ----- diff --git a/src/Symfony/Component/Debug/Exception/FlattenException.php b/src/Symfony/Component/Debug/Exception/FlattenException.php index d8d5c5b9214d5..cc633d5b1073a 100644 --- a/src/Symfony/Component/Debug/Exception/FlattenException.php +++ b/src/Symfony/Component/Debug/Exception/FlattenException.php @@ -72,6 +72,7 @@ class FlattenException extends LegacyFlattenException private $headers; private $file; private $line; + private $extras = array(); public static function create(\Exception $exception, $statusCode = null, array $headers = array()) { @@ -219,7 +220,8 @@ public function setTraceFromException(\Exception $exception) public function setTrace($trace, $file, $line) { $this->trace = array(); - $this->trace[] = array( + + $this->trace[-1] = array( 'namespace' => '', 'short_class' => '', 'class' => '', @@ -229,7 +231,8 @@ public function setTrace($trace, $file, $line) 'line' => $line, 'args' => array(), ); - foreach ($trace as $entry) { + + foreach ($trace as $key => $entry) { $class = ''; $namespace = ''; if (isset($entry['class'])) { @@ -238,7 +241,7 @@ public function setTrace($trace, $file, $line) $namespace = implode('\\', $parts); } - $this->trace[] = array( + $this->trace[$key] = array( 'namespace' => $namespace, 'short_class' => $class, 'class' => isset($entry['class']) ? $entry['class'] : '', @@ -251,6 +254,50 @@ public function setTrace($trace, $file, $line) } } + /** + * Replaces trace. + * + * @param array $trace The trace + */ + public function replaceTrace($trace) + { + $this->trace = (array) $trace; + } + + /** + * Returns all extras. + * + * @return array + */ + public function getExtras() + { + return $this->extras; + } + + /** + * Returns an extra value. + * + * @param string $name The name of the extra + * @param mixed $default The value to return if the extra doesn't exist + * + * @return mixed + */ + public function getExtra($name, $default = null) + { + return array_key_exists($name, $this->extras) ? $this->extras[$name] : $default; + } + + /** + * Sets an extra value. + * + * @param string $name The name of the extra + * @param mixed $value The value + */ + public function setExtra($name, $value) + { + $this->extras[$name] = $value; + } + private function flattenArgs($args, $level = 0, &$count = 0) { $result = array(); diff --git a/src/Symfony/Component/Debug/ExceptionFlattener.php b/src/Symfony/Component/Debug/ExceptionFlattener.php new file mode 100644 index 0000000000000..d31d169762782 --- /dev/null +++ b/src/Symfony/Component/Debug/ExceptionFlattener.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug; + +use Symfony\Component\Debug\Exception\FlattenException; + +/** + * ExceptionFlattener converts an Exception to FlattenException. + * + * @author Martin Hasoň + */ +class ExceptionFlattener +{ + /** + * @var FlattenExceptionProcessorInterface[] + */ + private $processors = array(); + + /** + * Constructor. + * + * @param array $processors The collection of processors + */ + public function __construct($processors = array()) + { + foreach ($processors as $processor) { + $this->addProcessor($processor); + } + } + + /** + * Adds an exception processor. + * + * @param FlattenExceptionProcessorInterface $processor + */ + public function addProcessor(FlattenExceptionProcessorInterface $processor) + { + $this->processors[] = $processor; + } + + /** + * Flattens an exception. + * + * @param \Exception $exception The raw exception + * + * @return FlattenException + */ + public function flatten(\Exception $exception) + { + $exceptions = array(); + do { + $exceptions[] = $exception; + } while ($exception = $exception->getPrevious()); + + $previous = null; + foreach (array_reverse($exceptions, true) as $position => $exception) { + $e = new FlattenException(); + $e->setMessage($exception->getMessage()); + $e->setCode($exception->getCode()); + $e->setClass(get_class($exception)); + $e->setFile($exception->getFile()); + $e->setLine($exception->getLine()); + $e->setTraceFromException($exception); + if (null !== $previous) { + $e->setPrevious($previous); + } + + foreach ($this->processors as $processor) { + if ($newE = $processor->process($exception, $e, 0 === $position)) { + $e = $newE; + } + } + + $previous = $e; + } + + return $e; + } +} diff --git a/src/Symfony/Component/Debug/FlattenExceptionProcessorInterface.php b/src/Symfony/Component/Debug/FlattenExceptionProcessorInterface.php new file mode 100644 index 0000000000000..4a867a4e2d959 --- /dev/null +++ b/src/Symfony/Component/Debug/FlattenExceptionProcessorInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug; + +use Symfony\Component\Debug\Exception\FlattenException; + +/** + * @author Martin Hasoň + */ +interface FlattenExceptionProcessorInterface +{ + /** + * Process a flattened exception. + * + * @param \Exception $exception The raw exception + * @param FlattenException $flattenException The flattened exception + * @param bool $master Whether it is a master exception + * + * @return FlattenException + */ + public function process(\Exception $exception, FlattenException $flattenException, $master); +} diff --git a/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php index 99eaf497d5b4d..542a16246fcd6 100644 --- a/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php +++ b/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php @@ -161,7 +161,7 @@ public function testToArray(\Exception $exception, $statusCode) array( 'message' => 'test', 'class' => 'Exception', - 'trace' => array(array( + 'trace' => array(-1 => array( 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => 'foo.php', 'line' => 123, 'args' => array(), )), @@ -235,12 +235,12 @@ public function testSetTraceIncompleteClass() 'message' => 'test', 'class' => 'Exception', 'trace' => array( - array( + -1 => array( 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => 'foo.php', 'line' => 123, 'args' => array(), ), - array( + 0 => array( 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => 'test', 'file' => __FILE__, 'line' => 123, 'args' => array( diff --git a/src/Symfony/Component/Debug/Tests/ExceptionFlattenerTest.php b/src/Symfony/Component/Debug/Tests/ExceptionFlattenerTest.php new file mode 100644 index 0000000000000..ea1e20b24eedc --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/ExceptionFlattenerTest.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Tests; + +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\ExceptionFlattener; +use Symfony\Component\Debug\FlattenExceptionProcessorInterface; + +class ExceptionFlattenerTest extends \PHPUnit_Framework_TestCase +{ + private $flattener; + + protected function setUp() + { + $this->flattener = new ExceptionFlattener(); + } + + public function testFlattenException() + { + $exception = new \RuntimeException('Runtime exception'); + $flattened = $this->flattener->flatten($exception); + + $this->assertEquals($exception->getMessage(), $flattened->getMessage()); + $this->assertEquals($exception->getCode(), $flattened->getCode()); + $this->assertEquals($exception->getFile(), $flattened->getFile()); + $this->assertEquals($exception->getLine(), $flattened->getLine()); + $this->assertInstanceOf($flattened->getClass(), $exception); + } + + public function testFlattenPreviousException() + { + $exception1 = new \OutOfRangeException('Out of range exception'); + $exception2 = new \InvalidArgumentException('Invalid argument exception', null, $exception1); + $exception3 = new \RuntimeException('Runtime exception', null, $exception2); + + $flattened = $this->flattener->flatten($exception3); + $this->assertCount(2, $flattened->getAllPrevious()); + $this->assertInstanceOf('Symfony\Component\Debug\Exception\FlattenException', $flattened->getPrevious()); + $this->assertInstanceOf( + 'Symfony\Component\Debug\Exception\FlattenException', + $flattened->getPrevious()->getPrevious() + ); + } + + public function testFlattenWithProcessor() + { + $this->flattener->addProcessor(new TagTraceProcessor()); + + $exception = new \RuntimeException('Runtime exception'); + $flattened = $this->flattener->flatten($exception); + foreach ($flattened->getTrace() as $position => $entry) { + if (-1 === $position) { + $this->assertFalse(array_key_exists('tag', $entry)); + } else { + $this->assertArrayHasKey('tag', $entry); + } + } + } + + public function testProcessorReplaceException() + { + $this->flattener->addProcessor(new EmptyExceptionProcessor()); + + $exception = new \RuntimeException('Runtime exception'); + $flattened = $this->flattener->flatten($exception); + + $this->assertNull($flattened->getMessage()); + $this->assertNull($flattened->getCode()); + $this->assertNull($flattened->getFile()); + $this->assertNull($flattened->getLine()); + } + + public function testProcessOnlyMaterException() + { + $exception1 = new \OutOfRangeException('Out of range exception'); + $exception2 = new \InvalidArgumentException('Invalid argument exception', null, $exception1); + $exception3 = new \RuntimeException('Runtime exception', null, $exception2); + + $this->flattener->addProcessor(new MasterExtraProcessor()); + $flattened = $this->flattener->flatten($exception3); + + $this->assertEquals(array('tags' => array('master')), $flattened->getExtras()); + foreach ($flattened->getAllPrevious() as $exception) { + $this->assertEquals(array(), $exception->getExtras()); + } + } +} + +class TagTraceProcessor implements FlattenExceptionProcessorInterface +{ + public function process(\Exception $exception, FlattenException $flattenException, $master) + { + $trace = $flattenException->getTrace(); + + foreach ($exception->getTrace() as $key => $entry) { + if (!isset($trace[$key])) { + continue; + } + + $trace[$key]['tag'] = 'value'; + } + + $flattenException->replaceTrace($trace); + } +} + +class EmptyExceptionProcessor implements FlattenExceptionProcessorInterface +{ + public function process(\Exception $exception, FlattenException $flattenException, $master) + { + return new FlattenException(); + } +} + +class MasterExtraProcessor implements FlattenExceptionProcessorInterface +{ + public function process(\Exception $exception, FlattenException $flattenException, $master) + { + if (!$master) { + return; + } + + $flattenException->setExtra('tags', array('master')); + } +} From bab9b5c74e21fb6bd304a15e4843b824cbcdd517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Haso=C5=88?= Date: Thu, 30 Oct 2014 22:40:45 +0100 Subject: [PATCH 2/6] Integrated the ExceptionFlattener into components and bundles --- .../Bundle/DebugBundle/DebugBundle.php | 2 + .../AddFlattenExceptionProcessorPass.php | 50 +++++++++++++++++++ .../DebugBundle/Resources/config/services.xml | 4 ++ .../AddFlattenExceptionProcessorPassTest.php | 42 ++++++++++++++++ src/Symfony/Bundle/DebugBundle/composer.json | 3 +- .../Resources/config/collectors.xml | 1 + .../Controller/PreviewErrorController.php | 9 +++- .../TwigBundle/Resources/config/twig.xml | 1 + .../Component/Debug/ExceptionHandler.php | 9 ++-- .../DataCollector/ExceptionDataCollector.php | 18 +++++-- .../EventListener/ExceptionListener.php | 9 +++- .../ExceptionDataCollectorTest.php | 22 ++++++-- .../EventListener/ExceptionListenerTest.php | 19 +++++++ 13 files changed, 171 insertions(+), 18 deletions(-) create mode 100644 src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/AddFlattenExceptionProcessorPass.php create mode 100644 src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/AddFlattenExceptionProcessorPassTest.php diff --git a/src/Symfony/Bundle/DebugBundle/DebugBundle.php b/src/Symfony/Bundle/DebugBundle/DebugBundle.php index 3aa536dba7860..5d2dba852a4d9 100644 --- a/src/Symfony/Bundle/DebugBundle/DebugBundle.php +++ b/src/Symfony/Bundle/DebugBundle/DebugBundle.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\DebugBundle; +use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\AddFlattenExceptionProcessorPass; use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\DumpDataCollectorPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -48,6 +49,7 @@ public function build(ContainerBuilder $container) { parent::build($container); + $container->addCompilerPass(new AddFlattenExceptionProcessorPass()); $container->addCompilerPass(new DumpDataCollectorPass()); } } diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/AddFlattenExceptionProcessorPass.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/AddFlattenExceptionProcessorPass.php new file mode 100644 index 0000000000000..726ef19a1ee99 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/AddFlattenExceptionProcessorPass.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\Bundle\DebugBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Registers the exception processors. + * + * @author Martin Hasoň + */ +class AddFlattenExceptionProcessorPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('exception_flattener')) { + return; + } + + $processors = array(); + foreach ($container->findTaggedServiceIds('exception.processor') as $id => $tags) { + $priority = isset($tags[0]['priority']) ? $tags[0]['priority'] : 0; + $processors[$priority][] = new Reference($id); + } + + if (empty($processors)) { + return; + } + + // sort by priority and flatten + krsort($processors); + $processors = call_user_func_array('array_merge', $processors); + + $container->getDefinition('exception_flattener')->replaceArgument(0, $processors); + } +} diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml index 16e27a22c584d..47521201879e4 100644 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml @@ -30,6 +30,10 @@ null %kernel.charset% + + + + diff --git a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/AddFlattenExceptionProcessorPassTest.php b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/AddFlattenExceptionProcessorPassTest.php new file mode 100644 index 0000000000000..5dfe88018dcfe --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/Compiler/AddFlattenExceptionProcessorPassTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\DebugBundle\Tests\DependencyInjection\Compiler; + +use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\AddFlattenExceptionProcessorPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class AddFlattenExceptionProcessorPassTest extends \PHPUnit_Framework_TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new AddFlattenExceptionProcessorPass()); + + $definition = new Definition('Symfony\Component\Debug\ExceptionFlattener', array(array())); + $container->setDefinition('exception_flattener', $definition); + + $processor1 = new Definition('Symfony\Component\Debug\FlattenExceptionProcessorInterface'); + $processor1->addTag('exception.processor', array('priority' => -100)); + + $processor2 = new Definition('Symfony\Component\Debug\FlattenExceptionProcessorInterface'); + $processor2->addTag('exception.processor', array('priority' => 100)); + + $container->setDefinition('processor_1', $processor1); + $container->setDefinition('processor_2', $processor2); + + $container->compile(); + + $this->assertEquals(array(array(new Reference('processor_2'), new Reference('processor_1'))), $definition->getArguments()); + } +} diff --git a/src/Symfony/Bundle/DebugBundle/composer.json b/src/Symfony/Bundle/DebugBundle/composer.json index 66d77643479dd..c767a61746b02 100644 --- a/src/Symfony/Bundle/DebugBundle/composer.json +++ b/src/Symfony/Bundle/DebugBundle/composer.json @@ -19,7 +19,8 @@ "php": ">=5.3.9", "symfony/http-kernel": "~2.6|~3.0.0", "symfony/twig-bridge": "~2.6|~3.0.0", - "symfony/var-dumper": "~2.6|~3.0.0" + "symfony/var-dumper": "~2.6|~3.0.0", + "symfony/debug": "~2.6|~3.0.0" }, "require-dev": { "symfony/phpunit-bridge": "~2.7|~3.0.0", diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml index 01a271797ae94..396332c9d1ccb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml @@ -32,6 +32,7 @@ + diff --git a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php index a907ef0abe5b2..7c5c4f9857118 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\TwigBundle\Controller; use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\ExceptionFlattener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; @@ -26,16 +27,20 @@ class PreviewErrorController { protected $kernel; protected $controller; + protected $flattener; - public function __construct(HttpKernelInterface $kernel, $controller) + public function __construct(HttpKernelInterface $kernel, $controller, ExceptionFlattener $flattener = null) { $this->kernel = $kernel; $this->controller = $controller; + $this->flattener = $flattener; } public function previewErrorPageAction(Request $request, $code) { - $exception = FlattenException::create(new \Exception('Something has intentionally gone wrong.'), $code); + $e = new \Exception('Something has intentionally gone wrong.'); + $exception = null === $this->flattener ? FlattenException::create($e, $code) : $this->flattener->flatten($e); + $exception->setStatusCode($code); /* * This Request mimics the parameters set by diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index bb871f440a20d..3abbb3e462dfb 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -154,6 +154,7 @@ %twig.exception_listener.controller% + diff --git a/src/Symfony/Component/Debug/ExceptionHandler.php b/src/Symfony/Component/Debug/ExceptionHandler.php index 4889b18562789..7176a12425fe8 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -36,7 +36,7 @@ class ExceptionHandler private $caughtLength; private $fileLinkFormat; - public function __construct($debug = true, $charset = null, $fileLinkFormat = null) + public function __construct($debug = true, $charset = null, $fileLinkFormat = null, ExceptionFlattener $flattener = null) { if (false !== strpos($charset, '%')) { // Swap $charset and $fileLinkFormat for BC reasons @@ -47,6 +47,7 @@ public function __construct($debug = true, $charset = null, $fileLinkFormat = nu $this->debug = $debug; $this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8'; $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); + $this->flattener = $flattener; } /** @@ -183,7 +184,7 @@ private function failSafeHandle(\Exception $exception) public function sendPhpResponse($exception) { if (!$exception instanceof FlattenException) { - $exception = FlattenException::create($exception); + $exception = null !== $this->flattener ? $this->flattener->flatten($exception) : FlattenException::create($exception); } if (!headers_sent()) { @@ -211,7 +212,7 @@ public function createResponse($exception) @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); if (!$exception instanceof FlattenException) { - $exception = FlattenException::create($exception); + $exception = null !== $this->flattener ? $this->flattener->flatten($exception) : FlattenException::create($exception); } return Response::create($this->getHtml($exception), $exception->getStatusCode(), $exception->getHeaders())->setCharset($this->charset); @@ -227,7 +228,7 @@ public function createResponse($exception) public function getHtml($exception) { if (!$exception instanceof FlattenException) { - $exception = FlattenException::create($exception); + $exception = null !== $this->flattener ? $this->flattener->flatten($exception) : FlattenException::create($exception); } return $this->decorate($this->getContent($exception), $this->getStylesheet($exception)); diff --git a/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php index 9fe826446b195..bb88145860e52 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\ExceptionFlattener; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -22,16 +23,25 @@ */ class ExceptionDataCollector extends DataCollector { + private $flattener; + + public function __construct(ExceptionFlattener $flattener = null) + { + $this->flattener = $flattener; + } + /** * {@inheritdoc} */ public function collect(Request $request, Response $response, \Exception $exception = null) { - if (null !== $exception) { - $this->data = array( - 'exception' => FlattenException::create($exception), - ); + if (null === $exception) { + return; } + + $this->data = array( + 'exception' => null === $this->flattener ? FlattenException::create($exception) : $this->flattener->flatten($exception), + ); } /** diff --git a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php index fc2efed86bd6a..3b8bd24f277be 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php @@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\ExceptionFlattener; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; @@ -30,11 +31,13 @@ class ExceptionListener implements EventSubscriberInterface { protected $controller; protected $logger; + protected $flattener; - public function __construct($controller, LoggerInterface $logger = null) + public function __construct($controller, LoggerInterface $logger = null, ExceptionFlattener $flattener = null) { $this->controller = $controller; $this->logger = $logger; + $this->flattener = $flattener; } public function onKernelException(GetResponseForExceptionEvent $event) @@ -103,9 +106,11 @@ protected function logException(\Exception $exception, $message) */ protected function duplicateRequest(\Exception $exception, Request $request) { + $flattenException = null === $this->flattener ? FlattenException::create($exception) : $this->flattener->flatten($exception); + $attributes = array( '_controller' => $this->controller, - 'exception' => FlattenException::create($exception), + 'exception' => $flattenException, 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, // keep for BC -- as $format can be an argument of the controller callable // see src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php index 6c71f4c9ebdd5..2ee19a4dae338 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php @@ -12,17 +12,18 @@ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\ExceptionFlattener; use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class ExceptionDataCollectorTest extends \PHPUnit_Framework_TestCase { - public function testCollect() + /** + * @dataProvider getData + */ + public function testCollect($c, $e, $flattened) { - $e = new \Exception('foo', 500); - $c = new ExceptionDataCollector(); - $flattened = FlattenException::create($e); $trace = $flattened->getTrace(); $this->assertFalse($c->hasException()); @@ -34,6 +35,17 @@ public function testCollect() $this->assertSame('foo', $c->getMessage()); $this->assertSame(500, $c->getCode()); $this->assertSame('exception', $c->getName()); - $this->assertSame($trace, $c->getTrace()); + $this->assertEquals($trace, $c->getTrace()); + } + + public function getData() + { + $e = new \Exception('foo', 500); + $flattener = new ExceptionFlattener(); + + return array( + array(new ExceptionDataCollector(), $e, FlattenException::create($e)), + array(new ExceptionDataCollector($flattener), $e, $flattener->flatten($e)), + ); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php index 8fb00f51c1e6e..27f093d10ade8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ExceptionListenerTest.php @@ -119,6 +119,25 @@ public function testSubRequestFormat() $response = $event->getResponse(); $this->assertEquals('xml', $response->getContent()); } + + public function testUseFlattener() + { + $exception = new \Exception('foo'); + + $flattener = $this->getMock('Symfony\Component\Debug\ExceptionFlattener'); + $flattener->expects($this->once())->method('flatten')->with($exception); + + $listener = new ExceptionListener('foo', null, $flattener); + + $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'); + $kernel->expects($this->once())->method('handle')->will($this->returnCallback(function () { + return new Response(); + })); + + $request = Request::create('/'); + $event = new GetResponseForExceptionEvent($kernel, $request, 'foo', $exception); + $listener->onKernelException($event); + } } class TestLogger extends Logger implements DebugLoggerInterface From 0f558bcd1ca2cb75855d04b03735f6c3341861f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Haso=C5=88?= Date: Fri, 9 Jan 2015 14:44:57 +0100 Subject: [PATCH 3/6] [Twig] Added an exception processor for TwigError --- src/Symfony/Bridge/Twig/CHANGELOG.md | 5 + .../Debug/TwigFlattenExceptionProcessor.php | 140 ++++++++++++++++++ .../TwigFlattenExceptionProcessorTest.php | 71 +++++++++ .../Tests/Fixtures/templates/error.html.twig | 2 + src/Symfony/Bridge/Twig/composer.json | 4 +- .../TwigBundle/Resources/config/twig.xml | 5 + 6 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Bridge/Twig/Debug/TwigFlattenExceptionProcessor.php create mode 100644 src/Symfony/Bridge/Twig/Tests/Debug/TwigFlattenExceptionProcessorTest.php create mode 100644 src/Symfony/Bridge/Twig/Tests/Fixtures/templates/error.html.twig diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index b4df596fa2c05..ea99cb17d15c4 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +2.8.0 +----- + + * added TwigFlattenExceptionProcessor which adds twig files into the FlattenException + 2.7.0 ----- diff --git a/src/Symfony/Bridge/Twig/Debug/TwigFlattenExceptionProcessor.php b/src/Symfony/Bridge/Twig/Debug/TwigFlattenExceptionProcessor.php new file mode 100644 index 0000000000000..64555864ea95b --- /dev/null +++ b/src/Symfony/Bridge/Twig/Debug/TwigFlattenExceptionProcessor.php @@ -0,0 +1,140 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Debug; + +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\FlattenExceptionProcessorInterface; + +/** + * TwigFlattener adds twig files into FlattenException. + * + * @author Martin Hasoň + */ +class TwigFlattenExceptionProcessor implements FlattenExceptionProcessorInterface +{ + private $files = array(); + private $twig; + private $loadedTemplates; + + public function __construct(\Twig_Environment $twig) + { + $this->twig = $twig; + $this->loadedTemplates = new \ReflectionProperty($twig, 'loadedTemplates'); + $this->loadedTemplates->setAccessible(true); + } + + /** + * {@inheritdoc} + */ + public function process(\Exception $exception, FlattenException $flattenException, $master) + { + $trace = $flattenException->getTrace(); + $origTrace = $exception->getTrace(); + + foreach ($origTrace as $key => $entry) { + $prevKey = $key - 1; + + if (!isset($origTrace[$prevKey]) || !isset($entry['class']) || 'Twig_Template' === $entry['class'] || !is_subclass_of($entry['class'], 'Twig_Template')) { + continue; + } + + $template = $this->getLoadedTemplate($entry['class']); + + if (!$template instanceof \Twig_Template) { + continue; + } + + $file = $this->findOrCreateFile($template); + + $data = array('file' => $file); + if (isset($origTrace[$prevKey]['line'])) { + $data['line'] = $this->findLineInTemplate($origTrace[$prevKey]['line'], $template); + } + + $trace[$prevKey]['related_codes'][] = $data; + } + + if (isset($trace[-1]) && $exception instanceof \Twig_Error) { + $name = $exception->getTemplateFile(); + $file = $this->findOrCreateFile($name, $name); + + $trace[-1]['related_codes'][] = array('file' => $file, 'line' => $exception->getTemplateLine()); + } + + $flattenException->replaceTrace($trace); + } + + private function getLoadedTemplate($class) + { + $loadedTemplates = $this->loadedTemplates->getValue($this->twig); + + return isset($loadedTemplates[$class]) ? $loadedTemplates[$class] : null; + } + + private function findOrCreateFile($template, $path = null) + { + $name = $template instanceof \Twig_Template ? $template->getTemplateName() : $template; + + if (isset($this->files[$name])) { + return $this->files[$name]; + } + + foreach ($this->files as $key => $file) { + if (isset($file->path) && $file->path == $name) { + return $file; + } + } + + $file = (object) array('name' => $name, 'type' => 'twig'); + + try { + $path = $path ?: $this->twig->getLoader()->getCacheKey($name); + } catch (\Twig_Error_Loader $e) { + } + + if (is_file($path)) { + $file->path = $path; + } else { + $source = null; + + if (method_exists($template, 'getSource')) { + $source = $template->getSource(); + } + + if (null === $source) { + try { + $source = $this->twig->getLoader()->getSource($name); + } catch (\Twig_Error_Loader $e) { + } + } + + $file->content = $source; + } + + return $this->files[$name] = $file; + } + + private function findLineInTemplate($line, $template) + { + if (!method_exists($template, 'getDebugInfo')) { + return 1; + } + + foreach ($template->getDebugInfo() as $codeLine => $templateLine) { + if ($codeLine <= $line) { + return $templateLine; + } + } + + return 1; + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Debug/TwigFlattenExceptionProcessorTest.php b/src/Symfony/Bridge/Twig/Tests/Debug/TwigFlattenExceptionProcessorTest.php new file mode 100644 index 0000000000000..fd067e3593e16 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Debug/TwigFlattenExceptionProcessorTest.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests; + +use Symfony\Bridge\Twig\Debug\TwigFlattenExceptionProcessor; +use Symfony\Component\Debug\ExceptionFlattener; + +class TwigFlattenExceptionProcessorTest extends \PHPUnit_Framework_TestCase +{ + public function testProcess() + { + $contents = array( + 'base' => "{% block content '' %}", + 'layout' => "{% extends 'base' %}\n{% block content %}\nfoo\n{{ foo.foo }}\n{% endblock %}", + 'index' => "{% extends 'layout' %}\n{% block content %}\n{{ parent() }}\n{% endblock %}", + ); + + $files = array( + 'base' => (object) array('name' => 'base', 'content' => $contents['base'], 'type' => 'twig'), + 'layout' => (object) array('name' => 'layout', 'content' => $contents['layout'], 'type' => 'twig'), + 'index' => (object) array('name' => 'index', 'content' => $contents['index'], 'type' => 'twig'), + ); + + $twig = new \Twig_Environment(new \Twig_Loader_Array($contents), array('strict_variables' => true)); + + try { + $twig->render('index', array('foo' => 'foo')); + } catch (\Twig_Error $exception) { + } + + $flattener = new ExceptionFlattener(array(new TwigFlattenExceptionProcessor($twig))); + $trace = $flattener->flatten($exception)->getTrace(); + + $this->assertEquals(array(array('line' => 4, 'file' => $files['layout'])), $trace[-1]['related_codes']); + $this->assertEquals(array(array('line' => 4, 'file' => $files['layout'])), $trace[0]['related_codes']); + $this->assertEquals(array(array('line' => 3, 'file' => $files['index'])), $trace[3]['related_codes']); + $this->assertEquals(array(array('line' => 1, 'file' => $files['base'])), $trace[5]['related_codes']); + $this->assertEquals(array(array('line' => 1, 'file' => $files['layout'])), $trace[8]['related_codes']); + $this->assertEquals(array(array('line' => 1, 'file' => $files['index'])), $trace[11]['related_codes']); + } + + public function testProcessRealTwigFile() + { + $file = (object) array( + 'name' => 'error.html.twig', + 'path' => dirname(__DIR__).'/Fixtures/templates/error.html.twig', + 'type' => 'twig', + ); + + $twig = new \Twig_Environment(new \Twig_Loader_Filesystem(array(dirname($file->path))), array('strict_variables' => true)); + + try { + $twig->render($file->name, array('foo' => 'foo')); + } catch (\Twig_Error $exception) { + } + + $flattener = new ExceptionFlattener(array(new TwigFlattenExceptionProcessor($twig))); + $trace = $flattener->flatten($exception)->getTrace(); + + $this->assertEquals(array(array('line' => 2, 'file' => $file)), $trace[-1]['related_codes']); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Fixtures/templates/error.html.twig b/src/Symfony/Bridge/Twig/Tests/Fixtures/templates/error.html.twig new file mode 100644 index 0000000000000..ba8e5ea576957 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Fixtures/templates/error.html.twig @@ -0,0 +1,2 @@ +foo +{{ foo.foo }} diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index f56133daaab53..42d185d298577 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -35,7 +35,8 @@ "symfony/stopwatch": "~2.2|~3.0.0", "symfony/console": "~2.7|~3.0.0", "symfony/var-dumper": "~2.6|~3.0.0", - "symfony/expression-language": "~2.4|~3.0.0" + "symfony/expression-language": "~2.4|~3.0.0", + "symfony/debug": "~2.6|~3.0.0" }, "suggest": { "symfony/finder": "", @@ -48,6 +49,7 @@ "symfony/yaml": "For using the YamlExtension", "symfony/security": "For using the SecurityExtension", "symfony/stopwatch": "For using the StopwatchExtension", + "symfony/debug": "For using the TwigFlattenExceptionProcessor", "symfony/var-dumper": "For using the DumpExtension", "symfony/expression-language": "For using the ExpressionExtension" }, diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 3abbb3e462dfb..214f6c0fed9b2 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -175,5 +175,10 @@ + + + + + From 8f4ab6ab605c1955b022e0aa09f67fd8b829033b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Haso=C5=88?= Date: Fri, 27 Mar 2015 23:08:32 +0100 Subject: [PATCH 4/6] [HttpKernel] Added exception processor for HTTP Exceptions --- .../DebugBundle/Resources/config/services.xml | 5 + .../Debug/Exception/FlattenException.php | 2 +- .../Debug/HttpFlattenExceptionProcessor.php | 37 +++++++ .../HttpFlattenExceptionProcessorTest.php | 100 ++++++++++++++++++ 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/HttpKernel/Debug/HttpFlattenExceptionProcessor.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Debug/HttpFlattenExceptionProcessorTest.php diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml index 47521201879e4..988e616685490 100644 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml @@ -34,6 +34,11 @@ + + + + + diff --git a/src/Symfony/Component/Debug/Exception/FlattenException.php b/src/Symfony/Component/Debug/Exception/FlattenException.php index cc633d5b1073a..943fe8ba380c8 100644 --- a/src/Symfony/Component/Debug/Exception/FlattenException.php +++ b/src/Symfony/Component/Debug/Exception/FlattenException.php @@ -69,7 +69,7 @@ class FlattenException extends LegacyFlattenException private $trace; private $class; private $statusCode; - private $headers; + private $headers = array(); private $file; private $line; private $extras = array(); diff --git a/src/Symfony/Component/HttpKernel/Debug/HttpFlattenExceptionProcessor.php b/src/Symfony/Component/HttpKernel/Debug/HttpFlattenExceptionProcessor.php new file mode 100644 index 0000000000000..d91c2c562ce06 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Debug/HttpFlattenExceptionProcessor.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Debug; + +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\FlattenExceptionProcessorInterface; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; + +/** + * @author Martin Hasoň + */ +class HttpFlattenExceptionProcessor implements FlattenExceptionProcessorInterface +{ + /** + * {@inheritdoc} + */ + public function process(\Exception $exception, FlattenException $flattenException, $master) + { + if (!$exception instanceof HttpExceptionInterface) { + $flattenException->setStatusCode($flattenException->getStatusCode() ?: 500); + + return; + } + + $flattenException->setStatusCode($exception->getStatusCode() ?: 500); + $flattenException->setHeaders(array_merge($flattenException->getHeaders(), $exception->getHeaders())); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Debug/HttpFlattenExceptionProcessorTest.php b/src/Symfony/Component/HttpKernel/Tests/Debug/HttpFlattenExceptionProcessorTest.php new file mode 100644 index 0000000000000..c3c3103b3b9f4 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Debug/HttpFlattenExceptionProcessorTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Debug; + +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\HttpKernel\Debug\HttpFlattenExceptionProcessor; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; +use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; +use Symfony\Component\HttpKernel\Exception\ConflictHttpException; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\GoneHttpException; +use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException; +use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException; +use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; +use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; + +class HttpFlattenExceptionProcessorTest extends \PHPUnit_Framework_TestCase +{ + private $processor; + + protected function setUp() + { + $this->processor = new HttpFlattenExceptionProcessor(); + } + + public function getStatusCode() + { + return array( + array(400, new BadRequestHttpException()), + array(401, new UnauthorizedHttpException('Basic realm="My Realm"')), + array(403, new AccessDeniedHttpException()), + array(404, new NotFoundHttpException()), + array(405, new MethodNotAllowedHttpException(array('POST'))), + array(406, new NotAcceptableHttpException()), + array(409, new ConflictHttpException()), + array(410, new GoneHttpException()), + array(411, new LengthRequiredHttpException()), + array(412, new PreconditionFailedHttpException()), + array(415, new UnsupportedMediaTypeHttpException()), + array(428, new PreconditionRequiredHttpException()), + array(429, new TooManyRequestsHttpException()), + array(500, new \RuntimeException()), + array(503, new ServiceUnavailableHttpException()), + ); + } + + /** + * @dataProvider getStatusCode + */ + public function testStatusCode($code, $exception) + { + $flattened = new FlattenException(); + $this->processor->process($exception, $flattened, true); + $this->assertEquals($code, $flattened->getStatusCode()); + } + + public function testCustomStatusCode() + { + $flattened = new FlattenException(); + $flattened->setStatusCode(403); + $this->processor->process(new \RuntimeException(), $flattened, true); + $this->assertEquals(403, $flattened->getStatusCode()); + } + + public function getHeaders() + { + return array( + array(array('Allow' => 'POST'), new MethodNotAllowedHttpException(array('POST'))), + array(array('WWW-Authenticate' => 'Basic realm="My Realm"'), new UnauthorizedHttpException('Basic realm="My Realm"')), + array(array('Retry-After' => 'Fri, 31 Dec 1999 23:59:59 GMT'), new ServiceUnavailableHttpException('Fri, 31 Dec 1999 23:59:59 GMT')), + array(array('Retry-After' => 120), new ServiceUnavailableHttpException(120)), + array(array('Retry-After' => 'Fri, 31 Dec 1999 23:59:59 GMT'), new TooManyRequestsHttpException('Fri, 31 Dec 1999 23:59:59 GMT')), + array(array('Retry-After' => 120), new TooManyRequestsHttpException(120)), + ); + } + + /** + * @dataProvider getHeaders + */ + public function testHeadersForHttpException($headers, $exception) + { + $flattened = new FlattenException(); + $this->processor->process($exception, $flattened, true); + $this->assertEquals($headers, $flattened->getHeaders()); + } +} From 74f7357ce29a9b1b9108d8ecef4928aaa840ad63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Haso=C5=88?= Date: Mon, 14 Sep 2015 17:07:39 +0200 Subject: [PATCH 5/6] [Debug] Added an exception processor for dumping arguments --- .../Bridge/Twig/Extension/CodeExtension.php | 43 ++--- .../DebugBundle/Resources/config/services.xml | 7 + .../TwigBundle/Resources/config/twig.xml | 1 + .../views/Exception/exception.html.twig | 2 +- .../views/Exception/exception.xml.twig | 2 +- .../Resources/views/Exception/trace.html.twig | 2 +- .../Resources/views/Exception/trace.txt.twig | 2 +- .../views/Exception/traces.html.twig | 2 +- .../Resources/views/Exception/traces.txt.twig | 2 +- .../Resources/views/Exception/traces.xml.twig | 2 +- .../views/Exception/traces_text.html.twig | 2 +- .../ArgumentsFlattenExceptionProcessor.php | 168 ++++++++++++++++++ .../Debug/Exception/FlattenException.php | 48 +---- ...ArgumentsFlattenExceptionProcessorTest.php | 110 ++++++++++++ .../Tests/Exception/FlattenExceptionTest.php | 25 +-- .../Component/Debug/Utils/HtmlUtils.php | 91 ++++++++++ 16 files changed, 418 insertions(+), 91 deletions(-) create mode 100644 src/Symfony/Component/Debug/ArgumentsFlattenExceptionProcessor.php create mode 100644 src/Symfony/Component/Debug/Tests/ArgumentsFlattenExceptionProcessorTest.php create mode 100644 src/Symfony/Component/Debug/Utils/HtmlUtils.php diff --git a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php index b7c3605d9572f..efa3b2591258c 100644 --- a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php @@ -10,6 +10,8 @@ */ namespace Symfony\Bridge\Twig\Extension; +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\Utils\HtmlUtils; /** * Twig extension relate to PHP code and used by the profiler and the default exception templates. @@ -21,6 +23,7 @@ class CodeExtension extends \Twig_Extension private $fileLinkFormat; private $rootDir; private $charset; + private $htmlUtils; /** * Constructor. @@ -29,11 +32,12 @@ class CodeExtension extends \Twig_Extension * @param string $rootDir The project root directory * @param string $charset The charset */ - public function __construct($fileLinkFormat, $rootDir, $charset) + public function __construct($fileLinkFormat, $rootDir, $charset, HtmlUtils $htmlUtils = null) { $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); $this->rootDir = str_replace('/', DIRECTORY_SEPARATOR, dirname($rootDir)).DIRECTORY_SEPARATOR; $this->charset = $charset; + $this->htmlUtils = $htmlUtils; } /** @@ -78,48 +82,27 @@ public function abbrMethod($method) /** * Formats an array as a string. * - * @param array $args The argument array + * @param array $args The argument array + * @param FlattenException|null $exception The flatten exception * * @return string */ - public function formatArgs($args) + public function formatArgs($args, $exception = null) { - $result = array(); - foreach ($args as $key => $item) { - if ('object' === $item[0]) { - $parts = explode('\\', $item[1]); - $short = array_pop($parts); - $formattedValue = sprintf('object(%s)', $item[1], $short); - } elseif ('array' === $item[0]) { - $formattedValue = sprintf('array(%s)', is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); - } elseif ('string' === $item[0]) { - $formattedValue = sprintf("'%s'", htmlspecialchars($item[1], ENT_QUOTES, $this->charset)); - } elseif ('null' === $item[0]) { - $formattedValue = 'null'; - } elseif ('boolean' === $item[0]) { - $formattedValue = ''.strtolower(var_export($item[1], true)).''; - } elseif ('resource' === $item[0]) { - $formattedValue = 'resource'; - } else { - $formattedValue = str_replace("\n", '', var_export(htmlspecialchars((string) $item[1], ENT_QUOTES, $this->charset), true)); - } - - $result[] = is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue); - } - - return implode(', ', $result); + return $this->htmlUtils->formatArgs($args, $exception); } /** * Formats an array as a string. * - * @param array $args The argument array + * @param array $args The argument array + * @param FlattenException|null $exception The flatten exception * * @return string */ - public function formatArgsAsText($args) + public function formatArgsAsText($args, $exception = null) { - return strip_tags($this->formatArgs($args)); + return strip_tags($this->formatArgs($args, $exception)); } /** diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml index 988e616685490..9ffddcd84624b 100644 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml @@ -39,6 +39,13 @@ + + + + + + + diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 214f6c0fed9b2..cb4916f7a9be4 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -101,6 +101,7 @@ %kernel.root_dir% %kernel.charset% + diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig index f09ffb3c658de..8279cc6aebd2a 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.html.twig @@ -36,7 +36,7 @@ -{% for position, e in exception.toarray %} +{% for position, e in [exception]|merge(exception.allprevious) %} {% include 'TwigBundle:Exception:traces.html.twig' with { 'exception': e, 'position': position, 'count': previous_count } only %} {% endfor %} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.xml.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.xml.twig index fa99d447f7585..90c5e143bc4af 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.xml.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.xml.twig @@ -1,7 +1,7 @@ -{% for e in exception.toarray %} +{% for e in [exception]|merge(exception.allprevious) %} {% include 'TwigBundle:Exception:traces.xml.twig' with { 'exception': e } only %} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/trace.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/trace.html.twig index d00a376a4589e..de8af410c49a3 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/trace.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/trace.html.twig @@ -4,7 +4,7 @@ {{ trace.short_class }} {{ trace.type ~ trace.function }} - ({{ trace.args|format_args }}) + ({{ trace.args|format_args(exception|default) }}) {% endif %} {% if trace.file is defined and trace.file and trace.line is defined and trace.line %} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/trace.txt.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/trace.txt.twig index ff20469bdbee8..5dd8cb698a261 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/trace.txt.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/trace.txt.twig @@ -1,5 +1,5 @@ {% if trace.function %} - at {{ trace.class ~ trace.type ~ trace.function }}({{ trace.args|format_args_as_text }}) + at {{ trace.class ~ trace.type ~ trace.function }}({{ trace.args|format_args_as_text(exception|default) }}) {% else %} at n/a {% endif %} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.html.twig index cf49082cf4ecf..dfb4c2694c4e5 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.html.twig @@ -18,7 +18,7 @@
    {% for i, trace in exception.trace %}
  1. - {% include 'TwigBundle:Exception:trace.html.twig' with { 'prefix': position, 'i': i, 'trace': trace } only %} + {% include 'TwigBundle:Exception:trace.html.twig' with { exception: exception, 'prefix': position, 'i': i, 'trace': trace } only %}
  2. {% endfor %}
diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.txt.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.txt.twig index 2cb3ba4e09d45..f973cf1ee35df 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.txt.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.txt.twig @@ -1,6 +1,6 @@ {% if exception.trace|length %} {% for trace in exception.trace %} -{% include 'TwigBundle:Exception:trace.txt.twig' with { 'trace': trace } only %} +{% include 'TwigBundle:Exception:trace.txt.twig' with { exception: exception, 'trace': trace } only %} {% endfor %} {% endif %} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.xml.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.xml.twig index 133a6260f8caf..c0940e4d91e1b 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.xml.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.xml.twig @@ -1,7 +1,7 @@ {% for trace in exception.trace %} -{% include 'TwigBundle:Exception:trace.txt.twig' with { 'trace': trace } only %} +{% include 'TwigBundle:Exception:trace.txt.twig' with { exception: exception, 'trace': trace } only %} {% endfor %} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces_text.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces_text.html.twig index 3ea3d7bba8ccf..829260c0d3f5d 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces_text.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces_text.html.twig @@ -10,7 +10,7 @@