Skip to content

Fix compiled templates depending on the directory of their source when debug is disabled - #4987

Open
nicolas-grekas wants to merge 1 commit into
twigphp:3.xfrom
nicolas-grekas:lazy-source-path
Open

nicolas-grekas wants to merge 1 commit into
twigphp:3.xfrom
nicolas-grekas:lazy-source-path

Conversation

@nicolas-grekas

@nicolas-grekas nicolas-grekas commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Fix #3218

When debug is disabled, compiled templates now ask the loader for the path of their source when it's needed, instead of embedding it. The compiled code then doesn't depend on the directory of the project anymore: a cache warmed from a build server in another directory, as doc/api.rst suggests, is byte-identical, and it reports the paths where the app actually runs instead of the build directory's ones, which don't exist there.

To do so, Source accepts a closure as path, called on the first getPath(). Only that call asks the loader the template was instantiated with, typically when an error is rendered. Debug mode still embeds the path: tools like VarDumper instantiate compiled templates without their constructor to read it.

@stof

stof commented Oct 9, 2026

Copy link
Copy Markdown
Member

The suggestion in #3218 (comment) was to compile the path relative to __DIR__ for the case where the compiled template is written in a filesystem cache, instead of this logic making the path lazy-loaded and needing a special case in debug mode.

@nicolas-grekas

Copy link
Copy Markdown
Contributor Author

__DIR__ needs the compiler to know where the code ends up, and it doesn't: compileSource() doesn't know about the cache, ChainCache writes the same code to all its caches, at different paths, and NullCache evals it. That's the drawback you mentioned in #3218. Resolving through the loader works with any cache. The debug special case mirrors the code one: debug mode already embeds the code for the same reason, VarDumper instantiates templates without their constructor.

@stof

stof commented Oct 9, 2026

Copy link
Copy Markdown
Member

but resolving through the loader implies that the loader gives the same result back at the time the error is triggered than at the time the template was loaded. This is not the case when using Environment::createTemplate for instance, as this works by temporarily swapping the loader:

Twig/src/Environment.php

Lines 459 to 479 in d79aaf7

public function createTemplate(string $template, ?string $name = null): TemplateWrapper
{
$hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false);
if (null !== $name) {
$name = \sprintf('%s (string template %s)', $name, $hash);
} else {
$name = \sprintf('__string_template__%s', $hash);
}
$loader = new ChainLoader([
new ArrayLoader([$name => $template]),
$current = $this->getLoader(),
]);
$this->setLoader($loader);
try {
return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name));
} finally {
$this->setLoader($current);
}
}

@nicolas-grekas

Copy link
Copy Markdown
Contributor Author

createTemplate() itself is fine: its sources come from an ArrayLoader and have no path, so they're compiled as before and never ask the loader. But you're right that swapping the loader after loading a template would break it. PR updated: templates now keep the loader they were instantiated with (which also removes the reflection check).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Twig cache contains absolute paths

2 participants