From 0439fa4641956e82bcbe563f75a4eb8fd79d8805 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 11:40:51 +0200 Subject: [PATCH 1/6] wip --- packages/container/src/GenericContainer.php | 29 +- packages/container/src/Lifetime.php | 14 + packages/container/src/Singleton.php | 1 + .../container/tests/ContainerLifetimeTest.php | 248 ++++++++++++++++++ packages/container/tests/ContainerTest.php | 2 +- .../Fixtures/ProcessLifetimeSingleton.php | 10 + .../RequestLifetimeClassInitializer.php | 19 ++ .../Fixtures/RequestLifetimeDependency.php | 12 + .../RequestLifetimeDynamicInitializer.php | 26 ++ .../Fixtures/RequestLifetimeInitializer.php | 19 ++ .../Fixtures/RequestLifetimeInterface.php | 7 + .../Fixtures/RequestLifetimeSingleton.php | 13 + 12 files changed, 396 insertions(+), 4 deletions(-) create mode 100644 packages/container/src/Lifetime.php create mode 100644 packages/container/tests/ContainerLifetimeTest.php create mode 100644 packages/container/tests/Fixtures/ProcessLifetimeSingleton.php create mode 100644 packages/container/tests/Fixtures/RequestLifetimeClassInitializer.php create mode 100644 packages/container/tests/Fixtures/RequestLifetimeDependency.php create mode 100644 packages/container/tests/Fixtures/RequestLifetimeDynamicInitializer.php create mode 100644 packages/container/tests/Fixtures/RequestLifetimeInitializer.php create mode 100644 packages/container/tests/Fixtures/RequestLifetimeInterface.php create mode 100644 packages/container/tests/Fixtures/RequestLifetimeSingleton.php diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index 583ed878e..3cc224752 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -21,6 +21,7 @@ use Tempest\Reflection\TypeReflector; use Throwable; use UnitEnum; +use WeakMap; use const ARRAY_FILTER_USE_BOTH; @@ -31,6 +32,9 @@ final class GenericContainer implements Container /** @var array, DynamicInitializer> */ private array $resolvedDynamicInitializers = []; + /** @var WeakMap */ + private WeakMap $singletonLifetimes; + public function __construct( /** @var ArrayIterator $definitions */ private(set) ArrayIterator $definitions = new ArrayIterator(), @@ -55,6 +59,7 @@ public function __construct( private(set) ?DependencyChain $chain = null, ) { + $this->singletonLifetimes = new WeakMap(); $this->singleton(Container::class, $this); $this->singleton(ContainerInterface::class, $this); $this->singleton(GenericContainer::class, $this); @@ -407,9 +412,12 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag $initializer instanceof DynamicInitializer => $initializer->initialize($class, $tag, $this->clone()), }; - $singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class); + $singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass + ->getMethod('initialize') + ->getAttribute(Singleton::class) ?? $class->getAttribute(Singleton::class); if ($singleton !== null) { + $this->singletonLifetimes[$object] = $singleton->lifetime; $this->singleton($className, $object, $tag); } @@ -506,8 +514,9 @@ private function autowire(string $className, string|UnitEnum|null $tag, mixed .. if ( ! $classReflector->getType()->matches(Initializer::class) && ! $classReflector->getType()->matches(DynamicInitializer::class) - && $classReflector->hasAttribute(Singleton::class) + && ($singleton = $classReflector->getAttribute(Singleton::class)) !== null ) { + $this->singletonLifetimes[$instance] = $singleton->lifetime; $this->singleton($className, $instance, $tag); } @@ -744,7 +753,6 @@ public function addResettable(string|ClassReflector $resettableClass): Container public function reset(): self { - $this->resolvedSingletons = new ArrayIterator(); $this->resolvedDynamicInitializers = []; foreach ($this->resettables as $resettableClass) { @@ -754,6 +762,21 @@ public function reset(): self $resettable->reset(); } + foreach ([$this->singletonDefinitions, $this->resolvedSingletons] as $singletons) { + foreach ($singletons->getArrayCopy() as $dependencyName => $instance) { + // Factories remain registered so the next request can create a fresh instance. + if (! is_object($instance) || $instance instanceof Closure) { + continue; + } + + $this->singletonLifetimes[$instance] ??= new ClassReflector($instance)->getAttribute(Singleton::class)->lifetime ?? Lifetime::PROCESS; + + if ($this->singletonLifetimes[$instance] === Lifetime::REQUEST) { + unset($singletons[$dependencyName]); + } + } + } + return $this; } diff --git a/packages/container/src/Lifetime.php b/packages/container/src/Lifetime.php new file mode 100644 index 000000000..d07dbc34b --- /dev/null +++ b/packages/container/src/Lifetime.php @@ -0,0 +1,14 @@ +get(ProcessLifetimeSingleton::class); + + for ($request = 0; $request < 3; $request++) { + $instance = $container->get(RequestLifetimeSingleton::class); + $tagged = $container->get(RequestLifetimeSingleton::class, 'tag'); + $this->assertSame($instance, $container->get(RequestLifetimeSingleton::class)); + $this->assertSame($tagged, $container->get(RequestLifetimeSingleton::class, 'tag')); + $this->assertNotSame($instance, $tagged); + + $container->reset(); + + $this->assertNotSame($instance, $container->get(RequestLifetimeSingleton::class)); + $this->assertNotSame($tagged, $container->get(RequestLifetimeSingleton::class, 'tag')); + $this->assertSame($process, $container->get(ProcessLifetimeSingleton::class)); + } + } + + #[Test] + public function lifetimes_are_tracked_when_resolving_nested_dependencies(): void + { + $container = new GenericContainer(); + $instance = $container->get(RequestLifetimeDependency::class)->singleton; + $this->assertSame($instance, $container->get(RequestLifetimeSingleton::class)); + + $container->reset(); + + $this->assertNotSame($instance, $container->get(RequestLifetimeDependency::class)->singleton); + } + + #[Test] + public function initializer_singletons_are_shared_until_reset(): void + { + foreach ([RequestLifetimeInitializer::class, RequestLifetimeClassInitializer::class, RequestLifetimeDynamicInitializer::class] as $initializer) { + $container = new GenericContainer(); + $discovery = new InitializerDiscovery($container); + $discovery->setItems(new DiscoveryItems()); + $discovery->discover(new DiscoveryLocation(__NAMESPACE__, __DIR__), new ClassReflector($initializer)); + $discovery->apply(); + + for ($request = 0; $request < 3; $request++) { + $instance = $container->get(RequestLifetimeInterface::class); + $this->assertSame($instance, $container->get(RequestLifetimeInterface::class)); + + $container->reset(); + + $this->assertNotSame($instance, $container->get(RequestLifetimeInterface::class)); + } + } + } + + #[Test] + public function tagged_initializer_singletons_are_cleared(): void + { + $container = new GenericContainer(); + $container->addInitializer(RequestLifetimeInitializer::class); + $instance = $container->get(RequestLifetimeInterface::class, 'tag'); + $this->assertSame($instance, $container->get(RequestLifetimeInterface::class, 'tag')); + + $container->reset(); + + $this->assertNotSame($instance, $container->get(RequestLifetimeInterface::class, 'tag')); + } + + #[Test] + public function discovered_interface_bindings_follow_the_class_lifetime(): void + { + $container = new GenericContainer(); + $discovery = new AutowireDiscovery($container); + $discovery->setItems(new DiscoveryItems()); + $discovery->discover(new DiscoveryLocation(__NAMESPACE__, __DIR__), new ClassReflector(RequestLifetimeSingleton::class)); + $discovery->apply(); + + for ($request = 0; $request < 3; $request++) { + $instance = $container->get(RequestLifetimeInterface::class); + $this->assertSame($instance, $container->get(RequestLifetimeSingleton::class)); + $this->assertSame($instance, $container->get(RequestLifetimeInterface::class)); + + $container->reset(); + + $next = $container->get(RequestLifetimeInterface::class); + $this->assertNotSame($instance, $next); + $this->assertSame($next, $container->get(RequestLifetimeSingleton::class)); + } + } + + #[Test] + public function registered_factories_follow_their_lifetime(): void + { + $container = new GenericContainer(); + $container->singleton(SingletonClass::class, fn () => new SingletonClass()); + $container->singleton(RequestLifetimeInterface::class, fn () => new RequestLifetimeSingleton(), 'request'); + $process = $container->get(SingletonClass::class); + + for ($request = 0; $request < 3; $request++) { + $instance = $container->get(RequestLifetimeInterface::class, 'request'); + $this->assertSame($instance, $container->get(RequestLifetimeInterface::class, 'request')); + + $container->reset(); + + $this->assertNotSame($instance, $container->get(RequestLifetimeInterface::class, 'request')); + $this->assertSame($process, $container->get(SingletonClass::class)); + } + } + + #[Test] + public function process_initializer_singletons_survive_reset(): void + { + $container = new GenericContainer(); + $container->addInitializer(SingletonInitializer::class); + $instance = $container->get(ContainerObjectE::class); + + $container->reset(); + + $this->assertSame($instance, $container->get(ContainerObjectE::class)); + } + + #[Test] + public function initialized_classes_can_declare_their_own_lifetime(): void + { + $initializer = new class implements Initializer { + public function initialize(Container $container): RequestLifetimeSingleton + { + return new RequestLifetimeSingleton(); + } + }; + $container = new GenericContainer(); + $container->addInitializer($initializer::class); + $instance = $container->get(RequestLifetimeSingleton::class); + $this->assertSame($instance, $container->get(RequestLifetimeSingleton::class)); + + $container->reset(); + + $this->assertNotSame($instance, $container->get(RequestLifetimeSingleton::class)); + } + + #[Test] + public function registered_request_instances_are_removed(): void + { + $container = new GenericContainer(); + $container->singleton(RequestLifetimeSingleton::class, $instance = new RequestLifetimeSingleton()); + $this->assertSame($instance, $container->get(RequestLifetimeSingleton::class)); + + $container->reset(); + + $this->assertFalse($container->has(RequestLifetimeSingleton::class)); + $this->assertNotSame($instance, $container->get(RequestLifetimeSingleton::class)); + } + + #[Test] + public function initializer_lifetime_is_shared_by_all_bindings_of_the_instance(): void + { + $initializer = new class implements Initializer { + #[Singleton(lifetime: Lifetime::REQUEST)] + public function initialize(Container $container): SingletonClass + { + return new SingletonClass(); + } + }; + $container = new GenericContainer(); + $container->addInitializer($initializer::class); + $container->singleton(SingletonClass::class, fn (Container $container) => $container->get(SingletonClass::class), 'alias'); + + for ($request = 0; $request < 3; $request++) { + // Parameter resolution uses an internal container clone. + $instance = $container->invoke(fn (SingletonClass $instance) => $instance); + $container->singleton(SingletonClass::class, $instance, 'instance'); + $this->assertSame($instance, $container->get(SingletonClass::class, 'alias')); + $this->assertSame($instance, $container->get(SingletonClass::class, 'instance')); + + $container->reset(); + + $this->assertFalse($container->has(SingletonClass::class, 'instance')); + $next = $container->get(SingletonClass::class, 'alias'); + $this->assertNotSame($instance, $next); + $this->assertSame($next, $container->get(SingletonClass::class)); + } + } + + #[Test] + public function initializer_lifetime_takes_precedence_over_the_class_lifetime(): void + { + $initializer = new class implements Initializer { + #[Singleton(lifetime: Lifetime::PROCESS)] + public function initialize(Container $container): RequestLifetimeSingleton + { + return new RequestLifetimeSingleton(); + } + }; + $container = new GenericContainer(); + $container->addInitializer($initializer::class); + $instance = $container->get(RequestLifetimeSingleton::class); + $container->singleton(RequestLifetimeInterface::class, $instance); + + $container->reset(); + + $this->assertSame($instance, $container->get(RequestLifetimeSingleton::class)); + $this->assertSame($instance, $container->get(RequestLifetimeInterface::class)); + } + + #[Test] + public function replacing_a_request_singleton_updates_its_lifetime(): void + { + $container = new GenericContainer(); + $container->singleton(SingletonClass::class, new RequestLifetimeSingleton()); + $container->get(SingletonClass::class); + $container->singleton(SingletonClass::class, $process = new SingletonClass()); + + $container->reset(); + + $this->assertSame($process, $container->get(SingletonClass::class)); + } +} diff --git a/packages/container/tests/ContainerTest.php b/packages/container/tests/ContainerTest.php index b37f7e8e6..7b833f7b3 100644 --- a/packages/container/tests/ContainerTest.php +++ b/packages/container/tests/ContainerTest.php @@ -860,7 +860,7 @@ public function reset(): void $container->get(SingletonClass::class); $container->get(SingletonClass::class); - $this->assertSame(2, SingletonClass::$count); // constructed twice, once before and once after reset + $this->assertSame(1, SingletonClass::$count); $this->assertTrue(ResettableDependency::$reset); } } diff --git a/packages/container/tests/Fixtures/ProcessLifetimeSingleton.php b/packages/container/tests/Fixtures/ProcessLifetimeSingleton.php new file mode 100644 index 000000000..a447a0c3c --- /dev/null +++ b/packages/container/tests/Fixtures/ProcessLifetimeSingleton.php @@ -0,0 +1,10 @@ +getName() === RequestLifetimeInterface::class; + } + + #[Singleton(lifetime: Lifetime::REQUEST)] + public function initialize(ClassReflector $class, string|UnitEnum|null $tag, Container $container): object + { + return new RequestLifetimeSingleton(); + } +} diff --git a/packages/container/tests/Fixtures/RequestLifetimeInitializer.php b/packages/container/tests/Fixtures/RequestLifetimeInitializer.php new file mode 100644 index 000000000..2265cceed --- /dev/null +++ b/packages/container/tests/Fixtures/RequestLifetimeInitializer.php @@ -0,0 +1,19 @@ + Date: Wed, 9 Sep 2026 11:44:35 +0200 Subject: [PATCH 2/6] wip --- docs/1-essentials/05-container.md | 39 ++++++++++++++++++++- packages/container/src/GenericContainer.php | 9 ++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/docs/1-essentials/05-container.md b/docs/1-essentials/05-container.md index 9de0854cc..855d2a712 100644 --- a/docs/1-essentials/05-container.md +++ b/docs/1-essentials/05-container.md @@ -224,7 +224,7 @@ final readonly class Client } ``` -Furthermore, an initializer method can be annotated as a `#[Singleton]`, meaning its return object will only ever be resolved once: +Furthermore, an initializer method can be annotated as a `#[Singleton]`, meaning its return object will be reused for the singleton’s lifetime: ```php app/MarkdownInitializer.php use Tempest\Console\ConsoleCommand; @@ -241,6 +241,43 @@ final readonly class MarkdownInitializer implements Initializer } ``` +### Singleton lifetimes + +By default, singletons use {`Tempest\Container\Lifetime::PROCESS`} as their lifetime: the container keeps the same instance for the lifetime of the process, including across requests in long-running applications. + +For objects that hold request-specific state, use `Lifetime::REQUEST`: + +```php +use Tempest\Container\Lifetime; +use Tempest\Container\Singleton; + +#[Singleton(lifetime: Lifetime::REQUEST)] +final class RequestContext +{ + public ?string $tenantId = null; +} +``` + +The container reuses this instance within a request. When the container is reset, it clears the instance so that the next request receives a fresh one, this happens after every request in long-running applications. + +Note that you can also declare the lifetime via initializers: + +```php app/RequestContextInitializer.php +use Tempest\Container\Container; +use Tempest\Container\Initializer; +use Tempest\Container\Lifetime; +use Tempest\Container\Singleton; + +final class RequestContextInitializer implements Initializer +{ + #[Singleton(lifetime: Lifetime::REQUEST)] + public function initialize(Container $container): RequestContext + { + return new RequestContext(); + } +} +``` + ### Tagged singletons In some cases, you want more control over singleton definitions. diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index 3cc224752..1736dffde 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -58,7 +58,8 @@ public function __construct( private(set) ArrayIterator $resettables = new ArrayIterator(), private(set) ?DependencyChain $chain = null, - ) { + ) + { $this->singletonLifetimes = new WeakMap(); $this->singleton(Container::class, $this); $this->singleton(ContainerInterface::class, $this); @@ -412,9 +413,9 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag $initializer instanceof DynamicInitializer => $initializer->initialize($class, $tag, $this->clone()), }; - $singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass - ->getMethod('initialize') - ->getAttribute(Singleton::class) ?? $class->getAttribute(Singleton::class); + $singleton = $initializerClass->getAttribute(Singleton::class) + ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class) + ?? $class->getAttribute(Singleton::class); if ($singleton !== null) { $this->singletonLifetimes[$object] = $singleton->lifetime; From 1056a47c3c342ce255d147861954aa7d0e36e015 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 11:45:35 +0200 Subject: [PATCH 3/6] wip --- docs/1-essentials/05-container.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/1-essentials/05-container.md b/docs/1-essentials/05-container.md index 855d2a712..a83f9836a 100644 --- a/docs/1-essentials/05-container.md +++ b/docs/1-essentials/05-container.md @@ -245,7 +245,7 @@ final readonly class MarkdownInitializer implements Initializer By default, singletons use {`Tempest\Container\Lifetime::PROCESS`} as their lifetime: the container keeps the same instance for the lifetime of the process, including across requests in long-running applications. -For objects that hold request-specific state, use `Lifetime::REQUEST`: +For objects that hold request-specific state, you can specify `Lifetime::REQUEST` instead: ```php use Tempest\Container\Lifetime; @@ -262,7 +262,7 @@ The container reuses this instance within a request. When the container is reset Note that you can also declare the lifetime via initializers: -```php app/RequestContextInitializer.php +```php use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Lifetime; From d0da26ad3f1f98f3dcd3e1960da992eb31869165 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 11:48:45 +0200 Subject: [PATCH 4/6] wip --- packages/container/src/GenericContainer.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index 1736dffde..dccd7d049 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -58,8 +58,7 @@ public function __construct( private(set) ArrayIterator $resettables = new ArrayIterator(), private(set) ?DependencyChain $chain = null, - ) - { + ) { $this->singletonLifetimes = new WeakMap(); $this->singleton(Container::class, $this); $this->singleton(ContainerInterface::class, $this); @@ -413,9 +412,10 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag $initializer instanceof DynamicInitializer => $initializer->initialize($class, $tag, $this->clone()), }; - $singleton = $initializerClass->getAttribute(Singleton::class) - ?? $initializerClass->getMethod('initialize')->getAttribute(Singleton::class) - ?? $class->getAttribute(Singleton::class); + $singleton = + $initializerClass->getAttribute(Singleton::class) ?? $initializerClass + ->getMethod('initialize') + ->getAttribute(Singleton::class) ?? $class->getAttribute(Singleton::class); if ($singleton !== null) { $this->singletonLifetimes[$object] = $singleton->lifetime; @@ -766,10 +766,12 @@ public function reset(): self foreach ([$this->singletonDefinitions, $this->resolvedSingletons] as $singletons) { foreach ($singletons->getArrayCopy() as $dependencyName => $instance) { // Factories remain registered so the next request can create a fresh instance. - if (! is_object($instance) || $instance instanceof Closure) { + if (! is_object($instance)) { + continue; + } + if ($instance instanceof Closure) { continue; } - $this->singletonLifetimes[$instance] ??= new ClassReflector($instance)->getAttribute(Singleton::class)->lifetime ?? Lifetime::PROCESS; if ($this->singletonLifetimes[$instance] === Lifetime::REQUEST) { From d8f31329257e6431511b0a3a528e8f477a2df1f2 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 11:50:33 +0200 Subject: [PATCH 5/6] wip --- packages/container/tests/ContainerLifetimeTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/container/tests/ContainerLifetimeTest.php b/packages/container/tests/ContainerLifetimeTest.php index 11ba5e20c..bbe790edd 100644 --- a/packages/container/tests/ContainerLifetimeTest.php +++ b/packages/container/tests/ContainerLifetimeTest.php @@ -89,6 +89,7 @@ public function tagged_initializer_singletons_are_cleared(): void { $container = new GenericContainer(); $container->addInitializer(RequestLifetimeInitializer::class); + $instance = $container->get(RequestLifetimeInterface::class, 'tag'); $this->assertSame($instance, $container->get(RequestLifetimeInterface::class, 'tag')); @@ -125,6 +126,7 @@ public function registered_factories_follow_their_lifetime(): void $container = new GenericContainer(); $container->singleton(SingletonClass::class, fn () => new SingletonClass()); $container->singleton(RequestLifetimeInterface::class, fn () => new RequestLifetimeSingleton(), 'request'); + $process = $container->get(SingletonClass::class); for ($request = 0; $request < 3; $request++) { @@ -143,6 +145,7 @@ public function process_initializer_singletons_survive_reset(): void { $container = new GenericContainer(); $container->addInitializer(SingletonInitializer::class); + $instance = $container->get(ContainerObjectE::class); $container->reset(); @@ -161,6 +164,7 @@ public function initialize(Container $container): RequestLifetimeSingleton }; $container = new GenericContainer(); $container->addInitializer($initializer::class); + $instance = $container->get(RequestLifetimeSingleton::class); $this->assertSame($instance, $container->get(RequestLifetimeSingleton::class)); @@ -224,6 +228,7 @@ public function initialize(Container $container): RequestLifetimeSingleton }; $container = new GenericContainer(); $container->addInitializer($initializer::class); + $instance = $container->get(RequestLifetimeSingleton::class); $container->singleton(RequestLifetimeInterface::class, $instance); From 94917bc01f779e93d42566b7c60f9db08c5e36de Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 12:09:53 +0200 Subject: [PATCH 6/6] wip --- packages/console/tests/TerminalTest.php | 1 + packages/container/src/GenericContainer.php | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/console/tests/TerminalTest.php b/packages/console/tests/TerminalTest.php index e99cd5b09..ff249f96c 100644 --- a/packages/console/tests/TerminalTest.php +++ b/packages/console/tests/TerminalTest.php @@ -61,6 +61,7 @@ public function cursor_is_hidden_while_redrawing_content(): void $terminal = new Terminal($console); $terminal->disableTty(); $terminal->cursor = $cursor; + $events = []; iterator_to_array($terminal->render(new TextInputComponent(label: 'Name'))); diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index dccd7d049..01508ec9f 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -412,10 +412,9 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag $initializer instanceof DynamicInitializer => $initializer->initialize($class, $tag, $this->clone()), }; - $singleton = - $initializerClass->getAttribute(Singleton::class) ?? $initializerClass - ->getMethod('initialize') - ->getAttribute(Singleton::class) ?? $class->getAttribute(Singleton::class); + $singleton = $initializerClass->getAttribute(Singleton::class) ?? $initializerClass + ->getMethod('initialize') + ->getAttribute(Singleton::class) ?? $class->getAttribute(Singleton::class); if ($singleton !== null) { $this->singletonLifetimes[$object] = $singleton->lifetime; @@ -769,9 +768,11 @@ public function reset(): self if (! is_object($instance)) { continue; } + if ($instance instanceof Closure) { continue; } + $this->singletonLifetimes[$instance] ??= new ClassReflector($instance)->getAttribute(Singleton::class)->lifetime ?? Lifetime::PROCESS; if ($this->singletonLifetimes[$instance] === Lifetime::REQUEST) {