Commit Graph

7716 Commits

Author SHA1 Message Date
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
Fabien Potencier 7b19e1561b bug #4918 Check that the use tag is allowed before resolving trait templates (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Check that the use tag is allowed before resolving trait templates

Commits
-------

18863f0371 Check that the use tag is allowed before resolving trait templates
2026-09-07 19:06:37 +02:00
Fabien Potencier e5bf15b7b2 bug #4922 Wrap dynamic parent expression errors (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Wrap dynamic parent expression errors

Commits
-------

b6da5e67f3 Wrap dynamic parent expression errors
2026-09-07 15:10:38 +02:00
Fabien Potencier b6da5e67f3 Wrap dynamic parent expression errors 2026-09-07 15:08:50 +02:00
Fabien Potencier 32acc4c3b9 bug #4921 Fix TemplateWrapper::hasBlock() and TemplateWrapper::getBlockNames() omitting environment globals (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Fix `TemplateWrapper::hasBlock()` and `TemplateWrapper::getBlockNames()` omitting environment globals

Commits
-------

0e1852632f Fix `TemplateWrapper::hasBlock()` and `TemplateWrapper::getBlockNames()` omitting environment globals
2026-09-07 14:15:36 +02:00
Fabien Potencier 0e1852632f Fix TemplateWrapper::hasBlock() and TemplateWrapper::getBlockNames() omitting environment globals 2026-09-07 14:15:32 +02:00
Fabien Potencier 18863f0371 Check that the use tag is allowed before resolving trait templates 2026-09-07 09:29:38 +02:00
Fabien Potencier 9d2d35faa7 Make extra integration tests compatible with Twig 4 2026-09-06 22:35:36 +02:00
Fabien Potencier a320927535 bug #4916 Fix html_attr dropping style declarations whose value is zero (dylanpulver)
This PR was merged into the 3.x branch.

Discussion
----------

Fix html_attr dropping style declarations whose value is zero

`InlineStyle::getValue()` skips a declaration when `empty($value)` is true. That also matches `0`, `0.0` and `'0'`, which are ordinary CSS values.

```twig
{{ html_attr({style: {opacity: 0}}) }}                    {# "" #}
{{ html_attr({style: {'flex-grow': 0, color: 'red'}}) }}  {# style="color: red;" #}
{{ html_attr({style: ['opacity: 0']}) }}                  {# style="opacity: 0;" #}
{{ html_attr({class: [0, 'a']}) }}                        {# class="0 a" #}
```

Same declaration, opposite result. The numeric-key branch never consults `empty()`, and the sibling `SeparatedTokenList::getValue()` already uses an explicit `null !== $v && false !== $v` test, so token lists keep a `0`. When every declaration is dropped the attribute is omitted entirely, so `{style: {opacity: 0}}` renders nothing at all, and `HtmlExtension::htmlAttrValue('style', ['opacity' => 0])` returns `null`.

Silently affects `opacity`, `z-index`, `margin`, `padding`, `border`, `flex-grow` and custom properties.

The guard now lists the values that carry no declaration. `null`, `false`, `''` and `true` still skip, and `[]` is kept in that list so empty arrays behave exactly as before.

`html_attr.rst` documents the `null`/`false`/`true` omission rules and says nothing about zero, so no doc change is needed. `HtmlAttrTest.php` already has a case named "zero is not treated as falsy", but only for a plain attribute value.

3 tests added. Reverting the fix fails the first; a naive `null`/`false`-only guard fails the other two.

Commits
-------

c072ff85b3 Fix html_attr dropping style declarations whose value is zero
2026-09-05 11:35:27 +02:00
Dylan Pulver c072ff85b3 Fix html_attr dropping style declarations whose value is zero
InlineStyle::getValue() skipped a declaration when empty($value) was true,
which also matches 0, 0.0 and '0'. Those are ordinary CSS values (opacity: 0,
z-index: 0, margin: 0, flex-grow: 0), so they were silently dropped, and a
style map containing only such declarations omitted the attribute entirely.

The sibling SeparatedTokenList::getValue() already uses an explicit
null/false test, so class token lists keep a 0 while style declarations did
not. The numeric-key branch of InlineStyle itself never consulted empty(),
so {style: ['opacity: 0']} printed while {style: {opacity: 0}} did not.
2026-09-03 20:24:11 +03:00
Fabien Potencier cb80d7ac89 bug #4915 Fix the default filter fallback reusing a null-safe temporary variable (lazerg, fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Fix the default filter fallback reusing a null-safe temporary variable

The `default` filter reuses the fallback argument node in both branches of the conditional it compiles to. Because `GetAttrExpression` records the temporary variable it allocated for a null-safe chain on the node itself, the second compilation skipped the assignment and emitted a bare `$_vN` reference, so `{{ item?.label|default(item?.name) }}` warned about an undefined variable whenever `item` was null.

