Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
59 changes: 12 additions & 47 deletions doc/functions/include.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,70 +8,35 @@ 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 #}
Included templates have access to the variables of the active context.

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).
.. tip::

Included templates have access to the variables of the active context.
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.

If you are using the filesystem loader, the templates are looked for in the
paths defined by it.
Its documentation also covers the template loading, ``ignore_missing`` and
return-value behavior shared by both functions.

The context is passed by default to the template but you can also pass
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
``false``:

.. 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.

Expand Down
97 changes: 97 additions & 0 deletions doc/functions/include_only.rst
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions doc/functions/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Functions
html_classes
html_cva
include
include_only
max
min
parent
Expand Down
17 changes: 17 additions & 0 deletions src/Extension/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ public function getFunctions(): array
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]),
Expand Down Expand Up @@ -1532,6 +1533,22 @@ public static function include(Environment $env, $context, $template, $variables
}
}

/**
* 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.
*
Expand Down
10 changes: 10 additions & 0 deletions tests/Fixtures/functions/include_only/assignment_autoescaping.test
Original file line number Diff line number Diff line change
@@ -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]
17 changes: 17 additions & 0 deletions tests/Fixtures/functions/include_only/basic.test
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions tests/Fixtures/functions/include_only/ignore_missing.test
Original file line number Diff line number Diff line change
@@ -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--
10 changes: 10 additions & 0 deletions tests/Fixtures/functions/include_only/no_context.test
Original file line number Diff line number Diff line change
@@ -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]
12 changes: 12 additions & 0 deletions tests/Fixtures/functions/include_only/with_variables.test
Original file line number Diff line number Diff line change
@@ -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
Loading