Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the render function, deprecate the include one #4434

Open
wants to merge 1 commit into
base: 3.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# 3.15.0 (2024-XX-XX)


* Deprecate the `include` function, use `render` instead
Copy link
Contributor

@lyrixx lyrixx Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be good to also deprecate the include tag in 3.15.

Yes, Let's deprecate the include tag too

* Add the `render` function to replace the `include` one
* Add Spanish inflector support for the `plural` and `singular` filters in the String extension
* Deprecate `TempNameExpression` in favor of `LocalVariable`
* Deprecate `NameExpression` in favor of `ContextVariable`
Expand Down
5 changes: 5 additions & 0 deletions doc/deprecated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ Functions

Note that it won't be removed in 4.0 to allow a smoother upgrade path.

* The ``include`` function is deprecated as of Twig 3.15, use ``render``
instead. The ``render`` function does the same as ``include``, but the
rendered template does **not** have access to the current context (all
variables must be passed explicitly).

Extensions
----------

Expand Down
5 changes: 5 additions & 0 deletions doc/functions/include.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
``include``
===========

.. warning::

The ``include`` function is deprecated as of Twig 3.15, use the ``render``
function instead.

The ``include`` function returns the rendered content of a template:

.. code-block:: twig
Expand Down
1 change: 1 addition & 0 deletions doc/functions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Functions
html_classes
html_cva
include
render
max
min
parent
Expand Down
91 changes: 91 additions & 0 deletions doc/functions/render.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
``render``
==========

.. versionadded:: 3.15

The ``render`` function was added in Twig 3.15.

The ``render`` function returns the rendered content of a template:

.. code-block:: twig

{{ render('template.html') }}

Templates
---------

Templates are loaded via the current Twig loader.

Templates can be a string or an expression evaluating to a string:

.. code-block:: twig

{{ render('template.html') }}
{{ render(template_var) }}

A template can also be an instance of ``\Twig\Template`` or a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing a Template instead of a TemplateWrapper is deprecated, afaik

``\Twig\TemplateWrapper``::

$template = $twig->load('some_template.twig');
$twig->display('template.twig', ['template' => $template]);

// You can render it like this:
// {{ render(template) }}

When you set the ``ignore_missing`` flag, Twig will return an empty string if
the template does not exist:

.. code-block:: twig

{{ render('sidebar.html', ignore_missing = true) }}

You can also provide a list of templates that are checked for existence. The
first template that exists will be rendered:

.. code-block:: twig

{{ render(['page_detailed.html', 'page.html']) }}

If ``ignore_missing`` is set, it will fall back to rendering nothing if none
of the templates exist, otherwise it will throw an exception.

Context
-------

Rendered templates **do not** have access to the variables defined in the
context, but you can pass some explicitly:

.. code-block:: twig

{# template.html will have access to the "name" variable #}

{{ render('template.html', { name: 'Fabien' }) }}

When passing a variable from the current context, you can use the following
shortcut:

.. code-block:: twig

{{ render('template.html', { first_name, last_name }) }}

{# is equivalent to #}

{{ render('template.html', { first_name: first_name, last_name: last_name }) }}

Sandboxing
----------

When rendering a template created by an end user, you should consider
:doc:`sandboxing<../sandbox>` it:

.. code-block:: twig

{{ render('page.html', sandboxed: true) }}

Arguments
---------

* ``template``: The template to render
* ``variables``: The variables to pass to the template
* ``ignore_missing``: Whether to ignore missing templates or not
* ``sandboxed``: Whether to sandbox the template or not
49 changes: 48 additions & 1 deletion src/Extension/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ public function getFunctions(): array
new TwigFunction('cycle', [self::class, 'cycle']),
new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]),
new TwigFunction('date', [$this, 'convertDate']),
new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.15', 'render')]),
new TwigFunction('render', [self::class, 'render'], ['needs_environment' => true, 'is_safe' => ['all']]),
new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]),
new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]),
new TwigFunction('enum', [self::class, 'enum'], ['node_class' => EnumFunction::class]),
Expand Down Expand Up @@ -1468,6 +1469,52 @@ public static function include(Environment $env, $context, $template, $variables
}
}

