Skip to content

Commit cfd3254

Browse files
committed
Add the render function, deprecate the include one
1 parent b99d73c commit cfd3254

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+461
-90
lines changed

CHANGELOG

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# 3.15.0 (2024-XX-XX)
2-
2+
3+
* Deprecate the `include` function, use `render` instead
4+
* Add the `render` function to replace the `include` one
35
* Add Spanish inflector support for the `plural` and `singular` filters in the String extension
46
* Deprecate `TempNameExpression` in favor of `LocalVariable`
57
* Deprecate `NameExpression` in favor of `ContextVariable`

doc/deprecated.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ Functions
2828
2929
Note that it won't be removed in 4.0 to allow a smoother upgrade path.
3030

31+
* The ``include`` function is deprecated as of Twig 3.15, use ``render``
32+
instead. The ``render`` function does the same as ``include``, but the
33+
rendered template never has access to the current context (all variables must
34+
be passed explicitely).
35+
3136
Extensions
3237
----------
3338

doc/functions/include.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
``include``
22
===========
33

4+
.. warning::
5+
6+
The ``include`` function is deprecated as of Twig 3.15, use the ``render``
7+
function instead.
8+
49
The ``include`` function returns the rendered content of a template:
510

611
.. code-block:: twig

doc/functions/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Functions
1515
html_classes
1616
html_cva
1717
include
18+
render
1819
max
1920
min
2021
parent

doc/functions/render.rst

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
``render``
2+
==========
3+
4+
.. versionadded:: 3.15
5+
6+
The ``render`` function was added in Twig 3.15.
7+
8+
The ``render`` function returns the rendered content of a template:
9+
10+
.. code-block:: twig
11+
12+
{{ render('template.html') }}
13+
14+
Templates
15+
---------
16+
17+
Templates are loaded via the current Twig loader.
18+
19+
Templates can be a string or an expression evaluating to a string:
20+
21+
.. code-block:: twig
22+
23+
{{ render('template.html') }}
24+
{{ render(template_var) }}
25+
26+
A template can also be an instance of ``\Twig\Template`` or a
27+
``\Twig\TemplateWrapper``::
28+
29+
$template = $twig->load('some_template.twig');
30+
$twig->display('template.twig', ['template' => $template]);
31+
32+
// You can render it like this:
33+
// {{ render(template) }}
34+
35+
When you set the ``ignore_missing`` flag, Twig will return an empty string if
36+
the template does not exist:
37+
38+
.. code-block:: twig
39+
40+
{{ render('sidebar.html', ignore_missing = true) }}
41+
42+
You can also provide a list of templates that are checked for existence. The
43+
first template that exists will be rendered:
44+
45+
.. code-block:: twig
46+
47+
{{ render(['page_detailed.html', 'page.html']) }}
48+
49+
If ``ignore_missing`` is set, it will fall back to rendering nothing if none
50+
of the templates exist, otherwise it will throw an exception.
51+
52+
Variables
53+
---------
54+
55+
Rendered templates **do not** have access to the variables defined in the
56+
context, but you can pass variables explicitely:
57+
58+
.. code-block:: twig
59+
60+
{# template.html will have access to the "name" variable #}
61+
62+
{{ render('template.html', { name: 'Fabien' }) }}
63+
64+
You can pass existing variables from the current context:
65+
66+
.. code-block:: twig
67+
68+
{{ render('template.html', { first_name: first_name, last_name: last_name }) }}
69+
70+
{# or using the following shortcut #}
71+
72+
{{ render('template.html', { first_name, last_name }) }}
73+
74+
Sandboxing
75+
----------
76+
77+
When rendering a template created by an end user, you should consider
78+
:doc:`sandboxing<../sandbox>` it:
79+
80+
.. code-block:: twig
81+
82+
{{ render('page.html', sandboxed: true) }}
83+
84+
Arguments
85+
---------
86+
87+
* ``template``: The template to render
88+
* ``variables``: The variables to pass to the template
89+
* ``ignore_missing``: Whether to ignore missing templates or not
90+
* ``sandboxed``: Whether to sandbox the template or not

src/Extension/CoreExtension.php

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,8 @@ public function getFunctions(): array
265265
new TwigFunction('cycle', [self::class, 'cycle']),
266266
new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]),
267267
new TwigFunction('date', [$this, 'convertDate']),
268-
new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
268+
new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.15', 'render')]),
269+
new TwigFunction('render', [self::class, 'render'], ['needs_environment' => true, 'is_safe' => ['all']]),
269270
new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]),
270271
new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]),
271272
new TwigFunction('enum', [self::class, 'enum'], ['node_class' => EnumFunction::class]),
@@ -1468,6 +1469,52 @@ public static function include(Environment $env, $context, $template, $variables
14681469
}
14691470
}
14701471

