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

  1. Use a closure wrapper: $container->extend('twig', fn ($twig, $c) => new TwigDecorator($twig));
  2. Add __invoke($service, Container $container) to the extension object and return the decorated service.
  3. 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

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


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