Twig 3.28.0 is out. This release sharpens error reporting with column numbers, brings back dynamic macro calls through the dot operator, and continues to polish the sandbox with less runtime overhead and finer-grained allow-listing. As usual, it also ships a batch of deprecations that pave the way for Twig 4.0.

Column numbers in syntax errors

Contributed by Fabien Potencier in #4834

Until now, a Twig error only told you which line was at fault. On a dense line, that left you scanning for the actual problem. Twig now tracks the source offset of every token, so errors can point at the exact column.

The column is exposed through the new Error::getTemplateColumn() method, and the underlying token offset through Token::getOffset().

Dynamic macro names through the dot operator

Contributed by Fabien Potencier in #4838

Calling a macro whose name is only known at runtime used to rely on the attribute() function, which was deprecated in favor of the dot operator; but the dot operator did not support macros. That gap is now closed:

{% import "forms.html.twig" as forms %}

{% set field = "text" %}

{{ forms.(field)(name, value) }}

The parenthesized expression is evaluated and used as the macro name, so you can pick a macro dynamically without falling back to a deprecated function.

Sandbox: mark callables and tags as always allowed

Contributed by Fabien Potencier in #4816 and #4819

Some filters, functions, tests, and tags are pure and inherently safe in a sandboxed template. Forcing every security policy to allow-list them by hand is noisy and error-prone.

Filters, functions, and tests now accept an always_allowed_in_sandbox option, and token parsers can implement isAlwaysAllowedInSandbox(). When set, the sandbox skips recording the name entirely, so it never reaches the policy's security check: no allow-list entry required and no runtime cost. The security policy also gains a dedicated allow-list for tests, with the safe built-in tests flagged as always allowed so they keep working out of the box.

Faster sandboxed templates

Contributed by Fabien Potencier in #4823

The sandbox used to wrap every argument of every callable with a __toString check. Twig now wraps an argument only when its PHP parameter type can actually coerce a value to a string. Arguments typed as int, for instance, are left untouched, which cuts the amount of generated code and speeds up sandboxed rendering. The approach stays conservative: untyped, mixed, string, array, iterable, object, Stringable, and unknown class types all keep their check.

Stricter template correctness checks

Contributed by Fabien Potencier in #4292

Being able to parse and compile a template does not mean it is semantically correct. A new CorrectnessNodeVisitor centralizes those checks and deprecates a few constructs that happened to work but were never meant to: using a block tag inside a capture node such as set, and using a macro, extends, or use tag somewhere other than the root of a template.

Markup is now effectively final

Contributed by Fabien Potencier in #4840

Twig\Markup is now marked @final and will become truly final in Twig 4.0.

Notable bug fixes

  • The include() function now returns a Markup object, so a result assigned

to a variable is no longer re-escaped when printed. Nested block() calls now resolve against the overriding template when a block rendered through block(name, template) calls parent(). Printed expressions are cast to string so values that cannot be converted (arrays, non-Stringable objects) report a usable stack trace at the print location. Backed enums are now rendered using their backing value in the html_attr function. Empty Markup values are no longer treated as truthy in boolean expressions.

Full Changelog

  • #4850 Render backed enums using their backing value in the html_attr function (@fabpot)
  • #4841 Fix Markup truthiness in boolean expressions (@xtrime-ru)
  • #4842 Fix a PHP 8.5 chr() deprecation when decoding octal string escapes (@austinderrick)
  • #4292 Introduce a CorrectnessNodeVisitor to validate that templates are semantically correct (@fabpot)
  • #4840 Mark Markup as final (@fabpot)
  • #4838 Allow calling a macro with a dynamic name via the dot operator (@fabpot)
  • #4835 Fix markdown_to_html mangling content that starts with a blank line (@fabpot)
  • #4819 Add an allow-list for tests to the sandbox security policy (@fabpot)
  • #4816 Add an always_allowed_in_sandbox flag for filters, functions, and tags (@fabpot)
  • #4834 Track the source offset of each token and expose it in syntax errors (@fabpot)
  • #4825 Make the include() function return a Markup object (@fabpot)
  • #4830 Fix nested block() resolution when a directly rendered block calls parent() (@fabpot)
  • #4828 Stop reporting a skipped test in IntegrationTestCase when there is no legacy test to run (@fabpot)
  • #4824 Cast printed expressions to string so unconvertible values report a usable stack trace at the print location (@stof, @fabpot)
  • #4826 Make IntegrationTestCase and NodeTestCase compatible with PHPUnit 11 (@fabpot)
  • #4823 Skip the sandbox __toString check on arguments whose PHP parameter type cannot implicitly coerce to string (@fabpot)
Sponsor the Symfony project.