Skip to content

Commit

Permalink
feature #2236 Expose a way to access template data and methods in a p…
Browse files Browse the repository at this point in the history
…ortable way (fabpot)

This PR was merged into the 1.x branch.

Discussion
----------

Expose a way to access template data and methods in a portable way

`Twig_Template` is an internal class, and most of its methods are marked as being `@internal` as they are not "safe" to use by end users.

But some of its features are interesting like `renderBlock()` which is very useful when rendering emails for instance (see #1873).

This PR tries to address both issues by marking the whole `Twig_Template` class as being internal  and by exposing a new way to safely interact with a template besides the obvious `render()`/`display()` methods via `$twig->load("...")`.

If also add some convenient methods like `hasBlock()` or `getBlocks()` (see ~#1831~ and #1882):

```php
$template = $twig->load('index');

echo $template->render($context);
$template->display($context);

if ($template->hasBlock('name') {
   $template->displayBlock('name', $context);
}

foreach ($template->getBlocks() as $block) {
    echo $template->render($block, $context);
}
```

Commits
-------

d7062f3 exposed a way to access template data and methods in a portable way
  • Loading branch information
fabpot committed Nov 15, 2016
2 parents 08bd6f1 + d7062f3 commit 4b48d61
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 19 deletions.
1 change: 1 addition & 0 deletions 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
Expand Down
22 changes: 19 additions & 3 deletions doc/api.rst
Expand Up @@ -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::

Expand All @@ -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
Expand Down
10 changes: 7 additions & 3 deletions doc/functions/include.rst
Expand Up @@ -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:
Expand Down
8 changes: 6 additions & 2 deletions doc/tags/extends.rst
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions doc/tags/include.rst
Expand Up @@ -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.
Expand Down
27 changes: 26 additions & 1 deletion lib/Twig/Environment.php
Expand Up @@ -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
Expand All @@ -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)
{
Expand Down
41 changes: 34 additions & 7 deletions lib/Twig/Template.php
Expand Up @@ -13,7 +13,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 <fabien@symfony.com>
*
* @internal
*/
abstract class Twig_Template implements Twig_TemplateInterface
{
Expand Down Expand Up @@ -303,18 +309,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)
Expand All @@ -328,6 +350,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()) {
Expand Down Expand Up @@ -648,9 +674,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());
}

Expand Down
134 changes: 134 additions & 0 deletions lib/Twig/TemplateWrapper.php
@@ -0,0 +1,134 @@
<?php

/*
* This file is part of Twig.
*
* (c) 2016 Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/**
* Exposes a template to userland.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
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();
}
}
1 change: 1 addition & 0 deletions test/Twig/Tests/TemplateTest.php
Expand Up @@ -130,6 +130,7 @@ public function getGetAttributeWithSandbox()

/**
* @dataProvider getGetAttributeWithTemplateAsObject
* @group legacy
*/
public function testGetAttributeWithTemplateAsObject($useExt)
{
Expand Down

0 comments on commit 4b48d61

Please sign in to comment.