Browse code

feature #2236 Expose a way to access template data and methods in a portable 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

Fabien Potencier authored on 15/11/2016 21:12:57
Showing 10 changed files
... ...
@@ -1,5 +1,6 @@
1 1
 * 1.28.0 (2016-XX-XX)
2 2
 
3
+ * exposed a way to access template data and methods in a portable way
3 4
  * changed context access to use the PHP 7 null coalescing operator when available
4 5
  * added the "with" tag
5 6
  * added support for a custom template on the block() function
... ...
@@ -43,10 +43,18 @@ templates from a database or other resources.
43 43
     the evaluated templates. For such a need, you can use any available PHP
44 44
     cache library.
45 45
 
46
-To load a template from this environment you just have to call the
47
-``loadTemplate()`` method which then returns a ``Twig_Template`` instance::
46
+Rendering Templates
47
+-------------------
48
+
49
+To load a template from a Twig environment, call the ``load()`` method which
50
+returns a ``Twig_TemplateWrapper`` instance::
51
+
52
+    $template = $twig->load('index.html');
53
+
54
+.. note::
48 55
 
49
-    $template = $twig->loadTemplate('index.html');
56
+    Before Twig 1.28, you should use ``loadTemplate()`` instead which returns a
57
+    ``Twig_Template`` instance.
50 58
 
51 59
 To render the template with some variables, call the ``render()`` method::
52 60
 
... ...
@@ -60,6 +68,14 @@ You can also load and render the template in one fell swoop::
60 68
 
61 69
     echo $twig->render('index.html', array('the' => 'variables', 'go' => 'here'));
62 70
 
71
+.. versionadded:: 1.28
72
+    The possibility to render blocks from the API was added in Twig 1.28.
73
+
74
+If a template defines blocks, they can be rendered individually via the
75
+``renderBlock()`` call::
76
+
77
+    echo $template->renderBlock('block_name', array('the' => 'variables', 'go' => 'here'));
78
+
63 79
 .. _environment_options:
64 80
 
65 81
 Environment Options
... ...
@@ -37,14 +37,18 @@ You can disable access to the context by setting ``with_context`` to
37 37
     {# no variables will be accessible #}
38 38
     {{ include('template.html', with_context = false) }}
39 39
 
40
-And if the expression evaluates to a ``Twig_Template`` object, Twig will use it
41
-directly::
40
+And if the expression evaluates to a ``Twig_Template`` or a
41
+``Twig_TemplateWrapper`` instance, Twig will use it directly::
42 42
 
43 43
     // {{ include(template) }}
44 44
 
45
+    // deprecated as of Twig 1.28
45 46
     $template = $twig->loadTemplate('some_template.twig');
46 47
 
47
-    $twig->loadTemplate('template.twig')->display(array('template' => $template));
48
+    // as of Twig 1.28
49
+    $template = $twig->load('some_template.twig');
50
+
51
+    $twig->display('template.twig', array('template' => $template));
48 52
 
49 53
 When you set the ``ignore_missing`` flag, Twig will return an empty string if
50 54
 the template does not exist:
... ...
@@ -153,13 +153,17 @@ Twig supports dynamic inheritance by using a variable as the base template:
153 153
 
154 154
     {% extends some_var %}
155 155
 
156
-If the variable evaluates to a ``Twig_Template`` object, Twig will use it as
157
-the parent template::
156
+If the variable evaluates to a ``Twig_Template`` or a ``Twig_TemplateWraper``
157
+instance, Twig will use it as the parent template::
158 158
 
159 159
     // {% extends layout %}
160 160
 
161
+    // deprecated as of Twig 1.28
161 162
     $layout = $twig->loadTemplate('some_layout_template.twig');
162 163
 
164
+    // as of Twig 1.28
165
+    $layout = $twig->load('some_layout_template.twig');
166
+
163 167
     $twig->display('template.twig', array('layout' => $layout));
164 168
 
165 169
 .. versionadded:: 1.2
... ...
@@ -50,14 +50,18 @@ The template name can be any valid Twig expression:
50 50
     {% include some_var %}
51 51
     {% include ajax ? 'ajax.html' : 'not_ajax.html' %}
52 52
 
53
-And if the expression evaluates to a ``Twig_Template`` object, Twig will use it
54
-directly::
53
+And if the expression evaluates to a ``Twig_Template`` or a
54
+``Twig_TemplateWrapper`` instance, Twig will use it directly::
55 55
 
56 56
     // {% include template %}
57 57
 
58
+    // deprecated as of Twig 1.28
58 59
     $template = $twig->loadTemplate('some_template.twig');
59 60
 
60
-    $twig->loadTemplate('template.twig')->display(array('template' => $template));
61
+    // as of Twig 1.28
62
+    $template = $twig->load('some_template.twig');
63
+
64
+    $twig->display('template.twig', array('template' => $template));
61 65
 
62 66
 .. versionadded:: 1.2
63 67
     The ``ignore missing`` feature has been added in Twig 1.2.
... ...
@@ -378,7 +378,30 @@ class Twig_Environment
378 378
     }
