Commit Graph

7758 Commits

Author SHA1 Message Date
Fabien Potencier c8d782ea0b Deprecate calling TemplateWrapper::unwrap() without arguments as of 3.30 and list the fix in the CHANGELOG 2026-09-25 07:44:32 +02:00
Fabien Potencier 9c3d58e638 Fix coding standards 2026-09-25 07:44:12 +02:00
Fabien Potencier 4d073d938a Drop a comment restating the code and clarify the fully named macro call fast path 2026-09-25 07:44:00 +02:00
Fabien Potencier 9d33a10324 minor #4945 Speed up adding extensions to an environment (nicolas-grekas)
This PR was merged into the 3.x branch.

Discussion
----------

Speed up adding extensions to an environment

`Environment::addExtension()` recomputes the options hash on every call, JSON-encoding the class names of all the extensions registered so far. This computes it only when a template class name is needed. With the 24 extensions TwigBundle registers on the Symfony Demo, building the environment drops from 87µs to 19µs, and calling `setExtensions()` once instead brings nothing more.

Merge-up to 4.x: the property is typed there, so it has to become `private ?string $optionsHash = null;` (I ran the 4.x suite with that change). The CHANGELOG entry doesn't go to 4.x.

Commits
-------

d3d13f9b1a Speed up adding extensions to an environment
2026-09-24 19:20:19 +02:00
Nicolas Grekas d3d13f9b1a Speed up adding extensions to an environment 2026-09-24 18:47:14 +02:00
Fabien Potencier efa368c8d7 minor #4940 Reuse the already resolved callable when compiling call arguments (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Reuse the already resolved callable when compiling call arguments

Commits
-------

16be8af8f5 Reuse the already resolved callable when compiling call arguments
2026-09-23 07:18:00 +01:00
Fabien Potencier 16be8af8f5 Reuse the already resolved callable when compiling call arguments 2026-09-23 07:17:53 +01:00
Fabien Potencier 154ae335bb minor #4939 Compile the generator guard as an unreachable yield instead of a yield from (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Compile the generator guard as an unreachable yield instead of a yield from

Commits
-------

4d5d233776 Compile the generator guard as an unreachable yield instead of a yield from
2026-09-23 07:16:36 +01:00
Fabien Potencier 4d5d233776 Compile the generator guard as an unreachable yield instead of a yield from 2026-09-22 22:00:07 +01:00
Fabien Potencier c33dd1c473 feature #4938 Speed up loading a template that the environment has already loaded (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Speed up loading a template that the environment has already loaded

`Environment::load()` allocated a fresh `TemplateWrapper` on every call, even when the template was already loaded. Since `TemplateWrapper` is immutable, the same instance can be reused.

**Performance** (PHP 8.5, interleaved A/B, min of 5 runs):

| Workload | Before | After | |
|---|---:|---:|---:|
| Repeated `load()` of a loaded template | ~800 ns | ~610 ns | **−24%** |
| Page render: layout + 41 `include()` calls | 133 µs | 127 µs | **−5%** |

Commits
-------

6f94a47ce1 Reuse the template wrapper of an already loaded template
2026-09-22 21:03:17 +01:00
Fabien Potencier 6f94a47ce1 Reuse the template wrapper of an already loaded template 2026-09-22 21:02:38 +01:00
Fabien Potencier ff4a1c6575 bug #4937 Stop the escaping safe analysis from retaining every analyzed node (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Stop the escaping safe analysis from retaining every analyzed node

Commits
-------

8274571d04 Stop the escaping safe analysis from retaining every analyzed node
2026-09-22 20:42:55 +01:00
Fabien Potencier 8274571d04 Stop the escaping safe analysis from retaining every analyzed node 2026-09-22 16:47:34 +01:00
Fabien Potencier a890011699 feature #4936 Allow to call TemplateWrapper::unwrap() without arguments (derrabus)
This PR was merged into the 3.x branch.

Discussion
----------

Allow to call `TemplateWrapper::unwrap()` without arguments

Fixes #4935, symfony/symfony#66193.

#4910 changed an internal API that has famously been used by Symfony since almost forever. We would now need to change Symfony's 5.4 branch which is security-only. Because of that, I'd like to propose to allow calling `TemplateWrapper::unwrap()` without parameters again. This should allow running Symfony 5.4 with the latest Twig 3 again.

