getgrav/grav · error · Pimple\Exception\ExpectedInvokableException

Callable is not a Closure or invokable object.

Error message

Callable is not a Closure or invokable object.

What it means

Container::protect() prevents a callable definition from being executed when it is read, allowing it to be stored as a parameter. Because the protected value is tracked as an object, it must be a Closure or invokable object; a non-invokable object throws ExpectedInvokableException.

Source

Thrown at system/src/Pimple/Container.php:236

        return $callable;
    }

    /**
     * Protects a callable from being interpreted as a service.
     *
     * This is useful when you want to store a callable as a parameter.
     *
     * @param callable $callable A callable to protect from being evaluated
     *
     * @return callable The passed callable
     *
     * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object
     */
    public function protect(object $callable): object
    {
        if (!method_exists($callable, '__invoke')) {
            throw new ExpectedInvokableException('Callable is not a Closure or invokable object.');
        }

        $this->protected->attach($callable);

        return $callable;
    }

    /**
     * Gets a parameter or the closure defining an object.
     *
     * @param string $id The unique identifier for the parameter or object
     *
     * @return mixed The value of the parameter or the closure defining an object
     *
     * @throws UnknownIdentifierException If the identifier is not defined
     */
    public function raw(string $id): mixed
    {

View on GitHub (pinned to 6040efed04)

Solutions

  1. Use $container['callback'] = $container->protect(fn () => $callback()); for a callable that must not be invoked on access.
  2. Store a non-callable value directly: $container['formatter'] = $formatter.
  3. Add __invoke() to the object if it really is the callable being protected.

Example fix

// before
$container['normalizer'] = $container->protect(new Normalizer()); // no __invoke

// after
$container['normalizer'] = new Normalizer(); // plain parameter
$container['normalizer_factory'] = $container->protect(fn () => new Normalizer()); // protected callable
Defensive patterns

Strategy: type-guard

Validate before calling

if (!is_object($callback) || !method_exists($callback, '__invoke')) {
    throw new InvalidArgumentException('Only closures and invokable objects can be protected.');
}
$container['callback'] = $container->protect($callback);

Type guard

function isProtectableCallable(mixed $value): bool
{
    return is_object($value) && method_exists($value, '__invoke');
}

Try / catch

try {
    $container->protect($callable);
} catch (ExpectedInvokableException $e) {
    throw new InvalidArgumentException('Store non-callable values directly; protect() expects a closure or invokable object.', 0, $e);
}

Prevention

When it happens

Trigger: Calling $container->protect(new Formatter()) when Formatter has no __invoke(), or trying to protect a native function name or array callable, which instead fails the object parameter type before the method body.

Common situations: Storing a callback in configuration, preserving a closure for later dispatch, or passing a normal value object to protect() because its name suggests general value protection.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/8631e3accc3265bf. Report an issue: GitHub.