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

  1. 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))
  2. Replace array-callables like [$obj, 'method'] or 'self::method' with a closure / first-class callable created in an accessible scope
  3. Verify the class/method exist with no typos in the callable
  4. 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

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


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)