379 379
 
380 380
     /**
381
-     * Loads a template by name.
381
+     * Loads a template.
382
+     *
383
+     * @param string|Twig_TemplateWrapper|Twig_Template $name The template name
384
+     *
385
+     * @return Twig_TemplateWrapper
386
+     */
387
+    public function load($name)
388
+    {
389
+        if ($name instanceof Twig_TemplateWrapper) {
390
+            return $name;
391
+        }
392
+
393
+        if ($name instanceof Twig_Template) {
394
+            return new Twig_TemplateWrapper($this, $name);
395
+        }
396
+
397
+        return new Twig_TemplateWrapper($this, $this->loadTemplate($name));
398
+    }
399
+
400
+    /**
401
+     * Loads a template internal representation.
402
+     *
403
+     * This method is for internal use only and should never be called
404
+     * directly.
382 405
      *
383 406
      * @param string $name  The template name
384 407
      * @param int    $index The index if it is an embedded template
... ...
@@ -387,6 +410,8 @@ class Twig_Environment
387 410
      *
388 411
      * @throws Twig_Error_Loader When the template cannot be found
389 412
      * @throws Twig_Error_Syntax When an error occurred during compilation
413
+     *
414
+     * @internal
390 415
      */
391 416
     public function loadTemplate($name, $index = null)
392 417
     {
... ...
@@ -13,7 +13,13 @@
13 13
 /**
14 14
  * Default base class for compiled templates.
15 15
  *
16
+ * This class is an implementation detail of how template compilation currently
17
+ * works, which might change. It should never be used directly. Use $twig->load()
18
+ * instead, which returns an instance of Twig_TemplateWrapper.
19
+ *
16 20
  * @author Fabien Potencier <fabien@symfony.com>
21
+ *
22
+ * @internal
17 23
  */
18 24
 abstract class Twig_Template implements Twig_TemplateInterface
19 25
 {
... ...
@@ -303,18 +309,34 @@ abstract class Twig_Template implements Twig_TemplateInterface
303 309
     }
304 310
 
305 311
     /**
306
-     * Returns all block names.
312
+     * Returns all block names in the current context of the template.
307 313
      *
308
-     * This method is for internal use only and should never be called
309
-     * directly.
314
+     * This method checks blocks defined in the current template
315
+     * or defined in "used" traits or defined in parent templates.
316
+     *
317
+     * @param string $name    The block name
318
+     * @param array  $context The context
319
+     * @param array  $blocks  The current set of blocks
310 320
      *
311 321
      * @return array An array of block names
312 322
      *
313 323
      * @internal
314 324
      */
315
-    public function getBlockNames()
325
+    public function getBlockNames(array $context = null, array $blocks = array())
316 326
     {
317
-        return array_keys($this->blocks);
327
+        if (null === $context) {
328
+            @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);
329
+
330
+            return array_keys($this->blocks);
331
+        }
332
+
333
+        $names = array_merge(array_keys($blocks), array_keys($this->blocks));
334
+
335
+        if (false !== $parent = $this->getParent($context)) {
336
+            $names = array_merge($names, $parent->getBlockNames($context));
337
+        }
338
+
339
+        return array_unique($names);
318 340
     }
