getgrav/grav · error · Pimple\Exception\ExpectedInvokableException
Service definition is not a Closure or invokable object.
Error message
Service definition is not a Closure or invokable object.
What it means
Container::factory() marks a service definition so every retrieval invokes it again instead of caching and freezing the result. Pimple tracks definitions in SplObjectStorage, so the definition must be a Closure or another object with __invoke(); a plain object without __invoke() throws ExpectedInvokableException.
Source
Thrown at system/src/Pimple/Container.php:214
}
unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]);
}
}
/**
* Marks a callable as being a factory service.
*
* @param callable $callable A service definition to be used as a factory
*
* @return callable The passed callable
*
* @throws ExpectedInvokableException Service definition has to be a closure or an invokable object
*/
public function factory(object $callable): object
{
if (!method_exists($callable, '__invoke')) {
throw new ExpectedInvokableException('Service definition is not a Closure or invokable object.');
}
$this->factories->attach($callable);
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
*/View on GitHub (pinned to 6040efed04)
Solutions
- Wrap construction in a closure: $container['service'] = $container->factory(fn (Container $c) => new SomeFactory(...));
- Make the passed object invokable by implementing __invoke(Container $container).
- Do not call factory() on an already constructed service; assign it as a normal value if no invocation is wanted.
Example fix
// before
$container['report'] = $container->factory(new ReportBuilder()); // ReportBuilder has no __invoke
// after
$container['report'] = $container->factory(
fn (Container $c) => new ReportBuilder($c['logger'])
); Defensive patterns
Strategy: type-guard
Validate before calling
if (!is_object($definition) || !method_exists($definition, '__invoke')) {
throw new InvalidArgumentException('A factory definition must be a Closure or invokable object.');
}
$definition = $container->factory($definition); Type guard
function isInvokableDefinition(mixed $definition): bool
{
return is_object($definition) && method_exists($definition, '__invoke');
} Try / catch
try {
$container->factory($definition);
} catch (ExpectedInvokableException $e) {
throw new InvalidArgumentException('Wrap object construction in a closure before marking it as a factory.', 0, $e);
} Prevention
- Use fn (Container $c) => new Service(...) as the factory shape.
- Do not pass instantiated services to factory().
- Add tests that resolve every registered service twice when factory semantics matter.
- Document whether a definition is shared, protected, or a factory.
When it happens
Trigger: Calling $container->factory(new SomeFactory()) where SomeFactory has no __invoke() method, or passing an object that is merely an instantiated service rather than a factory definition. String and array callables fail the object parameter type before reaching this check.
Common situations: Confusing the factory definition with the product it creates, migrating callable code from another container that accepts first-class callable strings, or decorating a class without adding an invoker.
Related errors
- Callable 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/a588eecc08631fb2.
Report an issue: GitHub.