twigphp/Twig · error · LogicException

A block chain cannot contain templates from different Twig…

Error message

A block chain cannot contain templates from different Twig environments.

What it means

All templates in a BlockChain must belong to the same Environment so block inheritance resolution stays consistent. After unwrapping each TemplateWrapper, the constructor checks Template::isOwnedBy($env) and throws this LogicException when a template was loaded from a different Environment instance.

Solutions

  1. Load every template in the chain from the same Environment instance that is passed as $env.
  2. Create a fresh BlockChain per Environment instead of sharing chains across them.
  3. Trace where the mismatched wrapper came from (cache, container, static) and load it from the current Environment instead.

Example fix

// before
$chain = new \Twig\BlockChain($envA, [$envA->load('a.twig'), $envB->load('b.twig')]);
// after
$chain = new \Twig\BlockChain($envA, [$envA->load('a.twig'), $envA->load('b.twig')]);
Defensive patterns

Strategy: validation

Validate before calling

foreach ($templates as $t) {
    $wrapper = is_string($t) ? $env->load($t) : $t;
    if (!$wrapper->unwrap()->isOwnedBy($env)) {
        throw new \LogicException('Template loaded from a different Environment.');
    }
}

Type guard

function ownedByEnv(\Twig\TemplateWrapper $w, \Twig\Environment $env): bool {
    return $w->unwrap()->isOwnedBy($env);
}

Try / catch

try {
    $chain = new \Twig\BlockChain($env, $templates);
} catch (\LogicException $e) {
    // reload all entries from $env by name before retrying
}

Prevention

When it happens

Trigger: Constructing BlockChain with templates loaded via $envA->load('a.html.twig') and $envB->load('b.html.twig'), or a cached/shared template wrapper reused across two Environment instances.

Common situations: Multi-tenant or multi-app setups with several Twig environments sharing template files; test code mixing an isolated test Environment with the app's Environment; frameworks that register template wrappers globally.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of twigphp/Twig@a414c3a491 (2026-09-13). Data as JSON: /api/errors/9c99cdb5a0207316. Report an issue: GitHub.

Appendix: source

Thrown at src/BlockChain.php:54

     * @param iterable<string|TemplateWrapper> $templates Templates ordered from highest to lowest precedence
     * @param array<string, mixed>             $context   Default variables used to resolve dynamic parent expressions
     */
    public function __construct(
        private Environment $env,
        iterable $templates,
        private array $context = [],
    ) {
        foreach ($templates as $template) {
            if (\is_string($template)) {
                $template = $env->load($template);
            }
            if (!$template instanceof TemplateWrapper) {
                throw new \TypeError(\sprintf('Block chain templates must be strings or "%s" instances, "%s" given.', TemplateWrapper::class, get_debug_type($template)));
            }

            $template = $template->unwrap();
            if (!$template->isOwnedBy($env)) {
                throw new \LogicException('A block chain cannot contain templates from different Twig environments.');
            }

            $this->templates[] = $template;
        }

        if (!$this->templates) {
            throw new \InvalidArgumentException('A block chain requires at least one template.');
        }
    }

    /**
     * @param array<string, mixed> $context
     */
    public function hasBlock(string $name, array $context = []): bool
    {
        $blocks = $this->fixed ? $this->blocks : $this->resolveBlocks($context + $this->context + $this->env->getGlobals());

        return isset($blocks[$name]);

View on GitHub (pinned to a414c3a491)