We can revert my change on the 4.x branch.

Commits
-------

bfbe89e3a3 Allow to call TemplateWrapper::unwrap() without arguments
2026-09-22 11:27:53 +01:00
Alexander M. Turek bfbe89e3a3 Allow to call TemplateWrapper::unwrap() without arguments 2026-09-22 09:22:57 +02:00
Fabien Potencier 4b7c937485 feature #4934 Expose the escaping strategy a template was compiled with (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Expose the escaping strategy a template was compiled with

Compiled templates now record the escaping strategy they were compiled with and expose it via `TemplateWrapper::getDefaultEscapeStrategy()`, so the strategy of a template can be known without deriving it again from its name, which is both slower and can disagree with how its content was actually escaped.

Commits
-------

26812ebc0d Expose the escaping strategy a template was compiled with
2026-09-21 11:30:14 +01:00
Fabien Potencier 26812ebc0d Expose the escaping strategy a template was compiled with 2026-09-21 11:30:11 +01:00
Fabien Potencier ea3258396c minor #4933 Improve macro call performance (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Improve macro call performance

This is mostly about getting back to the same performance we had in 3.28.

Commits
-------

a8ee6dd762 Improve macro call performance
2026-09-20 21:28:10 +01:00
Fabien Potencier a8ee6dd762 Improve macro call performance 2026-09-20 21:28:03 +01:00
Fabien Potencier 66406ea727 bug #4932 Fix IntlExtension inheriting values derived by ICU from a date formatter prototype (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Fix IntlExtension inheriting values derived by ICU from a date formatter prototype

Commits
-------

6671288259 Fix IntlExtension inheriting values derived by ICU from a date formatter prototype
2026-09-20 20:33:34 +01:00
Fabien Potencier 6671288259 Fix IntlExtension inheriting values derived by ICU from a date formatter prototype 2026-09-20 20:33:31 +01:00
Fabien Potencier d722e8af93 bug #4931 Fix array access with a Stringable key on subclasses of ArrayObject and ArrayIterator (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Fix array access with a Stringable key on subclasses of ArrayObject and ArrayIterator

Commits
-------

4aae99e92c Fix array access with a Stringable key on subclasses of ArrayObject and ArrayIterator
2026-09-20 20:17:49 +01:00
Fabien Potencier 4aae99e92c Fix array access with a Stringable key on subclasses of ArrayObject and ArrayIterator 2026-09-20 20:17:42 +01:00
Fabien Potencier 33437bc409 Ignore the PHPUnit 10+ cache directory 2026-09-18 18:50:53 +01:00
Fabien Potencier 0037a49ac4 bug #4930 Report a clear error when a string cannot be split into characters (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Report a clear error when a string cannot be split into characters

Commits
-------

1ee9dae55a Cover and document the matches operator throwing on an invalid UTF-8 subject
5c1be030c5 Report a clear error when a string cannot be split into characters
2026-09-18 17:49:12 +01:00
Fabien Potencier 1ee9dae55a Cover and document the matches operator throwing on an invalid UTF-8 subject 2026-09-18 17:15:53 +01:00
Fabien Potencier 5c1be030c5 Report a clear error when a string cannot be split into characters 2026-09-18 17:15:50 +01:00
Fabien Potencier 8eb76e2b7e feature #4929 Deprecate cloning a Twig environment (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Deprecate cloning a Twig environment

Commits
-------

55afb2e084 Deprecate cloning a Twig environment
2026-09-18 16:54:00 +01:00
Fabien Potencier 55afb2e084 Deprecate cloning a Twig environment 2026-09-18 16:52:58 +01:00
Fabien Potencier 5b3a60a31b minor #4928 Report the macro call parentheses deprecation once per call site and name the macro (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Report the macro call parentheses deprecation once per call site and name the macro

Commits
-------

40d57c6445 Report the macro call parentheses deprecation once per call site and name the macro
2026-09-18 16:51:15 +01:00
Fabien Potencier 40d57c6445 Report the macro call parentheses deprecation once per call site and name the macro 2026-09-18 12:17:08 +02:00
Fabien Potencier b1fe79b610 Bump version 2026-09-18 11:10:56 +02:00
Fabien Potencier 45a3c6e922 Prepare the 3.29.0 release v3.29.0 2026-09-18 11:10:14 +02:00
Fabien Potencier 15207e0090 Update CHANGELOG 2026-09-18 11:10:01 +02:00
Fabien Potencier dea0483027 feature #4910 Reject template wrappers from another environment (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Reject template wrappers from another environment

