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
- Load the template via $env->load($name) and pass the resulting Template instance, not the name string.
- Ensure custom code invoking displayBlock/renderBlock passes null or a Template for the template argument.
- 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
- Pass Template instances, never class-name strings, to block() arguments
- Never feed user-controlled context values into block()/renderBlock() template arguments (sandbox RCE guard)
- Type-hint internal APIs with Twig\Template
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
- You cannot enable the "use_yield" option of Twig as node
- The sandbox requires a strict security policy, call…
- The environment passed to
- The "format_list" filter requires the "IntlListFormatter"…
- SpanishInflector is not available.
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)