319 341
 
320 342
     protected function loadTemplate($template, $templateName = null, $line = null, $index = null)
... ...
@@ -328,6 +350,10 @@ abstract class Twig_Template implements Twig_TemplateInterface
328 350
                 return $template;
329 351
             }
330 352
 
353
+            if ($template instanceof Twig_TemplateWrapper) {
354
+                return $template;
355
+            }
356
+
331 357
             return $this->env->loadTemplate($template, $index);
332 358
         } catch (Twig_Error $e) {
333 359
             if (!$e->getTemplateName()) {
... ...
@@ -648,9 +674,10 @@ abstract class Twig_Template implements Twig_TemplateInterface
648 674
             throw $e;
649 675
         }
650 676
 
651
-        // useful when calling a template method from a template
652
-        // this is not supported but unfortunately heavily used in the Symfony profiler
677
+        // @deprecated in 1.28
653 678
         if ($object instanceof Twig_TemplateInterface) {
679
+            @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);
680
+
654 681
             return $ret === '' ? '' : new Twig_Markup($ret, $this->env->getCharset());
655 682
         }
656 683
 
657 684
new file mode 100644
... ...
@@ -0,0 +1,134 @@
1
+<?php
2
+
3
+/*
4
+ * This file is part of Twig.
5
+ *
6
+ * (c) 2016 Fabien Potencier
7
+ *
8
+ * For the full copyright and license information, please view the LICENSE
9
+ * file that was distributed with this source code.
10
+ */
11
+
12
+/**
13
+ * Exposes a template to userland.
14
+ *
15
+ * @author Fabien Potencier <fabien@symfony.com>
16
+ */
17
+final class Twig_TemplateWrapper
18
+{
19
+    private $env;
20
+    private $template;
21
+
22
+    /**
23
+     * This method is for internal use only and should never be called
24
+     * directly (use Twig_Environment::load() instead).
25
+     *
26
+     * @internal
27
+     */
28
+    public function __construct(Twig_Environment $env, Twig_Template $template)
29
+    {
30
+        $this->env = $env;
31
+        $this->template = $template;
32
+    }
33
+
34
+    /**
35
+     * Renders the template.
36
+     *
37
+     * @param array $context An array of parameters to pass to the template
38
+     *
39
+     * @return string The rendered template
40
+     */
41
+    public function render($context = array())
42
+    {
43
+        return $this->template->render($context);
44
+    }
45
+
46
+    /**
47
+     * Displays the template.
48
+     *
49
+     * @param array $context An array of parameters to pass to the template
50
+     */
51
+    public function display($context = array())
52
+    {
53
+        return $this->template->display($context);
54
+    }
55
+
56
+    /**
57
+     * Checks if a block is defined.
58
+     *
59
+     * @param string $name    The block name
60
+     * @param array  $context An array of parameters to pass to the template
61
+     *
62
+     * @return bool
63
+     */
64
+    public function hasBlock($name, $context = array())
65
+    {
66
+        return $this->template->hasBlock($name, $context);
67
+    }
68
+
69
+    /**
70
+     * Returns defined block names in the template.
71
+     *
72
+     * @param array $context An array of parameters to pass to the template
73
+     *
74
+     * @return string[] An array of defined template block names
75
+     */
76
+    public function getBlockNames($context = array())
77
+    {
78
+        return $this->template->getBlockNames($context);
79
+    }
80
+
81
+    /**
82
+     * Renders a template block.
83
+     *
84
+     * @param string $name    The block name to render
85
+     * @param array  $context An array of parameters to pass to the template
86
+     *
87
+     * @return string The rendered block
88
+     */
89
+    public function renderBlock($name, $context = array())
90
+    {
91
+        ob_start();
92
+        $this->displayBlock($name, $context);
93
+
94
+        return ob_get_clean();
95
+    }
96
+
97
+    /**
98
+     * Displays a template block.
99
+     *
100
+     * @param string $name    The block name to render
101
+     * @param array  $context An array of parameters to pass to the template
102
+     */
103
+    public function displayBlock($name, $context = array())
104
+    {
105
+        $context = $this->env->mergeGlobals($context);
106
+        $level = ob_get_level();
107
+        ob_start();
108
+        try {
109
+            $this->template->displayBlock($name, $context);
110
+        } catch (Exception $e) {
111
+            while (ob_get_level() > $level) {
112
+                ob_end_clean();
113
+            }
114
+
115
+            throw $e;
116
+        } catch (Throwable $e) {
117
+            while (ob_get_level() > $level) {
118
+                ob_end_clean();
119
+            }
120
+
121
+            throw $e;
122
+        }
123
+
124
+        return ob_get_clean();
125
+    }
126
+
127
+    /**
128
+     * @return Twig_Source
129
+     */
130
+    public function getSourceContext()
131
+    {
132
+        return $this->template->getSourceContext();
133
+    }
134
+}
... ...
@@ -130,6 +130,7 @@ class Twig_Tests_TemplateTest extends PHPUnit_Framework_TestCase
130 130
 