Twig now rejects `TemplateWrapper` instances created by another `Environment`.

This prevents templates from unexpectedly using another environment's loader, extensions, globals, or sandbox policy.

Commits
-------

83e8f7e123 Reject cross-environment template wrappers in block chains
c1fc112047 Reject cross-environment template wrappers
2026-09-14 14:11:48 +02:00
Fabien Potencier 0f5c902d85 bug #4927 Report a clear error when using macros imported in a template body that was not rendered (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Report a clear error when using macros imported in a template body that was not rendered

Commits
-------

72c2f669bd Report a clear error when using macros imported in a template body that was not rendered
2026-09-14 14:05:45 +02:00
Fabien Potencier 56e5c0794b Clarify the exception message for nested block chains from another environment 2026-09-14 11:59:20 +02:00
Fabien Potencier 053200bc89 feature #4926 Allow block chains to be composed of other block chains (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Allow block chains to be composed of other block chains

This is something that is needed by Symfony (and other OSS projects), so better to have it directly in Twig.

Commits
-------

d6b81f9074 Allow block chains to be composed of other block chains
2026-09-14 11:26:36 +02:00
Fabien Potencier d6b81f9074 Allow block chains to be composed of other block chains 2026-09-14 11:17:44 +02:00
Fabien Potencier 72c2f669bd Report a clear error when using macros imported in a template body that was not rendered 2026-09-12 12:17:17 +02:00
Fabien Potencier 83e8f7e123 Reject cross-environment template wrappers in block chains 2026-09-12 09:57:01 +02:00
Fabien Potencier c1fc112047 Reject cross-environment template wrappers 2026-09-12 09:51:36 +02:00
Fabien Potencier a414c3a491 feature #4925 Resolve block chains against the render context (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Resolve block chains against the render context

While working on the Symfony PR for #4917, I realize that the performance was worse with the Twig's way. #4924 fixes part of the performance "regression". This one closes the gap.

`BlockChain` currently freezes each template's lineage by cloning it, so the block map and `parent()` both resolve `{% extends %}` against the constructor context.

This PR changes that to resolve the chain against the render context instead. `hasBlock()` and `getBlockNames()` take a context, like `TemplateWrapper` already does, and the third constructor argument becomes a default the render context can override. A lineage whose templates all have a fixed parent (none, or constant) is resolved once and cached, so the common case costs nothing; a dynamic `{% extends %}` re-resolves per call (not use by Symfony anyway).

This fixes also an inconsistency: a chained template with a dynamic parent now behaves exactly as it does when rendered directly. It also removes all changes in the "core" logic of Template.

```
                       construct    render   1 chain + 40 renders
Symfony's engine today   13.44us    0.78us         50.3us
#4917                    ~70us      0.96us        ~117us
this PR                   0.61us    0.84us         53.0us
```

Commits
-------

897717d78f Resolve block chains against the render context instead of freezing lineages
2026-09-12 09:49:05 +02:00
Fabien Potencier 897717d78f Resolve block chains against the render context instead of freezing lineages 2026-09-12 00:03:19 +02:00
Fabien Potencier bf3636ca77 bug #4924 Resolve constant parent templates once instead of on every lookup (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Resolve constant parent templates once instead of on every lookup

Currently, `Template::$parent` is only memoized by the compiled `doDisplay()`. Anything that reaches a template through `getParent()` without rendering it (`TemplateWrapper::hasBlock()`, `getBlockNames()`, `renderBlock()`, `yieldParentBlock()`, `MacroNamespace::getParent()`) re-evaluates `doGetParent()` and re-runs the sandbox check on every call, even when the parent is a string literal.

`doGetParent()` now does `$this->parent ??= $this->load("name", $line)` for a constant parent, which is what `doDisplay()` already does one line later. Dynamic parents are untouched and still resolve per context.

This also fixes a bug: `getParent()` loaded a constant parent with line `-1`, so a missing parent reported through `hasBlock()`/`getBlockNames()` lost its line number, while `render()` reported the `{% extends %}` line. All three now agree.

Commits
-------

