From d7062f35b81db56d5d6e0dc61801912bdeed434a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 12 Nov 2016 08:50:38 -0800 Subject: [PATCH] exposed a way to access template data and methods in a portable way --- CHANGELOG | 1 + doc/api.rst | 22 +++- doc/functions/include.rst | 10 +- doc/tags/extends.rst | 8 +- doc/tags/include.rst | 10 +- lib/Twig/Environment.php | 27 ++++- lib/Twig/Template.php | 41 ++++++-- lib/Twig/TemplateWrapper.php | 134 ++++++++++++++++++++++++ test/Twig/Tests/TemplateTest.php | 1 + test/Twig/Tests/TemplateWrapperTest.php | 38 +++++++ 10 files changed, 273 insertions(+), 19 deletions(-) create mode 100644 lib/Twig/TemplateWrapper.php create mode 100644 test/Twig/Tests/TemplateWrapperTest.php diff --git a/CHANGELOG b/CHANGELOG index ea4121c343..cd701d3ff2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ * 1.28.0 (2016-XX-XX) + * exposed a way to access template data and methods in a portable way * changed context access to use the PHP 7 null coalescing operator when available * added the "with" tag * added support for a custom template on the block() function diff --git a/doc/api.rst b/doc/api.rst index fcb7a2f7e7..8be2b8af8e 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -43,10 +43,18 @@ templates from a database or other resources. the evaluated templates. For such a need, you can use any available PHP cache library. -To load a template from this environment you just have to call the -``loadTemplate()`` method which then returns a ``Twig_Template`` instance:: +Rendering Templates +------------------- + +To load a template from a Twig environment, call the ``load()`` method which +returns a ``Twig_TemplateWrapper`` instance:: + + $template = $twig->load('index.html'); + +.. note:: - $template = $twig->loadTemplate('index.html'); + Before Twig 1.28, you should use ``loadTemplate()`` instead which returns a + ``Twig_Template`` instance. To render the template with some variables, call the ``render()`` method:: @@ -60,6 +68,14 @@ You can also load and render the template in one fell swoop:: echo $twig->render('index.html', array('the' => 'variables', 'go' => 'here')); +.. versionadded:: 1.28 + The possibility to render blocks from the API was added in Twig 1.28. + +If a template defines blocks, they can be rendered individually via the +``renderBlock()`` call:: + + echo $template->renderBlock('block_name', array('the' => 'variables', 'go' => 'here')); + .. _environment_options: Environment Options diff --git a/doc/functions/include.rst b/doc/functions/include.rst index 33bd56d116..2f88ed7760 100644 --- a/doc/functions/include.rst +++ b/doc/functions/include.rst @@ -37,14 +37,18 @@ You can disable access to the context by setting ``with_context`` to {# no variables will be accessible #} {{ include('template.html', with_context = false) }} -And if the expression evaluates to a ``Twig_Template`` object, Twig will use it -directly:: +And if the expression evaluates to a ``Twig_Template`` or a +``Twig_TemplateWrapper`` instance, Twig will use it directly:: // {{ include(template) }} + // deprecated as of Twig 1.28 $template = $twig->loadTemplate('some_template.twig'); - $twig->loadTemplate('template.twig')->display(array('template' => $template)); + // as of Twig 1.28 + $template = $twig->load('some_template.twig'); + + $twig->display('template.twig', array('template' => $template)); When you set the ``ignore_missing`` flag, Twig will return an empty string if the template does not exist: diff --git a/doc/tags/extends.rst b/doc/tags/extends.rst index a31df89dfe..df7bdcb7da 100644 --- a/doc/tags/extends.rst +++ b/doc/tags/extends.rst @@ -153,13 +153,17 @@ Twig supports dynamic inheritance by using a variable as the base template: {% extends some_var %} -If the variable evaluates to a ``Twig_Template`` object, Twig will use it as -the parent template:: +If the variable evaluates to a ``Twig_Template`` or a ``Twig_TemplateWraper`` +instance, Twig will use it as the parent template:: // {% extends layout %} + // deprecated as of Twig 1.28 $layout = $twig->loadTemplate('some_layout_template.twig'); + // as of Twig 1.28 + $layout = $twig->load('some_layout_template.twig'); + $twig->display('template.twig', array('layout' => $layout)); .. versionadded:: 1.2 diff --git a/doc/tags/include.rst b/doc/tags/include.rst index da18dc65ec..24ff24db69 100644 --- a/doc/tags/include.rst +++ b/doc/tags/include.rst @@ -50,14 +50,18 @@ The template name can be any valid Twig expression: {% include some_var %} {% include ajax ? 'ajax.html' : 'not_ajax.html' %} -And if the expression evaluates to a ``Twig_Template`` object, Twig will use it -directly:: +And if the expression evaluates to a ``Twig_Template`` or a +``Twig_TemplateWrapper`` instance, Twig will use it directly:: // {% include template %} + // deprecated as of Twig 1.28 $template = $twig->loadTemplate('some_template.twig'); - $twig->loadTemplate('template.twig')->display(array('template' => $template)); + // as of Twig 1.28 + $template = $twig->load('some_template.twig'); + + $twig->display('template.twig', array('template' => $template)); .. versionadded:: 1.2 The ``ignore missing`` feature has been added in Twig 1.2. diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index 58dd7e7ae9..369b18dccf 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -378,7 +378,30 @@ public function display($name, array $context = array()) } /** - * Loads a template by name. + * Loads a template. + * + * @param string|Twig_TemplateWrapper|Twig_Template $name The template name + * + * @return Twig_TemplateWrapper + */ + public function load($name) + { + if ($name instanceof Twig_TemplateWrapper) { + return $name; + } + + if ($name instanceof Twig_Template) { + return new Twig_TemplateWrapper($this, $name); + } + + return new Twig_TemplateWrapper($this, $this->loadTemplate($name)); + } + + /** + * Loads a template internal representation. + * + * This method is for internal use only and should never be called + * directly. * * @param string $name The template name * @param int $index The index if it is an embedded template @@ -387,6 +410,8 @@ public function display($name, array $context = array()) * * @throws Twig_Error_Loader When the template cannot be found * @throws Twig_Error_Syntax When an error occurred during compilation + * + * @internal */ public function loadTemplate($name, $index = null) { diff --git a/lib/Twig/Template.php b/lib/Twig/Template.php index e75fbb865f..df87c2a277 100644 --- a/lib/Twig/Template.php +++ b/lib/Twig/Template.php @@ -17,7 +17,13 @@ /** * Default base class for compiled templates. * + * This class is an implementation detail of how template compilation currently + * works, which might change. It should never be used directly. Use $twig->load() + * instead, which returns an instance of Twig_TemplateWrapper. + * * @author Fabien Potencier + * + * @internal */ abstract class Twig_Template implements Twig_TemplateInterface { @@ -307,18 +313,34 @@ public function hasBlock($name, array $context = null, array $blocks = array()) } /** - * Returns all block names. + * Returns all block names in the current context of the template. * - * This method is for internal use only and should never be called - * directly. + * This method checks blocks defined in the current template + * or defined in "used" traits or defined in parent templates. + * + * @param string $name The block name + * @param array $context The context + * @param array $blocks The current set of blocks * * @return array An array of block names * * @internal */ - public function getBlockNames() + public function getBlockNames(array $context = null, array $blocks = array()) { - return array_keys($this->blocks); + if (null === $context) { + @trigger_error('The '.__METHOD__.' method is internal and should never be called; calling it directly is deprecated since version 1.28 and won\'t be possible anymore in 2.0.', E_USER_DEPRECATED); + + return array_keys($this->blocks); + } + + $names = array_merge(array_keys($blocks), array_keys($this->blocks)); + + if (false !== $parent = $this->getParent($context)) { + $names = array_merge($names, $parent->getBlockNames($context)); + } + + return array_unique($names); } protected function loadTemplate($template, $templateName = null, $line = null, $index = null) @@ -332,6 +354,10 @@ protected function loadTemplate($template, $templateName = null, $line = null, $ return $template; } + if ($template instanceof Twig_TemplateWrapper) { + return $template; + } + return $this->env->loadTemplate($template, $index); } catch (Twig_Error $e) { if (!$e->getTemplateName()) { @@ -654,9 +680,10 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ throw $e; } - // useful when calling a template method from a template - // this is not supported but unfortunately heavily used in the Symfony profiler + // @deprecated in 1.28 if ($object instanceof Twig_TemplateInterface) { + @trigger_error('Using the dot notation on an instance of '.__CLASS.' is deprecated since version 1.28 and won\'t be supported anymore in 2.0.', E_USER_DEPRECATED); + return $ret === '' ? '' : new Twig_Markup($ret, $this->env->getCharset()); } diff --git a/lib/Twig/TemplateWrapper.php b/lib/Twig/TemplateWrapper.php new file mode 100644 index 0000000000..643a493d4c --- /dev/null +++ b/lib/Twig/TemplateWrapper.php @@ -0,0 +1,134 @@ + + */ +final class Twig_TemplateWrapper +{ + private $env; + private $template; + + /** + * This method is for internal use only and should never be called + * directly (use Twig_Environment::load() instead). + * + * @internal + */ + public function __construct(Twig_Environment $env, Twig_Template $template) + { + $this->env = $env; + $this->template = $template; + } + + /** + * Renders the template. + * + * @param array $context An array of parameters to pass to the template + * + * @return string The rendered template + */ + public function render($context = array()) + { + return $this->template->render($context); + } + + /** + * Displays the template. + * + * @param array $context An array of parameters to pass to the template + */ + public function display($context = array()) + { + return $this->template->display($context); + } + + /** + * Checks if a block is defined. + * + * @param string $name The block name + * @param array $context An array of parameters to pass to the template + * + * @return bool + */ + public function hasBlock($name, $context = array()) + { + return $this->template->hasBlock($name, $context); + } + + /** + * Returns defined block names in the template. + * + * @param array $context An array of parameters to pass to the template + * + * @return string[] An array of defined template block names + */ + public function getBlockNames($context = array()) + { + return $this->template->getBlockNames($context); + } + + /** + * Renders a template block. + * + * @param string $name The block name to render + * @param array $context An array of parameters to pass to the template + * + * @return string The rendered block + */ + public function renderBlock($name, $context = array()) + { + ob_start(); + $this->displayBlock($name, $context); + + return ob_get_clean(); + } + + /** + * Displays a template block. + * + * @param string $name The block name to render + * @param array $context An array of parameters to pass to the template + */ + public function displayBlock($name, $context = array()) + { + $context = $this->env->mergeGlobals($context); + $level = ob_get_level(); + ob_start(); + try { + $this->template->displayBlock($name, $context); + } catch (Exception $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } catch (Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } + + return ob_get_clean(); + } + + /** + * @return Twig_Source + */ + public function getSourceContext() + { + return $this->template->getSourceContext(); + } +} diff --git a/test/Twig/Tests/TemplateTest.php b/test/Twig/Tests/TemplateTest.php index ba10e6895b..78f63a3150 100644 --- a/test/Twig/Tests/TemplateTest.php +++ b/test/Twig/Tests/TemplateTest.php @@ -130,6 +130,7 @@ public function getGetAttributeWithSandbox() /** * @dataProvider getGetAttributeWithTemplateAsObject + * @group legacy */ public function testGetAttributeWithTemplateAsObject($useExt) { diff --git a/test/Twig/Tests/TemplateWrapperTest.php b/test/Twig/Tests/TemplateWrapperTest.php new file mode 100644 index 0000000000..fbc4b4a202 --- /dev/null +++ b/test/Twig/Tests/TemplateWrapperTest.php @@ -0,0 +1,38 @@ + '{% block foo %}{% endblock %}', + 'index_with_use' => '{% use "imported" %}{% block foo %}{% endblock %}', + 'index_with_extends' => '{% extends "extended" %}{% block foo %}{% endblock %}', + 'imported' => '{% block imported %}{% endblock %}', + 'extended' => '{% block extended %}{% endblock %}', + ))); + + $wrapper = new Twig_TemplateWrapper($twig, $twig->loadTemplate('index')); + $this->assertTrue($wrapper->hasBlock('foo')); + $this->assertFalse($wrapper->hasBlock('bar')); + $this->assertEquals(array('foo'), $wrapper->getBlockNames()); + + $wrapper = new Twig_TemplateWrapper($twig, $twig->loadTemplate('index_with_use')); + $this->assertTrue($wrapper->hasBlock('foo')); + $this->assertTrue($wrapper->hasBlock('imported')); + $this->assertEquals(array('imported', 'foo'), $wrapper->getBlockNames()); + + $wrapper = new Twig_TemplateWrapper($twig, $twig->loadTemplate('index_with_extends')); + $this->assertTrue($wrapper->hasBlock('foo')); + $this->assertTrue($wrapper->hasBlock('extended')); + $this->assertEquals(array('foo', 'extended'), $wrapper->getBlockNames()); + } +}