twigphp/Twig · error · LogicException
Callback for " " is not callable in the current scope.
Error message
Callback for %s "%s" is not callable in the current scope.
What it means
Twig's ReflectionCallable wraps a Twig callable (filter, function, test, macro) into a Closure and ReflectionFunction for argument introspection. Closure::fromCallable() performs a scope-aware callability check at call time; when the callable cannot actually be invoked from the current scope (e.g. a private/protected method), Twig converts the resulting TypeError into this LogicException naming the Twig callable's type and name.
Solutions
- Make the registered callback method public, or wrap it in a closure defined where it is visible: new TwigFilter('f', fn ($v) => $this->privateMethod($v))
- Replace array-callables like [$obj, 'method'] or 'self::method' with a closure / first-class callable created in an accessible scope
- Verify the class/method exist with no typos in the callable
- Read the chained TypeError (previous exception) for the exact reason Closure::fromCallable failed
Example fix
// before
$twig->addFilter(new \Twig\TwigFilter('shout', [$this, 'shoutInternal'])); // private method
// after
$twig->addFilter(new \Twig\TwigFilter('shout', fn ($v) => $this->shoutInternal($v))); // closure bound to class scope Defensive patterns
Strategy: validation
Validate before calling
try {
\Closure::fromCallable($callback);
} catch (\TypeError $e) {
throw new \InvalidArgumentException('Callback not callable in current scope', 0, $e);
} Type guard
function isScopeCallable(callable|array|string $cb): bool
{
try { \Closure::fromCallable($cb); return true; }
catch (\TypeError) { return false; }
} Try / catch
try {
$twig->addFilter(new \Twig\TwigFilter('f', $callback));
} catch (\LogicException $e) {
// callback not callable in this scope; wrap in a closure with access
$twig->addFilter(new \Twig\TwigFilter('f', fn ($v) => $obj->method($v)));
} Prevention
- Expose only public methods as Twig filters/functions/tests
- Prefer closures or first-class callable syntax over array-callables to private/protected methods
- Verify with Closure::fromCallable/is_callable before registering
- Watch for typos in string/array method callables during refactors
- Smoke-test each registered filter/function by rendering a template
When it happens
Trigger: Registering a Twig filter/function/test whose callback is a private or protected method (Closure::fromCallable fails from outside the class scope), e.g. new TwigFilter('f', [$obj, 'privateMethod']); using scope-dependent array-callables like 'self::method'; passing a method name string that is not accessible in the calling scope.
Common situations: Custom Twig extension exposing an internal/private helper as a filter; refactoring a method to private/protected after registering it; typos in the method name of an array callable; wrong scope prefix ('self::' instead of the class name).
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- The last parameter of
- The "format_list" filter requires the "IntlListFormatter"…
- SpanishInflector is not available.
- The " " filter is part of the , which is not…
- The " " function is part of the , which is not…
AI-assisted analysis of twigphp/Twig@a414c3a491 (2026-09-13).
Data as JSON: /api/errors/1e5dd83d0e87b04c.
Report an issue: GitHub.
Appendix: source
Thrown at src/Util/ReflectionCallable.php:48
) {
$callable = $twigCallable->getCallable();
if (\is_string($callable) && false !== $pos = strpos($callable, '::')) {
$callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)];
}
if (\is_array($callable) && method_exists($callable[0], $callable[1])) {
$this->reflector = $r = new \ReflectionMethod($callable[0], $callable[1]);
$this->callable = $callable;
$this->name = $r->class.'::'.$r->name;
return;
}
$checkVisibility = $callable instanceof \Closure;
try {
$closure = \Closure::fromCallable($callable);
} catch (\TypeError $e) {
throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $twigCallable->getType(), $twigCallable->getName()), 0, $e);
}
$this->reflector = $r = new \ReflectionFunction($closure);
if (str_contains($r->name, '{closure')) {
$this->callable = $callable;
$this->name = 'Closure';
return;
}
if ($object = $r->getClosureThis()) {
$callable = [$object, $r->name];
$this->name = get_debug_type($object).'::'.$r->name;
} elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) {
$callable = [$class->name, $r->name];
$this->name = $class->name.'::'.$r->name;
} elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) {
$callable = [\is_array($callable) ? $callable[0] : $class->name, $r->name];View on GitHub (pinned to a414c3a491)