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
- Use $container['callback'] = $container->protect(fn () => $callback()); for a callable that must not be invoked on access.
- Store a non-callable value directly: $container['formatter'] = $formatter.
- 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
- Call protect() only when reading the value would otherwise invoke it.
- Store ordinary parameters without protect().
- Use closures for callbacks stored in the container.
- Add __invoke() when an object itself must be callable.
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
- Service definition is not a Closure or invokable object.
- Identifier "%s" does not contain an object definition.
- Extension service definition is not a Closure or invokable o
- Cannot override frozen service "%s".
- Identifier "%s" is not defined.
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/8631e3accc3265bf.
Report an issue: GitHub.