The name passed to MacroReferenceExpression is emitted as raw PHP in
compile() via "->{$name}(...)". Callers were expected to validate
the name, but a missing check led to CVE-2026-XXXXX (PHP code injection
via _self / import macro reference): defense-in-depth, validate the
name in the constructor so the class is safe by construction.
This is a defense-in-depth measure: callers must always concatenate the
result into a double-quoted PHP context, but if one ever (mistakenly)
embeds it inside a single-quoted PHP literal, an attacker-controlled
single quote in the source value could break out of that context. The
previous commit fixed exactly such a bug in ModuleNode for the {% use %}
template name.
Encoding ' as the hex escape \x27 guarantees that the emitted PHP source
never contains a literal single quote derived from user input, while the
decoded runtime value is unchanged. \' is not used because it is not a
recognized escape sequence in PHP double-quoted strings (the backslash
would be kept literally).
The HtmlDumper output is intended to be rendered in a browser, and the
template and macro/block names it interpolates are loader-controlled
(e.g. the key for ArrayLoader or a database row id), so they can carry
arbitrary HTML when an application stores templates under user-supplied
identifiers.
This PR was merged into the 3.x branch.
Discussion
----------
Lazy load `EscaperRuntime` in `EscaperExtension`
This allows overriding `EscaperRuntime` via a custom runtime loader.
Previously, `EscaperExtension::setEnvironment()` was calling `$environment->getRuntime(EscaperRuntime::class)` eagerly. Since this method is called from `Environment::__construct()`, the runtime was resolved before any custom runtime loader could be injected, making it impossible to override `EscaperRuntime`.
- Required by https://github.com/symfony/symfony/pull/63929
Commits
-------
b73ab8cfd9 Lazy load EscaperRuntime in EscaperExtension
Previously, setEnvironment() called getRuntime(EscaperRuntime::class) eagerly,
which prevented overriding EscaperRuntime via a custom runtime loader since
Environment::__construct() calls setEnvironment() before any loader can be injected.
This PR was merged into the 3.x branch.
Discussion
----------
Fix null coalescing operator with imported macros
Closes#4776
When using the null coalescing operator with a macro imported via the `from` tag, the `TemplateVariable` node inside `MacroReferenceExpression` was deep-cloned, causing the clone to generate a different `$macros` key than the one assigned by `AssignTemplateVariable`. This resulted in a `Call to a member function hasMacro() on null` error.
Commits
-------
efa004caab Fix null coalescing operator with imported macros
This PR was merged into the 3.x branch.
Discussion
----------
Support short-circuiting in null-safe operator chains
This PR adds short-circuiting for null-safe operator chains, using the same rules as PHP, `PropertyAccess`, and the `ExpressionLanguage`.
Previously, only the immediate null-safe access was guarded. With this change, as soon as a `null` is encountered at a null-safe access, the rest of the chain is skipped.
My approach was to move the null check outside of the `getAttribute()` calls so the expression can immediately return `null`, eg:
```twig
foo?.bar.baz
```
Before:
```php
yield $this->env
->getRuntime('Twig\Runtime\EscaperRuntime')
->escape(
CoreExtension::getAttribute(
$this->env,
$this->source,
(
null === (
$_v0 = (
isset($context['foo']) || array_key_exists('foo', $context)
? $context['foo']
: throw new RuntimeError('Variable "foo" does not exist.', 3, $this->source)
)
)
? null
: CoreExtension::getAttribute(
$this->env,
$this->source,
$_v0,
'bar',
[],
'any',
false,
false,
false,
3
)
),
'baz',
[],
'any',
false,
false,
false,
3
),
'html',
null,
true
);
```
Now:
```php
yield $this->env
->getRuntime('Twig\Runtime\EscaperRuntime')
->escape(
(
null === (
$_v0 = (
isset($context['foo']) || array_key_exists('foo', $context)
? $context['foo']
: throw new RuntimeError('Variable "foo" does not exist.', 3, $this->source)
)
)
? null
: CoreExtension::getAttribute(
$this->env,
$this->source,
CoreExtension::getAttribute(
$this->env,
$this->source,
$_v0,
'bar',
[],
'any',
false,
false,
false,
3
),
'baz',
[],
'any',
false,
false,
false,
3
)
),
'html',
null,
true
);
```
Commits
-------
d56e8e2dba Support short-circuiting in null-safe operator chains
This PR was merged into the 3.x branch.
Discussion
----------
Add support for renaming variables in object destructuring
Closes#4747
Commits
-------
3cc1b5233c Add support for renaming variables in object destructuring
This PR was squashed before being merged into the 3.x branch.
Discussion
----------
Create attributes `AsTwigFilter`, `AsTwigFunction` and `AsTwigTest` to ease extension development
One drawback to writing extensions at present is that the declaration of functions/filters/tests is not directly adjacent to the methods. It's worse for runtime extensions because they need to be in 2 different classes. See [`SerializerExtension`](https://github.com/symfony/symfony/blob/7.0/src/Symfony/Bridge/Twig/Extension/SerializerExtension.php) and [`SerializerRuntime`](https://github.com/symfony/symfony/blob/7.0/src/Symfony/Bridge/Twig/Extension/SerializerRuntime.php) as an example.
By using attributes for filters, functions and tests definition, we can make writing extensions more expressive, and use reflection to detect particular options (`needs_environment`, `needs_context`, `is_variadic`).
Example if we implemented the `formatDate` filter: https://github.com/twigphp/Twig/blob/aeeec9a5e907a79e50a6bb78979154599401726e/extra/intl-extra/IntlExtension.php#L392-L395
By using the `AsTwigFilter` attribute, it is not necessary to create the `getFilters()` method. The `needs_environment` option is detected from method signature. The name is still required as the method naming convention (camelCase) doesn't match with Twig naming convention (snake_case).
```php
use Twig\Extension\Attribute\AsTwigFilter;
class IntlExtension
{
#[AsTwigFilter(name: 'format_date')]
public function formatDate(Environment $env, $date, ?string $dateFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): string
{
return $this->formatDateTime($env, $date, $dateFormat, 'none', $pattern, $timezone, $calendar, $locale);
}
}
```
This approach does not totally replace the current definition of extensions, which is still necessary for advanced needs. It does, however, make for more pleasant reading and writing.
This makes writing lazy-loaded runtime extension the easiest way to create Twig extension in Symfony: https://github.com/symfony/symfony/pull/52748
Related to https://github.com/symfony/symfony/issues/50016
Is there any need to cache the parsing of method attributes? They are only read at compile time, but that can have a performance impact during development or when using dynamic templates.
Commits
-------
5886907b28 Create attributes `AsTwigFilter`, `AsTwigFunction` and `AsTwigTest` to ease extension development