1472+
/**
1473+
* Renders a template.
1474+
*
1475+
* @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively
1476+
* @param array $variables The variables to pass to the template
1477+
* @param bool $ignoreMissing Whether to ignore missing templates or not
1478+
* @param bool $sandboxed Whether to sandbox the template or not
1479+
*
1480+
* @internal
1481+
*/
1482+
public static function render(Environment $env, $template, $variables = [], $ignoreMissing = false, $sandboxed = false): string
1483+
{
1484+
$alreadySandboxed = false;
1485+
$sandbox = null;
1486+
1487+
if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) {
1488+
$sandbox = $env->getExtension(SandboxExtension::class);
1489+
if (!$alreadySandboxed = $sandbox->isSandboxed()) {
1490+
$sandbox->enableSandbox();
1491+
}
1492+
}
1493+
1494+
try {
1495+
$loaded = null;
1496+
try {
1497+
$loaded = $env->resolveTemplate($template);
1498+
} catch (LoaderError $e) {
1499+
if (!$ignoreMissing) {
1500+
throw $e;
1501+
}
1502+
1503+
return '';
1504+
}
1505+
1506+
if ($isSandboxed) {
1507+
$loaded->unwrap()->checkSecurity();
1508+
}
1509+
1510+
return $loaded->render($variables);
1511+
} finally {
1512+
if ($isSandboxed && !$alreadySandboxed) {
1513+
$sandbox->disableSandbox();
1514+
}
1515+
}
1516+
}
1517+
14711518
/**
14721519
* Returns a template content without rendering it.
14731520
*

src/NodeVisitor/OptimizerNodeVisitor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ private function enterOptimizeFor(Node $node): void
158158
}
159159

160160
// include function without the with_context=false parameter
161+
// to be removed in 4.0
161162
elseif ($node instanceof FunctionExpression
162163
&& 'include' === $node->getAttribute('name')
163164
&& (!$node->getNode('arguments')->hasNode('with_context')

tests/Extension/CoreTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -360,13 +360,13 @@ public static function provideCompareCases()
360360
];
361361
}
362362

363-
public function testSandboxedInclude()
363+
public function testSandboxedRender()
364364
{
365365
$twig = new Environment(new ArrayLoader([
366-
'index' => '{{ include("included", sandboxed: true) }}',
366+
'index' => '{{ render("included", sandboxed: true) }}',
367367
'included' => '{{ "included"|e }}',
368368
]));
369-
$policy = new SecurityPolicy(allowedFunctions: ['include']);
369+
$policy = new SecurityPolicy(allowedFunctions: ['render']);
370370
$sandbox = new SandboxExtension($policy, false);
371371
$twig->addExtension($sandbox);
372372

@@ -378,10 +378,10 @@ public function testSandboxedInclude()
378378
public function testSandboxedIncludeWithPreloadedTemplate()
379379
{
380380
$twig = new Environment(new ArrayLoader([
381-
'index' => '{{ include("included", sandboxed: true) }}',
381+
'index' => '{{ render("included", sandboxed: true) }}',
382382
'included' => '{{ "included"|e }}',
383383
]));
384-
$policy = new SecurityPolicy(allowedFunctions: ['include']);
384+
$policy = new SecurityPolicy(allowedFunctions: ['render']);
385385
$sandbox = new SandboxExtension($policy, false);
386386
$twig->addExtension($sandbox);
387387

tests/Extension/SandboxTest.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,18 @@ protected function setUp(): void
5050
'1_basic3' => '{% if name %}foo{% endif %}',
5151
'1_basic4' => '{{ obj.bar }}',
5252
'1_basic5' => '{{ obj }}',
53-
'1_basic7' => '{{ cycle(["foo","bar"], 1) }}',
53+
'1_basic7' => '{{ cycle(["foo", "bar"], 1) }}',
5454
'1_basic8' => '{{ obj.getfoobar }}{{ obj.getFooBar }}',
5555
'1_basic9' => '{{ obj.foobar }}{{ obj.fooBar }}',
5656
'1_basic' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}',
5757
'1_layout' => '{% block content %}{% endblock %}',
5858
'1_child' => "{% extends \"1_layout\" %}\n{% block content %}\n{{ \"a\"|json_encode }}\n{% endblock %}",
59-
'1_include' => '{{ include("1_basic1", sandboxed=true) }}',
60-
'1_basic2_include_template_from_string_sandboxed' => '{{ include(template_from_string("{{ name|upper }}"), sandboxed=true) }}',
61-
'1_basic2_include_template_from_string' => '{{ include(template_from_string("{{ name|upper }}")) }}',
59+
'1_render' => '{{ render("1_basic1", { obj }, sandboxed: true) }}',
60+
'1_basic2_include_template_from_string_sandboxed' => '{{ render(template_from_string("{{ name|upper }}"), sandboxed=true) }}',
61+
'1_basic2_include_template_from_string' => '{{ render(template_from_string("{{ name|upper }}"), { name }) }}',
6262
'1_range_operator' => '{{ (1..2)[0] }}',
6363
'1_syntax_error_wrapper_legacy' => '{% sandbox %}{% include "1_syntax_error" %}{% endsandbox %}',
64-
'1_syntax_error_wrapper' => '{{ include("1_syntax_error", sandboxed: true) }}',
64+
'1_syntax_error_wrapper' => '{{ render("1_syntax_error", sandboxed: true) }}',
6565
'1_syntax_error' => '{% syntax error }}',
6666
'1_childobj_parentmethod' => '{{ child_obj.ParentMethod() }}',
6767
'1_childobj_childmethod' => '{{ child_obj.ChildMethod() }}',
@@ -193,7 +193,7 @@ public function testSandboxGloballyFalseUnallowedFilterWithIncludeTemplateFromSt
193193

194194
public function testSandboxGloballyTrueUnallowedFilterWithIncludeTemplateFromStringSandboxed()
195195
{
196-
$twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], ['include', 'template_from_string']);
196+
$twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], ['render', 'template_from_string']);
197197
$twig->addExtension(new StringLoaderExtension());
198198
try {
199199
$twig->load('1_basic2_include_template_from_string_sandboxed')->render(self::$params);
@@ -212,7 +212,7 @@ public function testSandboxGloballyFalseUnallowedFilterWithIncludeTemplateFromSt
212212

213213
public function testSandboxGloballyTrueUnallowedFilterWithIncludeTemplateFromStringNotSandboxed()
214214
{
215-
$twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], ['include', 'template_from_string']);
215+
$twig = $this->getEnvironment(true, [], self::$templates, [], [], [], [], ['render', 'template_from_string']);
216216
$twig->addExtension(new StringLoaderExtension());
217217
try {
218218
$twig->load('1_basic2_include_template_from_string')->render(self::$params);
@@ -414,11 +414,11 @@ public function testSandboxLocallySetForAnInclude()
414414
$this->assertEquals('fooFOOfoo', $twig->load('2_basic')->render(self::$params), 'Sandbox does nothing if disabled globally and sandboxed not used for the include');
415415

416416
self::$templates = [
417-
'3_basic' => '{{ include("3_included", sandboxed: true) }}',
417+
'3_basic' => '{{ render("3_included", sandboxed: true) }}',
418418
'3_included' => '{% if true %}{{ "foo"|upper }}{% endif %}',
419419
];
420420

421-
$twig = $this->getEnvironment(true, [], self::$templates, functions: ['include']);
421+
$twig = $this->getEnvironment(true, [], self::$templates, functions: ['render']);
422422
try {
423423
$twig->load('3_basic')->render(self::$params);
424424
$this->fail('Sandbox throws a SecurityError exception when the included file is sandboxed');
@@ -441,20 +441,20 @@ public function testMacrosInASandbox()
441441
$this->assertEquals('<p>username</p>', $twig->load('index')->render([]));
442442
}
443443

444-
public function testSandboxDisabledAfterIncludeFunctionError()
444+
public function testSandboxDisabledAfterRenderFunctionError()
445445
{
446446
$twig = $this->getEnvironment(false, [], self::$templates);
447447

448448
$e = null;
449449
try {
450-
$twig->load('1_include')->render(self::$params);
450+
$twig->load('1_render')->render(self::$params);
451451
} catch (\Throwable $e) {
452452
}
453453
if (null === $e) {
454454
$this->fail('An exception should be thrown for this test to be valid.');
455455
}
456456

457-
$this->assertFalse($twig->getExtension(SandboxExtension::class)->isSandboxed(), 'Sandboxed include() function call should not leave Sandbox enabled when an error occurs.');
457+
$this->assertFalse($twig->getExtension(SandboxExtension::class)->isSandboxed(), 'Sandboxed render() function call should not leave Sandbox enabled when an error occurs.');
458458
}
459459

460460
public function testSandboxWithNoClosureFilter()

tests/Fixtures/autoescape/block.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
--TEST--
22
blocks and autoescape
33
--TEMPLATE--
4-
{{ include('unrelated.txt.twig') -}}
5-
{{ include('template.html.twig') -}}
4+
{{ render('unrelated.txt.twig') -}}
5+
{{ render('template.html.twig', { br }) -}}
66
--TEMPLATE(unrelated.txt.twig)--
77
{% block content %}{% endblock %}
88
--TEMPLATE(template.html.twig)--

0 commit comments

Comments
 (0)