From 0439fa4641956e82bcbe563f75a4eb8fd79d8805 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 11:40:51 +0200 Subject: [PATCH 01/20] 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 583ed878e9..3cc224752e 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 0000000000..d07dbc34bd --- /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 b37f7e8e6b..7b833f7b34 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 0000000000..a447a0c3c7 --- /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 0000000000..2265cceedf --- /dev/null +++ b/packages/container/tests/Fixtures/RequestLifetimeInitializer.php @@ -0,0 +1,19 @@ + Date: Wed, 9 Sep 2026 11:44:35 +0200 Subject: [PATCH 02/20] 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 9de0854cc5..855d2a7123 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 3cc224752e..1736dffde7 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 03/20] 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 855d2a7123..a83f9836ae 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 04/20] 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 1736dffde7..dccd7d0495 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 05/20] 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 11ba5e20c9..bbe790edd5 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 06/20] 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 e99cd5b090..ff249f96c2 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 dccd7d0495..01508ec9f9 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) { From e634d4093b74fba6296b0e826e73a9a725bf85b4 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 11:40:51 +0200 Subject: [PATCH 07/20] 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 583ed878e9..3cc224752e 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 0000000000..d07dbc34bd --- /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 b37f7e8e6b..7b833f7b34 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 0000000000..a447a0c3c7 --- /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 0000000000..2265cceedf --- /dev/null +++ b/packages/container/tests/Fixtures/RequestLifetimeInitializer.php @@ -0,0 +1,19 @@ + Date: Wed, 9 Sep 2026 11:44:35 +0200 Subject: [PATCH 08/20] 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 9de0854cc5..855d2a7123 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 3cc224752e..1736dffde7 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 12aa71fce06a8ae3281835aef837c9178c5b711e Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 11:45:35 +0200 Subject: [PATCH 09/20] 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 855d2a7123..a83f9836ae 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 1c8b57cca7528774f46b448e816b1297ed25f42a Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 11:48:45 +0200 Subject: [PATCH 10/20] 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 1736dffde7..dccd7d0495 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 f8ef1da83c1b736bed37f73a76dabc38b9b61a46 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 11:50:33 +0200 Subject: [PATCH 11/20] 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 11ba5e20c9..bbe790edd5 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 0cf89ab6e45f4cd78f7c5c1d224b7da6cf602ace Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 9 Sep 2026 12:09:53 +0200 Subject: [PATCH 12/20] wip --- packages/container/src/GenericContainer.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index dccd7d0495..01508ec9f9 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) { From 6e88e3bdb9832e353b5924d4352b31acb9301fe2 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 23 Oct 2026 08:51:38 +0200 Subject: [PATCH 13/20] wip --- packages/container/src/Container.php | 2 +- packages/container/src/GenericContainer.php | 21 ++++++++++- .../container/tests/ContainerLifetimeTest.php | 37 +++++++++++++++++++ .../Commands/ContainerShowCommandTest.php | 5 ++- 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/packages/container/src/Container.php b/packages/container/src/Container.php index dba11e198a..7331cc9408 100644 --- a/packages/container/src/Container.php +++ b/packages/container/src/Container.php @@ -16,7 +16,7 @@ public function register(string $className, callable $definition): self; public function unregister(string $className, bool $tagged = false): self; - public function singleton(string $className, mixed $definition, string|UnitEnum|null $tag = null): self; + public function singleton(string $className, mixed $definition, string|UnitEnum|null $tag = null, ?Lifetime $lifetime = null): self; public function config(object $config): self; diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index 01508ec9f9..9d7a0e6472 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -35,6 +35,9 @@ final class GenericContainer implements Container /** @var WeakMap */ private WeakMap $singletonLifetimes; + /** @var array */ + private array $registeredSingletonLifetimes = []; + public function __construct( /** @var ArrayIterator $definitions */ private(set) ArrayIterator $definitions = new ArrayIterator(), @@ -151,6 +154,7 @@ public function unregister(string $className, bool $tagged = false): self unset($this->definitions[$className]); unset($this->singletonDefinitions[$className]); unset($this->resolvedSingletons[$className]); + unset($this->registeredSingletonLifetimes[$className]); if ($tagged) { foreach ($this->singletonDefinitions as $key => $definition) { @@ -159,6 +163,7 @@ public function unregister(string $className, bool $tagged = false): self } unset($this->singletonDefinitions[$key]); + unset($this->registeredSingletonLifetimes[$key]); } foreach ($this->resolvedSingletons as $key => $definition) { @@ -178,7 +183,7 @@ public function has(string $className, string|UnitEnum|null $tag = null): bool return isset($this->definitions[$className]) || isset($this->singletonDefinitions[$this->resolveTaggedName($className, $tag)]); } - public function singleton(string $className, mixed $definition, string|UnitEnum|null $tag = null): self + public function singleton(string $className, mixed $definition, string|UnitEnum|null $tag = null, ?Lifetime $lifetime = null): self { if ($definition instanceof HasTag) { $tag = $definition->tag; @@ -186,6 +191,16 @@ public function singleton(string $className, mixed $definition, string|UnitEnum| $dependencyName = $this->resolveTaggedName($className, $tag); + if ($lifetime !== null) { + $this->registeredSingletonLifetimes[$dependencyName] = $lifetime; + + if (is_object($definition) && ! $definition instanceof Closure) { + $this->singletonLifetimes[$definition] = $lifetime; + } + } else { + unset($this->registeredSingletonLifetimes[$dependencyName]); + } + $this->singletonDefinitions[$dependencyName] = $definition; unset($this->resolvedSingletons[$dependencyName]); @@ -387,6 +402,10 @@ private function resolveDependency(string $className, string|UnitEnum|null $tag if ($singletonDefinition = $this->singletonDefinitions[$dependencyName] ?? null) { $instance = $singletonDefinition instanceof Closure ? $singletonDefinition($this) : $singletonDefinition; + if (isset($this->registeredSingletonLifetimes[$dependencyName])) { + $this->singletonLifetimes[$instance] = $this->registeredSingletonLifetimes[$dependencyName]; + } + $this->resolvedSingletons[$dependencyName] = $instance; $this->resolveChain()->add($class); diff --git a/packages/container/tests/ContainerLifetimeTest.php b/packages/container/tests/ContainerLifetimeTest.php index bbe790edd5..284945b18e 100644 --- a/packages/container/tests/ContainerLifetimeTest.php +++ b/packages/container/tests/ContainerLifetimeTest.php @@ -140,6 +140,43 @@ public function registered_factories_follow_their_lifetime(): void } } + #[Test] + public function registered_factory_can_specify_a_request_lifetime(): void + { + $container = new GenericContainer(); + $container->singleton(SingletonClass::class, fn () => new SingletonClass(), lifetime: Lifetime::REQUEST); + + $instance = $container->get(SingletonClass::class); + $this->assertSame($instance, $container->get(SingletonClass::class)); + + $container->reset(); + + $this->assertNotSame($instance, $container->get(SingletonClass::class)); + } + + #[Test] + public function registered_instance_can_override_its_attribute_lifetime(): void + { + $container = new GenericContainer(); + $instance = new RequestLifetimeSingleton(); + $container->singleton(RequestLifetimeInterface::class, $instance, lifetime: Lifetime::PROCESS); + + $container->reset(); + + $this->assertSame($instance, $container->get(RequestLifetimeInterface::class)); + } + + #[Test] + public function registered_instance_can_specify_a_request_lifetime(): void + { + $container = new GenericContainer(); + $container->singleton(SingletonClass::class, new SingletonClass(), lifetime: Lifetime::REQUEST); + + $container->reset(); + + $this->assertFalse($container->has(SingletonClass::class)); + } + #[Test] public function process_initializer_singletons_survive_reset(): void { diff --git a/tests/Integration/Container/Commands/ContainerShowCommandTest.php b/tests/Integration/Container/Commands/ContainerShowCommandTest.php index 6de904d3e4..2e89df6628 100644 --- a/tests/Integration/Container/Commands/ContainerShowCommandTest.php +++ b/tests/Integration/Container/Commands/ContainerShowCommandTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\Attributes\Test; use Tempest\Container\Commands\ContainerShowCommand; use Tempest\Container\Container; +use Tempest\Container\Lifetime; use Tempest\Reflection\ClassReflector; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; use UnitEnum; @@ -45,9 +46,9 @@ public function unregister(string $className, bool $tagged = false): self return $this; } - public function singleton(string $className, mixed $definition, string|UnitEnum|null $tag = null): self + public function singleton(string $className, mixed $definition, string|UnitEnum|null $tag = null, ?Lifetime $lifetime = null): self { - $this->container->singleton($className, $definition, $tag); + $this->container->singleton($className, $definition, $tag, $lifetime); return $this; } From 9b60678144c2d944a19f8734627e8ad9c18a57ad Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 23 Oct 2026 08:54:25 +0200 Subject: [PATCH 14/20] wip --- .../config/sets/level/up-to-tempest-321.php | 18 ++++++ packages/upgrade/config/sets/tempest321.php | 13 ++++ .../UpdateContainerImplementationsRector.php | 60 +++++++++++++++++++ .../ContainerImplementation.input.php | 14 +++++ .../Tempest321/Fixtures/Unchanged.input.php | 23 +++++++ .../tests/Tempest321/Tempest321RectorTest.php | 29 +++++++++ .../tests/Tempest321/tempest321_rector.php | 11 ++++ 7 files changed, 168 insertions(+) create mode 100644 packages/upgrade/config/sets/level/up-to-tempest-321.php create mode 100644 packages/upgrade/config/sets/tempest321.php create mode 100644 packages/upgrade/src/Tempest321/UpdateContainerImplementationsRector.php create mode 100644 packages/upgrade/tests/Tempest321/Fixtures/ContainerImplementation.input.php create mode 100644 packages/upgrade/tests/Tempest321/Fixtures/Unchanged.input.php create mode 100644 packages/upgrade/tests/Tempest321/Tempest321RectorTest.php create mode 100644 packages/upgrade/tests/Tempest321/tempest321_rector.php diff --git a/packages/upgrade/config/sets/level/up-to-tempest-321.php b/packages/upgrade/config/sets/level/up-to-tempest-321.php new file mode 100644 index 0000000000..158f0d316e --- /dev/null +++ b/packages/upgrade/config/sets/level/up-to-tempest-321.php @@ -0,0 +1,18 @@ +sets([ + TempestSetList::TEMPEST_20, + TempestSetList::TEMPEST_28, + TempestSetList::TEMPEST_30, + TempestSetList::TEMPEST_34, + TempestSetList::TEMPEST_310, + TempestSetList::TEMPEST_314, + TempestSetList::TEMPEST_321, + ]); +}; diff --git a/packages/upgrade/config/sets/tempest321.php b/packages/upgrade/config/sets/tempest321.php new file mode 100644 index 0000000000..e4f70a37fd --- /dev/null +++ b/packages/upgrade/config/sets/tempest321.php @@ -0,0 +1,13 @@ +rule(UpdateContainerImplementationsRector::class); +}; diff --git a/packages/upgrade/src/Tempest321/UpdateContainerImplementationsRector.php b/packages/upgrade/src/Tempest321/UpdateContainerImplementationsRector.php new file mode 100644 index 0000000000..273841520c --- /dev/null +++ b/packages/upgrade/src/Tempest321/UpdateContainerImplementationsRector.php @@ -0,0 +1,60 @@ +implementsContainer($node)) { + return null; + } + + $singleton = $node->getMethod('singleton'); + + if (! $singleton instanceof ClassMethod || count($singleton->params) !== 3) { + return null; + } + + $singleton->params[] = new Param( + var: new Node\Expr\Variable('lifetime'), + default: new ConstFetch(new Name('null')), + type: new NullableType(new Name\FullyQualified(Lifetime::class)), + ); + + return $node; + } + + private function implementsContainer(Class_ $class): bool + { + foreach ($class->implements as $interface) { + $resolvedName = $interface->getAttribute('resolvedName'); + + if ($resolvedName instanceof Name && $resolvedName->toString() === Container::class) { + return true; + } + + if ($interface->toString() === Container::class) { + return true; + } + } + + return false; + } +} diff --git a/packages/upgrade/tests/Tempest321/Fixtures/ContainerImplementation.input.php b/packages/upgrade/tests/Tempest321/Fixtures/ContainerImplementation.input.php new file mode 100644 index 0000000000..210fc7cb71 --- /dev/null +++ b/packages/upgrade/tests/Tempest321/Fixtures/ContainerImplementation.input.php @@ -0,0 +1,14 @@ + new RectorTester(__DIR__ . '/tempest321_rector.php'); + } + + #[Test] + public function container_implementations_receive_the_optional_lifetime(): void + { + $this->rector + ->runFixture(__DIR__ . '/Fixtures/ContainerImplementation.input.php') + ->assertContains('use Tempest\\Container\\Lifetime;') + ->assertContains('public function singleton(string $className, mixed $definition, string|UnitEnum|null $tag = null, ?Lifetime $lifetime = null): self'); + } + + #[Test] + public function unrelated_or_already_updated_methods_are_unchanged(): void + { + $this->assertSame('', $this->rector->runFixture(__DIR__ . '/Fixtures/Unchanged.input.php')->actual); + } +} diff --git a/packages/upgrade/tests/Tempest321/tempest321_rector.php b/packages/upgrade/tests/Tempest321/tempest321_rector.php new file mode 100644 index 0000000000..1db881515c --- /dev/null +++ b/packages/upgrade/tests/Tempest321/tempest321_rector.php @@ -0,0 +1,11 @@ +withSets([TempestSetList::TEMPEST_321]) + ->withCache(cacheClass: MemoryCacheStorage::class); From ac7a42242619d98251acd0e1733ad8ddf74afdc1 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 23 Oct 2026 08:59:10 +0200 Subject: [PATCH 15/20] wip --- docs/1-essentials/05-container.md | 12 ++++++++++++ packages/container/src/GenericContainer.php | 2 +- packages/upgrade/src/Set/TempestLevelSetList.php | 2 ++ packages/upgrade/src/Set/TempestSetList.php | 2 ++ .../UpdateContainerImplementationsRector.php | 6 ++++-- 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/1-essentials/05-container.md b/docs/1-essentials/05-container.md index a83f9836ae..ea231fba98 100644 --- a/docs/1-essentials/05-container.md +++ b/docs/1-essentials/05-container.md @@ -260,6 +260,18 @@ final class RequestContext 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. +You can also set the lifetime when registering a singleton with the container. Use a factory for request-lifetime singletons so the container can create a fresh instance after each reset: + +```php +use Tempest\Container\Lifetime; + +$container->singleton( + RequestContext::class, + fn () => new RequestContext(), + lifetime: Lifetime::REQUEST, +); +``` + Note that you can also declare the lifetime via initializers: ```php diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index 9d7a0e6472..bb24e5706b 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -191,7 +191,7 @@ public function singleton(string $className, mixed $definition, string|UnitEnum| $dependencyName = $this->resolveTaggedName($className, $tag); - if ($lifetime !== null) { + if ($lifetime instanceof Lifetime) { $this->registeredSingletonLifetimes[$dependencyName] = $lifetime; if (is_object($definition) && ! $definition instanceof Closure) { diff --git a/packages/upgrade/src/Set/TempestLevelSetList.php b/packages/upgrade/src/Set/TempestLevelSetList.php index 458d7f992e..a9836c64f8 100644 --- a/packages/upgrade/src/Set/TempestLevelSetList.php +++ b/packages/upgrade/src/Set/TempestLevelSetList.php @@ -17,4 +17,6 @@ final class TempestLevelSetList public const string UP_TO_TEMPEST_310 = __DIR__ . '/../../config/sets/level/up-to-tempest-310.php'; public const string UP_TO_TEMPEST_314 = __DIR__ . '/../../config/sets/level/up-to-tempest-314.php'; + + public const string UP_TO_TEMPEST_321 = __DIR__ . '/../../config/sets/level/up-to-tempest-321.php'; } diff --git a/packages/upgrade/src/Set/TempestSetList.php b/packages/upgrade/src/Set/TempestSetList.php index e786902548..cf475b8265 100644 --- a/packages/upgrade/src/Set/TempestSetList.php +++ b/packages/upgrade/src/Set/TempestSetList.php @@ -17,4 +17,6 @@ final class TempestSetList public const string TEMPEST_310 = __DIR__ . '/../../config/sets/tempest310.php'; public const string TEMPEST_314 = __DIR__ . '/../../config/sets/tempest314.php'; + + public const string TEMPEST_321 = __DIR__ . '/../../config/sets/tempest321.php'; } diff --git a/packages/upgrade/src/Tempest321/UpdateContainerImplementationsRector.php b/packages/upgrade/src/Tempest321/UpdateContainerImplementationsRector.php index 273841520c..e8743d44bf 100644 --- a/packages/upgrade/src/Tempest321/UpdateContainerImplementationsRector.php +++ b/packages/upgrade/src/Tempest321/UpdateContainerImplementationsRector.php @@ -2,6 +2,8 @@ namespace Tempest\Upgrade\Tempest321; +use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Name; @@ -33,9 +35,9 @@ public function refactor(Node $node): ?Node } $singleton->params[] = new Param( - var: new Node\Expr\Variable('lifetime'), + var: new Variable('lifetime'), default: new ConstFetch(new Name('null')), - type: new NullableType(new Name\FullyQualified(Lifetime::class)), + type: new NullableType(new FullyQualified(Lifetime::class)), ); return $node; From 3229beff8d0019cd7066cea20702343d92b2a597 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 6 Nov 2026 11:25:47 +0100 Subject: [PATCH 16/20] test(container): move discovery-based lifetime tests to integration suite tempest/container does not depend on tempest/discovery, so these tests failed when the package is tested in isolation. Co-Authored-By: Claude Opus 5.5 --- .../container/tests/ContainerLifetimeTest.php | 50 ------------- .../ContainerLifetimeDiscoveryTest.php | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+), 50 deletions(-) create mode 100644 tests/Integration/Container/ContainerLifetimeDiscoveryTest.php diff --git a/packages/container/tests/ContainerLifetimeTest.php b/packages/container/tests/ContainerLifetimeTest.php index 284945b18e..c40eb30b91 100644 --- a/packages/container/tests/ContainerLifetimeTest.php +++ b/packages/container/tests/ContainerLifetimeTest.php @@ -6,26 +6,19 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use Tempest\Container\AutowireDiscovery; use Tempest\Container\Container; use Tempest\Container\GenericContainer; use Tempest\Container\Initializer; -use Tempest\Container\InitializerDiscovery; use Tempest\Container\Lifetime; use Tempest\Container\Singleton; use Tempest\Container\Tests\Fixtures\ContainerObjectE; use Tempest\Container\Tests\Fixtures\ProcessLifetimeSingleton; -use Tempest\Container\Tests\Fixtures\RequestLifetimeClassInitializer; use Tempest\Container\Tests\Fixtures\RequestLifetimeDependency; -use Tempest\Container\Tests\Fixtures\RequestLifetimeDynamicInitializer; use Tempest\Container\Tests\Fixtures\RequestLifetimeInitializer; use Tempest\Container\Tests\Fixtures\RequestLifetimeInterface; use Tempest\Container\Tests\Fixtures\RequestLifetimeSingleton; use Tempest\Container\Tests\Fixtures\SingletonClass; use Tempest\Container\Tests\Fixtures\SingletonInitializer; -use Tempest\Discovery\DiscoveryItems; -use Tempest\Discovery\DiscoveryLocation; -use Tempest\Reflection\ClassReflector; /** @internal */ final class ContainerLifetimeTest extends TestCase @@ -63,27 +56,6 @@ public function lifetimes_are_tracked_when_resolving_nested_dependencies(): void $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 { @@ -98,28 +70,6 @@ public function tagged_initializer_singletons_are_cleared(): void $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 { diff --git a/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php b/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php new file mode 100644 index 0000000000..05e9a4de54 --- /dev/null +++ b/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php @@ -0,0 +1,70 @@ +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 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)); + } + } +} From 433501e39dba4330117ecd4a00167784d232fac3 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 6 Nov 2026 11:36:39 +0100 Subject: [PATCH 17/20] fix ci --- tests/Integration/Container/ContainerLifetimeDiscoveryTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php b/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php index 05e9a4de54..c0ed3af7c9 100644 --- a/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php +++ b/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php @@ -17,13 +17,14 @@ use Tempest\Discovery\DiscoveryItems; use Tempest\Discovery\DiscoveryLocation; use Tempest\Reflection\ClassReflector; +use Tests\Tempest\Integration\FrameworkIntegrationTestCase; /** * These tests rely on `tempest/discovery`, which is not a dependency of `tempest/container`. * * @internal */ -final class ContainerLifetimeDiscoveryTest extends TestCase +final class ContainerLifetimeDiscoveryTest extends FrameworkIntegrationTestCase { #[Test] public function initializer_singletons_are_shared_until_reset(): void From bac4af80f023bb7e2069a56fdcaa78b98a4baf54 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 6 Nov 2026 11:52:58 +0100 Subject: [PATCH 18/20] fix ci --- .../Container/ContainerLifetimeDiscoveryTest.php | 11 +++++------ .../Fixtures/RequestLifetimeClassInitializer.php | 2 +- .../Fixtures/RequestLifetimeDynamicInitializer.php | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) rename {packages/container/tests => tests/Integration/Container}/Fixtures/RequestLifetimeClassInitializer.php (88%) rename {packages/container/tests => tests/Integration/Container}/Fixtures/RequestLifetimeDynamicInitializer.php (92%) diff --git a/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php b/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php index c0ed3af7c9..a2489e17c7 100644 --- a/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php +++ b/tests/Integration/Container/ContainerLifetimeDiscoveryTest.php @@ -5,18 +5,17 @@ namespace Tests\Tempest\Integration\Container; use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\TestCase; use Tempest\Container\AutowireDiscovery; use Tempest\Container\GenericContainer; use Tempest\Container\InitializerDiscovery; -use Tempest\Container\Tests\Fixtures\RequestLifetimeClassInitializer; -use Tempest\Container\Tests\Fixtures\RequestLifetimeDynamicInitializer; -use Tempest\Container\Tests\Fixtures\RequestLifetimeInitializer; -use Tempest\Container\Tests\Fixtures\RequestLifetimeInterface; -use Tempest\Container\Tests\Fixtures\RequestLifetimeSingleton; use Tempest\Discovery\DiscoveryItems; use Tempest\Discovery\DiscoveryLocation; use Tempest\Reflection\ClassReflector; +use Tests\Tempest\Integration\Container\Fixtures\RequestLifetimeClassInitializer; +use Tests\Tempest\Integration\Container\Fixtures\RequestLifetimeDynamicInitializer; +use Tests\Tempest\Integration\Container\Fixtures\RequestLifetimeInitializer; +use Tests\Tempest\Integration\Container\Fixtures\RequestLifetimeInterface; +use Tests\Tempest\Integration\Container\Fixtures\RequestLifetimeSingleton; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; /** diff --git a/packages/container/tests/Fixtures/RequestLifetimeClassInitializer.php b/tests/Integration/Container/Fixtures/RequestLifetimeClassInitializer.php similarity index 88% rename from packages/container/tests/Fixtures/RequestLifetimeClassInitializer.php rename to tests/Integration/Container/Fixtures/RequestLifetimeClassInitializer.php index 6cf4d0576b..2772ae2a9c 100644 --- a/packages/container/tests/Fixtures/RequestLifetimeClassInitializer.php +++ b/tests/Integration/Container/Fixtures/RequestLifetimeClassInitializer.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tempest\Container\Tests\Fixtures; +namespace Tests\Tempest\Integration\Container\Fixtures; use Tempest\Container\Container; use Tempest\Container\Initializer; diff --git a/packages/container/tests/Fixtures/RequestLifetimeDynamicInitializer.php b/tests/Integration/Container/Fixtures/RequestLifetimeDynamicInitializer.php similarity index 92% rename from packages/container/tests/Fixtures/RequestLifetimeDynamicInitializer.php rename to tests/Integration/Container/Fixtures/RequestLifetimeDynamicInitializer.php index 199918e5a3..233b13a006 100644 --- a/packages/container/tests/Fixtures/RequestLifetimeDynamicInitializer.php +++ b/tests/Integration/Container/Fixtures/RequestLifetimeDynamicInitializer.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tempest\Container\Tests\Fixtures; +namespace Tests\Tempest\Integration\Container\Fixtures; use Tempest\Container\Container; use Tempest\Container\DynamicInitializer; From 57a81d3d49ce7e259d3933aef77f8aef3b32cd24 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 6 Nov 2026 11:53:32 +0100 Subject: [PATCH 19/20] fix ci --- .../core/src/Exceptions/ExceptionTester.php | 2 +- packages/generation/src/Php/Attributes.php | 9 ++- packages/generation/src/Php/Changes.php | 30 ++++++--- packages/generation/src/Php/Cloner.php | 43 +++++++------ .../src/Php/DataObjects/StubFile.php | 6 +- packages/generation/src/Php/Imports.php | 35 ++++++----- packages/generation/src/Php/MethodBody.php | 19 ++++-- .../generation/src/Php/NameSimplifier.php | 63 +++++++++++-------- packages/generation/src/Php/PhpClass.php | 24 ++++--- packages/generation/src/Php/PhpFile.php | 62 ++++++++++-------- packages/generation/src/Php/PhpMethod.php | 3 +- packages/generation/src/Php/Printer.php | 55 +++++++++------- packages/generation/src/Php/SourceParser.php | 3 +- packages/generation/src/Php/TypeParser.php | 3 +- packages/generation/src/Php/ValueExporter.php | 27 +++++--- .../Fixtures/RequestLifetimeInitializer.php | 19 ++++++ .../Fixtures/RequestLifetimeInterface.php | 7 +++ .../Fixtures/RequestLifetimeSingleton.php | 13 ++++ 18 files changed, 272 insertions(+), 151 deletions(-) create mode 100644 tests/Integration/Container/Fixtures/RequestLifetimeInitializer.php create mode 100644 tests/Integration/Container/Fixtures/RequestLifetimeInterface.php create mode 100644 tests/Integration/Container/Fixtures/RequestLifetimeSingleton.php diff --git a/packages/core/src/Exceptions/ExceptionTester.php b/packages/core/src/Exceptions/ExceptionTester.php index a8e72ad894..a27ee15dfa 100644 --- a/packages/core/src/Exceptions/ExceptionTester.php +++ b/packages/core/src/Exceptions/ExceptionTester.php @@ -92,7 +92,7 @@ private function findRecordedProcessings(string|object $exception): array return true; } - return class_exists($exception) && is_a($reported, $exception, allow_string: true); + return class_exists($exception) && $reported instanceof $exception; }); } diff --git a/packages/generation/src/Php/Attributes.php b/packages/generation/src/Php/Attributes.php index cf7cd663cb..21e238b320 100644 --- a/packages/generation/src/Php/Attributes.php +++ b/packages/generation/src/Php/Attributes.php @@ -4,6 +4,9 @@ namespace Tempest\Generation\Php; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Attribute; use PhpParser\Node\AttributeGroup; use PhpParser\Node\Name; @@ -18,14 +21,14 @@ final class Attributes /** * @param array $arguments */ - public static function add(Stmt\Class_|Stmt\ClassMethod|Param $node, string $name, array $arguments = []): void + public static function add(Class_|ClassMethod|Param $node, string $name, array $arguments = []): void { $node->attrGroups[] = new AttributeGroup([ - new Attribute(new Name\FullyQualified(ltrim($name, '\\')), ValueExporter::exportArguments($arguments)), + new Attribute(new FullyQualified(ltrim($name, '\\')), ValueExporter::exportArguments($arguments)), ]); } - public static function remove(Stmt\Class_|Stmt\ClassMethod|Param $node, string $name): void + public static function remove(Class_|ClassMethod|Param $node, string $name): void { $groups = []; diff --git a/packages/generation/src/Php/Changes.php b/packages/generation/src/Php/Changes.php index 1eaa735ead..eadf6b1132 100644 --- a/packages/generation/src/Php/Changes.php +++ b/packages/generation/src/Php/Changes.php @@ -4,6 +4,16 @@ namespace Tempest\Generation\Php; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Use_; +use PhpParser\Node\Stmt\GroupUse; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Declare_; +use PhpParser\Node\DeclareItem; +use PhpParser\Node\AttributeGroup; +use PhpParser\Node\Param; +use PhpParser\Node\UseItem; use PhpParser\Node; use PhpParser\Node\Stmt; @@ -22,16 +32,16 @@ final class Changes * so any other node can only change when a name inside it is replaced, which marks it as changed. */ private const array MUTABLE_NODES = [ - Stmt\Namespace_::class => true, - Stmt\Use_::class => true, - Stmt\GroupUse::class => true, - Stmt\Class_::class => true, - Stmt\ClassMethod::class => true, - Stmt\Declare_::class => true, - Node\DeclareItem::class => true, - Node\AttributeGroup::class => true, - Node\Param::class => true, - Node\UseItem::class => true, + Namespace_::class => true, + Use_::class => true, + GroupUse::class => true, + Class_::class => true, + ClassMethod::class => true, + Declare_::class => true, + DeclareItem::class => true, + AttributeGroup::class => true, + Param::class => true, + UseItem::class => true, ]; /** diff --git a/packages/generation/src/Php/Cloner.php b/packages/generation/src/Php/Cloner.php index a61f8e8c39..1cea1ab44d 100644 --- a/packages/generation/src/Php/Cloner.php +++ b/packages/generation/src/Php/Cloner.php @@ -4,6 +4,13 @@ namespace Tempest\Generation\Php; +use PhpParser\Node\Stmt\Use_; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\GroupUse; +use PhpParser\Node\UseItem; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Name; @@ -34,7 +41,7 @@ final class Cloner public function cloneStatements(array $stmts): array { /** @var Stmt[] */ - return $this->cloneValue($stmts, Stmt\Use_::TYPE_NORMAL); + return $this->cloneValue($stmts, Use_::TYPE_NORMAL); } private function cloneValue(mixed $value, int $nameType): mixed @@ -65,10 +72,10 @@ private function cloneNode(Node $node, int $nameType): Node return $clone; } - if ($node instanceof Stmt\Namespace_) { + if ($node instanceof Namespace_) { $this->namespace = $node->name?->toString(); $this->imports = []; - } elseif ($node instanceof Stmt\Use_ || $node instanceof Stmt\GroupUse) { + } elseif ($node instanceof Use_ || $node instanceof GroupUse) { $this->import($node); } @@ -87,20 +94,20 @@ private function cloneNode(Node $node, int $nameType): Node private function nameTypeOf(Node $node): int { return match (true) { - $node instanceof Stmt\Namespace_, $node instanceof Stmt\Use_, $node instanceof Stmt\GroupUse, $node instanceof Node\UseItem => self::UNRESOLVED, - $node instanceof Expr\FuncCall => Stmt\Use_::TYPE_FUNCTION, - $node instanceof Expr\ConstFetch => Stmt\Use_::TYPE_CONSTANT, - default => Stmt\Use_::TYPE_NORMAL, + $node instanceof Namespace_, $node instanceof Use_, $node instanceof GroupUse, $node instanceof UseItem => self::UNRESOLVED, + $node instanceof FuncCall => Use_::TYPE_FUNCTION, + $node instanceof ConstFetch => Use_::TYPE_CONSTANT, + default => Use_::TYPE_NORMAL, }; } - private function import(Stmt\Use_|Stmt\GroupUse $node): void + private function import(Use_|GroupUse $node): void { foreach ($node->uses as $item) { - $type = $node->type !== Stmt\Use_::TYPE_UNKNOWN ? $node->type : $item->type; - $name = $node instanceof Stmt\GroupUse ? Name::concat($node->prefix, $item->name) : $item->name; + $type = $node->type !== Use_::TYPE_UNKNOWN ? $node->type : $item->type; + $name = $node instanceof GroupUse ? Name::concat($node->prefix, $item->name) : $item->name; - $this->imports[$type][self::aliasKey($type, $item->getAlias()->toString())] = $name->toString(); + $this->imports[$type][$this->aliasKey($type, $item->getAlias()->toString())] = $name->toString(); } } @@ -122,10 +129,10 @@ private function resolve(Name $name, int $type): void if ($name->isRelative()) { $resolved = $this->prefixNamespace($name->toString()); - } elseif ($type !== Stmt\Use_::TYPE_NORMAL && $name->isUnqualified()) { - $resolved = $this->imports[$type][self::aliasKey($type, $name->toString())] ?? $this->resolveGlobally($name, $type); + } elseif ($type !== Use_::TYPE_NORMAL && $name->isUnqualified()) { + $resolved = $this->imports[$type][$this->aliasKey($type, $name->toString())] ?? $this->resolveGlobally($name, $type); } else { - $import = $this->imports[Stmt\Use_::TYPE_NORMAL][strtolower($name->getFirst())] ?? null; + $import = $this->imports[Use_::TYPE_NORMAL][strtolower($name->getFirst())] ?? null; $resolved = match (true) { $import === null => $this->prefixNamespace($name->toString()), $name->isQualified() => $import . '\\' . $name->slice(1)?->toString(), @@ -134,7 +141,7 @@ private function resolve(Name $name, int $type): void } if ($resolved !== null) { - $name->setAttribute(Names::RESOLVED_NAME_ATTRIBUTE, new Name\FullyQualified($resolved)); + $name->setAttribute(Names::RESOLVED_NAME_ATTRIBUTE, new FullyQualified($resolved)); } } @@ -148,7 +155,7 @@ private function resolveGlobally(Name $name, int $type): ?string return null; } - if ($type === Stmt\Use_::TYPE_CONSTANT && in_array($name->toLowerString(), ['true', 'false', 'null'], strict: true)) { + if ($type === Use_::TYPE_CONSTANT && in_array($name->toLowerString(), ['true', 'false', 'null'], strict: true)) { return null; } @@ -160,8 +167,8 @@ private function prefixNamespace(string $name): string return $this->namespace === null ? $name : $this->namespace . '\\' . $name; } - private static function aliasKey(int $type, string $alias): string + private function aliasKey(int $type, string $alias): string { - return $type === Stmt\Use_::TYPE_CONSTANT ? $alias : strtolower($alias); + return $type === Use_::TYPE_CONSTANT ? $alias : strtolower($alias); } } diff --git a/packages/generation/src/Php/DataObjects/StubFile.php b/packages/generation/src/Php/DataObjects/StubFile.php index c391aa4406..e1b2573e17 100644 --- a/packages/generation/src/Php/DataObjects/StubFile.php +++ b/packages/generation/src/Php/DataObjects/StubFile.php @@ -4,6 +4,8 @@ namespace Tempest\Generation\Php\DataObjects; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Class_; use InvalidArgumentException; use PhpParser\Node\Stmt; use ReflectionClass; @@ -57,7 +59,7 @@ private static function declaresClass(string $path): bool } foreach ($stmts as $stmt) { - if (! $stmt instanceof Stmt\Namespace_) { + if (! $stmt instanceof Namespace_) { continue; } @@ -66,6 +68,6 @@ private static function declaresClass(string $path): bool break; } - return array_any($stmts, static fn (Stmt $stmt) => $stmt instanceof Stmt\Class_); + return array_any($stmts, static fn (Stmt $stmt) => $stmt instanceof Class_); } } diff --git a/packages/generation/src/Php/Imports.php b/packages/generation/src/Php/Imports.php index 9538d6d022..a3f4130fe1 100644 --- a/packages/generation/src/Php/Imports.php +++ b/packages/generation/src/Php/Imports.php @@ -4,6 +4,9 @@ namespace Tempest\Generation\Php; +use PhpParser\Node\Stmt\Use_; +use PhpParser\Node\Stmt\GroupUse; +use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Stmt; @@ -33,13 +36,13 @@ public static function from(array $stmts): self $imports = new self(); foreach ($stmts as $stmt) { - if ($stmt instanceof Stmt\Use_) { + if ($stmt instanceof Use_) { foreach ($stmt->uses as $item) { $imports->register($stmt->type, $item->name->toString(), $item->getAlias()->toString()); } - } elseif ($stmt instanceof Stmt\GroupUse) { + } elseif ($stmt instanceof GroupUse) { foreach ($stmt->uses as $item) { - $type = $stmt->type !== Stmt\Use_::TYPE_UNKNOWN ? $stmt->type : $item->type; + $type = $stmt->type !== Use_::TYPE_UNKNOWN ? $stmt->type : $item->type; $imports->register($type, Name::concat($stmt->prefix, $item->name)->toString(), $item->getAlias()->toString()); } } @@ -63,7 +66,7 @@ public static function keyOf(Name $name, int $type): ?string } if ($name->isQualified()) { - return self::key(Stmt\Use_::TYPE_NORMAL, $name->getFirst()); + return self::key(Use_::TYPE_NORMAL, $name->getFirst()); } return self::key($type, $name->getFirst()); @@ -89,7 +92,7 @@ public function resolveClassName(Name $name, ?string $namespace): string } if (! $name->isRelative()) { - $import = $this->resolve(Stmt\Use_::TYPE_NORMAL, $name->getFirst()); + $import = $this->resolve(Use_::TYPE_NORMAL, $name->getFirst()); if ($import !== null) { return $name->isQualified() ? $import . '\\' . $name->slice(1)?->toString() : $import; @@ -129,7 +132,7 @@ public function insertInto(array &$stmts): void continue; } - self::insertImport($stmts, $type, new Stmt\Use_([$item], $type)); + $this->insertImport($stmts, $type, new Use_([$item], $type)); } } @@ -140,7 +143,7 @@ public function insertInto(array &$stmts): void public static function hasUnused(array $stmts, array $used): bool { foreach ($stmts as $stmt) { - if (! $stmt instanceof Stmt\Use_) { + if (! $stmt instanceof Use_) { continue; } @@ -163,7 +166,7 @@ public static function hasUnused(array $stmts, array $used): bool public static function removeUnused(array &$stmts, array $previouslyUsed, array $used, string $comments): void { foreach ($stmts as $index => $stmt) { - if (! $stmt instanceof Stmt\Use_) { + if (! $stmt instanceof Use_) { continue; } @@ -204,16 +207,16 @@ private function updateExistingImport(array $stmts, int $type, string $name, Use { foreach ($stmts as $stmt) { $items = match (true) { - $stmt instanceof Stmt\Use_ && $stmt->type === $type => $stmt->uses, - $stmt instanceof Stmt\GroupUse => array_filter( + $stmt instanceof Use_ && $stmt->type === $type => $stmt->uses, + $stmt instanceof GroupUse => array_filter( $stmt->uses, - static fn (UseItem $item) => ($stmt->type !== Stmt\Use_::TYPE_UNKNOWN ? $stmt->type : $item->type) === $type, + static fn (UseItem $item) => ($stmt->type !== Use_::TYPE_UNKNOWN ? $stmt->type : $item->type) === $type, ), default => [], }; foreach ($items as $item) { - $itemName = $stmt instanceof Stmt\GroupUse ? Name::concat($stmt->prefix, $item->name) : $item->name; + $itemName = $stmt instanceof GroupUse ? Name::concat($stmt->prefix, $item->name) : $item->name; if (strcasecmp($itemName->toString(), $name) !== 0) { continue; @@ -233,18 +236,18 @@ private function updateExistingImport(array $stmts, int $type, string $name, Use /** * @param Stmt[] $stmts */ - private static function insertImport(array &$stmts, int $type, Stmt\Use_ $import): void + private function insertImport(array &$stmts, int $type, Use_ $import): void { $name = $import->uses[0]->name->toString(); $lastImportBefore = null; $lastImportType = null; foreach ($stmts as $index => $stmt) { - if (! $stmt instanceof Stmt\Use_ && ! $stmt instanceof Stmt\GroupUse && ! $stmt instanceof Stmt\Declare_ && ! Printer::isBlankLine($stmt)) { + if (! $stmt instanceof Use_ && ! $stmt instanceof GroupUse && ! $stmt instanceof Declare_ && ! Printer::isBlankLine($stmt)) { break; } - if (! $stmt instanceof Stmt\Use_) { + if (! $stmt instanceof Use_) { continue; } @@ -282,7 +285,7 @@ private static function insertImport(array &$stmts, int $type, Stmt\Use_ $import $position = 0; - while (isset($stmts[$position]) && $stmts[$position] instanceof Stmt\Declare_) { + while (isset($stmts[$position]) && $stmts[$position] instanceof Declare_) { $position++; } diff --git a/packages/generation/src/Php/MethodBody.php b/packages/generation/src/Php/MethodBody.php index a3dfb3fb16..9cc8337572 100644 --- a/packages/generation/src/Php/MethodBody.php +++ b/packages/generation/src/Php/MethodBody.php @@ -4,6 +4,9 @@ namespace Tempest\Generation\Php; +use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Scalar\InterpolatedString; +use PhpParser\Node\Expr\ShellExec; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Name; @@ -96,10 +99,12 @@ public function print(string $indentation): string $lines = explode("\n", $code); foreach ($lines as $index => $line) { - if ($line === '' || isset($this->verbatimLines[$index])) { + if ($line === '') { + continue; + } + if (isset($this->verbatimLines[$index])) { continue; } - $lines[$index] = $indentation . substr($line, min($this->indentation, strspn($line, " \t"))); } @@ -119,7 +124,7 @@ private static function verbatimLines(array $stmts, array $lines): array $strings = new NodeFinder()->find( $stmts, static fn (Node $node) => ( - ($node instanceof Scalar\String_ || $node instanceof Scalar\InterpolatedString || $node instanceof Expr\ShellExec) + ($node instanceof String_ || $node instanceof InterpolatedString || $node instanceof ShellExec) && $node->getStartLine() !== $node->getEndLine() ), ); @@ -129,7 +134,7 @@ private static function verbatimLines(array $stmts, array $lines): array foreach ($strings as $string) { // The parser counts the opening tag as the first line, and a string's first line keeps its indentation. $stringLines = range($string->getStartLine() - 1, $string->getEndLine() - 2); - $isHeredoc = in_array($string->getAttribute('kind'), [Scalar\String_::KIND_HEREDOC, Scalar\String_::KIND_NOWDOC], strict: true); + $isHeredoc = in_array($string->getAttribute('kind'), [String_::KIND_HEREDOC, String_::KIND_NOWDOC], strict: true); if ($isHeredoc && ! self::hasTabIndentation(array_intersect_key($lines, array_flip($stringLines)))) { continue; @@ -150,10 +155,12 @@ private static function indentationOf(array $lines, array $verbatimLines): int $indentation = PHP_INT_MAX; foreach ($lines as $index => $line) { - if (isset($verbatimLines[$index]) || trim($line) === '') { + if (isset($verbatimLines[$index])) { + continue; + } + if (trim($line) === '') { continue; } - $indentation = min($indentation, strspn($line, " \t")); } diff --git a/packages/generation/src/Php/NameSimplifier.php b/packages/generation/src/Php/NameSimplifier.php index 78fdb445e1..725540fb87 100644 --- a/packages/generation/src/Php/NameSimplifier.php +++ b/packages/generation/src/Php/NameSimplifier.php @@ -4,6 +4,15 @@ namespace Tempest\Generation\Php; +use PhpParser\Node\Stmt\Use_; +use PhpParser\Node\Stmt\GroupUse; +use PhpParser\Node\Stmt\Function_; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Name; @@ -55,17 +64,17 @@ public function simplify(array $stmts): void } foreach ($stmts as $stmt) { - $this->visit($stmt, null, Stmt\Use_::TYPE_NORMAL, inBody: false, inImplements: false, methodBody: null); + $this->visit($stmt, null, Use_::TYPE_NORMAL, inBody: false, inImplements: false, methodBody: null); } foreach ($this->names as [$name, $type, $inBody, $inImplements, $methodBody]) { $simplified = match ($type) { - Stmt\Use_::TYPE_NORMAL => $this->simplifyClassName($name, $inBody, $inImplements), - Stmt\Use_::TYPE_FUNCTION => $this->simplifyFunctionName($name, $inBody), + Use_::TYPE_NORMAL => $this->simplifyClassName($name, $inBody, $inImplements), + Use_::TYPE_FUNCTION => $this->simplifyFunctionName($name, $inBody), default => $this->qualifyBrokenName($name, $type), }; - if ($simplified !== null) { + if ($simplified instanceof Name) { $this->replace($name, $simplified); $methodBody?->replaceName($name, $simplified); } @@ -85,7 +94,7 @@ public function simplify(array $stmts): void */ private function visit(Node $node, ?Node $parent, int $nameType, bool $inBody, bool $inImplements, ?MethodBody $methodBody): void { - if ($parent !== null) { + if ($parent instanceof Node) { $this->parents[spl_object_id($node)] = $parent; } @@ -98,29 +107,29 @@ private function visit(Node $node, ?Node $parent, int $nameType, bool $inBody, b return; } - if ($node instanceof Stmt\Use_ || $node instanceof Stmt\GroupUse) { + if ($node instanceof Use_ || $node instanceof GroupUse) { return; } - if ($node instanceof Stmt\Function_) { + if ($node instanceof Function_) { $this->reservedFunctions[$node->name->toLowerString()] = true; } $childNameType = match (true) { - $node instanceof Stmt\Namespace_ => self::IGNORED, - $node instanceof Expr\FuncCall => Stmt\Use_::TYPE_FUNCTION, - $node instanceof Expr\ConstFetch => Stmt\Use_::TYPE_CONSTANT, - default => Stmt\Use_::TYPE_NORMAL, + $node instanceof Namespace_ => self::IGNORED, + $node instanceof FuncCall => Use_::TYPE_FUNCTION, + $node instanceof ConstFetch => Use_::TYPE_CONSTANT, + default => Use_::TYPE_NORMAL, }; - $hasBody = $node instanceof Stmt\ClassMethod || $node instanceof Stmt\Function_; - $bodyMethodBody = $node instanceof Stmt\ClassMethod ? $node->getAttribute(MethodBody::ATTRIBUTE) ?? $methodBody : $methodBody; + $hasBody = $node instanceof ClassMethod || $node instanceof Function_; + $bodyMethodBody = $node instanceof ClassMethod ? $node->getAttribute(MethodBody::ATTRIBUTE) ?? $methodBody : $methodBody; foreach ($node->getSubNodeNames() as $subNodeName) { $value = $node->{$subNodeName}; $isBody = $hasBody && $subNodeName === 'stmts'; $childInBody = $inBody || $isBody; $childMethodBody = $isBody ? $bodyMethodBody : $methodBody; - $childInImplements = $node instanceof Stmt\Class_ && $subNodeName === 'implements'; + $childInImplements = $node instanceof Class_ && $subNodeName === 'implements'; foreach (is_array($value) ? $value : [$value] as $child) { if (! $child instanceof Node) { @@ -143,15 +152,15 @@ private function reserve(Name $name, int $type): void $first = $name->getFirst(); - if ($type !== Stmt\Use_::TYPE_NORMAL && $name->isUnqualified()) { - if ($type === Stmt\Use_::TYPE_FUNCTION && $this->imports->resolve($type, $first) === null) { + if ($type !== Use_::TYPE_NORMAL && $name->isUnqualified()) { + if ($type === Use_::TYPE_FUNCTION && $this->imports->resolve($type, $first) === null) { $this->reservedFunctions[strtolower($first)] = true; } return; } - if ($this->imports->resolve(Stmt\Use_::TYPE_NORMAL, $first) !== null) { + if ($this->imports->resolve(Use_::TYPE_NORMAL, $first) !== null) { return; } @@ -226,16 +235,16 @@ private function simplifyClassName(Name $name, bool $inBody, bool $inImplements) $alias = $this->importClass($fullyQualifiedName); if ($alias === null) { - return $name->isFullyQualified() ? null : new Name\FullyQualified($fullyQualifiedName); + return $name->isFullyQualified() ? null : new FullyQualified($fullyQualifiedName); } - return new Name($alias, [Names::RESOLVED_NAME_ATTRIBUTE => new Name\FullyQualified($fullyQualifiedName)]); + return new Name($alias, [Names::RESOLVED_NAME_ATTRIBUTE => new FullyQualified($fullyQualifiedName)]); } private function simplifyFunctionName(Name $name, bool $inBody): ?Name { if (! $name->isFullyQualified()) { - return $this->qualifyBrokenName($name, Stmt\Use_::TYPE_FUNCTION); + return $this->qualifyBrokenName($name, Use_::TYPE_FUNCTION); } if ($inBody && ! $this->simplifyBodies) { @@ -248,16 +257,16 @@ private function simplifyFunctionName(Name $name, bool $inBody): ?Name return null; } - $alias = $this->imports->aliasOf(Stmt\Use_::TYPE_FUNCTION, $fullyQualifiedName); + $alias = $this->imports->aliasOf(Use_::TYPE_FUNCTION, $fullyQualifiedName); if ($alias === null) { $alias = Names::lastSegment($fullyQualifiedName); - if ($this->imports->resolve(Stmt\Use_::TYPE_FUNCTION, $alias) !== null || isset($this->reservedFunctions[strtolower($alias)])) { + if ($this->imports->resolve(Use_::TYPE_FUNCTION, $alias) !== null || isset($this->reservedFunctions[strtolower($alias)])) { return null; } - $this->imports->add(Stmt\Use_::TYPE_FUNCTION, $fullyQualifiedName); + $this->imports->add(Use_::TYPE_FUNCTION, $fullyQualifiedName); } return new Name($alias); @@ -282,7 +291,7 @@ private function qualifyBrokenName(Name $name, int $type): ?Name return null; } - return new Name\FullyQualified($fullyQualifiedName); + return new FullyQualified($fullyQualifiedName); } /** @@ -290,7 +299,7 @@ private function qualifyBrokenName(Name $name, int $type): ?Name */ private function importClass(string $fullyQualifiedName): ?string { - $alias = $this->imports->aliasOf(Stmt\Use_::TYPE_NORMAL, $fullyQualifiedName); + $alias = $this->imports->aliasOf(Use_::TYPE_NORMAL, $fullyQualifiedName); if ($alias !== null) { return $alias; @@ -298,7 +307,7 @@ private function importClass(string $fullyQualifiedName): ?string $shortName = Names::lastSegment($fullyQualifiedName); - if ($this->imports->resolve(Stmt\Use_::TYPE_NORMAL, $shortName) !== null) { + if ($this->imports->resolve(Use_::TYPE_NORMAL, $shortName) !== null) { return null; } @@ -314,7 +323,7 @@ private function importClass(string $fullyQualifiedName): ?string return null; } - $this->imports->add(Stmt\Use_::TYPE_NORMAL, $fullyQualifiedName); + $this->imports->add(Use_::TYPE_NORMAL, $fullyQualifiedName); return $shortName; } diff --git a/packages/generation/src/Php/PhpClass.php b/packages/generation/src/Php/PhpClass.php index 2084cd91ed..31f5fbdfb2 100644 --- a/packages/generation/src/Php/PhpClass.php +++ b/packages/generation/src/Php/PhpClass.php @@ -4,6 +4,10 @@ namespace Tempest\Generation\Php; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Name\FullyQualified; +use PhpParser\Node\Stmt\TraitUse; +use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Modifiers; use PhpParser\Node\Identifier; use PhpParser\Node\Name; @@ -16,7 +20,7 @@ * @internal */ public function __construct( - private Stmt\Class_ $node, + private Class_ $node, ) {} public function getName(): string @@ -48,7 +52,7 @@ public function setAbstract(bool $abstract = true): self public function setExtends(?string $class): self { - $this->node->extends = $class !== null ? new Name\FullyQualified(ltrim($class, '\\')) : null; + $this->node->extends = $class !== null ? new FullyQualified(ltrim($class, '\\')) : null; return $this; } @@ -59,18 +63,18 @@ public function addImplement(string $interface): self return $this; } - $this->node->implements[] = new Name\FullyQualified(ltrim($interface, '\\')); + $this->node->implements[] = new FullyQualified(ltrim($interface, '\\')); return $this; } public function addTrait(string $trait): self { - $traitUse = new Stmt\TraitUse([new Name\FullyQualified(ltrim($trait, '\\'))]); + $traitUse = new TraitUse([new FullyQualified(ltrim($trait, '\\'))]); $lastTraitUse = null; foreach ($this->node->stmts as $index => $stmt) { - if (! $stmt instanceof Stmt\TraitUse) { + if (! $stmt instanceof TraitUse) { continue; } @@ -112,7 +116,7 @@ public function setComment(?string $comment): self public function hasMethod(string $name): bool { - return $this->findMethod($name) !== null; + return $this->findMethod($name) instanceof ClassMethod; } public function getMethod(string $name): PhpMethod @@ -127,7 +131,7 @@ public function getMethod(string $name): PhpMethod */ public function addMethod(string $name): PhpMethod { - $method = new Stmt\ClassMethod($name, ['flags' => Modifiers::PUBLIC, 'stmts' => []]); + $method = new ClassMethod($name, ['flags' => Modifiers::PUBLIC, 'stmts' => []]); $index = $this->findMethodIndex($name); if ($index !== null) { @@ -159,13 +163,13 @@ private function setModifier(int $modifier, bool $enabled): self return $this; } - private function findMethod(string $name): ?Stmt\ClassMethod + private function findMethod(string $name): ?ClassMethod { - return array_find($this->node->getMethods(), static fn (Stmt\ClassMethod $method) => $method->name->toLowerString() === strtolower($name)); + return array_find($this->node->getMethods(), static fn (ClassMethod $method) => $method->name->toLowerString() === strtolower($name)); } private function findMethodIndex(string $name): ?int { - return array_find_key($this->node->stmts, static fn (Stmt $stmt) => $stmt instanceof Stmt\ClassMethod && $stmt->name->toLowerString() === strtolower($name)); + return array_find_key($this->node->stmts, static fn (Stmt $stmt) => $stmt instanceof ClassMethod && $stmt->name->toLowerString() === strtolower($name)); } } diff --git a/packages/generation/src/Php/PhpFile.php b/packages/generation/src/Php/PhpFile.php index 2baaa960be..5dc5661a32 100644 --- a/packages/generation/src/Php/PhpFile.php +++ b/packages/generation/src/Php/PhpFile.php @@ -4,6 +4,12 @@ namespace Tempest\Generation\Php; +use PhpParser\Node\Stmt\ClassLike; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Declare_; +use PhpParser\Node\Stmt\Nop; +use PhpParser\Node\Stmt\Use_; +use PhpParser\Node\Stmt\Class_; use PhpParser\Node\DeclareItem; use PhpParser\Node\Name; use PhpParser\Node\Scalar\Int_; @@ -39,7 +45,7 @@ private function __construct( public static function create(?string $namespace = null): self { - return new self($namespace ? [new Stmt\Namespace_(new Name($namespace))] : []); + return new self($namespace ? [new Namespace_(new Name($namespace))] : []); } public static function fromCode(string $code): self @@ -66,16 +72,16 @@ public function setNamespace(string $namespace): self { $existing = $this->findNamespace(); - if ($existing !== null) { + if ($existing instanceof Namespace_) { $existing->name = new Name($namespace); return $this; } - $declares = array_filter($this->stmts, static fn (Stmt $stmt) => $stmt instanceof Stmt\Declare_); - $stmts = array_filter($this->stmts, static fn (Stmt $stmt) => ! $stmt instanceof Stmt\Declare_); + $declares = array_filter($this->stmts, static fn (Stmt $stmt) => $stmt instanceof Declare_); + $stmts = array_filter($this->stmts, static fn (Stmt $stmt) => ! $stmt instanceof Declare_); - $this->stmts = [...$declares, new Stmt\Namespace_(new Name($namespace), array_values($stmts))]; + $this->stmts = [...$declares, new Namespace_(new Name($namespace), array_values($stmts))]; return $this; } @@ -91,16 +97,16 @@ public function setStrictTypes(bool $strictTypes = true): self { $declaration = $this->findStrictTypesDeclaration(); - if ($strictTypes && $declaration !== null) { + if ($strictTypes && $declaration instanceof DeclareItem) { $declaration->value = new Int_(1); } elseif ($strictTypes) { $position = $this->hasComment() ? 1 : 0; array_splice($this->stmts, $position, 0, [ - new Stmt\Declare_([new DeclareItem('strict_types', new Int_(1))]), + new Declare_([new DeclareItem('strict_types', new Int_(1))]), Printer::blankLine(), ]); - } elseif ($declaration !== null) { + } elseif ($declaration instanceof DeclareItem) { $this->removeDeclaration($declaration); } @@ -114,7 +120,7 @@ public function setComment(?string $comment): self } if ($comment !== null) { - array_unshift($this->stmts, new Stmt\Nop(['comments' => [DocComment::from($comment)]])); + array_unshift($this->stmts, new Nop(['comments' => [DocComment::from($comment)]])); } return $this; @@ -128,7 +134,7 @@ public function addUse(string $name, ?string $alias = null): self $stmts = &$this->body(); $imports = Imports::from($stmts); - $imports->add(Stmt\Use_::TYPE_NORMAL, ltrim($name, '\\'), $alias); + $imports->add(Use_::TYPE_NORMAL, ltrim($name, '\\'), $alias); $imports->insertInto($stmts); return $this; @@ -136,7 +142,7 @@ public function addUse(string $name, ?string $alias = null): self public function hasClass(?string $name = null): bool { - return $this->findClass($name) !== null; + return $this->findClass($name) instanceof Class_; } public function getClass(?string $name = null): PhpClass @@ -146,7 +152,7 @@ public function getClass(?string $name = null): PhpClass public function addClass(string $name): PhpClass { - $class = new Stmt\Class_($name); + $class = new Class_($name); $stmts = &$this->body(); array_push($stmts, ...$stmts === [] ? [$class] : [Printer::blankLine(), $class]); @@ -219,7 +225,7 @@ private function resolveNames(): void private function &body(): array { foreach ($this->stmts as $stmt) { - if ($stmt instanceof Stmt\Namespace_) { + if ($stmt instanceof Namespace_) { return $stmt->stmts; } } @@ -227,18 +233,20 @@ private function &body(): array return $this->stmts; } - private function findNamespace(): ?Stmt\Namespace_ + private function findNamespace(): ?Namespace_ { - return array_find($this->stmts, static fn (Stmt $stmt) => $stmt instanceof Stmt\Namespace_); + return array_find($this->stmts, static fn (Stmt $stmt) => $stmt instanceof Namespace_); } - private function findClass(?string $name): ?Stmt\Class_ + private function findClass(?string $name): ?Class_ { foreach ($this->body() as $stmt) { - if (! $stmt instanceof Stmt\Class_ || $stmt->name === null) { + if (! $stmt instanceof Class_) { + continue; + } + if ($stmt->name === null) { continue; } - if ($name === null || $stmt->name->toLowerString() === strtolower($name)) { return $stmt; } @@ -255,10 +263,12 @@ private function declaredClasses(): array $classes = []; foreach ($this->body() as $stmt) { - if (! $stmt instanceof Stmt\ClassLike || $stmt->name === null) { + if (! $stmt instanceof ClassLike) { + continue; + } + if ($stmt->name === null) { continue; } - $classes[$stmt->name->toLowerString()] = true; } @@ -268,7 +278,7 @@ private function declaredClasses(): array private function findStrictTypesDeclaration(): ?DeclareItem { foreach ($this->stmts as $stmt) { - if (! $stmt instanceof Stmt\Declare_) { + if (! $stmt instanceof Declare_) { continue; } @@ -285,10 +295,12 @@ private function findStrictTypesDeclaration(): ?DeclareItem private function removeDeclaration(DeclareItem $declaration): void { foreach ($this->stmts as $index => $stmt) { - if (! $stmt instanceof Stmt\Declare_ || ! in_array($declaration, $stmt->declares, strict: true)) { + if (! $stmt instanceof Declare_) { + continue; + } + if (! in_array($declaration, $stmt->declares, strict: true)) { continue; } - $stmt->declares = array_values(array_filter($stmt->declares, static fn (DeclareItem $item) => $item !== $declaration)); if ($stmt->declares === []) { @@ -319,7 +331,7 @@ private function comments(): string $comments .= $comment->getText() . "\n"; } - if (! $stmt instanceof Stmt\Class_) { + if (! $stmt instanceof Class_) { continue; } @@ -335,6 +347,6 @@ private function hasComment(): bool { $first = $this->stmts[0] ?? null; - return $first instanceof Stmt\Nop && ! Printer::isBlankLine($first) && $first->getComments() !== []; + return $first instanceof Nop && ! Printer::isBlankLine($first) && $first->getComments() !== []; } } diff --git a/packages/generation/src/Php/PhpMethod.php b/packages/generation/src/Php/PhpMethod.php index 7f34a56e57..303806d072 100644 --- a/packages/generation/src/Php/PhpMethod.php +++ b/packages/generation/src/Php/PhpMethod.php @@ -4,6 +4,7 @@ namespace Tempest\Generation\Php; +use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Param; use PhpParser\Node\Stmt; @@ -14,7 +15,7 @@ * @internal */ public function __construct( - private Stmt\ClassMethod $node, + private ClassMethod $node, ) {} public function getName(): string diff --git a/packages/generation/src/Php/Printer.php b/packages/generation/src/Php/Printer.php index 56543998db..9c96e4b97c 100644 --- a/packages/generation/src/Php/Printer.php +++ b/packages/generation/src/Php/Printer.php @@ -4,6 +4,19 @@ namespace Tempest\Generation\Php; +use PhpParser\Internal\TokenStream; +use PhpParser\Node\Name; +use PhpParser\Node\Identifier; +use PhpParser\Node\Stmt\Nop; +use PhpParser\Node\Stmt\InlineHTML; +use PhpParser\Node\Stmt\Declare_; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Param; +use PhpParser\Node\Stmt\Use_; +use PhpParser\Node\Stmt\TraitUse; +use PhpParser\Node\Stmt\EnumCase; use Override; use PhpParser\Node; use PhpParser\Node\Stmt; @@ -35,14 +48,14 @@ public function __construct() /** * A statement that separates the surrounding statements by a blank line when they are inserted into existing code. */ - public static function blankLine(): Stmt\Nop + public static function blankLine(): Nop { - return new Stmt\Nop([self::BLANK_LINE_ATTRIBUTE => true]); + return new Nop([self::BLANK_LINE_ATTRIBUTE => true]); } public static function isBlankLine(Node $node): bool { - return $node instanceof Stmt\Nop && $node->getAttribute(self::BLANK_LINE_ATTRIBUTE) === true; + return $node instanceof Nop && $node->getAttribute(self::BLANK_LINE_ATTRIBUTE) === true; } /** @@ -72,12 +85,12 @@ public function printChangedFile(array $stmts, array $originalStmts, array $toke #[Override] protected function p(Node $node, int $precedence = self::MAX_PRECEDENCE, int $lhsPrecedence = self::MAX_PRECEDENCE, bool $parentFormatPreserved = false): string { - if ($this->origTokens === null) { + if (!$this->origTokens instanceof TokenStream) { return $this->{'p' . $node->getType()}($node, $precedence, $lhsPrecedence); } $original = $node->getAttribute('origNode'); - $canBeCopied = $parentFormatPreserved || ! $node instanceof Stmt\InlineHTML; + $canBeCopied = $parentFormatPreserved || ! $node instanceof InlineHTML; if ($original instanceof Node && $canBeCopied && Changes::isUnchanged($node)) { $start = $original->getStartTokenPos(); @@ -97,19 +110,19 @@ protected function pArray(array $nodes, array $origNodes, int &$pos, int $indent } $code = $this->origTokens->getTokenCode($pos, $origNodes[0]->getStartTokenPos(), $indentAdjustment); - $pos = $this->origTokens->skipRightWhitespace($origNodes[array_key_last($origNodes)]->getEndTokenPos() + 1); + $pos = $this->origTokens->skipRightWhitespace(array_last($origNodes)->getEndTokenPos() + 1); return $code; } #[Override] - protected function pStmt_Nop(Stmt\Nop $node): string + protected function pStmt_Nop(Nop $node): string { return self::isBlankLine($node) ? self::BLANK_LINE_MARKER : ''; } #[Override] - protected function pStmt_Declare(Stmt\Declare_ $node): string + protected function pStmt_Declare(Declare_ $node): string { $stmts = $node->stmts; @@ -117,11 +130,11 @@ protected function pStmt_Declare(Stmt\Declare_ $node): string } #[Override] - protected function pStmt_Namespace(Stmt\Namespace_ $node): string + protected function pStmt_Namespace(Namespace_ $node): string { $name = $node->name; - if (! $this->canUseSemicolonNamespaces || $name === null) { + if (! $this->canUseSemicolonNamespaces || !$name instanceof Name) { return parent::pStmt_Namespace($node); } @@ -131,15 +144,15 @@ protected function pStmt_Namespace(Stmt\Namespace_ $node): string } #[Override] - protected function pClassCommon(Stmt\Class_ $node, string $afterClassToken): string + protected function pClassCommon(Class_ $node, string $afterClassToken): string { $extends = $node->extends; $declaration = - $this->pAttrGroups($node->attrGroups, $node->name === null) + $this->pAttrGroups($node->attrGroups, !$node->name instanceof Identifier) . $this->pModifiers($node->flags) . 'class' . $afterClassToken - . ($extends !== null ? ' extends ' . $this->p($extends) : '') + . ($extends instanceof Name ? ' extends ' . $this->p($extends) : '') . ($node->implements !== [] ? ' implements ' . $this->pCommaSeparated($node->implements) : ''); $this->indent(); @@ -154,7 +167,7 @@ protected function pClassCommon(Stmt\Class_ $node, string $afterClassToken): str } #[Override] - protected function pStmt_ClassMethod(Stmt\ClassMethod $node): string + protected function pStmt_ClassMethod(ClassMethod $node): string { $multiline = $this->shouldBreakParameters($node->params); $returnType = $node->returnType; @@ -168,7 +181,7 @@ protected function pStmt_ClassMethod(Stmt\ClassMethod $node): string . '(' . $this->pParams($node->params) . ')' - . ($returnType !== null ? ': ' . $this->p($returnType) : ''); + . ($returnType instanceof Node ? ': ' . $this->p($returnType) : ''); if ($node->stmts === null) { return $signature . ';'; @@ -199,7 +212,7 @@ protected function pParams(array $params): string } #[Override] - protected function pParam(Node\Param $node): string + protected function pParam(Param $node): string { if (! $this->multilineParameters || $node->attrGroups === []) { return parent::pParam($node); @@ -235,7 +248,7 @@ private function pBlocks(array $nodes): string $comments = $node->getComments(); - if ($node instanceof Stmt\Nop) { + if ($node instanceof Nop) { $code .= $this->pComments($comments); } else { $code .= ($comments !== [] ? $this->pComments($comments) . $this->nl : '') . $this->p($node); @@ -249,14 +262,14 @@ private function pBlocks(array $nodes): string private function belongTogether(Node $previous, Node $node): bool { - if ($previous instanceof Stmt\Use_ && $node instanceof Stmt\Use_) { + if ($previous instanceof Use_ && $node instanceof Use_) { return $previous->type === $node->type; } - return $previous::class === $node::class && ($node instanceof Stmt\TraitUse || $node instanceof Stmt\EnumCase); + return $previous::class === $node::class && ($node instanceof TraitUse || $node instanceof EnumCase); } - private function pMethodBody(Stmt\ClassMethod $node): string + private function pMethodBody(ClassMethod $node): string { $methodBody = $node->getAttribute(MethodBody::ATTRIBUTE); @@ -274,7 +287,7 @@ private function pMethodBody(Stmt\ClassMethod $node): string */ private function shouldBreakParameters(array $params): bool { - return array_any($params, static fn (Node\Param $param) => $param->flags !== 0 || $param->attrGroups !== [] || $param->getComments() !== []); + return array_any($params, static fn (Param $param) => $param->flags !== 0 || $param->attrGroups !== [] || $param->getComments() !== []); } private function indentation(int $additionalLevels): string diff --git a/packages/generation/src/Php/SourceParser.php b/packages/generation/src/Php/SourceParser.php index 6eca336fd6..a83810d19c 100644 --- a/packages/generation/src/Php/SourceParser.php +++ b/packages/generation/src/Php/SourceParser.php @@ -4,6 +4,7 @@ namespace Tempest\Generation\Php; +use PhpParser\Node\Stmt\Expression; use PhpParser\Error; use PhpParser\Node\Expr; use PhpParser\Node\Stmt; @@ -40,7 +41,7 @@ public static function parseExpression(string $code): Expr { [$stmts] = self::parse(" SourceParser::parseExpression($value->code), - $value instanceof UnitEnum => new Expr\ClassConstFetch(new Name\FullyQualified($value::class), $value->name), - $value === null => new Expr\ConstFetch(new Name('null')), - is_bool($value) => new Expr\ConstFetch(new Name($value ? 'true' : 'false')), + $value instanceof UnitEnum => new ClassConstFetch(new FullyQualified($value::class), $value->name), + $value === null => new ConstFetch(new Name('null')), + is_bool($value) => new ConstFetch(new Name($value ? 'true' : 'false')), is_int($value) => self::exportInteger($value), - is_float($value) => new Scalar\Float_($value), - is_string($value) => new Scalar\String_($value), + is_float($value) => new Float_($value), + is_string($value) => new String_($value), is_array($value) => self::exportArray($value), default => throw new ValueWasNotExportable($value), }; @@ -51,13 +60,13 @@ public static function exportArguments(array $arguments): array private static function exportInteger(int $value): Expr { if ($value === PHP_INT_MIN) { - return new Expr\BinaryOp\Minus(new Expr\UnaryMinus(new Scalar\Int_(PHP_INT_MAX)), new Scalar\Int_(1)); + return new Minus(new UnaryMinus(new Int_(PHP_INT_MAX)), new Int_(1)); } - return $value < 0 ? new Expr\UnaryMinus(new Scalar\Int_(-$value)) : new Scalar\Int_($value); + return $value < 0 ? new UnaryMinus(new Int_(-$value)) : new Int_($value); } - private static function exportArray(array $values): Expr\Array_ + private static function exportArray(array $values): Array_ { $items = []; $nextIndex = 0; @@ -70,6 +79,6 @@ private static function exportArray(array $values): Expr\Array_ } } - return new Expr\Array_($items, ['kind' => Expr\Array_::KIND_SHORT]); + return new Array_($items, ['kind' => Array_::KIND_SHORT]); } } diff --git a/tests/Integration/Container/Fixtures/RequestLifetimeInitializer.php b/tests/Integration/Container/Fixtures/RequestLifetimeInitializer.php new file mode 100644 index 0000000000..645407f0a1 --- /dev/null +++ b/tests/Integration/Container/Fixtures/RequestLifetimeInitializer.php @@ -0,0 +1,19 @@ + Date: Fri, 6 Nov 2026 11:54:25 +0100 Subject: [PATCH 20/20] fix ci --- packages/generation/src/Php/Attributes.php | 6 ++-- packages/generation/src/Php/Changes.php | 16 +++++------ packages/generation/src/Php/Cloner.php | 14 +++++----- .../src/Php/DataObjects/StubFile.php | 4 +-- packages/generation/src/Php/Imports.php | 6 ++-- packages/generation/src/Php/MethodBody.php | 6 ++-- .../generation/src/Php/NameSimplifier.php | 18 ++++++------ packages/generation/src/Php/PhpClass.php | 8 +++--- packages/generation/src/Php/PhpFile.php | 12 ++++---- packages/generation/src/Php/PhpMethod.php | 2 +- packages/generation/src/Php/Printer.php | 28 +++++++++---------- packages/generation/src/Php/SourceParser.php | 2 +- packages/generation/src/Php/TypeParser.php | 2 +- packages/generation/src/Php/ValueExporter.php | 18 ++++++------ 14 files changed, 71 insertions(+), 71 deletions(-) diff --git a/packages/generation/src/Php/Attributes.php b/packages/generation/src/Php/Attributes.php index 21e238b320..66cd02160c 100644 --- a/packages/generation/src/Php/Attributes.php +++ b/packages/generation/src/Php/Attributes.php @@ -4,14 +4,14 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Stmt\Class_; -use PhpParser\Node\Stmt\ClassMethod; -use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Attribute; use PhpParser\Node\AttributeGroup; use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Param; use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassMethod; /** * @internal diff --git a/packages/generation/src/Php/Changes.php b/packages/generation/src/Php/Changes.php index eadf6b1132..ebdeaf781c 100644 --- a/packages/generation/src/Php/Changes.php +++ b/packages/generation/src/Php/Changes.php @@ -4,18 +4,18 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Stmt\Namespace_; -use PhpParser\Node\Stmt\Use_; -use PhpParser\Node\Stmt\GroupUse; +use PhpParser\Node; +use PhpParser\Node\AttributeGroup; +use PhpParser\Node\DeclareItem; +use PhpParser\Node\Param; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Declare_; -use PhpParser\Node\DeclareItem; -use PhpParser\Node\AttributeGroup; -use PhpParser\Node\Param; +use PhpParser\Node\Stmt\GroupUse; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Use_; use PhpParser\Node\UseItem; -use PhpParser\Node; -use PhpParser\Node\Stmt; /** * Tells which nodes of loaded code are identical to the node they were cloned from, diff --git a/packages/generation/src/Php/Cloner.php b/packages/generation/src/Php/Cloner.php index 1cea1ab44d..c28e4d68e8 100644 --- a/packages/generation/src/Php/Cloner.php +++ b/packages/generation/src/Php/Cloner.php @@ -4,17 +4,17 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Stmt\Use_; -use PhpParser\Node\Stmt\Namespace_; -use PhpParser\Node\Stmt\GroupUse; -use PhpParser\Node\UseItem; -use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Name\FullyQualified; use PhpParser\Node; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\GroupUse; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Use_; +use PhpParser\Node\UseItem; /** * Clones parsed code so it can be changed and printed format-preserving, resolving names diff --git a/packages/generation/src/Php/DataObjects/StubFile.php b/packages/generation/src/Php/DataObjects/StubFile.php index e1b2573e17..a6739786ed 100644 --- a/packages/generation/src/Php/DataObjects/StubFile.php +++ b/packages/generation/src/Php/DataObjects/StubFile.php @@ -4,10 +4,10 @@ namespace Tempest\Generation\Php\DataObjects; -use PhpParser\Node\Stmt\Namespace_; -use PhpParser\Node\Stmt\Class_; use InvalidArgumentException; use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\Namespace_; use ReflectionClass; use Tempest\Generation\Php\Exceptions\PhpCodeWasInvalid; use Tempest\Generation\Php\SourceParser; diff --git a/packages/generation/src/Php/Imports.php b/packages/generation/src/Php/Imports.php index a3f4130fe1..6ae2883f19 100644 --- a/packages/generation/src/Php/Imports.php +++ b/packages/generation/src/Php/Imports.php @@ -4,12 +4,12 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Stmt\Use_; -use PhpParser\Node\Stmt\GroupUse; -use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Declare_; +use PhpParser\Node\Stmt\GroupUse; +use PhpParser\Node\Stmt\Use_; use PhpParser\Node\UseItem; /** diff --git a/packages/generation/src/Php/MethodBody.php b/packages/generation/src/Php/MethodBody.php index 9cc8337572..274e13650e 100644 --- a/packages/generation/src/Php/MethodBody.php +++ b/packages/generation/src/Php/MethodBody.php @@ -4,13 +4,13 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Scalar\String_; -use PhpParser\Node\Scalar\InterpolatedString; -use PhpParser\Node\Expr\ShellExec; use PhpParser\Node; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\ShellExec; use PhpParser\Node\Name; use PhpParser\Node\Scalar; +use PhpParser\Node\Scalar\InterpolatedString; +use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt; use PhpParser\NodeFinder; diff --git a/packages/generation/src/Php/NameSimplifier.php b/packages/generation/src/Php/NameSimplifier.php index 725540fb87..1145996b11 100644 --- a/packages/generation/src/Php/NameSimplifier.php +++ b/packages/generation/src/Php/NameSimplifier.php @@ -4,19 +4,19 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Stmt\Use_; -use PhpParser\Node\Stmt\GroupUse; -use PhpParser\Node\Stmt\Function_; -use PhpParser\Node\Stmt\Namespace_; -use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Stmt\ClassMethod; -use PhpParser\Node\Stmt\Class_; -use PhpParser\Node\Name\FullyQualified; use PhpParser\Node; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Function_; +use PhpParser\Node\Stmt\GroupUse; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Use_; /** * Makes sure every name resolves correctly in its namespace, importing fully qualified names where possible. diff --git a/packages/generation/src/Php/PhpClass.php b/packages/generation/src/Php/PhpClass.php index 31f5fbdfb2..cc33946128 100644 --- a/packages/generation/src/Php/PhpClass.php +++ b/packages/generation/src/Php/PhpClass.php @@ -4,14 +4,14 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Stmt\Class_; -use PhpParser\Node\Name\FullyQualified; -use PhpParser\Node\Stmt\TraitUse; -use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Modifiers; use PhpParser\Node\Identifier; use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\TraitUse; use Tempest\Generation\Php\Exceptions\MethodWasMissing; final readonly class PhpClass diff --git a/packages/generation/src/Php/PhpFile.php b/packages/generation/src/Php/PhpFile.php index 5dc5661a32..b90140b9cd 100644 --- a/packages/generation/src/Php/PhpFile.php +++ b/packages/generation/src/Php/PhpFile.php @@ -4,16 +4,16 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Stmt\ClassLike; -use PhpParser\Node\Stmt\Namespace_; -use PhpParser\Node\Stmt\Declare_; -use PhpParser\Node\Stmt\Nop; -use PhpParser\Node\Stmt\Use_; -use PhpParser\Node\Stmt\Class_; use PhpParser\Node\DeclareItem; use PhpParser\Node\Name; use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassLike; +use PhpParser\Node\Stmt\Declare_; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Nop; +use PhpParser\Node\Stmt\Use_; use PhpParser\Token; use Tempest\Generation\Php\Exceptions\ClassWasMissing; use Tempest\Support\Filesystem; diff --git a/packages/generation/src/Php/PhpMethod.php b/packages/generation/src/Php/PhpMethod.php index 303806d072..5715341471 100644 --- a/packages/generation/src/Php/PhpMethod.php +++ b/packages/generation/src/Php/PhpMethod.php @@ -4,10 +4,10 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Param; use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\ClassMethod; final readonly class PhpMethod { diff --git a/packages/generation/src/Php/Printer.php b/packages/generation/src/Php/Printer.php index 9c96e4b97c..5558d8e5c7 100644 --- a/packages/generation/src/Php/Printer.php +++ b/packages/generation/src/Php/Printer.php @@ -4,22 +4,22 @@ namespace Tempest\Generation\Php; +use Override; use PhpParser\Internal\TokenStream; -use PhpParser\Node\Name; +use PhpParser\Node; use PhpParser\Node\Identifier; -use PhpParser\Node\Stmt\Nop; -use PhpParser\Node\Stmt\InlineHTML; -use PhpParser\Node\Stmt\Declare_; -use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Name; +use PhpParser\Node\Param; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; -use PhpParser\Node\Param; -use PhpParser\Node\Stmt\Use_; -use PhpParser\Node\Stmt\TraitUse; +use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Stmt\EnumCase; -use Override; -use PhpParser\Node; -use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\InlineHTML; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Nop; +use PhpParser\Node\Stmt\TraitUse; +use PhpParser\Node\Stmt\Use_; use PhpParser\PhpVersion; use PhpParser\PrettyPrinter\Standard; use PhpParser\Token; @@ -85,7 +85,7 @@ public function printChangedFile(array $stmts, array $originalStmts, array $toke #[Override] protected function p(Node $node, int $precedence = self::MAX_PRECEDENCE, int $lhsPrecedence = self::MAX_PRECEDENCE, bool $parentFormatPreserved = false): string { - if (!$this->origTokens instanceof TokenStream) { + if (! $this->origTokens instanceof TokenStream) { return $this->{'p' . $node->getType()}($node, $precedence, $lhsPrecedence); } @@ -134,7 +134,7 @@ protected function pStmt_Namespace(Namespace_ $node): string { $name = $node->name; - if (! $this->canUseSemicolonNamespaces || !$name instanceof Name) { + if (! $this->canUseSemicolonNamespaces || ! $name instanceof Name) { return parent::pStmt_Namespace($node); } @@ -148,7 +148,7 @@ protected function pClassCommon(Class_ $node, string $afterClassToken): string { $extends = $node->extends; $declaration = - $this->pAttrGroups($node->attrGroups, !$node->name instanceof Identifier) + $this->pAttrGroups($node->attrGroups, ! $node->name instanceof Identifier) . $this->pModifiers($node->flags) . 'class' . $afterClassToken diff --git a/packages/generation/src/Php/SourceParser.php b/packages/generation/src/Php/SourceParser.php index a83810d19c..3d014f94e1 100644 --- a/packages/generation/src/Php/SourceParser.php +++ b/packages/generation/src/Php/SourceParser.php @@ -4,10 +4,10 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Stmt\Expression; use PhpParser\Error; use PhpParser\Node\Expr; use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Expression; use PhpParser\Parser; use PhpParser\ParserFactory; use PhpParser\PhpVersion; diff --git a/packages/generation/src/Php/TypeParser.php b/packages/generation/src/Php/TypeParser.php index b18721a16f..6ad4e52daf 100644 --- a/packages/generation/src/Php/TypeParser.php +++ b/packages/generation/src/Php/TypeParser.php @@ -4,11 +4,11 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\ComplexType; use PhpParser\Node\Identifier; use PhpParser\Node\IntersectionType; use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\NullableType; use PhpParser\Node\UnionType; diff --git a/packages/generation/src/Php/ValueExporter.php b/packages/generation/src/Php/ValueExporter.php index b894afc154..bcdc19366b 100644 --- a/packages/generation/src/Php/ValueExporter.php +++ b/packages/generation/src/Php/ValueExporter.php @@ -4,21 +4,21 @@ namespace Tempest\Generation\Php; -use PhpParser\Node\Expr\ClassConstFetch; -use PhpParser\Node\Name\FullyQualified; -use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Scalar\Float_; -use PhpParser\Node\Scalar\String_; -use PhpParser\Node\Expr\BinaryOp\Minus; -use PhpParser\Node\Expr\UnaryMinus; -use PhpParser\Node\Scalar\Int_; -use PhpParser\Node\Expr\Array_; use PhpParser\Node\Arg; use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\Array_; +use PhpParser\Node\Expr\BinaryOp\Minus; +use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node\Expr\UnaryMinus; use PhpParser\Node\Identifier; use PhpParser\Node\Name; +use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Scalar; +use PhpParser\Node\Scalar\Float_; +use PhpParser\Node\Scalar\Int_; +use PhpParser\Node\Scalar\String_; use Tempest\Generation\Php\Exceptions\ValueWasNotExportable; use UnitEnum;