c53b6468c9 Resolve constant parent templates once instead of on every lookup
2026-09-11 06:23:22 -07:00
Fabien Potencier c53b6468c9 Resolve constant parent templates once instead of on every lookup 2026-09-11 06:17:05 -07:00
Fabien Potencier 4de6bc3b06 bug #4923 Fix wrapping the Twig cache pool in a second tag aware adapter (nicolas-grekas)
This PR was merged into the 3.x branch.

Discussion
----------

Fix wrapping the Twig cache pool in a second tag aware adapter

`twig/extra-bundle` wires the `{% cache %}` pool like this:

```php
->set('twig.cache', TagAwareAdapter::class)
    ->args([$service('.twig.cache.inner')])

->set('.twig.cache.inner')
    ->parent('cache.app')
    ->tag('cache.pool', ['name' => 'twig.cache'])
```

`.twig.cache.inner` is a child of `cache.app`, so when an application configures
`framework.cache.app` with a natively tag aware adapter (`cache.adapter.redis_tag_aware`,
or the valkey, pdo and mongodb variants), the child pool is *already* a
`TagAwareAdapterInterface` and `twig.cache` wraps it in a second `TagAwareAdapter`.

Two nested `TagAwareAdapter`s never read back what they wrote:

```php
$pool = new TagAwareAdapter(new TagAwareAdapter(new ArrayAdapter()));

$item = $pool->getItem('k');
$item->set('value')->tag('t1');
$pool->save($item);

var_dump($pool->getItem('k')->isHit()); // bool(false)
```

So every `{% cache %}` block silently misses, on every request. This was reported here as
twigphp/Twig#3636 (the workaround in that thread is to redefine `twig.cache` and
`.twig.cache.inner` by hand as a `RedisTagAwareAdapter`), and again on the Symfony side as
symfony/symfony#54339 by `@rpkamp`. Symfony first tried to fix it in symfony/symfony#57927 by
deprecating passing a tag aware pool to `TagAwareAdapter`, but `@keulinho`'s analysis in
symfony/symfony#58830 showed that the deprecated thing was not the culprit, so that
deprecation was reverted in symfony/symfony#58950 and the defect was left where it is: in
this bundle. Symfony's own cache wiring already gets this right, it aliases
`cache.app.taggable` to `cache.app` when the configured adapter is natively tag aware
instead of decorating it.

## The fix

A small compiler pass resolves the parent chain of `.twig.cache.inner` and, when the
adapter it ends up on implements `TagAwareAdapterInterface`, drops the decorator and
aliases `twig.cache` to the pool. Nothing changes for the common case of a plain
`cache.app`, where the decorator is what makes the pool taggable and stays in place.

I picked this over declaring the pool through `framework.cache.pools` (with `tags: true`,
which would let Symfony make the same decision) because the pool is only registered when
the `cache` extension is enabled, and reproducing that condition inside a `prepend()` call
means processing the bundle's own configuration before `load()` runs. The compiler pass
stays entirely inside the bundle and only acts when `twig.cache` exists.

Detecting tag awareness from the resolved adapter class rather than from a list of adapter
ids keeps this working across `^5.4|^6.4|^7.0|^8.0` even though the set of natively tag
aware adapters grew over that range, and it also covers a `cache.app` overridden with a
custom tag aware adapter.

Two constraints are covered by the tests: `twig.cache` keeps its own namespace instead of
collapsing into the application pool, and the three argument aliases
(`TagAwareCacheInterface $twigCache`, `CacheInterface $twigCache`,
`CacheItemPoolInterface $twigCache`) keep resolving.

## Verification

Compiled containers with `framework.cache.app` left at its default and set to
`cache.adapter.redis_tag_aware`, before and after the patch:

| `framework.cache.app` | before | after |
| --- | --- | --- |
| default (filesystem) | `TagAwareAdapter(FilesystemAdapter)` | unchanged |
| `cache.adapter.redis_tag_aware` | `TagAwareAdapter(RedisTagAwareAdapter)` | `RedisTagAwareAdapter` |
| `cache.adapter.valkey_tag_aware` | `TagAwareAdapter(RedisTagAwareAdapter)` | `RedisTagAwareAdapter` |
| `cache.adapter.redis` | `TagAwareAdapter(RedisAdapter)` | unchanged |

In the fixed tag aware case the pool namespace stays distinct from `cache.app`'s.

`extra/twig-extra-bundle` test suite, on Symfony 8.2-dev and on framework-bundle 6.4:

```
PHPUnit 9.6.36 by Sebastian Bergmann and contributors.

Testing
...................                                               19 / 19 (100%)

Time: 00:00.534, Memory: 28.00 MB

OK (19 tests, 100 assertions)
```

The two new tests fail without the pass:

```
1) TwigCachePoolPassTest::testThePoolIsNotDecoratedWhenTheAppAdapterIsTagAware
Failed asserting that two strings are identical.
-'@.twig.cache.inner'
+'Symfony\Component\Cache\Adapter\TagAwareAdapter'
```

Commits
-------

bd939c8c3a Fix wrapping the Twig cache pool in a second tag aware adapter
2026-09-11 05:29:58 -07:00
Fabien Potencier 4c005c1ada feature #4917 Template runtime and block composition (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Template runtime and block composition

This PR addresses the Symfony compatibility break from #4910

It introduces runtime composition of templates used as collections of named block renderers: The renderer provides an ordered set of unrelated templates. The first matching block wins, nested `block()` calls see the complete composed set, and `parent()` remains within the block’s own inheritance or `use` hierarchy.

This feature is going to be useful for more than just Symfony.

## Strong non-Symfony use cases

### Ibexa Core

**Project:** `ibexa/core`
**Feature:** CMS field rendering through `FieldBlockRenderer`

Ibexa maintains prioritized field templates, selects blocks such as `ibexa_string_field`, walks parent templates, constructs a block map and passes it to `renderBlock()`.

This is the strongest independent fit for `BlockChain`:

```php
$blocks = new BlockChain($twig, [
    $localTemplate,
    ...$projectFieldThemes,
    ...$vendorFieldThemes,
]);

return $blocks->renderBlock($fieldType.'_field', $context);
```

### Data-grid and listing renderers

The audit found the same broad mechanism in:

- `Prezent/prezent-grid`, `src/Twig/GridRenderer.php`
- `pawellen/listing`, `Renderer/ListingRenderer.php`
- `Braunstetter/data-grid-bundle`, `src/GridRendererEngine.php`
- `AnoDataGrid`, `DataGridExtension.php`

Their common feature is **layered grid themes**:

1. Configure default grid templates.
2. Add per-grid or per-view overrides.
3. Map a column type to a block name.
4. Walk template inheritance.
5. Merge or cache available blocks.
6. Render the selected cell, header or filter block.

Several accessed `unwrap()`, `getBlocks()` or `getParent()` directly; others passed manually assembled block maps into `renderBlock()` or `displayBlock()`.

## Adjacent use cases

The audit also found block-library patterns that could benefit if they grow into multi-template composition:

- **iTop:** plugin-contributed login blocks such as `login_input`, `login_submit`, `login_form_footer` and `login_links`; independently renders `body`, `script`, `ready_script` and `css`.
- **Email renderers:** independently render `subject`, `body_text` and `body_html` blocks.
- **Runtime theme overlays:** tenant branding, application skins, email themes, reports and configurable admin interfaces.
- **Extension-provided block libraries:** enabled modules contribute blocks such as `toolbar`, `field_text`, `dashboard_metric` or `login_footer`.
- **Testing and preview tooling:** render a block against an exact theme stack without generating a synthetic host template.

## Important negative finding

Shopware-style plugin inheritance, and similar Drupal or Sylius layering, are **not** considered a direct fit. Those systems expect `parent()` to call the next plugin override. `BlockChain` deliberately keeps `parent()` inside the defining template’s normal lineage.

Commits
-------

49f814ea26 Template runtime and block composition
2026-09-11 04:57:43 -07:00
Fabien Potencier 49f814ea26 Template runtime and block composition 2026-09-11 04:57:38 -07:00
Nicolas Grekas bd939c8c3a Fix wrapping the Twig cache pool in a second tag aware adapter
The ".twig.cache.inner" pool is a child of "cache.app". When the application
configures a natively tag aware adapter for it (redis, valkey, pdo or mongodb),
the child pool is already tag aware and decorating it with a TagAwareAdapter
turns every read into a miss.

Alias "twig.cache" to the pool in that case, the way Symfony aliases
"cache.app.taggable" to "cache.app" instead of decorating it. The pool keeps
its own namespace, so it does not start sharing the application pool's one, and
the decorator stays in place for the plain adapters that need it.
2026-09-11 10:59:50 +02:00