The fallback node is now cloned, like the node used for the defined test already is, so each branch compiles its own temporary.

Fixes #4914

Commits
-------

2d0c30b075 Remove redundant default filter cases
0b4199c522 Strengthen the default filter regression test
86c830ef45 Fix the default filter fallback reusing a null-safe temporary variable
2026-09-03 08:51:35 +02:00
Fabien Potencier 2d0c30b075 Remove redundant default filter cases 2026-09-03 08:49:11 +02:00
Fabien Potencier 0b4199c522 Strengthen the default filter regression test 2026-09-03 08:32:22 +02:00
Lazizbek Ergashev 86c830ef45 Fix the default filter fallback reusing a null-safe temporary variable 2026-09-01 16:06:07 +05:00
Fabien Potencier c32656815b minor #4908 Remove the documentation comments compilation overhead (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Remove the documentation comments compilation overhead

Documentation comments are now attached to semantic nodes during parsing, removing the dedicated AST traversal. Token parsers returning placeholder nodes can select the semantic documentation target.

Commits
-------

60eed4ccd5 Remove the documentation comments compilation overhead
2026-08-29 00:19:33 +02:00
Fabien Potencier 60eed4ccd5 Remove the documentation comments compilation overhead 2026-08-29 00:19:29 +02:00
Fabien Potencier dd0bd0ffbc minor #4904 Release destructuring temporaries after assignment (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Release destructuring temporaries after assignment

The compiled destructuring code kept the right-hand side alive in a hidden local until the end of rendering, wasting memory on large arrays and iterators. The temporary is now cleared as part of the compiled expression.

The way it works:

For this Twig expression:

```twig
{% do {name, email: address} = user %}
```

The previous generated expression was conceptually:

```php
[$context["name"], $context["address"]] = [
    getAttribute($_v0 = $context["user"], "name"),
    getAttribute($_v0, "email"),
];
```

The branch now generates:

```php
[
    [$context["name"], $context["address"]] = [
        getAttribute($_v0 = $context["user"], "name"),
        getAttribute($_v0, "email"),
    ],
    $_v0 = null,
][0]
```

The important wrapper is:

```php
[$originalExpression, $_v0 = null][0]
```

PHP evaluates array elements from left to right:

1. The original destructuring assignment runs.
2. The temporary variable is set to `null`, releasing its reference.
3. `[0]` returns the result of the original expression.

This preserves Twig’s rule that a destructuring expression returns its right-hand value.

Commits
-------

c459ef0bdd Release destructuring temporaries after assignment
2026-08-29 00:15:46 +02:00
Fabien Potencier 2450c30b49 documentation #4911 Clarify source function trust requirements (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Clarify source function trust requirements

Commits
-------

fdfef2b14a Clarify source function trust requirements
2026-08-28 19:14:06 +02:00
Fabien Potencier fdfef2b14a Clarify source function trust requirements 2026-08-28 18:11:30 +02:00
Fabien Potencier 1d081e21f8 bug #4909 Report regular expression errors from the matches operator (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Report regular expression errors from the matches operator

The `matches` operator now throws a `RuntimeError` when PCRE cannot evaluate a regular expression instead of silently treating the error as a non-match.

This makes failures such as exhausted backtrack limits visible to template authors.

Commits
-------

e5347301d7 Throw on PCRE errors in the matches operator
2026-08-28 15:10:19 +02:00
Fabien Potencier e5347301d7 Throw on PCRE errors in the matches operator 2026-08-28 08:41:42 +02:00
Fabien Potencier 0c9d0c77c0 bug #4906 Deprecate prefixed macro definedness checks (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Deprecate prefixed macro definedness checks

Testing a macro through a legacy `macro_`-prefixed name would have silently returned `false` in Twig 4.0 without ever warning; `has()` now triggers the same deprecation as `call()`.

Commits
-------

977aef7172 Deprecate prefixed macro definedness checks
2026-08-27 17:57:48 +02:00
Fabien Potencier e1223b1ea8 bug #4907 Fix duplicate macro deprecation wording (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Fix duplicate macro deprecation wording

With three or more definitions of the same macro, the deprecation mislabeled the previous duplicate as the "first definition"; it now reports "previous definition" and "new definition" lines accurately.

Commits
-------

8a37332def Fix duplicate macro deprecation wording
2026-08-27 17:57:12 +02:00
Fabien Potencier 3bc3d2e62a bug #4905 Throw when list formatting fails (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Throw when list formatting fails

`format_list` silently returned an empty string when `IntlListFormatter` fails (for instance on malformed UTF-8); it now throws a `RuntimeError` with the formatter's error message.

Commits
-------

87b930ae67 Throw when list formatting fails
2026-08-27 17:52:14 +02:00
Fabien Potencier b1ef75c954 documentation #4903 Fix documentation inaccuracies found during the 3.29 review (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Fix documentation inaccuracies found during the 3.29 review

