bug #4797 Make embeds deterministic (itsalmostchristmas)

This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Make embeds deterministic

Using `{% embed 'foo.twig' %}...{% endembed %}` makes compiled templates non-deterministic. This poses a problem for developers who wish to provide reproducible, pre-compiled builds of Twig templates.

The cause is `mt_rand()` used to generate template indices. The change I propose uses an incrementing counter instead. To keep tests passing, the number must be non-zero and unique per PHP process (reusing indices across `Twig\Parser` instances causes fatal errors).

A demonstration of the problem is available on the [Twig Playground](https://twig.symfony.com/play?data=eyJ0ZW1wbGF0ZXMiOltbImluZGV4LnR3aWciLCJ7JSBlbWJlZCAnZGlhbG9nLnR3aWcnICV9XG4gICAgeyUgYmxvY2sgbWVzc2FnZSAlfVxuICAgICAgICBUYXNrIGZhaWxlZCBzdWNjZXNzZnVsbHlcbiAgICB7JSBlbmRibG9jayAlfVxueyUgZW5kZW1iZWQgJX0iXSxbImRpYWxvZy50d2lnIiwiPGRpYWxvZz57JSBibG9jayBtZXNzYWdlICV9eyUgZW5kYmxvY2sgJX08L2RpYWxvZz4iXV0sImNvbnRleHQiOnt9LCJ2ZXJzaW9uIjoiMy4yMS4xIiwib3B0aW9ucyI6eyJzdHJpY3RfdmFyaWFibGVzIjp0cnVlLCJjaGFyc2V0IjoiVVRGLTgiLCJhdXRvZXNjYXBlIjoiIn19), where the compiled output of `index.twig` differs on every recompilation.

Commits
-------

2b4aa4458a Make embeds deterministic
This commit is contained in:
Fabien Potencier
2026-05-16 09:48:31 +01:00
3 changed files with 24 additions and 4 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# 3.24.1 (2026-XX-XX)
# 3.25.0 (2026-XX-XX)
* n/a
* Use deterministic suffixes for generated embed classes
# 3.24.0 (2026-03-17)
+8 -2
View File
@@ -54,6 +54,7 @@ class Parser
private $importedSymbols;
private $traits;
private $embeddedTemplates = [];
private int $lastEmbedIndex = 0;
private $varNameSalt = 0;
private $ignoreUnknownTwigCallables = false;
private ExpressionParsers $parsers;
@@ -81,8 +82,13 @@ class Parser
*/
public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode
{
// reset on root parse() calls only, so the counter spans nested/reentrant parses
if (!$this->stack) {
$this->lastEmbedIndex = 0;
}
$vars = get_object_vars($this);
unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['varNameSalt']);
unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['lastEmbedIndex'], $vars['varNameSalt']);
$this->stack[] = $vars;
// node visitors
@@ -319,7 +325,7 @@ class Parser
*/
public function embedTemplate(ModuleNode $template)
{
$template->setIndex(mt_rand());
$template->setIndex(++$this->lastEmbedIndex);
$this->embeddedTemplates[] = $template;
}
+14
View File
@@ -206,6 +206,20 @@ EOF, 'index')));
$this->assertTrue($argumentNodes->getNode(3)->getAttribute('value'));
}
public function testEmbeddedTemplatesHaveSequentialIndices(): void
{
$template = new Source('{% embed "first" %}{% endembed %}{% embed "second" %}{% endembed %}', 'index');
$lexer = new Lexer(new Environment(new ArrayLoader()));
$stream = $lexer->tokenize($template);
$embeds = $this->getParser()
->parse($stream)
->getAttribute('embedded_templates');
$this->assertSame(1, $embeds->getNode(0)->getAttribute('index'));
$this->assertSame(2, $embeds->getNode(1)->getAttribute('index'));
}
protected function getParser()
{
$parser = new Parser(new Environment(new ArrayLoader()));