131 131
     /**
132 132
      * @dataProvider getGetAttributeWithTemplateAsObject
133
+     * @group legacy
133 134
      */
134 135
     public function testGetAttributeWithTemplateAsObject($useExt)
135 136
     {
136 137
new file mode 100644
... ...
@@ -0,0 +1,38 @@
1
+<?php
2
+
3
+/*
4
+ * This file is part of Twig.
5
+ *
6
+ * (c) Fabien Potencier
7
+ *
8
+ * For the full copyright and license information, please view the LICENSE
9
+ * file that was distributed with this source code.
10
+ */
11
+class Twig_Tests_TemplateWrapperTest extends PHPUnit_Framework_TestCase
12
+{
13
+    public function testHasGetBlocks()
14
+    {
15
+        $twig = new Twig_Environment(new Twig_Loader_Array(array(
16
+            'index' => '{% block foo %}{% endblock %}',
17
+            'index_with_use' => '{% use "imported" %}{% block foo %}{% endblock %}',
18
+            'index_with_extends' => '{% extends "extended" %}{% block foo %}{% endblock %}',
19
+            'imported' => '{% block imported %}{% endblock %}',
20
+            'extended' => '{% block extended %}{% endblock %}',
21
+        )));
22
+
23
+        $wrapper = new Twig_TemplateWrapper($twig, $twig->loadTemplate('index'));
24
+        $this->assertTrue($wrapper->hasBlock('foo'));
25
+        $this->assertFalse($wrapper->hasBlock('bar'));
26
+        $this->assertEquals(array('foo'), $wrapper->getBlockNames());
27
+
28
+        $wrapper = new Twig_TemplateWrapper($twig, $twig->loadTemplate('index_with_use'));
29
+        $this->assertTrue($wrapper->hasBlock('foo'));
30
+        $this->assertTrue($wrapper->hasBlock('imported'));
31
+        $this->assertEquals(array('imported', 'foo'), $wrapper->getBlockNames());
32
+
33
+        $wrapper = new Twig_TemplateWrapper($twig, $twig->loadTemplate('index_with_extends'));
34
+        $this->assertTrue($wrapper->hasBlock('foo'));
35
+        $this->assertTrue($wrapper->hasBlock('extended'));
36
+        $this->assertEquals(array('foo', 'extended'), $wrapper->getBlockNames());
37
+    }
38
+}