From ec37fd065f55c53dd80265de5af1cc9a1fd2512b Mon Sep 17 00:00:00 2001 From: Matias Griese Date: Fri, 3 Sep 2021 18:39:41 +0300 Subject: [PATCH] Throwing exceptions from Twig templates fires `onDisplayErrorPage.[code]` event allowing better error pages --- CHANGELOG.md | 1 + composer.lock | 143 +++++++++--------- system/src/Grav/Common/Grav.php | 8 + .../Grav/Common/Processors/PagesProcessor.php | 5 + .../Common/Twig/Exception/TwigException.php | 19 +++ .../Grav/Common/Twig/Node/TwigNodeThrow.php | 2 +- system/src/Grav/Common/Twig/Twig.php | 85 +++++++---- .../Extensions/Deferred/DeferredExtension.php | 70 +++++++++ 8 files changed, 232 insertions(+), 101 deletions(-) create mode 100644 system/src/Grav/Common/Twig/Exception/TwigException.php create mode 100644 system/src/Phive/Twig/Extensions/Deferred/DeferredExtension.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1edde533d..f6c162fec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ 1. [](#new) * Added `|yaml` filter to convert input to YAML * Added `route` and `request` to `onPageNotFound` event + * Throwing exceptions from Twig templates fires `onDisplayErrorPage.[code]` event allowing better error pages 3. [](#bugfix) * Fixed escaping in PageIndex::getLevelListing() * Fixed validation of `number` type [#3433](https://github.com/getgrav/grav/issues/3433) diff --git a/composer.lock b/composer.lock index f06c4a601..8d2907f41 100644 --- a/composer.lock +++ b/composer.lock @@ -311,26 +311,26 @@ }, { "name": "doctrine/collections", - "version": "1.6.7", + "version": "1.6.8", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "55f8b799269a1a472457bd1a41b4f379d4cfba4a" + "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/55f8b799269a1a472457bd1a41b4f379d4cfba4a", - "reference": "55f8b799269a1a472457bd1a41b4f379d4cfba4a", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1958a744696c6bb3bb0d28db2611dc11610e78af", + "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af", "shasum": "" }, "require": { "php": "^7.1.3 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan-shim": "^0.9.2", - "phpunit/phpunit": "^7.0", - "vimeo/psalm": "^3.8.1" + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5", + "vimeo/psalm": "^4.2.1" }, "type": "library", "autoload": { @@ -374,9 +374,9 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/1.6.7" + "source": "https://github.com/doctrine/collections/tree/1.6.8" }, - "time": "2020-07-27T17:53:49+00:00" + "time": "2021-08-10T18:51:53+00:00" }, { "name": "donatj/phpuseragentparser", @@ -642,16 +642,16 @@ }, { "name": "filp/whoops", - "version": "2.14.0", + "version": "2.14.1", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "fdf92f03e150ed84d5967a833ae93abffac0315b" + "reference": "15ead64e9828f0fc90932114429c4f7923570cb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/fdf92f03e150ed84d5967a833ae93abffac0315b", - "reference": "fdf92f03e150ed84d5967a833ae93abffac0315b", + "url": "https://api.github.com/repos/filp/whoops/zipball/15ead64e9828f0fc90932114429c4f7923570cb1", + "reference": "15ead64e9828f0fc90932114429c4f7923570cb1", "shasum": "" }, "require": { @@ -701,7 +701,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.14.0" + "source": "https://github.com/filp/whoops/tree/2.14.1" }, "funding": [ { @@ -709,7 +709,7 @@ "type": "github" } ], - "time": "2021-07-13T12:00:00+00:00" + "time": "2021-08-29T12:00:00+00:00" }, { "name": "getgrav/cache", @@ -2258,16 +2258,16 @@ }, { "name": "symfony/console", - "version": "v4.4.29", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b" + "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b", - "reference": "8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b", + "url": "https://api.github.com/repos/symfony/console/zipball/a3f7189a0665ee33b50e9e228c46f50f5acbed22", + "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22", "shasum": "" }, "require": { @@ -2328,7 +2328,7 @@ "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/console/tree/v4.4.29" + "source": "https://github.com/symfony/console/tree/v4.4.30" }, "funding": [ { @@ -2344,7 +2344,7 @@ "type": "tidelift" } ], - "time": "2021-07-27T19:04:53+00:00" + "time": "2021-08-25T19:27:26+00:00" }, { "name": "symfony/contracts", @@ -2442,16 +2442,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "958a128b184fcf0ba45ec90c0e88554c9327c2e9" + "reference": "2fe81680070043c4c80e7cedceb797e34f377bac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/958a128b184fcf0ba45ec90c0e88554c9327c2e9", - "reference": "958a128b184fcf0ba45ec90c0e88554c9327c2e9", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2fe81680070043c4c80e7cedceb797e34f377bac", + "reference": "2fe81680070043c4c80e7cedceb797e34f377bac", "shasum": "" }, "require": { @@ -2506,7 +2506,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.27" + "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.30" }, "funding": [ { @@ -2522,7 +2522,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/http-client", @@ -3009,16 +3009,16 @@ }, { "name": "symfony/process", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f" + "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f", - "reference": "0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f", + "url": "https://api.github.com/repos/symfony/process/zipball/13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d", + "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d", "shasum": "" }, "require": { @@ -3051,7 +3051,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v4.4.27" + "source": "https://github.com/symfony/process/tree/v4.4.30" }, "funding": [ { @@ -3067,20 +3067,20 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/var-dumper", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "391d6d0e7a06ab54eb7c38fab29b8d174471b3ba" + "reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/391d6d0e7a06ab54eb7c38fab29b8d174471b3ba", - "reference": "391d6d0e7a06ab54eb7c38fab29b8d174471b3ba", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c", + "reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c", "shasum": "" }, "require": { @@ -3140,7 +3140,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v4.4.27" + "source": "https://github.com/symfony/var-dumper/tree/v4.4.30" }, "funding": [ { @@ -3156,7 +3156,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/yaml", @@ -3580,20 +3580,20 @@ }, { "name": "codeception/lib-innerbrowser", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/Codeception/lib-innerbrowser.git", - "reference": "4b0d89b37fe454e060a610a85280a87ab4f534f1" + "reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/4b0d89b37fe454e060a610a85280a87ab4f534f1", - "reference": "4b0d89b37fe454e060a610a85280a87ab4f534f1", + "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2", + "reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2", "shasum": "" }, "require": { - "codeception/codeception": "*@dev", + "codeception/codeception": "4.*@dev", "ext-dom": "*", "ext-json": "*", "ext-mbstring": "*", @@ -3634,9 +3634,9 @@ ], "support": { "issues": "https://github.com/Codeception/lib-innerbrowser/issues", - "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.5.0" + "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.5.1" }, - "time": "2021-04-23T06:18:29+00:00" + "time": "2021-08-30T15:21:42+00:00" }, { "name": "codeception/module-asserts", @@ -4569,16 +4569,16 @@ }, { "name": "phpstan/phpstan", - "version": "0.12.94", + "version": "0.12.98", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "3d0ba4c198a24e3c3fc489f3ec6ac9612c4be5d6" + "reference": "3bb7cc246c057405dd5e290c3ecc62ab51d57e00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3d0ba4c198a24e3c3fc489f3ec6ac9612c4be5d6", - "reference": "3d0ba4c198a24e3c3fc489f3ec6ac9612c4be5d6", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3bb7cc246c057405dd5e290c3ecc62ab51d57e00", + "reference": "3bb7cc246c057405dd5e290c3ecc62ab51d57e00", "shasum": "" }, "require": { @@ -4609,7 +4609,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.94" + "source": "https://github.com/phpstan/phpstan/tree/0.12.98" }, "funding": [ { @@ -4629,7 +4629,7 @@ "type": "tidelift" } ], - "time": "2021-07-30T09:05:27+00:00" + "time": "2021-09-02T12:33:01+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -5002,16 +5002,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.8", + "version": "9.5.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "191768ccd5c85513b4068bdbe99bb6390c7d54fb" + "reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/191768ccd5c85513b4068bdbe99bb6390c7d54fb", - "reference": "191768ccd5c85513b4068bdbe99bb6390c7d54fb", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b", + "reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b", "shasum": "" }, "require": { @@ -5089,7 +5089,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.8" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.9" }, "funding": [ { @@ -5101,7 +5101,7 @@ "type": "github" } ], - "time": "2021-07-31T15:17:34+00:00" + "time": "2021-08-31T06:47:40+00:00" }, { "name": "psr/http-client", @@ -6008,6 +6008,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { @@ -6326,16 +6327,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v5.3.4", + "version": "v5.3.7", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "2dd8890bd01be59a5221999c05ccf0fcafcb354f" + "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2dd8890bd01be59a5221999c05ccf0fcafcb354f", - "reference": "2dd8890bd01be59a5221999c05ccf0fcafcb354f", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c7eef3a60ccfdd8eafe07f81652e769ac9c7146c", + "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c", "shasum": "" }, "require": { @@ -6381,7 +6382,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.3.4" + "source": "https://github.com/symfony/dom-crawler/tree/v5.3.7" }, "funding": [ { @@ -6397,20 +6398,20 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:55:36+00:00" + "time": "2021-08-29T19:32:13+00:00" }, { "name": "symfony/finder", - "version": "v5.3.4", + "version": "v5.3.7", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "17f50e06018baec41551a71a15731287dbaab186" + "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/17f50e06018baec41551a71a15731287dbaab186", - "reference": "17f50e06018baec41551a71a15731287dbaab186", + "url": "https://api.github.com/repos/symfony/finder/zipball/a10000ada1e600d109a6c7632e9ac42e8bf2fb93", + "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93", "shasum": "" }, "require": { @@ -6443,7 +6444,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.3.4" + "source": "https://github.com/symfony/finder/tree/v5.3.7" }, "funding": [ { @@ -6459,7 +6460,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:54:19+00:00" + "time": "2021-08-04T21:20:46+00:00" }, { "name": "theseer/tokenizer", diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index c9097eea8..ffbb50818 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -9,6 +9,7 @@ namespace Grav\Common; +use Composer\Autoload\ClassLoader; use Grav\Common\Config\Config; use Grav\Common\Config\Setup; use Grav\Common\Helpers\Exif; @@ -152,6 +153,13 @@ class Grav extends Container { if (null === self::$instance) { self::$instance = static::load($values); + + /** @var ClassLoader|null $loader */ + $loader = self::$instance['loader'] ?? null; + if ($loader) { + // Load fix for Deferred Twig Extension + $loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true); + } } elseif ($values) { $instance = self::$instance; foreach ($values as $key => $value) { diff --git a/system/src/Grav/Common/Processors/PagesProcessor.php b/system/src/Grav/Common/Processors/PagesProcessor.php index 96e078690..470ca907b 100644 --- a/system/src/Grav/Common/Processors/PagesProcessor.php +++ b/system/src/Grav/Common/Processors/PagesProcessor.php @@ -10,6 +10,7 @@ namespace Grav\Common\Processors; use Grav\Common\Page\Interfaces\PageInterface; +use Grav\Framework\RequestHandler\Exception\RequestException; use Grav\Plugin\Form\Forms; use RocketTheme\Toolbox\Event\Event; use Psr\Http\Message\ResponseInterface; @@ -48,10 +49,14 @@ class PagesProcessor extends ProcessorBase $page = $this->container['page']; if (!$page->routable()) { + $exception = new RequestException($request, 'Page Not Found', 404); $route = $this->container['route']; // If no page found, fire event $event = new Event([ 'page' => $page, + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), + 'exception' => $exception, 'route' => $route, 'request' => $request ]); diff --git a/system/src/Grav/Common/Twig/Exception/TwigException.php b/system/src/Grav/Common/Twig/Exception/TwigException.php new file mode 100644 index 000000000..8f543dcc1 --- /dev/null +++ b/system/src/Grav/Common/Twig/Exception/TwigException.php @@ -0,0 +1,19 @@ +addDebugInfo($this); $compiler - ->write('throw new \RuntimeException(') + ->write('throw new \Grav\Common\Twig\Exception\TwigException(') ->subcompile($this->getNode('message')) ->write(', ') ->write($this->getAttribute('code') ?: 500) diff --git a/system/src/Grav/Common/Twig/Twig.php b/system/src/Grav/Common/Twig/Twig.php index 2796c0d45..ef74ce75a 100644 --- a/system/src/Grav/Common/Twig/Twig.php +++ b/system/src/Grav/Common/Twig/Twig.php @@ -16,6 +16,7 @@ use Grav\Common\Language\Language; use Grav\Common\Language\LanguageCodes; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Pages; +use Grav\Common\Twig\Exception\TwigException; use Grav\Common\Twig\Extension\FilesystemExtension; use Grav\Common\Twig\Extension\GravExtension; use Grav\Common\Utils; @@ -26,6 +27,7 @@ use RuntimeException; use Twig\Cache\FilesystemCache; use Twig\Environment; use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; use Twig\Extension\CoreExtension; use Twig\Extension\DebugExtension; use Twig\Extension\StringLoaderExtension; @@ -404,38 +406,63 @@ class Twig */ public function processSite($format = null, array $vars = []) { - // set the page now its been processed - $this->grav->fireEvent('onTwigSiteVariables'); - /** @var Pages $pages */ - $pages = $this->grav['pages']; - /** @var PageInterface $page */ - $page = $this->grav['page']; - $content = $page->content(); - - $twig_vars = $this->twig_vars; - - $twig_vars['theme'] = $this->grav['config']->get('theme'); - $twig_vars['pages'] = $pages->root(); - $twig_vars['page'] = $page; - $twig_vars['header'] = $page->header(); - $twig_vars['media'] = $page->media(); - $twig_vars['content'] = $content; - - // determine if params are set, if so disable twig cache - $params = $this->grav['uri']->params(null, true); - if (!empty($params)) { - $this->twig->setCache(false); - } - - // Get Twig template layout - $template = $this->getPageTwigTemplate($page, $format); - $page->templateFormat($format); - try { + $grav = $this->grav; + + // set the page now its been processed + $grav->fireEvent('onTwigSiteVariables'); + + /** @var Pages $pages */ + $pages = $grav['pages']; + + /** @var PageInterface $page */ + $page = $grav['page']; + + $twig_vars = $this->twig_vars; + $twig_vars['theme'] = $grav['config']->get('theme'); + $twig_vars['pages'] = $pages->root(); + $twig_vars['page'] = $page; + $twig_vars['header'] = $page->header(); + $twig_vars['media'] = $page->media(); + $twig_vars['content'] = $page->content(); + + // determine if params are set, if so disable twig cache + $params = $grav['uri']->params(null, true); + if (!empty($params)) { + $this->twig->setCache(false); + } + + // Get Twig template layout + $template = $this->getPageTwigTemplate($page, $format); + $page->templateFormat($format); + $output = $this->twig->render($template, $vars + $twig_vars); } catch (LoaderError $e) { - $error_msg = $e->getMessage(); - throw new RuntimeException($error_msg, 400, $e); + throw new RuntimeException($e->getMessage(), 400, $e); + } catch (RuntimeError $e) { + $prev = $e->getPrevious(); + if ($prev instanceof TwigException) { + $code = $prev->getCode() ?: 500; + // Fire onPageNotFound event. + $event = new Event([ + 'page' => $page, + 'code' => $code, + 'message' => $prev->getMessage(), + 'exception' => $prev, + 'route' => $grav['route'], + 'request' => $grav['request'] + ]); + $event = $grav->fireEvent("onDisplayErrorPage.{$code}", $event); + $newPage = $event['page']; + if ($newPage && $newPage !== $page) { + unset($grav['page']); + $grav['page'] = $newPage; + + return $this->processSite($newPage->templateFormat(), $vars); + } + } + + throw $e; } return $output; diff --git a/system/src/Phive/Twig/Extensions/Deferred/DeferredExtension.php b/system/src/Phive/Twig/Extensions/Deferred/DeferredExtension.php new file mode 100644 index 000000000..3a41e4a0e --- /dev/null +++ b/system/src/Phive/Twig/Extensions/Deferred/DeferredExtension.php @@ -0,0 +1,70 @@ +getTemplateName(); + $this->blocks[$templateName][] = [ob_get_level(), $blockName]; + } + + public function resolve(\Twig_Template $template, array $context, array $blocks) + { + $templateName = $template->getTemplateName(); + if (empty($this->blocks[$templateName])) { + return; + } + + while ($block = array_pop($this->blocks[$templateName])) { + [$level, $blockName] = $block; + if (ob_get_level() !== $level) { + continue; + } + + $buffer = ob_get_clean(); + + $blocks[$blockName] = array($template, 'block_'.$blockName.'_deferred'); + $template->displayBlock($blockName, $context, $blocks); + + echo $buffer; + } + + if ($parent = $template->getParent($context)) { + $this->resolve($parent, $context, $blocks); + } + } +}