/**
* Renders a template.
*
* @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively
* @param array $variables The variables to pass to the template
* @param bool $ignoreMissing Whether to ignore missing templates or not
* @param bool $sandboxed Whether to sandbox the template or not
*
* @internal
*/
public static function render(Environment $env, $template, $variables = [], $ignoreMissing = false, $sandboxed = false): string
{
$alreadySandboxed = false;
$sandbox = null;

if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) {
$sandbox = $env->getExtension(SandboxExtension::class);
if (!$alreadySandboxed = $sandbox->isSandboxed()) {
$sandbox->enableSandbox();
}
}

try {
$loaded = null;
try {
$loaded = $env->resolveTemplate($template);
} catch (LoaderError $e) {
if (!$ignoreMissing) {
throw $e;
}

return '';
}

if ($isSandboxed) {
$loaded->unwrap()->checkSecurity();
}

return $loaded->render($variables);
} finally {
if ($isSandboxed && !$alreadySandboxed) {
$sandbox->disableSandbox();
}
}
}

/**
* Returns a template content without rendering it.
*
Expand Down
1 change: 1 addition & 0 deletions src/NodeVisitor/OptimizerNodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ private function enterOptimizeFor(Node $node): void
}

// include function without the with_context=false parameter
// to be removed in 4.0
elseif ($node instanceof FunctionExpression
&& 'include' === $node->getAttribute('name')
&& (!$node->getNode('arguments')->hasNode('with_context')
Expand Down
10 changes: 5 additions & 5 deletions tests/Extension/CoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -360,13 +360,13 @@ public static function provideCompareCases()
];
}

public function testSandboxedInclude()
public function testSandboxedRender()
{
$twig = new Environment(new ArrayLoader([
'index' => '{{ include("included", sandboxed: true) }}',
'index' => '{{ render("included", sandboxed: true) }}',
'included' => '{{ "included"|e }}',
]));
$policy = new SecurityPolicy(allowedFunctions: ['include']);
$policy = new SecurityPolicy(allowedFunctions: ['render']);
$sandbox = new SandboxExtension($policy, false);
$twig->addExtension($sandbox);

Expand All @@ -378,10 +378,10 @@ public function testSandboxedInclude()
public function testSandboxedIncludeWithPreloadedTemplate()
{
$twig = new Environment(new ArrayLoader([
'index' => '{{ include("included", sandboxed: true) }}',
'index' => '{{ render("included", sandboxed: true) }}',
'included' => '{{ "included"|e }}',
]));
$policy = new SecurityPolicy(allowedFunctions: ['include']);
$policy = new SecurityPolicy(allowedFunctions: ['render']);
$sandbox = new SandboxExtension($policy, false);
$twig->addExtension($sandbox);

Expand Down
24 changes: 12 additions & 12 deletions tests/Extension/SandboxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,18 @@ protected function setUp(): void
'1_basic3' => '{% if name %}foo{% endif %}',
'1_basic4' => '{{ obj.bar }}',
'1_basic5' => '{{ obj }}',
'1_basic7' => '{{ cycle(["foo","bar"], 1) }}',
'1_basic7' => '{{ cycle(["foo", "bar"], 1) }}',
'1_basic8' => '{{ obj.getfoobar }}{{ obj.getFooBar }}',
'1_basic9' => '{{ obj.foobar }}{{ obj.fooBar }}',
'1_basic' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}',
'1_layout' => '{% block content %}{% endblock %}',
'1_child' => "{% extends \"1_layout\" %}\n{% block content %}\n{{ \"a\"|json_encode }}\n{% endblock %}",
'1_include' => '{{ include("1_basic1", sandboxed=true) }}',
'1_basic2_include_template_from_string_sandboxed' => '{{ include(template_from_string("{{ name|upper }}"), sandboxed=true) }}',
'1_basic2_include_template_from_string' => '{{ include(template_from_string("{{ name|upper }}")) }}',
'1_render' => '{{ render("1_basic1", { obj }, sandboxed: true) }}',
'1_basic2_include_template_from_string_sandboxed' => '{{ render(template_from_string("{{ name|upper }}"), sandboxed=true) }}',
'1_basic2_include_template_from_string' => '{{ render(template_from_string("{{ name|upper }}"), { name }) }}',
'1_range_operator' => '{{ (1..2)[0] }}',
'1_syntax_error_wrapper_legacy' => '{% sandbox %}{% include "1_syntax_error" %}{% endsandbox %}',
'1_syntax_error_wrapper' => '{{ include("1_syntax_error", sandboxed: true) }}',
'1_syntax_error_wrapper' => '{{ render("1_syntax_error", sandboxed: true) }}',
'1_syntax_error' => '{% syntax error }}',
'1_childobj_parentmethod' => '{{ child_obj.ParentMethod() }}',
'1_childobj_childmethod' => '{{ child_obj.ChildMethod() }}',
Expand Down Expand Up @@ -193,7 +193,7 @@ public function testSandboxGloballyFalseUnallowedFilterWithIncludeTemplateFromSt

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

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

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

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

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

$e = null;
try {
$twig->load('1_include')->render(self::$params);
$twig->load('1_render')->render(self::$params);
} catch (\Throwable $e) {
}
if (null === $e) {
$this->fail('An exception should be thrown for this test to be valid.');
}

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

public function testSandboxWithNoClosureFilter()
Expand Down
4 changes: 2 additions & 2 deletions tests/Fixtures/autoescape/block.test
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
--TEST--
blocks and autoescape
--TEMPLATE--
{{ include('unrelated.txt.twig') -}}
{{ include('template.html.twig') -}}
{{ render('unrelated.txt.twig') -}}
{{ render('template.html.twig', { br }) -}}
--TEMPLATE(unrelated.txt.twig)--
{% block content %}{% endblock %}
--TEMPLATE(template.html.twig)--
Expand Down
6 changes: 3 additions & 3 deletions tests/Fixtures/autoescape/name.test
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"name" autoescape strategy
--TEMPLATE--
{{ br -}}
{{ include('index.js.twig') -}}
{{ include('index.html.twig') -}}
{{ include('index.txt.twig') -}}
{{ render('index.js.twig', { br }) -}}
{{ render('index.html.twig', { br }) -}}
{{ render('index.txt.twig', { br }) -}}
--TEMPLATE(index.js.twig)--
{{ br -}}
--TEMPLATE(index.html.twig)--
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
--TEST--
Exception for multile function with undefined variable
Exception for multi-line function with undefined variable
--TEMPLATE--
{{ include('foo',
with_context=with_context
{{ render('foo',
sandboxed=with_sandbox
) }}
--TEMPLATE(foo)--
Foo
--DATA--
return []
--EXCEPTION--
Twig\Error\RuntimeError: Variable "with_context" does not exist in "index.twig" at line 3.
Twig\Error\RuntimeError: Variable "with_sandbox" does not exist in "index.twig" at line 3.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Exception for an undefined template in a child template
{% extends 'base.twig' %}

{% block sidebar %}
{{ include('include.twig') }}
{{ render('include.twig') }}
{% endblock %}
--TEMPLATE(base.twig)--
{% block sidebar %}
Expand Down
15 changes: 15 additions & 0 deletions tests/Fixtures/functions/include/assignment.legacy.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
"include" function
--DEPRECATION--
Since twig/twig 3.15: Twig Function "include" is deprecated; use "render" instead in index.twig at line 2.
--TEMPLATE--
{% set tmp = include("foo.twig") %}

FOO{{ tmp }}BAR
--TEMPLATE(foo.twig)--
FOOBAR
--DATA--
return []
--EXPECT--
FOO
FOOBARBAR
12 changes: 12 additions & 0 deletions tests/Fixtures/functions/include/autoescaping.legacy.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
"include" function is safe for auto-escaping
--DEPRECATION--
Since twig/twig 3.15: Twig Function "include" is deprecated; use "render" instead in index.twig at line 2.
--TEMPLATE--
{{ include("foo.twig") }}
--TEMPLATE(foo.twig)--
<p>Test</p>
--DATA--
return []
--EXPECT--
<p>Test</p>
Loading
Loading