twigphp/Twig · error · LogicException
Circular template inheritance detected while building a…
Error message
Circular template inheritance detected while building a block chain from "%s".
What it means
Twig throws this LogicException when resolving the parent chain of templates (a 'block chain') encounters a cycle, i.e. a template whose getParent() eventually returns a template already seen in the chain. Template inheritance must form a finite linear chain; a cycle would loop forever, so Twig aborts eagerly in resolveLineage.
Solutions
- Inspect the template named in the message and trace its {% extends %} chain; remove the cycle so each template extends a strict ancestor-free parent.
- If the parent is dynamic, add a guard so the evaluated parent never equals or extends the current template.
- Log or dump the resolved parent value to find where the variable pointing back to the template comes from.
- Restructure shared blocks into a common base template instead of cross-extending templates.
Example fix
// before
{# base.html.twig #}
{% extends dynamic_parent %} {# dynamic_parent resolves to 'base.html.twig' itself #}
// after
{% if dynamic_parent != 'base.html.twig' %}
{% extends dynamic_parent %}
{% endif %} Defensive patterns
Strategy: validation
Validate before calling
function validatesLineage(\Twig\Environment $env, string $name): bool {
$visited = [];
$tpl = $env->load($name)->unwrap();
while (false !== $tpl) {
$id = spl_object_id($tpl);
if (isset($visited[$id])) { return false; }
$visited[$id] = true;
$tpl = $tpl->getParent([]);
}
return true;
} Try / catch
try { $twig->render($template, $vars); } catch (\LogicException $e) { if (str_contains($e->getMessage(), 'Circular template inheritance')) { log('cycle in extends chain', ['template' => $template]); } throw $e; } Prevention
- Never let dynamic {% extends %} variables resolve to the current template or its ancestors.
- Validate parent names against a whitelist that excludes the template itself.
- Keep inheritance strictly hierarchical and one-directional across templates.
- Add a startup smoke-test that loads every base template to surface cycles early.
When it happens
Trigger: A template's parent expression resolves back to itself or an ancestor — e.g. {% extends self %}-style dynamic parents, a dynamic parent like {% extends someVar %} where someVar names the same or an ancestor template, or two templates extending each other. Triggered when a template using blocks is loaded via resolveBlocks -> resolveLineage.
Common situations: Dynamic parent expressions built from variables or config whose values were computed incorrectly; refactoring that accidentally makes template A extend B while B extends A; programmatic template loading where the 'parent' name is built from user data that points back to the same template.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Calling the "parent" function outside of a block is…
- Calling the "parent" function on a template that does not…
- Template " " cannot be used as a trait.
- Block " " is not defined in trait " ".
- A template that extends another one cannot include content…
AI-assisted analysis of twigphp/Twig@a414c3a491 (2026-09-13).
Data as JSON: /api/errors/c17e826959a58169.
Report an issue: GitHub.
Appendix: source
Thrown at src/BlockChain.php:168
return $this->blocks = $blocks;
}
/**
* @param array<string, mixed> $context
*
* @return array{list<Template>, bool}
*/
private function resolveLineage(array $context): array
{
$lineage = [];
$fixed = true;
foreach ($this->templates as $template) {
$seen = [];
do {
if (isset($seen[$id = spl_object_id($template)])) {
throw new \LogicException(\sprintf('Circular template inheritance detected while building a block chain from "%s".', $template->getTemplateName()));
}
$seen[$id] = true;
$lineage[] = $template;
$parent = $template->getParent($context);
$fixed = $fixed && $template->hasFixedParent();
// a dynamic parent expression can evaluate to a template from another environment
$template = $parent instanceof TemplateWrapper ? $parent->unwrap() : $parent;
if (false !== $template && !$template->isOwnedBy($this->env)) {
throw new \LogicException('A block chain cannot contain templates from different Twig environments.');
}
} while (false !== $template);
}
return [$lineage, $fixed];
}
View on GitHub (pinned to a414c3a491)