twigphp/Twig · error · LogicException

A block must be a method on a \Twig\Template instance.

Error message

A block must be a method on a \Twig\Template instance.

What it means

Template::yieldBlock() renders a named block, optionally delegating to a $template passed in the $arguments. As a security measure (mainly against RCE when the sandbox is enabled), the passed template must be an instance of Twig\Template; anything else (string class name, callable, arbitrary object) is rejected with this LogicException.

Solutions

  1. Load the template via $env->load($name) and pass the resulting Template instance, not the name string.
  2. Ensure custom code invoking displayBlock/renderBlock passes null or a Template for the template argument.
  3. In templates, pass a Template object (e.g. from the template's getParent()) to block(), not a raw value from context.

Example fix

// before
$tpl = 'partials/menu.html.twig';
$template->renderBlock('content', $context, $blocks, false, ['template' => $tpl]); // throws

// after
$tpl = $env->load('partials/menu.html.twig');
$template->renderBlock('content', $context, $blocks, false, ['template' => $tpl]);
Defensive patterns

Strategy: type-guard

Validate before calling

if (!$target instanceof \Twig\Template) {
    $target = $env->load($target); // resolve name strings to Template
}

Type guard

function asTemplate(mixed $t, \Twig\Environment $env): \Twig\Template
{
    if ($t instanceof \Twig\Template) return $t;
    if (is_string($t)) return $env->load($t);
    throw new \InvalidArgumentException('Expected Template or template name');
}

Try / catch

try { $out = $template->renderBlock('content', $ctx, $blocks, false, ['template' => $tpl]); } catch (\LogicException $e) { if (str_contains($e->getMessage(), 'A block must be a method on a')) { /* resolve $tpl to Template */ } throw $e; }

Prevention

When it happens

Trigger: Calling $template->yieldBlock($name, $context, $blocks, $useExtends, ['template' => $notATemplate]) where the 'template' entry is a class-name string or non-Template object. Also reached via displayBlock()/renderBlock() with invalid arguments, e.g. {{ block('x', someObject) }} in templates where someObject is not a Template.

Common situations: Passing a template name string instead of a loaded Template object as the inheritance target to block(); custom template-wrapping code that stores templates as class strings; sandbox-escape exploit attempts (the check exists precisely for that).

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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

Appendix: source

Thrown at src/Template.php:470

     */
    public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable
    {
        if ($useBlocks && isset($blocks[$name])) {
            $template = $blocks[$name][0];
            $block = $blocks[$name][1];
        } elseif (isset($this->blocks[$name])) {
            $template = $this->blocks[$name][0];
            $block = $this->blocks[$name][1];
            // expose this template's own blocks so nested block() calls resolve against them when the block is rendered directly (e.g. block(name, template))
            $blocks = array_merge($this->blocks, $blocks);
        } else {
            $template = null;
            $block = null;
        }

        // avoid RCEs when sandbox is enabled
        if (null !== $template && !$template instanceof self) {
            throw new \LogicException('A block must be a method on a \Twig\Template instance.');
        }

        if (null !== $template) {
            try {
                $template->ensureSecurityChecked();
                yield from $template->$block($context, $blocks);
            } catch (\Throwable $e) {
                $template->handleException($e);
            }
        } elseif ($parent = $this->getParent($context)) {
            yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this);
        } elseif (isset($blocks[$name])) {
            throw new RuntimeError(\sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext());
        } else {
            throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext());
        }
    }

View on GitHub (pinned to a414c3a491)