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/Debug/TwigHighlighter.php b/src/Symfony/Bridge/Twig/Debug/TwigHighlighter.php new file mode 100644 index 0000000000000..e80d7a5bb57b8 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Debug/TwigHighlighter.php @@ -0,0 +1,82 @@ + + * + * 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\Highlighter\Highlighter; + +/** + * Simple Twig syntax highlighter + * + * @author Martin Hasoň + */ +class TwigHighlighter extends Highlighter +{ + protected $regexString = '"[^"\\\\]*?(?:\\\\.[^"\\\\]*?)*?"|\'[^\'\\\\]*?(?:\\\\.[^\'\\\\]*?)*?\''; + protected $regexTags; + protected $regex; + + public function __construct() + { + $this->regexTags = '{({{-?|{%-?|{#-?)((?:'.$this->regexString.'|[^"\']*?)+?)(-?}}|-?%}|-?#})}s'; + $this->regexKeywords = 'and|or|with'; + $this->regex = ' + /(?: + (?P'.$this->regexString.')| + (?P\b\d+(?:\.\d+)?\b)| + (?P(?\b[a-z0-9_]+(?=\s*\()|(?<=\||\|\s)[a-z0-9_]+\b)| + (?P(?:\*\*|\.\.|==|!=|>=|<=|\/\/|\?:|[+\-~\*\/%\.=><\|\(\)\[\]\{\}\?:,]))| + (?P\b(?:if|and|or|b-and|b-xor|b-or|in|matches|starts with|ends with|is|not|as|import|with|true|false|null|none)\b) + )/xi' + ; + } + + /** + * {@inheritdoc} + */ + public function highlight($code, $from = 1, $to = -1, $line = -1) + { + $oldCode = htmlspecialchars(str_replace(array("\r\n", "\r"), "\n", $code), ENT_NOQUOTES); + $regex = $this->regex; + $code = preg_replace_callback($this->regexTags, function ($matches) use ($regex) { + if ($matches[1] == '{#') { + return '' . $matches[0] . ''; + } + + $matches[2] = preg_replace_callback($regex, function ($match) { + $keys = array_keys($match); + + return sprintf('%s', $keys[count($match) - 2], $match[0]); + }, $matches[2]); + + if ($matches[1][1] == '%') { + $matches[2] = preg_replace('/^(\s*)([a-z0-9_]+)/i', '\\1\\2', $matches[2]); + } + + return ''.$matches[1].''.$matches[2].''.$matches[3].''; + }, $oldCode); + + if ('' == $code) { + $code = $oldCode; + } + + return $this->createLines(explode("\n", $code), $from, $to, $line); + } + + /** + * {@inheritdoc} + */ + public function supports($file) + { + return 'twig' === pathinfo($file, PATHINFO_EXTENSION); + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php index b7c3605d9572f..d5744e8755f49 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; } /** @@ -50,6 +54,7 @@ public function getFilters() new \Twig_SimpleFilter('format_file', array($this, 'formatFile'), array('is_safe' => array('html'))), new \Twig_SimpleFilter('format_file_from_text', array($this, 'formatFileFromText'), array('is_safe' => array('html'))), new \Twig_SimpleFilter('file_link', array($this, 'getFileLink')), + new \Twig_SimpleFilter('highlight_code', array($this->htmlUtils, 'highlight'), array('is_safe' => array('html'))), ); } @@ -78,48 +83,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/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/DebugBundle/DebugBundle.php b/src/Symfony/Bundle/DebugBundle/DebugBundle.php index 3aa536dba7860..3be1cd0cdf271 100644 --- a/src/Symfony/Bundle/DebugBundle/DebugBundle.php +++ b/src/Symfony/Bundle/DebugBundle/DebugBundle.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\DebugBundle; +use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\AddFlattenExceptionProcessorPass; +use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\AddHighlighterPass; use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\DumpDataCollectorPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -48,6 +50,8 @@ public function build(ContainerBuilder $container) { parent::build($container); + $container->addCompilerPass(new AddFlattenExceptionProcessorPass()); $container->addCompilerPass(new DumpDataCollectorPass()); + $container->addCompilerPass(new AddHighlighterPass()); } } 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/DependencyInjection/Compiler/AddHighlighterPass.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/AddHighlighterPass.php new file mode 100644 index 0000000000000..cb78aff87f43e --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/AddHighlighterPass.php @@ -0,0 +1,56 @@ + + * + * 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 code highlighters. + * + * @author Martin Hasoň + */ +class AddHighlighterPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('debug.html_utils')) { + return; + } + + $highlighters = array(); + foreach ($container->findTaggedServiceIds('code_highlighter') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $alias = isset($attributes[0]['alias']) ? $attributes[0]['alias'] : null; + + if (null === $alias) { + $highlighters[$priority][] = new Reference($id); + } else { + $highlighters[$priority][$alias] = new Reference($id); + } + } + + if (empty($highlighters)) { + return; + } + + // sort by priority and flatten + krsort($highlighters); + $highlighters = call_user_func_array('array_merge', $highlighters); + + $container->getDefinition('debug.html_utils')->replaceArgument(0, $highlighters); + } +} diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml index 16e27a22c584d..160924dead051 100644 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml @@ -30,6 +30,27 @@ 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..d748503ceb7dc 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% + @@ -154,6 +155,7 @@ %twig.exception_listener.controller% + @@ -174,5 +176,14 @@ + + + + + + + + + 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..530e2fbba8bd4 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 %} @@ -16,7 +16,20 @@ + {% endspaceless %} +
- {{ trace.file|file_excerpt(trace.line) }} + {% if trace.file is defined and trace.file and trace.line is defined and trace.line %} + {{ trace.file|format_file(trace.line) }} +
+ {{ trace.file|highlight_code(trace.line, highlighter='php') }} +
+ {% endif %} + + {% for code in trace.related_codes|default([]) if code.file.path is defined %} + {{ code.file.path|format_file(code.line|default(1), code.file.name|default) }} +
+ {{ code.file.path|highlight_code(code.line|default(1), highlighter=code.file.type|default) }} +
+ {% endfor %}
{% endif %} 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 @@