{"record":{"id":"3a9ab227ce3f8099","repo":"getgrav/grav","slug":"extension-service-definition-is-not-a-closure-or-i","errorCode":null,"errorMessage":"Extension service definition is not a Closure or invokable object.","messagePattern":"Extension service definition is not a Closure or invokable object\\.","errorType":"exception","errorClass":"Pimple\\Exception\\ExpectedInvokableException","httpStatus":null,"severity":"error","filePath":"system/src/Pimple/Container.php","lineNumber":301,"sourceCode":"    {\n        if (!isset($this->keys[$id])) {\n            throw new UnknownIdentifierException($id);\n        }\n\n        if (isset($this->frozen[$id])) {\n            throw new FrozenServiceException($id);\n        }\n\n        if (!is_object($this->values[$id]) || !method_exists($this->values[$id], '__invoke')) {\n            throw new InvalidServiceIdentifierException($id);\n        }\n\n        if (isset($this->protected[$this->values[$id]])) {\n            @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);\n        }\n\n        if (!method_exists($callable, '__invoke')) {\n            throw new ExpectedInvokableException('Extension service definition is not a Closure or invokable object.');\n        }\n\n        $factory = $this->values[$id];\n\n        $extended = function (self $container) use ($callable, $factory): mixed {\n            return $callable($factory($container), $container);\n        };\n\n        if (isset($this->factories[$factory])) {\n            $this->factories->detach($factory);\n            $this->factories->attach($extended);\n        }\n\n        return $this[$id] = $extended;\n    }\n\n    /**\n     * Returns all defined value names.","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Pimple/Container.php#L283-L319","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n$container->extend('mailer', new RetryingMailerDecorator()); // object has no __invoke\n\n// after\n$container->extend(\n    'mailer',\n    fn (Mailer $mailer, Container $c) => new RetryingMailerDecorator($mailer, $c['retry'])\n);","handlingStrategy":"type-guard","validationCode":"if (!is_object($extension) || !method_exists($extension, '__invoke')) {\n    throw new InvalidArgumentException('Container extensions must be closures or invokable objects.');\n}\n$container->extend($id, $extension);","typeGuard":"function isContainerExtension(mixed $extension): bool\n{\n    return is_object($extension) && method_exists($extension, '__invoke');\n}","tryCatchPattern":"try {\n    $container->extend($id, $extension);\n} catch (ExpectedInvokableException $e) {\n    throw new InvalidArgumentException(sprintf('Wrap the extension for \"%s\" in a closure.', $id), 0, $e);\n}","preventionTips":["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."],"tags":["pimple","dependency-injection","php","extend","invokable","decorator"],"backgroundTag":"non-invokable-service-definition","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}