Aligns the docs with actual behavior: `include_only` globals, `html_attr` `data-*` iterables, destructuring slot consumption and iterator reuse, a caution for the Tempest converter, `MacroNode::compile()` overrides, and a duplicated CHANGELOG entry.

Commits
-------

59f7d67848 Document that reusing a non-rewindable iterator after destructuring is unsupported
099fa3471a Document that sequence destructuring consumes one value per pattern slot
abbdf82823 Fix the html_attr documentation about iterables in data attributes
be220e6fd8 Warn about untrusted input with the default Tempest markdown converter
1205b8b6ca Document that overriding MacroNode::compile() is not supported anymore
7bd052dd91 Merge overlapping CHANGELOG entries for the destructuring fatal error fix
207f873739 Document that include_only keeps global variables available
2026-08-27 17:51:17 +02:00
Fabien Potencier 59f7d67848 Document that reusing a non-rewindable iterator after destructuring is unsupported 2026-08-27 13:43:07 +02:00
Fabien Potencier c459ef0bdd Release destructuring temporaries after assignment 2026-08-27 13:32:19 +02:00
Fabien Potencier 977aef7172 Deprecate prefixed macro definedness checks 2026-08-27 13:32:19 +02:00
Fabien Potencier 8a37332def Fix duplicate macro deprecation wording 2026-08-27 13:32:19 +02:00
Fabien Potencier 87b930ae67 Throw when list formatting fails 2026-08-27 13:32:19 +02:00
Fabien Potencier 099fa3471a Document that sequence destructuring consumes one value per pattern slot 2026-08-27 13:07:47 +02:00
Fabien Potencier abbdf82823 Fix the html_attr documentation about iterables in data attributes 2026-08-27 13:07:41 +02:00
Fabien Potencier be220e6fd8 Warn about untrusted input with the default Tempest markdown converter 2026-08-27 13:07:34 +02:00
Fabien Potencier 1205b8b6ca Document that overriding MacroNode::compile() is not supported anymore 2026-08-27 13:07:26 +02:00
Fabien Potencier 7bd052dd91 Merge overlapping CHANGELOG entries for the destructuring fatal error fix 2026-08-27 13:07:19 +02:00
Fabien Potencier 207f873739 Document that include_only keeps global variables available 2026-08-27 13:07:13 +02:00
Fabien Potencier 9a8a76c86d feature #4902 Remove lazy macro import resolution (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Remove lazy macro import resolution

Reverts the fix for #4879

This removes the unreleased lazy resolution of template-level macro imports, which introduced complex and surprising behavior around template state, blocks, interrupted renders and sandboxing.

Macros called from another template should import their dependencies inside their own body. The documentation now explains this pattern.

Commits
-------

cf971e1a59 Remove lazy macro import resolution
2026-08-27 12:17:14 +02:00
Fabien Potencier cf971e1a59 Remove lazy macro import resolution 2026-08-27 12:17:08 +02:00
Fabien Potencier d850901a18 bug #4900 Honor date formatter prototype calendars (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Honor date formatter prototype calendars

This fixes calendar selection when an `IntlDateFormatter` prototype is configured.

An explicit calendar now takes precedence, followed by the prototype calendar, with Gregorian used as the final fallback. In particular, the `TRADITIONAL` calendar is no longer mistaken for an absent value because its constant value is zero.

Commits
-------

1de0bfceb4 Honor date formatter prototype calendars
2026-08-27 08:44:30 +02:00
Fabien Potencier 1de0bfceb4 Honor date formatter prototype calendars 2026-08-27 08:44:26 +02:00
Fabien Potencier 3265884e93 bug #4899 Fix Stringable keys for ArrayAccess implementations (fabpot)
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Fix Stringable keys for ArrayAccess implementations

This restores support for `Stringable` keys on string-keyed `ArrayAccess` implementations such as `ArrayObject` and `ArrayIterator`, while preserving object keys for `SplObjectStorage`.

The object key is attempted first and is converted to a string only when the implementation rejects it. The optimized and strict lookup paths now share the same behavior without duplicate existence checks.

Commits
-------

f3f1649955 Fix Stringable keys for ArrayAccess implementations
2026-08-27 08:42:51 +02:00
Fabien Potencier f3f1649955 Fix Stringable keys for ArrayAccess implementations 2026-08-27 08:42:47 +02:00
Fabien Potencier 62076874e8 bug #4901 Evaluate object destructuring expressions once (fabpot)
This PR was merged into the 3.x branch.

Discussion
----------

Evaluate object destructuring expressions once

This ensures the right-hand expression of an object or mapping destructuring assignment is evaluated exactly once.

All properties are now read from the same resolved value, avoiding repeated side effects and unnecessary work while preserving assignment order and return semantics.

Commits
-------

609376f491 Fix repeated object destructuring evaluation
2026-08-27 07:37:21 +02:00