getgrav/grav · error · Pimple\Exception\ExpectedInvokableException
Extension service definition is not a Closure or invokable o
Error message
Extension service definition is not a Closure or invokable object.
What it means
In Container::extend(), the second argument is the extension implementation. Pimple invokes it as $callable($factory($container), $container), so it must be a Closure or object with __invoke(); Container.php:300-302 rejects a non-invokable object with ExpectedInvokableException.
Source
Thrown at system/src/Pimple/Container.php:301
{
if (!isset($this->keys[$id])) {
throw new UnknownIdentifierException($id);
}
if (isset($this->frozen[$id])) {
throw new FrozenServiceException($id);
}
if (!is_object($this->values[$id]) || !method_exists($this->values[$id], '__invoke')) {
throw new InvalidServiceIdentifierException($id);
}
if (isset($this->protected[$this->values[$id]])) {
@trigger_error(sprintf('How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "%s" should be protected?', $id), E_USER_DEPRECATED);
}
if (!method_exists($callable, '__invoke')) {
throw new ExpectedInvokableException('Extension service definition is not a Closure or invokable object.');
}
$factory = $this->values[$id];
$extended = function (self $container) use ($callable, $factory): mixed {
return $callable($factory($container), $container);
};
if (isset($this->factories[$factory])) {
$this->factories->detach($factory);
$this->factories->attach($extended);
}
return $this[$id] = $extended;
}
/**
* Returns all defined value names.View on GitHub (pinned to 6040efed04)
Solutions
- Use a closure wrapper: $container->extend('twig', fn ($twig, $c) => new TwigDecorator($twig));
- Add __invoke($service, Container $container) to the extension object and return the decorated service.
- Keep the extension signature two-argument aware because Pimple passes the original result and then the container.
Example fix
// before
$container->extend('mailer', new RetryingMailerDecorator()); // object has no __invoke
// after
$container->extend(
'mailer',
fn (Mailer $mailer, Container $c) => new RetryingMailerDecorator($mailer, $c['retry'])
); Defensive patterns
Strategy: type-guard
Validate before calling
if (!is_object($extension) || !method_exists($extension, '__invoke')) {
throw new InvalidArgumentException('Container extensions must be closures or invokable objects.');
}
$container->extend($id, $extension); Type guard
function isContainerExtension(mixed $extension): bool
{
return is_object($extension) && method_exists($extension, '__invoke');
} Try / catch
try {
$container->extend($id, $extension);
} catch (ExpectedInvokableException $e) {
throw new InvalidArgumentException(sprintf('Wrap the extension for "%s" in a closure.', $id), 0, $e);
} Prevention
- Use fn ($service, Container $c) => new Decorator($service) wrappers.
- Give decorator classes __invoke() when they are passed directly.
- Test extension registration separately from service resolution.
- Remember Pimple passes the original service first and container second.
When it happens
Trigger: Calling $container->extend('twig', new TwigDecorator()) where TwigDecorator lacks __invoke(), or passing an instantiated middleware/decorator object when a callable wrapper is required. String callables fail the object type declaration earlier.
Common situations: Decorator classes that expose decorate() but no __invoke(), code migrated from containers accepting array callables, and confusion between the decorator instance and the closure that creates it.
Related errors
- Service definition is not a Closure or invokable object.
- Callable is not a Closure or invokable object.
- Cannot override frozen service "%s".
- Identifier "%s" does not contain an object definition.
- Identifier "%s" is not defined.
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/3a9ab227ce3f8099.
Report an issue: GitHub.