Add the include_only function to render a template without access to the current context

This commit is contained in:
Fabien Potencier
2026-06-04 21:50:17 +02:00
parent 6fafa104c4
commit dc8f96df3e
10 changed files with 187 additions and 48 deletions
+1
View File
@@ -1,6 +1,7 @@
# 3.29.0 (2026-XX-XX)
* Fix imported macros not resolving their own template-level macro imports
* Add the `include_only` function to render a template without giving it access to the current context
* Add the `Twig\Sandbox\SandboxInterface` interface and `Twig\Sandbox\Sandbox` class to render untrusted templates through a dedicated, always-sandboxed environment crafted for it
* Add the `Twig\Extension\SandboxBridgeExtension` to render sandboxed templates from trusted templates with an explicit output escaping strategy
* Extract the sandbox runtime enforcement into a new internal `Twig\Sandbox\SecurityChecker` class used by compiled templates and `CoreExtension`
+13 -48
View File
@@ -8,29 +8,25 @@ The ``include`` function returns the rendered content of a template:
{{ include('template.html.twig') }}
{{ include(some_var) }}
The returned content is a ``\Twig\Markup`` instance, so it is considered safe
and is not escaped again when you store it in a variable and print it later:
.. code-block:: twig
{% set body = include('body.html.twig') %}
{{ body }} {# rendered as-is, not re-escaped #}
Beware that, like any safe value, it is not re-escaped for the context it ends
up in, so only embed it in the same context it was rendered for (typically
HTML).
Included templates have access to the variables of the active context.
If you are using the filesystem loader, the templates are looked for in the
paths defined by it.
.. tip::
The context is passed by default to the template but you can also pass
Prefer the :doc:`include_only() function </functions/include_only>` when
you can. Sharing the whole context lets a template silently rely on
variables defined by the caller, which hides its real inputs and couples it
to wherever it is included from. ``include_only`` takes only the variables
you pass, making the data flow explicit and partials easier to reuse.
Its documentation also covers the template loading, ``ignore_missing`` and
return-value behavior shared by both functions.
The current context is passed by default to the template but you can also pass
additional variables:
.. code-block:: twig
{# template.html.twig will have access to the variables from the current context and the additional ones provided #}
{# The included template can access "name" and the current context. #}
{{ include('template.html.twig', {name: 'Fabien'}) }}
You can disable access to the context by setting ``with_context`` to
@@ -38,40 +34,9 @@ You can disable access to the context by setting ``with_context`` to
.. code-block:: twig
{# only the name variable will be accessible #}
{# Only the "name" variable will be accessible. #}
{{ include('template.html.twig', {name: 'Fabien'}, with_context: false) }}
.. code-block:: twig
{# no variables will be accessible #}
{{ include('template.html.twig', with_context: false) }}
And if the expression evaluates to a ``\Twig\Template`` or a
``\Twig\TemplateWrapper`` instance, Twig will use it directly::
// {{ include(template) }}
$template = $twig->load('some_template.html.twig');
$twig->display('template.html.twig', ['template' => $template]);
When you set the ``ignore_missing`` flag, Twig will return an empty string if
the template does not exist:
.. code-block:: twig
{{ include('sidebar.html.twig', ignore_missing: true) }}
You can also provide a list of templates that are checked for existence before
inclusion. The first template that exists will be rendered:
.. code-block:: twig
{{ include(['page_detailed.html.twig', 'page.html.twig']) }}
If ``ignore_missing`` is set, it will fall back to rendering nothing if none
of the templates exist, otherwise it will throw an exception.
When including a template created by an end user, you should
:doc:`sandbox<../sandbox>` it.
+97
View File
@@ -0,0 +1,97 @@
``include_only``
================
.. versionadded:: 3.29
The ``include_only`` function was added in Twig 3.29.
The ``include_only`` function returns the rendered content of a template
without giving it access to the current context:
.. code-block:: twig
{{ include_only('template.html.twig') }}
{{ include_only(some_var) }}
Variables from the active context are not passed implicitly. This makes the
data a template relies on explicit, which is often clearer and easier to
reason about.
Returned Value
--------------
The returned content is a ``\Twig\Markup`` instance, so it is considered safe
and is not escaped again when you store it in a variable and print it later:
.. code-block:: twig
{% set body = include_only('body.html.twig') %}
{{ body }} {# rendered as-is, not re-escaped #}
Beware that, like any safe value, it is not re-escaped for the context it ends
up in, so only embed it in the same context it was rendered for (typically
HTML).
Passing Variables
-----------------
As the context is not passed, variables a template needs must be passed
explicitly:
.. code-block:: twig
{# template.html.twig will only have access to the "name" variable #}
{{ include_only('template.html.twig', {name: 'Fabien'}) }}
When passing a variable from the current context, you can use the following
shortcut:
.. code-block:: twig
{{ include_only('template.html.twig', {name, email}) }}
{# is equivalent to #}
{{ include_only('template.html.twig', {name: name, email: email}) }}
Loading Templates
-----------------
If you are using the filesystem loader, the templates are looked for in the
paths defined by it.
If the expression evaluates to a ``\Twig\TemplateWrapper`` instance, Twig
will use it directly::
// {{ include_only(template) }}
$template = $twig->load('some_template.html.twig');
$twig->display('template.html.twig', ['template' => $template]);
When you set the ``ignore_missing`` flag, Twig will return an empty string if
the template does not exist:
.. code-block:: twig
{{ include_only('sidebar.html.twig', ignore_missing: true) }}
You can also provide a list of templates that are checked for existence before
inclusion. The first template that exists will be rendered:
.. code-block:: twig
{{ include_only(['page_detailed.html.twig', 'page.html.twig']) }}
If ``ignore_missing`` is set, it will fall back to rendering nothing if none
of the templates exist, otherwise it will throw an exception.
To render a template created by an end user, use the
:doc:`render_sandboxed() function </functions/render_sandboxed>`.
Arguments
---------
* ``template``: The template to render
* ``variables``: The variables to pass to the template
* ``ignore_missing``: Whether to ignore missing templates or not
+1
View File
@@ -16,6 +16,7 @@ Functions
html_classes
html_cva
include
include_only
max
min
parent
+17
View File
@@ -303,6 +303,7 @@ final class CoreExtension extends AbstractExtension
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_only', [self::class, 'includeOnly'], ['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]),
@@ -1532,6 +1533,22 @@ final class CoreExtension extends AbstractExtension
}
}
/**
* Renders a template without giving it access to the current context.
*
* @param string|array<string|TemplateWrapper>|TemplateWrapper $template The template to render or an array of templates to try consecutively
* @param array<string, mixed> $variables The variables to pass to the template
* @param bool $ignoreMissing Whether to ignore missing templates or not
*
* @return string|Markup
*
* @internal
*/
public static function includeOnly(Environment $env, $template, array $variables = [], bool $ignoreMissing = false)
{
return self::include($env, [], $template, $variables, false, $ignoreMissing);
}
/**
* Returns a template content without rendering it.
*
@@ -0,0 +1,10 @@
--TEST--
"include_only" function returns Markup so an assigned result is not re-escaped
--TEMPLATE--
{% set assigned = include_only("included.twig") %}[{{ assigned }}]
--TEMPLATE(included.twig)--
{{- "a & b"|escape -}}
--DATA--
return []
--EXPECT--
[a &amp; b]
@@ -0,0 +1,17 @@
--TEST--
"include_only" function
--TEMPLATE--
FOO
{{ include_only("foo.twig") }}
BAR
--TEMPLATE(foo.twig)--
FOOBAR
--DATA--
return []
--EXPECT--
FOO
FOOBAR
BAR
@@ -0,0 +1,9 @@
--TEST--
"include_only" function ignores missing templates
--TEMPLATE--
{{ include_only(["foo.twig", "bar.twig"], ignore_missing = true) }}
{{ include_only("foo.twig", ignore_missing = true) }}
{{ include_only("foo.twig", ignore_missing = true, variables = {}) }}
--DATA--
return []
--EXPECT--
@@ -0,0 +1,10 @@
--TEST--
"include_only" function does not give access to the current context
--TEMPLATE--
{{ include_only("foo.twig") }}
--TEMPLATE(foo.twig)--
[{{ foo|default('undefined') }}]
--DATA--
return ['foo' => 'bar']
--EXPECT--
[undefined]
@@ -0,0 +1,12 @@
--TEST--
"include_only" function accepts variables
--TEMPLATE--
{{ include_only("foo.twig", {'foo': 'bar'}) }}
{{- include_only("foo.twig", vars) }}
--TEMPLATE(foo.twig)--
{{ foo }}
--DATA--
return ['vars' => ['foo' => 'bar']]
--EXPECT--
bar
bar