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,5 +1,6 @@
# 3.27.2 (2026-XX-XX)

* Add the `include_only` function to render a template without giving it access to the current context
* Fix nested `block()` calls to resolve against the overriding template when a block rendered through `block(name, template)` calls `parent()`
* Stop reporting a skipped test in `IntegrationTestCase` when there is no legacy test to run
* Make the `IntegrationTestCase` and `NodeTestCase` test helpers compatible with PHPUnit 11
Expand Down
8 changes: 8 additions & 0 deletions doc/functions/include.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ HTML).

Included templates have access to the variables of the active context.

.. tip::

Prefer the :doc:`include_only<include_only>` function 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.

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

.. versionadded:: 3.27.2

The ``include_only`` function was added in Twig 3.27.2.

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) }}

It behaves like the :doc:`include<include>` function, except that the rendered
template does **not** have access to the variables of the active context. This
makes the data a template relies on explicit, which is often clearer and
easier to reason about.

If you are using the filesystem loader, the templates are looked for in the
paths defined by it.

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}) }}

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

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

.. code-block:: twig

{{ include_only('page.html.twig', 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
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
16 changes: 16 additions & 0 deletions src/Extension/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,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 @@ -1531,6 +1532,21 @@ public static function include(Environment $env, $context, $template, $variables
}
}

/**
* Renders a template without giving it access to the current context.
*
* @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 includeOnly(Environment $env, $template, $variables = [], $ignoreMissing = false, $sandboxed = false): string
{
return self::include($env, [], $template, $variables, false, $ignoreMissing, $sandboxed);
}

/**
* Returns a template content without rendering it.
*
Expand Down
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]
13 changes: 13 additions & 0 deletions tests/Fixtures/functions/include_only/sandbox.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
"include_only" function sandboxed
--TEMPLATE--
{{ include_only("foo.twig", sandboxed = true) }}
--TEMPLATE(foo.twig)--


{{ 'foo'|e }}
{{ 'foo'|e }}
--DATA--
return []
--EXCEPTION--
Twig\Sandbox\SecurityNotAllowedFilterError: Filter "e" is not allowed in "foo.twig" at line 4.
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