From 7b66e87367439f0110d3f13eb1c24a9c852e5e28 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 17 Jun 2026 11:41:04 +0200 Subject: [PATCH 01/30] Make cookie and session resettable --- packages/http/src/Session/CookieReset.php | 18 ++++++++++++++++++ packages/http/src/Session/SessionReset.php | 16 ++++++++++++++++ tests/Integration/Http/CookieManagerTest.php | 13 +++++++++++++ tests/Integration/Http/SessionTest.php | 13 +++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 packages/http/src/Session/CookieReset.php create mode 100644 packages/http/src/Session/SessionReset.php diff --git a/packages/http/src/Session/CookieReset.php b/packages/http/src/Session/CookieReset.php new file mode 100644 index 0000000000..6c33ef280a --- /dev/null +++ b/packages/http/src/Session/CookieReset.php @@ -0,0 +1,18 @@ +container->unregister(CookieManager::class); + } +} \ No newline at end of file diff --git a/packages/http/src/Session/SessionReset.php b/packages/http/src/Session/SessionReset.php new file mode 100644 index 0000000000..519d6c9a96 --- /dev/null +++ b/packages/http/src/Session/SessionReset.php @@ -0,0 +1,16 @@ +container->unregister(Session::class); + } +} \ No newline at end of file diff --git a/tests/Integration/Http/CookieManagerTest.php b/tests/Integration/Http/CookieManagerTest.php index 108b97095c..2d6066dc1e 100644 --- a/tests/Integration/Http/CookieManagerTest.php +++ b/tests/Integration/Http/CookieManagerTest.php @@ -4,6 +4,7 @@ namespace Tests\Tempest\Integration\Http; +use PHPUnit\Framework\Attributes\Test; use Tempest\Core\AppConfig; use Tempest\Http\Cookie\Cookie; use Tempest\Http\Cookie\CookieManager; @@ -86,4 +87,16 @@ public function test_manually_adding_a_cookie(): void ->assertOk() ->assertHeaderMatches('set-cookie', 'key=%s; Expires=Sun, 01-Jan-2023 00:00:01 GMT; Max-Age=1; Domain=test.com; Path=/test; Secure; HttpOnly; SameSite=Strict'); } + + #[Test] + public function test_cookie_manager_is_reset(): void + { + $originalCookieManager = $this->container->get(CookieManager::class); + + $this->container->reset(); + + $newCookieManager = $this->container->get(CookieManager::class); + + $this->assertNotSame($originalCookieManager, $newCookieManager); + } } diff --git a/tests/Integration/Http/SessionTest.php b/tests/Integration/Http/SessionTest.php index 3ee186f965..5a9d81e1ae 100644 --- a/tests/Integration/Http/SessionTest.php +++ b/tests/Integration/Http/SessionTest.php @@ -5,6 +5,7 @@ namespace Tests\Tempest\Integration\Http; use PHPUnit\Framework\Attributes\Test; +use Tempest\Http\Cookie\CookieManager; use Tempest\Http\Session\Session; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; @@ -120,4 +121,16 @@ public function clear(): void $this->assertEmpty($this->session->all()); } + + #[Test] + public function test_session_is_reset(): void + { + $originalSession = $this->container->get(Session::class); + + $this->container->reset(); + + $newSession = $this->container->get(Session::class); + + $this->assertNotSame($originalSession, $newSession); + } } From 9f02d93b5a74320393cf2c30d5177128effdab64 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 17 Jun 2026 13:17:26 +0200 Subject: [PATCH 02/30] Support persistent database connections --- .../src/Connection/ConnectionInitializer.php | 15 ++++++- .../src/Connection/ConnectionReset.php | 18 ++++++++ packages/database/src/DatabaseInitializer.php | 42 ++++++++++++++----- packages/database/src/DatabaseReset.php | 18 ++++++++ packages/http/src/Session/CookieReset.php | 7 ++-- packages/http/src/Session/SessionReset.php | 6 ++- tests/Integration/Http/SessionTest.php | 1 - 7 files changed, 88 insertions(+), 19 deletions(-) create mode 100644 packages/database/src/Connection/ConnectionReset.php create mode 100644 packages/database/src/DatabaseReset.php diff --git a/packages/database/src/Connection/ConnectionInitializer.php b/packages/database/src/Connection/ConnectionInitializer.php index 11a08f128e..65cffc7e84 100644 --- a/packages/database/src/Connection/ConnectionInitializer.php +++ b/packages/database/src/Connection/ConnectionInitializer.php @@ -11,13 +11,24 @@ final class ConnectionInitializer implements Initializer { + private static ?Connection $connection = null; + #[Singleton] public function initialize(Container $container): Connection { $databaseConfig = $container->get(DatabaseConfig::class); - $connection = new PDOConnection($databaseConfig); - $connection->connect(); + $connection = self::$connection; + + if (! $connection instanceof Connection) { + $connection = new PDOConnection($databaseConfig); + $connection->connect(); + self::$connection = $connection; + } + + if ($connection instanceof PDOConnection && $connection->ping() === false) { + $connection->reconnect(); + } return $connection; } diff --git a/packages/database/src/Connection/ConnectionReset.php b/packages/database/src/Connection/ConnectionReset.php new file mode 100644 index 0000000000..f6869799ef --- /dev/null +++ b/packages/database/src/Connection/ConnectionReset.php @@ -0,0 +1,18 @@ +container->unregister(Connection::class); + } +} diff --git a/packages/database/src/DatabaseInitializer.php b/packages/database/src/DatabaseInitializer.php index c3f59bbad4..c31a86c2db 100644 --- a/packages/database/src/DatabaseInitializer.php +++ b/packages/database/src/DatabaseInitializer.php @@ -16,8 +16,11 @@ use Tempest\Reflection\ClassReflector; use UnitEnum; -final readonly class DatabaseInitializer implements DynamicInitializer +final class DatabaseInitializer implements DynamicInitializer { + /** @var Connection[] */ + private static array $connections = []; + public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): bool { return $class->getType()->matches(Database::class); @@ -26,21 +29,29 @@ public function canInitialize(ClassReflector $class, null|string|UnitEnum $tag): #[Singleton] public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Container $container): Database { - $container->singleton( - className: Connection::class, - definition: function () use ($tag, $container) { - $config = $container->get(DatabaseConfig::class, $tag); + $config = $container->get(DatabaseConfig::class, $tag); + $connectionKey = $this->getConnectionKey($config); - $connection = new PDOConnection($config); - $connection->connect(); + $connection = $config->usePersistentConnection + ? self::$connections[$connectionKey] ?? null + : null; - return $connection; - }, + if (! $connection) { + $connection = new PDOConnection($config); + $connection->connect(); + self::$connections[$connectionKey] = $connection; + } + + if ($connection instanceof PDOConnection && $connection->ping() === false) { + $connection->reconnect(); + } + + $container->singleton( + className: Connection::class, + definition: $connection, tag: $tag, ); - $connection = $container->get(Connection::class, $tag); - return new GenericDatabase( connection: $connection, transactionManager: new GenericTransactionManager($connection), @@ -48,4 +59,13 @@ className: Connection::class, eventBus: $container->get(EventBus::class), ); } + + private function getConnectionKey(DatabaseConfig $config): string + { + return hash('xxh128', serialize([ + $config->dsn, + $config->username, + $config->options, + ])); + } } diff --git a/packages/database/src/DatabaseReset.php b/packages/database/src/DatabaseReset.php new file mode 100644 index 0000000000..92f574e28b --- /dev/null +++ b/packages/database/src/DatabaseReset.php @@ -0,0 +1,18 @@ +container->unregister(Database::class); + } +} diff --git a/packages/http/src/Session/CookieReset.php b/packages/http/src/Session/CookieReset.php index 6c33ef280a..f0ccbeedfd 100644 --- a/packages/http/src/Session/CookieReset.php +++ b/packages/http/src/Session/CookieReset.php @@ -4,15 +4,16 @@ use Tempest\Container\Container; use Tempest\Container\Resettable; -use Tempest\Http\Cookie\Cookie; use Tempest\Http\Cookie\CookieManager; final readonly class CookieReset implements Resettable { - public function __construct(private Container $container) {} + public function __construct( + private Container $container, + ) {} public function reset(): void { $this->container->unregister(CookieManager::class); } -} \ No newline at end of file +} diff --git a/packages/http/src/Session/SessionReset.php b/packages/http/src/Session/SessionReset.php index 519d6c9a96..c27bcfa896 100644 --- a/packages/http/src/Session/SessionReset.php +++ b/packages/http/src/Session/SessionReset.php @@ -7,10 +7,12 @@ final readonly class SessionReset implements Resettable { - public function __construct(private Container $container) {} + public function __construct( + private Container $container, + ) {} public function reset(): void { $this->container->unregister(Session::class); } -} \ No newline at end of file +} diff --git a/tests/Integration/Http/SessionTest.php b/tests/Integration/Http/SessionTest.php index 5a9d81e1ae..5e6ab62f47 100644 --- a/tests/Integration/Http/SessionTest.php +++ b/tests/Integration/Http/SessionTest.php @@ -5,7 +5,6 @@ namespace Tests\Tempest\Integration\Http; use PHPUnit\Framework\Attributes\Test; -use Tempest\Http\Cookie\CookieManager; use Tempest\Http\Session\Session; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; From 86df8827b4af1445c62d7b0602cb0f5b0e7d607e Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 17 Jun 2026 13:19:44 +0200 Subject: [PATCH 03/30] Add TODO --- packages/database/src/Connection/Connection.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/database/src/Connection/Connection.php b/packages/database/src/Connection/Connection.php index afcb58aab6..a99578c7df 100644 --- a/packages/database/src/Connection/Connection.php +++ b/packages/database/src/Connection/Connection.php @@ -21,4 +21,6 @@ public function prepare(string $sql): PDOStatement; public function close(): void; public function connect(): void; + + // TODO: add ping and reconnect methods in 4.0 } From 6370d1077d3b620c0f42036eb01cb1cece1520b0 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 17 Jun 2026 13:30:04 +0200 Subject: [PATCH 04/30] Add tests --- .../Database/DatabaseInitializerTest.php | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/Integration/Database/DatabaseInitializerTest.php diff --git a/tests/Integration/Database/DatabaseInitializerTest.php b/tests/Integration/Database/DatabaseInitializerTest.php new file mode 100644 index 0000000000..f566120b14 --- /dev/null +++ b/tests/Integration/Database/DatabaseInitializerTest.php @@ -0,0 +1,127 @@ +container + ->removeInitializer(TestingDatabaseInitializer::class) + ->addInitializer(DatabaseInitializer::class); + } + + #[Test] + public function test_it_resolves_multiple_persistent_connections_by_tag(): void + { + $this->configureSqliteDatabase('main', 'multi-main.sqlite'); + $this->configureSqliteDatabase('backup', 'multi-backup.sqlite'); + + $main = $this->container->get(Database::class, 'main'); + $backup = $this->container->get(Database::class, 'backup'); + + $this->assertInstanceOf(GenericDatabase::class, $main); + $this->assertInstanceOf(GenericDatabase::class, $backup); + + $this->assertNotSame($main->connection, $backup->connection); + $this->assertSame($this->databasePath('multi-main.sqlite'), $main->connection->config->path); + $this->assertSame($this->databasePath('multi-backup.sqlite'), $backup->connection->config->path); + + $this->assertSame($main->connection, $this->container->get(Connection::class, 'main')); + $this->assertSame($backup->connection, $this->container->get(Connection::class, 'backup')); + } + + #[Test] + public function test_it_reuses_a_persistent_connection_for_the_same_connection_config(): void + { + $this->configureSqliteDatabase('main', 'persistent-main.sqlite'); + + $first = $this->container->get(Database::class, 'main'); + + $this->container->unregister(Database::class, tagged: true); + $this->container->unregister(Connection::class, tagged: true); + + $second = $this->container->get(Database::class, 'main'); + + $this->assertInstanceOf(GenericDatabase::class, $first); + $this->assertInstanceOf(GenericDatabase::class, $second); + + $this->assertSame($first->connection, $second->connection); + $this->assertSame($second->connection, $this->container->get(Connection::class, 'main')); + } + + #[Test] + public function test_it_does_not_reuse_a_non_persistent_connection_for_the_same_connection_config(): void + { + $this->configureSqliteDatabase('main', 'non-persistent-main.sqlite', persistent: false); + + $first = $this->container->get(Database::class, 'main'); + + $this->container->unregister(Database::class, tagged: true); + $this->container->unregister(Connection::class, tagged: true); + + $second = $this->container->get(Database::class, 'main'); + + $this->assertInstanceOf(GenericDatabase::class, $first); + $this->assertInstanceOf(GenericDatabase::class, $second); + + $this->assertNotSame($first->connection, $second->connection); + $this->assertSame($this->databasePath('non-persistent-main.sqlite'), $first->connection->config->path); + $this->assertSame($this->databasePath('non-persistent-main.sqlite'), $second->connection->config->path); + $this->assertSame($second->connection, $this->container->get(Connection::class, 'main')); + } + + #[Test] + public function test_it_does_not_reuse_a_persistent_connection_for_the_same_tag_with_a_different_connection_config(): void + { + $this->configureSqliteDatabase('main', 'first-main.sqlite'); + + $first = $this->container->get(Database::class, 'main'); + + $this->container->unregister(Database::class, tagged: true); + $this->container->unregister(Connection::class, tagged: true); + + $this->configureSqliteDatabase('main', 'second-main.sqlite'); + + $second = $this->container->get(Database::class, 'main'); + + $this->assertInstanceOf(GenericDatabase::class, $first); + $this->assertInstanceOf(GenericDatabase::class, $second); + + $this->assertNotSame($first->connection, $second->connection); + $this->assertSame($this->databasePath('first-main.sqlite'), $first->connection->config->path); + $this->assertSame($this->databasePath('second-main.sqlite'), $second->connection->config->path); + $this->assertSame($second->connection, $this->container->get(Connection::class, 'main')); + } + + private function configureSqliteDatabase(string $tag, string $filename, bool $persistent = true): void + { + $path = $this->databasePath($filename); + + if (is_file($path)) { + unlink($path); + } + + $this->container->config(new SQLiteConfig( + path: $path, + persistent: $persistent, + tag: $tag, + )); + } + + private function databasePath(string $filename): string + { + return $this->internalStorage . '/' . $filename; + } +} From 0073f19c9ccdefe53c7077edab9555da70dfeff7 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 17 Jun 2026 13:33:10 +0200 Subject: [PATCH 05/30] Move session auth reset --- .../src/Authentication/SessionAuthenticator.php | 10 ++-------- .../SessionAuthenticatorReset.php | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 packages/auth/src/Authentication/SessionAuthenticatorReset.php diff --git a/packages/auth/src/Authentication/SessionAuthenticator.php b/packages/auth/src/Authentication/SessionAuthenticator.php index fc5d98d1d1..572b4fcfef 100644 --- a/packages/auth/src/Authentication/SessionAuthenticator.php +++ b/packages/auth/src/Authentication/SessionAuthenticator.php @@ -4,11 +4,10 @@ namespace Tempest\Auth\Authentication; -use Tempest\Container\Resettable; use Tempest\Http\Session\Session; use Tempest\Http\Session\SessionManager; -final class SessionAuthenticator implements Authenticator, Resettable +final class SessionAuthenticator implements Authenticator { public const string AUTHENTICATABLE_KEY = '#authenticatable:id'; @@ -77,12 +76,7 @@ public function current(): ?Authenticatable return $this->current; } - public function reset(): void - { - $this->clearCurrent(); - } - - private function clearCurrent(): void + public function clearCurrent(): void { $this->currentId = null; $this->currentClass = null; diff --git a/packages/auth/src/Authentication/SessionAuthenticatorReset.php b/packages/auth/src/Authentication/SessionAuthenticatorReset.php new file mode 100644 index 0000000000..992b5e72bd --- /dev/null +++ b/packages/auth/src/Authentication/SessionAuthenticatorReset.php @@ -0,0 +1,17 @@ +sessionAuthenticator->clearCurrent(); + } +} From 6033bd7d3c0ba53a139edc8d4c1ef6b043bdc5a3 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 17 Jun 2026 13:35:23 +0200 Subject: [PATCH 06/30] Move session auth reset --- packages/auth/tests/SessionAuthenticatorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/auth/tests/SessionAuthenticatorTest.php b/packages/auth/tests/SessionAuthenticatorTest.php index c4aa78a3f4..a3cebccafe 100644 --- a/packages/auth/tests/SessionAuthenticatorTest.php +++ b/packages/auth/tests/SessionAuthenticatorTest.php @@ -9,6 +9,7 @@ use Tempest\Auth\Authentication\Authenticatable; use Tempest\Auth\Authentication\AuthenticatableResolver; use Tempest\Auth\Authentication\SessionAuthenticator; +use Tempest\Auth\Authentication\SessionAuthenticatorReset; use Tempest\Container\Resettable; use Tempest\DateTime\DateTime; use Tempest\Http\Session\Session; @@ -100,10 +101,9 @@ public function reset_clears_the_cached_current_authenticatable(): void authenticatableResolver: $resolver, ); - $this->assertInstanceOf(Resettable::class, $authenticator); $this->assertSame($authenticatable, $authenticator->current()); - $authenticator->reset(); + new SessionAuthenticatorReset($authenticator)->reset(); $this->assertSame($authenticatable, $authenticator->current()); $this->assertSame(2, $resolver->resolveCalls); From 4dfc6f17c29fa96f2276848b975774f9658b24da Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 17 Jun 2026 13:36:20 +0200 Subject: [PATCH 07/30] QA --- packages/auth/tests/SessionAuthenticatorTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/auth/tests/SessionAuthenticatorTest.php b/packages/auth/tests/SessionAuthenticatorTest.php index a3cebccafe..6e791e796d 100644 --- a/packages/auth/tests/SessionAuthenticatorTest.php +++ b/packages/auth/tests/SessionAuthenticatorTest.php @@ -10,7 +10,6 @@ use Tempest\Auth\Authentication\AuthenticatableResolver; use Tempest\Auth\Authentication\SessionAuthenticator; use Tempest\Auth\Authentication\SessionAuthenticatorReset; -use Tempest\Container\Resettable; use Tempest\DateTime\DateTime; use Tempest\Http\Session\Session; use Tempest\Http\Session\SessionId; From 4d23e94185934212cc2e58017472c1dc3589424a Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 17 Jun 2026 13:39:50 +0200 Subject: [PATCH 08/30] QA --- tests/Integration/Database/DatabaseInitializerTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Integration/Database/DatabaseInitializerTest.php b/tests/Integration/Database/DatabaseInitializerTest.php index f566120b14..3ee7b7b8f0 100644 --- a/tests/Integration/Database/DatabaseInitializerTest.php +++ b/tests/Integration/Database/DatabaseInitializerTest.php @@ -35,7 +35,9 @@ public function test_it_resolves_multiple_persistent_connections_by_tag(): void $this->assertInstanceOf(GenericDatabase::class, $backup); $this->assertNotSame($main->connection, $backup->connection); + // @phpstan-ignore-next-line $this->assertSame($this->databasePath('multi-main.sqlite'), $main->connection->config->path); + // @phpstan-ignore-next-line $this->assertSame($this->databasePath('multi-backup.sqlite'), $backup->connection->config->path); $this->assertSame($main->connection, $this->container->get(Connection::class, 'main')); @@ -77,7 +79,9 @@ public function test_it_does_not_reuse_a_non_persistent_connection_for_the_same_ $this->assertInstanceOf(GenericDatabase::class, $second); $this->assertNotSame($first->connection, $second->connection); + // @phpstan-ignore-next-line $this->assertSame($this->databasePath('non-persistent-main.sqlite'), $first->connection->config->path); + // @phpstan-ignore-next-line $this->assertSame($this->databasePath('non-persistent-main.sqlite'), $second->connection->config->path); $this->assertSame($second->connection, $this->container->get(Connection::class, 'main')); } @@ -100,7 +104,9 @@ public function test_it_does_not_reuse_a_persistent_connection_for_the_same_tag_ $this->assertInstanceOf(GenericDatabase::class, $second); $this->assertNotSame($first->connection, $second->connection); + // @phpstan-ignore-next-line $this->assertSame($this->databasePath('first-main.sqlite'), $first->connection->config->path); + // @phpstan-ignore-next-line $this->assertSame($this->databasePath('second-main.sqlite'), $second->connection->config->path); $this->assertSame($second->connection, $this->container->get(Connection::class, 'main')); } From 934382d4f0064622f0453b6ec16d8749184158bf Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 08:17:05 +0200 Subject: [PATCH 09/30] Small fixes --- .../src/Connection/ConnectionInitializer.php | 14 ++--- .../src/Connection/ConnectionReset.php | 2 +- packages/database/src/DatabaseInitializer.php | 5 +- packages/database/src/DatabaseReset.php | 2 +- .../Database/DatabaseInitializerTest.php | 55 +++++++++++++++++++ 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/packages/database/src/Connection/ConnectionInitializer.php b/packages/database/src/Connection/ConnectionInitializer.php index 65cffc7e84..3ed654f723 100644 --- a/packages/database/src/Connection/ConnectionInitializer.php +++ b/packages/database/src/Connection/ConnectionInitializer.php @@ -16,17 +16,17 @@ final class ConnectionInitializer implements Initializer #[Singleton] public function initialize(Container $container): Connection { - $databaseConfig = $container->get(DatabaseConfig::class); + $config = $container->get(DatabaseConfig::class); - $connection = self::$connection; + $connection = $config->usePersistentConnection + ? self::$connection + : null; - if (! $connection instanceof Connection) { - $connection = new PDOConnection($databaseConfig); + if (!$connection instanceof Connection) { + $connection = new PDOConnection($config); $connection->connect(); self::$connection = $connection; - } - - if ($connection instanceof PDOConnection && $connection->ping() === false) { + } elseif ($connection instanceof PDOConnection && $connection->ping() === false) { $connection->reconnect(); } diff --git a/packages/database/src/Connection/ConnectionReset.php b/packages/database/src/Connection/ConnectionReset.php index f6869799ef..e8d5822b01 100644 --- a/packages/database/src/Connection/ConnectionReset.php +++ b/packages/database/src/Connection/ConnectionReset.php @@ -13,6 +13,6 @@ public function __construct( public function reset(): void { - $this->container->unregister(Connection::class); + $this->container->unregister(Connection::class, tagged: true); } } diff --git a/packages/database/src/DatabaseInitializer.php b/packages/database/src/DatabaseInitializer.php index c31a86c2db..c2af285650 100644 --- a/packages/database/src/DatabaseInitializer.php +++ b/packages/database/src/DatabaseInitializer.php @@ -40,9 +40,7 @@ public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Con $connection = new PDOConnection($config); $connection->connect(); self::$connections[$connectionKey] = $connection; - } - - if ($connection instanceof PDOConnection && $connection->ping() === false) { + } elseif ($connection instanceof PDOConnection && $connection->ping() === false) { $connection->reconnect(); } @@ -66,6 +64,7 @@ private function getConnectionKey(DatabaseConfig $config): string $config->dsn, $config->username, $config->options, + $config->password, ])); } } diff --git a/packages/database/src/DatabaseReset.php b/packages/database/src/DatabaseReset.php index 92f574e28b..f6d8171610 100644 --- a/packages/database/src/DatabaseReset.php +++ b/packages/database/src/DatabaseReset.php @@ -13,6 +13,6 @@ public function __construct( public function reset(): void { - $this->container->unregister(Database::class); + $this->container->unregister(Database::class, tagged: true); } } diff --git a/tests/Integration/Database/DatabaseInitializerTest.php b/tests/Integration/Database/DatabaseInitializerTest.php index 3ee7b7b8f0..534c764582 100644 --- a/tests/Integration/Database/DatabaseInitializerTest.php +++ b/tests/Integration/Database/DatabaseInitializerTest.php @@ -3,8 +3,11 @@ namespace Tests\Tempest\Integration\Database; use PHPUnit\Framework\Attributes\Test; +use ReflectionMethod; +use Tempest\Database\Config\MysqlConfig; use Tempest\Database\Config\SQLiteConfig; use Tempest\Database\Connection\Connection; +use Tempest\Database\Connection\PDOConnection; use Tempest\Database\Database; use Tempest\Database\DatabaseInitializer; use Tempest\Database\GenericDatabase; @@ -111,6 +114,58 @@ public function test_it_does_not_reuse_a_persistent_connection_for_the_same_tag_ $this->assertSame($second->connection, $this->container->get(Connection::class, 'main')); } + #[Test] + public function test_it_does_not_reuse_a_persistent_connection_when_only_the_password_differs(): void + { + $initializer = new DatabaseInitializer(); + $method = new ReflectionMethod($initializer, 'getConnectionKey'); + + $first = new MysqlConfig( + host: 'localhost', + username: 'tempest', + password: 'first-password', // @mago-expect lint:no-literal-password + database: 'tempest', + persistent: true, + tag: 'main', + ); + + $second = new MysqlConfig( + host: 'localhost', + username: 'tempest', + password: 'second-password', // @mago-expect lint:no-literal-password + database: 'tempest', + persistent: true, + tag: 'main', + ); + + $this->assertNotSame( + $method->invoke($initializer, $first), + $method->invoke($initializer, $second), + ); + } + + #[Test] + public function test_it_reconnects_a_stale_persistent_connection_when_reusing_it(): void + { + $this->configureSqliteDatabase('main', 'stale-persistent-main.sqlite'); + + $first = $this->container->get(Database::class, 'main'); + + $this->assertInstanceOf(GenericDatabase::class, $first); + $this->assertInstanceOf(PDOConnection::class, $first->connection); + + $first->connection->close(); + + $this->container->unregister(Database::class, tagged: true); + $this->container->unregister(Connection::class, tagged: true); + + $second = $this->container->get(Database::class, 'main'); + + $this->assertInstanceOf(GenericDatabase::class, $second); + $this->assertSame($first->connection, $second->connection); + $this->assertTrue($second->connection->ping()); + } + private function configureSqliteDatabase(string $tag, string $filename, bool $persistent = true): void { $path = $this->databasePath($filename); From 8a0b1760980f36b88702431e93f28bc5f89961a0 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 08:18:12 +0200 Subject: [PATCH 10/30] QA --- packages/database/src/Connection/ConnectionInitializer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/database/src/Connection/ConnectionInitializer.php b/packages/database/src/Connection/ConnectionInitializer.php index 3ed654f723..0069cf050a 100644 --- a/packages/database/src/Connection/ConnectionInitializer.php +++ b/packages/database/src/Connection/ConnectionInitializer.php @@ -22,7 +22,7 @@ public function initialize(Container $container): Connection ? self::$connection : null; - if (!$connection instanceof Connection) { + if (! $connection instanceof Connection) { $connection = new PDOConnection($config); $connection->connect(); self::$connection = $connection; From 1e231c8f6835a458c2ed9943be204ece8fb72185 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 08:34:39 +0200 Subject: [PATCH 11/30] Add WorkerModeApplication --- packages/console/src/ConsoleApplication.php | 4 +- .../Exceptions/ConsoleExceptionHandler.php | 6 +- packages/core/src/FrameworkKernel.php | 4 +- packages/core/src/Kernel.php | 2 +- .../src/Exceptions/HttpExceptionHandler.php | 4 +- packages/router/src/HttpApplication.php | 2 + packages/router/src/WorkerModeApplication.php | 41 ++++++ .../Router/WorkerModeApplicationTest.php | 122 ++++++++++++++++++ 8 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 packages/router/src/WorkerModeApplication.php create mode 100644 tests/Integration/Router/WorkerModeApplicationTest.php diff --git a/packages/console/src/ConsoleApplication.php b/packages/console/src/ConsoleApplication.php index 7036f9c6b4..946579b0ce 100644 --- a/packages/console/src/ConsoleApplication.php +++ b/packages/console/src/ConsoleApplication.php @@ -58,6 +58,8 @@ public function run(): never throw new ExitCodeWasInvalid($exitCode); } - $this->container->get(Kernel::class)->shutdown($exitCode); + $this->container->get(Kernel::class)->shutdown(); + + exit($exitCode); } } diff --git a/packages/console/src/Exceptions/ConsoleExceptionHandler.php b/packages/console/src/Exceptions/ConsoleExceptionHandler.php index 3c9d20f9fe..0e55d9a05e 100644 --- a/packages/console/src/Exceptions/ConsoleExceptionHandler.php +++ b/packages/console/src/Exceptions/ConsoleExceptionHandler.php @@ -31,7 +31,7 @@ public function __construct( private ExceptionProcessor $exceptionProcessor, ) {} - public function handle(Throwable $throwable): void + public function handle(Throwable $throwable): never { try { $this->exceptionProcessor->process($throwable); @@ -70,7 +70,9 @@ public function handle(Throwable $throwable): void ? $throwable->getExitCode() : ExitCode::ERROR; - $this->kernel->shutdown($exitCode->value); + $this->kernel->shutdown(); + + exit($exitCode->value); } } diff --git a/packages/core/src/FrameworkKernel.php b/packages/core/src/FrameworkKernel.php index 527ea68557..bf64e6dffe 100644 --- a/packages/core/src/FrameworkKernel.php +++ b/packages/core/src/FrameworkKernel.php @@ -98,12 +98,10 @@ public function validateRoot(): self return $this; } - public function shutdown(int|string $status = ''): never + public function shutdown(): void { $this->finishDeferredTasks() ->event(KernelEvent::SHUTDOWN); - - exit($status); } public function loadComposer(): self diff --git a/packages/core/src/Kernel.php b/packages/core/src/Kernel.php index 41cef0af59..cd56b42ad5 100644 --- a/packages/core/src/Kernel.php +++ b/packages/core/src/Kernel.php @@ -23,5 +23,5 @@ public static function boot( ?string $internalStorage = null, ): self; - public function shutdown(int|string $status = ''): never; + public function shutdown(): void; } diff --git a/packages/router/src/Exceptions/HttpExceptionHandler.php b/packages/router/src/Exceptions/HttpExceptionHandler.php index da55626515..d23920f39b 100644 --- a/packages/router/src/Exceptions/HttpExceptionHandler.php +++ b/packages/router/src/Exceptions/HttpExceptionHandler.php @@ -25,7 +25,7 @@ public function __construct( private RouteConfig $routeConfig, ) {} - public function handle(Throwable $throwable): void + public function handle(Throwable $throwable): never { $request = $this->container->get(Request::class); @@ -34,6 +34,8 @@ public function handle(Throwable $throwable): void $this->responseSender->send($this->renderResponse($request, $throwable)); } finally { $this->kernel->shutdown(); + + exit(); } } diff --git a/packages/router/src/HttpApplication.php b/packages/router/src/HttpApplication.php index 965655a34a..4c48cc06fb 100644 --- a/packages/router/src/HttpApplication.php +++ b/packages/router/src/HttpApplication.php @@ -35,5 +35,7 @@ public function run(): never ); $this->container->get(Kernel::class)->shutdown(); + + exit(); } } diff --git a/packages/router/src/WorkerModeApplication.php b/packages/router/src/WorkerModeApplication.php new file mode 100644 index 0000000000..f723aff79b --- /dev/null +++ b/packages/router/src/WorkerModeApplication.php @@ -0,0 +1,41 @@ +get(WorkerModeApplication::class); + } + + public function run(): void + { + $router = $this->container->get(Router::class); + $psrRequest = $this->container->get(RequestFactory::class)->make(); + $responseSender = $this->container->get(ResponseSender::class); + + $responseSender->send( + response: $router->dispatch($psrRequest), + ); + + $this->container->get(Kernel::class)->shutdown(); + + $this->container->reset(); + } +} diff --git a/tests/Integration/Router/WorkerModeApplicationTest.php b/tests/Integration/Router/WorkerModeApplicationTest.php new file mode 100644 index 0000000000..9d6d48b1b3 --- /dev/null +++ b/tests/Integration/Router/WorkerModeApplicationTest.php @@ -0,0 +1,122 @@ +createApplication( + response: $response, + assertDispatchedRequest: function (ServerRequestInterface $request): void { + $this->assertSame(['name' => 'Tempest'], $request->getParsedBody()); + }, + assertSentResponse: function (Response $sentResponse) use ($response): void { + $this->assertSame($response, $sentResponse); + }, + ); + + $application->run(); + } + + #[Test] + public function test_it_shuts_down_the_kernel(): void + { + $kernel = $this->createMock(Kernel::class); + $kernel->expects($this->once())->method('shutdown'); + + $application = $this->createApplication(kernel: $kernel); + + $application->run(); + } + + #[Test] + public function test_it_resets_the_container(): void + { + $container = $this->createMock(Container::class); + $container->expects($this->once())->method('reset')->willReturnSelf(); + + $application = $this->createApplication(container: $container); + + $application->run(); + } + + private function createApplication( + ?Container $container = null, + ?Kernel $kernel = null, + ?Response $response = null, + ?callable $assertDispatchedRequest = null, + ?callable $assertSentResponse = null, + ): WorkerModeApplication { + if (! $container instanceof Container) { + $container = $this->createMock(Container::class); + $container->expects($this->once())->method('reset')->willReturnSelf(); + } + + if (! $kernel instanceof Kernel) { + $kernel = $this->createMock(Kernel::class); + $kernel->expects($this->once())->method('shutdown'); + } + + $response ??= new GenericResponse(Status::OK); + + $inputStream = $this->createMock(InputStream::class); + $inputStream + ->expects($this->once()) + ->method('parse') + ->willReturn(['name' => 'Tempest']); + + $requestFactory = new RequestFactory($inputStream); + + $router = $this->createMock(Router::class); + $router + ->expects($this->once()) + ->method('dispatch') + ->willReturnCallback(function (ServerRequestInterface $request) use ($response, $assertDispatchedRequest): Response { + $assertDispatchedRequest?->__invoke($request); + + return $response; + }); + + $responseSender = $this->createMock(ResponseSender::class); + $responseSender + ->expects($this->once()) + ->method('send') + ->willReturnCallback(function (Response $response) use ($assertSentResponse): Response { + $assertSentResponse?->__invoke($response); + + return $response; + }); + + $container + ->expects($this->exactly(4)) + ->method('get') + ->willReturnCallback(fn (string $className): object => match ($className) { + Router::class => $router, + RequestFactory::class => $requestFactory, + ResponseSender::class => $responseSender, + Kernel::class => $kernel, + }); + + return new WorkerModeApplication($container); + } +} From 210862a23c5bbc6154513a90d3d581e53b988226 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 09:39:33 +0200 Subject: [PATCH 12/30] Add WorkerModeApplication --- .../Router/WorkerModeApplicationTest.php | 122 ------------------ 1 file changed, 122 deletions(-) delete mode 100644 tests/Integration/Router/WorkerModeApplicationTest.php diff --git a/tests/Integration/Router/WorkerModeApplicationTest.php b/tests/Integration/Router/WorkerModeApplicationTest.php deleted file mode 100644 index 9d6d48b1b3..0000000000 --- a/tests/Integration/Router/WorkerModeApplicationTest.php +++ /dev/null @@ -1,122 +0,0 @@ -createApplication( - response: $response, - assertDispatchedRequest: function (ServerRequestInterface $request): void { - $this->assertSame(['name' => 'Tempest'], $request->getParsedBody()); - }, - assertSentResponse: function (Response $sentResponse) use ($response): void { - $this->assertSame($response, $sentResponse); - }, - ); - - $application->run(); - } - - #[Test] - public function test_it_shuts_down_the_kernel(): void - { - $kernel = $this->createMock(Kernel::class); - $kernel->expects($this->once())->method('shutdown'); - - $application = $this->createApplication(kernel: $kernel); - - $application->run(); - } - - #[Test] - public function test_it_resets_the_container(): void - { - $container = $this->createMock(Container::class); - $container->expects($this->once())->method('reset')->willReturnSelf(); - - $application = $this->createApplication(container: $container); - - $application->run(); - } - - private function createApplication( - ?Container $container = null, - ?Kernel $kernel = null, - ?Response $response = null, - ?callable $assertDispatchedRequest = null, - ?callable $assertSentResponse = null, - ): WorkerModeApplication { - if (! $container instanceof Container) { - $container = $this->createMock(Container::class); - $container->expects($this->once())->method('reset')->willReturnSelf(); - } - - if (! $kernel instanceof Kernel) { - $kernel = $this->createMock(Kernel::class); - $kernel->expects($this->once())->method('shutdown'); - } - - $response ??= new GenericResponse(Status::OK); - - $inputStream = $this->createMock(InputStream::class); - $inputStream - ->expects($this->once()) - ->method('parse') - ->willReturn(['name' => 'Tempest']); - - $requestFactory = new RequestFactory($inputStream); - - $router = $this->createMock(Router::class); - $router - ->expects($this->once()) - ->method('dispatch') - ->willReturnCallback(function (ServerRequestInterface $request) use ($response, $assertDispatchedRequest): Response { - $assertDispatchedRequest?->__invoke($request); - - return $response; - }); - - $responseSender = $this->createMock(ResponseSender::class); - $responseSender - ->expects($this->once()) - ->method('send') - ->willReturnCallback(function (Response $response) use ($assertSentResponse): Response { - $assertSentResponse?->__invoke($response); - - return $response; - }); - - $container - ->expects($this->exactly(4)) - ->method('get') - ->willReturnCallback(fn (string $className): object => match ($className) { - Router::class => $router, - RequestFactory::class => $requestFactory, - ResponseSender::class => $responseSender, - Kernel::class => $kernel, - }); - - return new WorkerModeApplication($container); - } -} From 8e94f11241a17d2e7e99db8f52f6256fbeb18f79 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 09:42:26 +0200 Subject: [PATCH 13/30] Add deferred tasks reset --- packages/core/src/DeferredTasksReset.php | 18 ++++++++++++++++++ tests/Integration/Core/DeferredTasksTest.php | 13 +++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 packages/core/src/DeferredTasksReset.php diff --git a/packages/core/src/DeferredTasksReset.php b/packages/core/src/DeferredTasksReset.php new file mode 100644 index 0000000000..86e164de45 --- /dev/null +++ b/packages/core/src/DeferredTasksReset.php @@ -0,0 +1,18 @@ +container->unregister(DeferredTasks::class); + } +} \ No newline at end of file diff --git a/tests/Integration/Core/DeferredTasksTest.php b/tests/Integration/Core/DeferredTasksTest.php index 129040906b..5a68a06210 100644 --- a/tests/Integration/Core/DeferredTasksTest.php +++ b/tests/Integration/Core/DeferredTasksTest.php @@ -4,6 +4,7 @@ namespace Tests\Tempest\Integration\Core; +use PHPUnit\Framework\Attributes\Test; use Tempest\Container\Container; use Tempest\Core\DeferredTasks; use Tempest\Core\Kernel\FinishDeferredTasks; @@ -47,4 +48,16 @@ public function test_deferred_tasks_are_executed_with_container_parameters(): vo $this->assertTrue($executed); $this->assertEmpty($this->container->get(DeferredTasks::class)->getTasks()); } + + #[Test] + public function test_tasks_are_reset(): void + { + $first = $this->container->get(DeferredTasks::class); + + $this->container->reset(); + + $second = $this->container->get(DeferredTasks::class); + + $this->assertNotSame($first, $second); + } } From 123b528bc5fe81251e9ede6025a927de17656bdc Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 09:42:37 +0200 Subject: [PATCH 14/30] Add deferred tasks reset --- packages/core/src/DeferredTasksReset.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/DeferredTasksReset.php b/packages/core/src/DeferredTasksReset.php index 86e164de45..cc069a01d3 100644 --- a/packages/core/src/DeferredTasksReset.php +++ b/packages/core/src/DeferredTasksReset.php @@ -15,4 +15,4 @@ public function reset(): void { $this->container->unregister(DeferredTasks::class); } -} \ No newline at end of file +} From 7f226a5b6536562c16e0bcde6088e651944884f4 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 10:04:06 +0200 Subject: [PATCH 15/30] Handle transactions during reset --- .../database/src/Connection/Connection.php | 1 + .../src/Connection/ConnectionReset.php | 12 +++++ .../database/src/Connection/PDOConnection.php | 9 ++++ .../Exceptions/CouldNotResetConnection.php | 13 +++++ .../Connection/ConnectionResetTest.php | 41 ++++++++++++++ .../Database/DatabaseResetTest.php | 53 +++++++++++++++++++ 6 files changed, 129 insertions(+) create mode 100644 packages/database/src/Exceptions/CouldNotResetConnection.php create mode 100644 tests/Integration/Database/Connection/ConnectionResetTest.php create mode 100644 tests/Integration/Database/DatabaseResetTest.php diff --git a/packages/database/src/Connection/Connection.php b/packages/database/src/Connection/Connection.php index a99578c7df..4cc73e8289 100644 --- a/packages/database/src/Connection/Connection.php +++ b/packages/database/src/Connection/Connection.php @@ -23,4 +23,5 @@ public function close(): void; public function connect(): void; // TODO: add ping and reconnect methods in 4.0 + // TODO: add inTransaction method in 4.0 } diff --git a/packages/database/src/Connection/ConnectionReset.php b/packages/database/src/Connection/ConnectionReset.php index e8d5822b01..77ad40b02e 100644 --- a/packages/database/src/Connection/ConnectionReset.php +++ b/packages/database/src/Connection/ConnectionReset.php @@ -3,7 +3,9 @@ namespace Tempest\Database\Connection; use Tempest\Container\Container; +use Tempest\Container\GenericContainer; use Tempest\Container\Resettable; +use Tempest\Database\Exceptions\CouldNotResetConnection; final readonly class ConnectionReset implements Resettable { @@ -13,6 +15,16 @@ public function __construct( public function reset(): void { + if ($this->container instanceof GenericContainer) { + $connections = $this->container->getSingletons(Connection::class); + + foreach ($connections as $connection) { + if ($connection instanceof PDOConnection && $connection->inTransaction()) { + throw new CouldNotResetConnection("There's still an active transaction, make sure to close it before ending the request"); + } + } + } + $this->container->unregister(Connection::class, tagged: true); } } diff --git a/packages/database/src/Connection/PDOConnection.php b/packages/database/src/Connection/PDOConnection.php index 16a5cd9e79..fed6d12d42 100644 --- a/packages/database/src/Connection/PDOConnection.php +++ b/packages/database/src/Connection/PDOConnection.php @@ -69,6 +69,15 @@ public function prepare(string $sql): PDOStatement return $statement; } + public function inTransaction(): bool + { + if (! $this->pdo instanceof PDO) { + return false; + } + + return $this->pdo->inTransaction(); + } + public function ping(): bool { try { diff --git a/packages/database/src/Exceptions/CouldNotResetConnection.php b/packages/database/src/Exceptions/CouldNotResetConnection.php new file mode 100644 index 0000000000..e352b0e404 --- /dev/null +++ b/packages/database/src/Exceptions/CouldNotResetConnection.php @@ -0,0 +1,13 @@ +container->get(Connection::class); + + $connection->beginTransaction(); + + $this->assertException(CouldNotResetConnection::class, function () { + $this->container->reset(); + }); + + $connection->close(); + } + + #[Test] + public function test_properly_closed_transaction_can_reset_connection(): void + { + /** @var Connection $connection */ + $connection = $this->container->get(Connection::class); + + $connection->beginTransaction(); + $connection->commit(); + + $this->container->reset(); + + $newConnection = $this->container->get(Connection::class); + $this->assertNotSame($connection, $newConnection); + } +} diff --git a/tests/Integration/Database/DatabaseResetTest.php b/tests/Integration/Database/DatabaseResetTest.php new file mode 100644 index 0000000000..bf9a533807 --- /dev/null +++ b/tests/Integration/Database/DatabaseResetTest.php @@ -0,0 +1,53 @@ +container->config(new SQLiteConfig( + path: __DIR__ . '/db-main.sqlite', + tag: 'sqlite-main', + )); + + /** @var \Tempest\Database\GenericDatabase $database */ + $database = $this->container->get(Database::class, 'sqlite-main'); + + $database->connection->beginTransaction(); + + $this->assertException(CouldNotResetConnection::class, function () { + $this->container->reset(); + }); + + $database->connection->close(); + } + + #[Test] + public function test_properly_closed_transaction_allows_database_reset(): void + { + $this->container->config(new SQLiteConfig( + path: __DIR__ . '/db-main.sqlite', + tag: 'sqlite-main', + )); + + /** @var \Tempest\Database\GenericDatabase $database */ + $database = $this->container->get(Database::class, 'sqlite-main'); + + $database->connection->beginTransaction(); + $database->connection->close(); + + $this->container->reset(); + + $new = $this->container->get(Database::class, 'sqlite-main'); + + $this->assertNotSame($database, $new); + } +} From 46df46949fca7f7c9d8049c991b900310e76f861 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 10:19:11 +0200 Subject: [PATCH 16/30] Kernel cleanup --- packages/core/src/FrameworkKernel.php | 19 +++++++++++- packages/core/src/Kernel.php | 2 ++ packages/core/src/KernelEvent.php | 3 ++ .../event-bus/src/Testing/EventBusTester.php | 4 +++ packages/router/src/WorkerModeApplication.php | 6 ++-- .../Http/Exceptions/ExceptionRendererTest.php | 2 ++ .../Exceptions/HttpExceptionHandlerTest.php | 2 ++ .../Router/WorkerModeApplicationTest.php | 29 +++++++++++++++++++ 8 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 tests/Integration/Router/WorkerModeApplicationTest.php diff --git a/packages/core/src/FrameworkKernel.php b/packages/core/src/FrameworkKernel.php index bf64e6dffe..4920a71bc9 100644 --- a/packages/core/src/FrameworkKernel.php +++ b/packages/core/src/FrameworkKernel.php @@ -98,9 +98,19 @@ public function validateRoot(): self return $this; } + public function reset(): void + { + $this + ->event(KernelEvent::RESETTING) + ->resetContainer() + ->event(KernelEvent::RESET); + } + public function shutdown(): void { - $this->finishDeferredTasks() + $this + ->event(KernelEvent::SHUTTING_DOWN) + ->finishDeferredTasks() ->event(KernelEvent::SHUTDOWN); } @@ -234,6 +244,13 @@ public function finishDeferredTasks(): self return $this; } + public function resetContainer(): self + { + $this->container->reset(); + + return $this; + } + public function event(object $event): self { if (interface_exists(EventBus::class)) { diff --git a/packages/core/src/Kernel.php b/packages/core/src/Kernel.php index cd56b42ad5..218a47ec0c 100644 --- a/packages/core/src/Kernel.php +++ b/packages/core/src/Kernel.php @@ -24,4 +24,6 @@ public static function boot( ): self; public function shutdown(): void; + + public function reset(): void; } diff --git a/packages/core/src/KernelEvent.php b/packages/core/src/KernelEvent.php index 0f91a34de2..0a0ce9f2a7 100644 --- a/packages/core/src/KernelEvent.php +++ b/packages/core/src/KernelEvent.php @@ -6,6 +6,9 @@ enum KernelEvent { + case SHUTTING_DOWN; + case RESETTING; + case RESET; case BOOTED; case SHUTDOWN; } diff --git a/packages/event-bus/src/Testing/EventBusTester.php b/packages/event-bus/src/Testing/EventBusTester.php index 383a7af975..869f093545 100644 --- a/packages/event-bus/src/Testing/EventBusTester.php +++ b/packages/event-bus/src/Testing/EventBusTester.php @@ -111,6 +111,10 @@ private function findDispatches(string|object $event): array return true; } + if (! is_string($event)) { + return false; + } + return class_exists($event) && $dispatched instanceof $event; }); } diff --git a/packages/router/src/WorkerModeApplication.php b/packages/router/src/WorkerModeApplication.php index f723aff79b..533af571ba 100644 --- a/packages/router/src/WorkerModeApplication.php +++ b/packages/router/src/WorkerModeApplication.php @@ -34,8 +34,8 @@ public function run(): void response: $router->dispatch($psrRequest), ); - $this->container->get(Kernel::class)->shutdown(); - - $this->container->reset(); + $kernel = $this->container->get(Kernel::class); + $kernel->shutdown(); + $kernel->reset(); } } diff --git a/tests/Integration/Http/Exceptions/ExceptionRendererTest.php b/tests/Integration/Http/Exceptions/ExceptionRendererTest.php index a932440ded..b42ee0f529 100644 --- a/tests/Integration/Http/Exceptions/ExceptionRendererTest.php +++ b/tests/Integration/Http/Exceptions/ExceptionRendererTest.php @@ -60,6 +60,8 @@ public function shutdown(int|string $status = ''): never { throw new Exception('Shutdown.'); } + + public function reset(): void {} }); $this->container->singleton(ResponseSender::class, fn () => new class($this) implements ResponseSender { diff --git a/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php b/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php index 7ad74cca00..1cb5f31ca7 100644 --- a/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php +++ b/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php @@ -62,6 +62,8 @@ public function shutdown(int|string $status = ''): never { throw new Exception('Shutdown.'); } + + public function reset(): void {} }, ); diff --git a/tests/Integration/Router/WorkerModeApplicationTest.php b/tests/Integration/Router/WorkerModeApplicationTest.php new file mode 100644 index 0000000000..4842300cfa --- /dev/null +++ b/tests/Integration/Router/WorkerModeApplicationTest.php @@ -0,0 +1,29 @@ +http->get('/'); + $this->eventBus->preventEventHandling(); + + $application = new WorkerModeApplication($this->container); + + ob_start(); + $application->run(); + ob_get_clean(); + + $this->eventBus->assertDispatched(KernelEvent::SHUTTING_DOWN); + $this->eventBus->assertDispatched(KernelEvent::SHUTDOWN); + $this->eventBus->assertDispatched(KernelEvent::RESETTING); + $this->eventBus->assertDispatched(KernelEvent::RESET); + } +} \ No newline at end of file From 244a0548539e911095e1bb2481c8ce880ae57f9d Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 10:21:41 +0200 Subject: [PATCH 17/30] QA --- tests/Integration/Router/WorkerModeApplicationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Router/WorkerModeApplicationTest.php b/tests/Integration/Router/WorkerModeApplicationTest.php index 4842300cfa..ce9420c34c 100644 --- a/tests/Integration/Router/WorkerModeApplicationTest.php +++ b/tests/Integration/Router/WorkerModeApplicationTest.php @@ -26,4 +26,4 @@ public function test_shutdown_and_reset_are_called(): void $this->eventBus->assertDispatched(KernelEvent::RESETTING); $this->eventBus->assertDispatched(KernelEvent::RESET); } -} \ No newline at end of file +} From ec3c70ee1c393819254c52c5d2bf189ee15d1f87 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 10:51:42 +0200 Subject: [PATCH 18/30] Refactor connection --- packages/database/src/Connection/Connection.php | 7 +++++-- packages/database/src/Connection/ConnectionInitializer.php | 2 +- packages/database/src/Connection/ConnectionReset.php | 3 ++- packages/database/src/DatabaseInitializer.php | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/database/src/Connection/Connection.php b/packages/database/src/Connection/Connection.php index 4cc73e8289..2bd4ac7e2e 100644 --- a/packages/database/src/Connection/Connection.php +++ b/packages/database/src/Connection/Connection.php @@ -10,6 +10,8 @@ interface Connection { public function beginTransaction(): bool; + public function inTransaction(): bool; + public function commit(): bool; public function rollback(): bool; @@ -22,6 +24,7 @@ public function close(): void; public function connect(): void; - // TODO: add ping and reconnect methods in 4.0 - // TODO: add inTransaction method in 4.0 + public function reconnect(): void; + + public function ping(): bool; } diff --git a/packages/database/src/Connection/ConnectionInitializer.php b/packages/database/src/Connection/ConnectionInitializer.php index 0069cf050a..247f0e7f7d 100644 --- a/packages/database/src/Connection/ConnectionInitializer.php +++ b/packages/database/src/Connection/ConnectionInitializer.php @@ -26,7 +26,7 @@ public function initialize(Container $container): Connection $connection = new PDOConnection($config); $connection->connect(); self::$connection = $connection; - } elseif ($connection instanceof PDOConnection && $connection->ping() === false) { + } elseif ($connection->ping() === false) { $connection->reconnect(); } diff --git a/packages/database/src/Connection/ConnectionReset.php b/packages/database/src/Connection/ConnectionReset.php index 77ad40b02e..157bfde253 100644 --- a/packages/database/src/Connection/ConnectionReset.php +++ b/packages/database/src/Connection/ConnectionReset.php @@ -16,10 +16,11 @@ public function __construct( public function reset(): void { if ($this->container instanceof GenericContainer) { + /** @var Connection[] $connections */ $connections = $this->container->getSingletons(Connection::class); foreach ($connections as $connection) { - if ($connection instanceof PDOConnection && $connection->inTransaction()) { + if ($connection->inTransaction()) { throw new CouldNotResetConnection("There's still an active transaction, make sure to close it before ending the request"); } } diff --git a/packages/database/src/DatabaseInitializer.php b/packages/database/src/DatabaseInitializer.php index c2af285650..d32474da35 100644 --- a/packages/database/src/DatabaseInitializer.php +++ b/packages/database/src/DatabaseInitializer.php @@ -40,7 +40,7 @@ public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Con $connection = new PDOConnection($config); $connection->connect(); self::$connections[$connectionKey] = $connection; - } elseif ($connection instanceof PDOConnection && $connection->ping() === false) { + } elseif ($connection->ping() === false) { $connection->reconnect(); } From 178aefc200d5b4eae7b81c6f7f4e5f20d867fe75 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 11:05:03 +0200 Subject: [PATCH 19/30] Add rectors --- .../config/sets/level/up-to-tempest-312.php | 17 +++ packages/upgrade/config/sets/tempest312.php | 15 +++ .../upgrade/src/Set/TempestLevelSetList.php | 2 + packages/upgrade/src/Set/TempestSetList.php | 2 + .../UpdateConnectionImplementationsRector.php | 93 ++++++++++++++ .../UpdateKernelImplementationsRector.php | 114 ++++++++++++++++++ .../Fixtures/AliasedImplementations.input.php | 67 ++++++++++ .../ConnectionImplementation.input.php | 42 +++++++ .../ExistingImplementations.input.php | 102 ++++++++++++++++ .../Fixtures/KernelImplementation.input.php | 29 +++++ .../tests/Tempest312/Tempest312RectorTest.php | 56 +++++++++ .../tests/Tempest312/tempest312_rector.php | 9 ++ 12 files changed, 548 insertions(+) create mode 100644 packages/upgrade/config/sets/level/up-to-tempest-312.php create mode 100644 packages/upgrade/config/sets/tempest312.php create mode 100644 packages/upgrade/src/Tempest312/UpdateConnectionImplementationsRector.php create mode 100644 packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php create mode 100644 packages/upgrade/tests/Tempest312/Fixtures/AliasedImplementations.input.php create mode 100644 packages/upgrade/tests/Tempest312/Fixtures/ConnectionImplementation.input.php create mode 100644 packages/upgrade/tests/Tempest312/Fixtures/ExistingImplementations.input.php create mode 100644 packages/upgrade/tests/Tempest312/Fixtures/KernelImplementation.input.php create mode 100644 packages/upgrade/tests/Tempest312/Tempest312RectorTest.php create mode 100644 packages/upgrade/tests/Tempest312/tempest312_rector.php diff --git a/packages/upgrade/config/sets/level/up-to-tempest-312.php b/packages/upgrade/config/sets/level/up-to-tempest-312.php new file mode 100644 index 0000000000..53892e6615 --- /dev/null +++ b/packages/upgrade/config/sets/level/up-to-tempest-312.php @@ -0,0 +1,17 @@ +sets([ + TempestSetList::TEMPEST_20, + TempestSetList::TEMPEST_28, + TempestSetList::TEMPEST_30, + TempestSetList::TEMPEST_34, + TempestSetList::TEMPEST_310, + TempestSetList::TEMPEST_312, + ]); +}; diff --git a/packages/upgrade/config/sets/tempest312.php b/packages/upgrade/config/sets/tempest312.php new file mode 100644 index 0000000000..2864e2972f --- /dev/null +++ b/packages/upgrade/config/sets/tempest312.php @@ -0,0 +1,15 @@ +rule(UpdateConnectionImplementationsRector::class); + $config->rule(UpdateKernelImplementationsRector::class); +}; diff --git a/packages/upgrade/src/Set/TempestLevelSetList.php b/packages/upgrade/src/Set/TempestLevelSetList.php index 5b3a714c12..4baad2e83d 100644 --- a/packages/upgrade/src/Set/TempestLevelSetList.php +++ b/packages/upgrade/src/Set/TempestLevelSetList.php @@ -15,4 +15,6 @@ final class TempestLevelSetList public const string UP_TO_TEMPEST_34 = __DIR__ . '/../../config/sets/level/up-to-tempest-34.php'; public const string UP_TO_TEMPEST_310 = __DIR__ . '/../../config/sets/level/up-to-tempest-310.php'; + + public const string UP_TO_TEMPEST_312 = __DIR__ . '/../../config/sets/level/up-to-tempest-312.php'; } diff --git a/packages/upgrade/src/Set/TempestSetList.php b/packages/upgrade/src/Set/TempestSetList.php index 62833a2036..e3f4abf683 100644 --- a/packages/upgrade/src/Set/TempestSetList.php +++ b/packages/upgrade/src/Set/TempestSetList.php @@ -15,4 +15,6 @@ final class TempestSetList public const string TEMPEST_34 = __DIR__ . '/../../config/sets/tempest34.php'; public const string TEMPEST_310 = __DIR__ . '/../../config/sets/tempest310.php'; + + public const string TEMPEST_312 = __DIR__ . '/../../config/sets/tempest312.php'; } diff --git a/packages/upgrade/src/Tempest312/UpdateConnectionImplementationsRector.php b/packages/upgrade/src/Tempest312/UpdateConnectionImplementationsRector.php new file mode 100644 index 0000000000..f8506c70bf --- /dev/null +++ b/packages/upgrade/src/Tempest312/UpdateConnectionImplementationsRector.php @@ -0,0 +1,93 @@ + 'bool', + 'ping' => 'bool', + 'reconnect' => 'void', + ]; + + public function getNodeTypes(): array + { + return [ + Class_::class, + ]; + } + + public function refactor(Node $node): ?Node + { + if (! $node instanceof Class_) { + return null; + } + + if (! $this->implementsConnection($node)) { + return null; + } + + $hasChanged = false; + + foreach (self::METHODS as $methodName => $returnType) { + if ($node->getMethod($methodName) instanceof ClassMethod) { + continue; + } + + $node->stmts[] = $this->createMethod($methodName, $returnType); + $hasChanged = true; + } + + return $hasChanged ? $node : null; + } + + private function implementsConnection(Class_ $class): bool + { + return array_any( + $class->implements, + fn (Name $name) => $this->isInterfaceName($name, Connection::class, 'Connection'), + ); + } + + private function isInterfaceName(Name $name, string $interfaceName, string $shortName): bool + { + $names = [ + ltrim($name->toString(), '\\'), + ]; + + $resolvedName = $name->getAttribute('resolvedName'); + + if ($resolvedName instanceof Name) { + $names[] = ltrim($resolvedName->toString(), '\\'); + } + + return array_any( + $names, + static fn (string $name) => in_array($name, [$interfaceName, $shortName], strict: true), + ); + } + + private function createMethod(string $methodName, string $returnType): ClassMethod + { + $statements = $returnType === 'bool' + ? [new Return_(new ConstFetch(new Name('false')))] + : []; + + return new ClassMethod($methodName, [ + 'flags' => Modifiers::PUBLIC, + 'returnType' => new Identifier($returnType), + 'stmts' => $statements, + ]); + } +} diff --git a/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php b/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php new file mode 100644 index 0000000000..5edd68f9e9 --- /dev/null +++ b/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php @@ -0,0 +1,114 @@ +implementsKernel($node)) { + return null; + } + + $hasChanged = false; + $shutdown = $node->getMethod('shutdown'); + + if ($shutdown instanceof ClassMethod && ! $this->isVoidReturnType($shutdown)) { + $shutdown->returnType = new Identifier('void'); + $this->removeReturnValues($shutdown); + $hasChanged = true; + } + + if (!$node->getMethod('reset') instanceof ClassMethod) { + $node->stmts[] = new ClassMethod('reset', [ + 'flags' => Modifiers::PUBLIC, + 'returnType' => new Identifier('void'), + 'stmts' => [], + ]); + + $hasChanged = true; + } + + return $hasChanged ? $node : null; + } + + private function implementsKernel(Class_ $class): bool + { + return array_any( + $class->implements, + fn (Name $name) => $this->isInterfaceName($name, Kernel::class, 'Kernel'), + ); + } + + private function isInterfaceName(Name $name, string $interfaceName, string $shortName): bool + { + $names = [ + ltrim($name->toString(), '\\'), + ]; + + $resolvedName = $name->getAttribute('resolvedName'); + + if ($resolvedName instanceof Name) { + $names[] = ltrim($resolvedName->toString(), '\\'); + } + + return array_any( + $names, + static fn (string $name) => in_array($name, [$interfaceName, $shortName], strict: true), + ); + } + + private function isVoidReturnType(ClassMethod $method): bool + { + return $method->returnType instanceof Identifier && $method->returnType->toString() === 'void'; + } + + private function removeReturnValues(ClassMethod $method): void + { + if ($method->stmts === null) { + return; + } + + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class extends NodeVisitorAbstract { + public function leaveNode(Node $node): ?array + { + if (! $node instanceof Return_ || ! $node->expr instanceof Expr) { + return null; + } + + return [ + new Expression($node->expr), + new Return_(), + ]; + } + }); + + $method->stmts = $traverser->traverse($method->stmts); + } +} diff --git a/packages/upgrade/tests/Tempest312/Fixtures/AliasedImplementations.input.php b/packages/upgrade/tests/Tempest312/Fixtures/AliasedImplementations.input.php new file mode 100644 index 0000000000..7c2d761dfd --- /dev/null +++ b/packages/upgrade/tests/Tempest312/Fixtures/AliasedImplementations.input.php @@ -0,0 +1,67 @@ +wasShutDown = true; + } + + public function reset(): void + { + $this->wasReset = true; + } +} + +final class ExistingConnection implements Connection +{ + public function beginTransaction(): bool + { + return true; + } + + public function inTransaction(): bool + { + return true; + } + + public function commit(): bool + { + return true; + } + + public function rollback(): bool + { + return true; + } + + public function lastInsertId(): false|string + { + return 'existing-id'; + } + + public function prepare(string $sql): PDOStatement + { + return new PDOStatement(); + } + + public function close(): void + { + $this->disconnect(); + } + + public function connect(): void + { + $this->bootConnection(); + } + + public function reconnect(): void + { + $this->close(); + $this->connect(); + } + + public function ping(): bool + { + return true; + } + + private function disconnect(): void + { + } + + private function bootConnection(): void + { + } +} diff --git a/packages/upgrade/tests/Tempest312/Fixtures/KernelImplementation.input.php b/packages/upgrade/tests/Tempest312/Fixtures/KernelImplementation.input.php new file mode 100644 index 0000000000..9b81a01b74 --- /dev/null +++ b/packages/upgrade/tests/Tempest312/Fixtures/KernelImplementation.input.php @@ -0,0 +1,29 @@ + new RectorTester(__DIR__ . '/tempest312_rector.php'); + } + + public function test_connection_implementation_methods_are_added(): void + { + $this->rector + ->runFixture(__DIR__ . '/Fixtures/ConnectionImplementation.input.php') + ->assertContains('public function inTransaction(): bool') + ->assertContains('public function ping(): bool') + ->assertContains('public function reconnect(): void') + ->assertContains('return false;'); + } + + public function test_kernel_implementation_is_updated(): void + { + $this->rector + ->runFixture(__DIR__ . '/Fixtures/KernelImplementation.input.php') + ->assertContains('public function shutdown(): void') + ->assertContains('public function reset(): void') + ->assertContains('return;') + ->assertNotContains('return $this;') + ->assertNotContains('public function shutdown(): self'); + } + + public function test_aliased_interface_implementations_are_updated(): void + { + $this->rector + ->runFixture(__DIR__ . '/Fixtures/AliasedImplementations.input.php') + ->assertContains('public function reset(): void') + ->assertContains('public function inTransaction(): bool') + ->assertContains('public function ping(): bool') + ->assertContains('public function reconnect(): void'); + } + + public function test_existing_interface_methods_are_not_overwritten(): void + { + $this->assertSame( + '', + $this->rector + ->runFixture(__DIR__ . '/Fixtures/ExistingImplementations.input.php') + ->actual, + ); + } +} diff --git a/packages/upgrade/tests/Tempest312/tempest312_rector.php b/packages/upgrade/tests/Tempest312/tempest312_rector.php new file mode 100644 index 0000000000..4dff2f73f9 --- /dev/null +++ b/packages/upgrade/tests/Tempest312/tempest312_rector.php @@ -0,0 +1,9 @@ +withSets([TempestSetList::TEMPEST_312]); From d71e8ed0b0b09e5f54d38cf8bbd9d70ce73a86b3 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 11:06:20 +0200 Subject: [PATCH 20/30] QA --- .../src/Tempest312/UpdateKernelImplementationsRector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php b/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php index 5edd68f9e9..488dcc4b19 100644 --- a/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php +++ b/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php @@ -44,7 +44,7 @@ public function refactor(Node $node): ?Node $hasChanged = true; } - if (!$node->getMethod('reset') instanceof ClassMethod) { + if (! $node->getMethod('reset') instanceof ClassMethod) { $node->stmts[] = new ClassMethod('reset', [ 'flags' => Modifiers::PUBLIC, 'returnType' => new Identifier('void'), From af86656d7504bb18825c5781d11938c594ad91e9 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 18 Jun 2026 11:43:31 +0200 Subject: [PATCH 21/30] QA --- .../src/Tempest312/UpdateKernelImplementationsRector.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php b/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php index 488dcc4b19..bcfbf2a7a5 100644 --- a/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php +++ b/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php @@ -7,6 +7,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Identifier; use PhpParser\Node\Name; +use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Expression; @@ -109,6 +110,9 @@ public function leaveNode(Node $node): ?array } }); - $method->stmts = $traverser->traverse($method->stmts); + $method->stmts = array_filter( + $traverser->traverse($method->stmts), + static fn (Node $node): bool => $node instanceof Stmt, + ); } } From 9f410965bf32c1ef6244453c60b50fc76b25cd99 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 19 Jun 2026 08:36:39 +0200 Subject: [PATCH 22/30] Improve kernel implementation --- packages/core/src/FrameworkKernel.php | 28 ++++++++++--------- packages/core/src/Kernel.php | 2 -- packages/core/src/Tempest.php | 9 ++++-- .../src/Exceptions/HttpExceptionHandler.php | 4 +-- packages/router/src/WorkerModeApplication.php | 8 ++++-- .../UpdateKernelImplementationsRector.php | 11 -------- .../ExistingImplementations.input.php | 5 ---- .../tests/Tempest312/Tempest312RectorTest.php | 2 -- .../Http/Exceptions/ExceptionRendererTest.php | 4 +-- .../Exceptions/HttpExceptionHandlerTest.php | 4 +-- 10 files changed, 31 insertions(+), 46 deletions(-) diff --git a/packages/core/src/FrameworkKernel.php b/packages/core/src/FrameworkKernel.php index 4920a71bc9..2eb1892345 100644 --- a/packages/core/src/FrameworkKernel.php +++ b/packages/core/src/FrameworkKernel.php @@ -35,9 +35,10 @@ final class FrameworkKernel implements Kernel public function __construct( public string $root, /** @var DiscoveryLocation[] */ - private array $discoveryLocations = [], + private readonly array $discoveryLocations = [], ?Container $container = null, ?string $internalStorage = null, + private readonly bool $longRunning = false, ) { $this->container = $container ?? $this->createContainer(); @@ -51,6 +52,7 @@ public static function boot( array $discoveryLocations = [], ?Container $container = null, ?string $internalStorage = null, + bool $longRunning = false, ): self { if (! defined('TEMPEST_START')) { define('TEMPEST_START', value: hrtime(as_number: true)); @@ -61,6 +63,7 @@ public static function boot( discoveryLocations: $discoveryLocations, container: $container, internalStorage: $internalStorage, + longRunning: $longRunning, ) ->registerKernel() ->validateRoot() @@ -98,20 +101,19 @@ public function validateRoot(): self return $this; } - public function reset(): void - { - $this - ->event(KernelEvent::RESETTING) - ->resetContainer() - ->event(KernelEvent::RESET); - } - public function shutdown(): void { - $this - ->event(KernelEvent::SHUTTING_DOWN) - ->finishDeferredTasks() - ->event(KernelEvent::SHUTDOWN); + $this->event(KernelEvent::SHUTTING_DOWN) + ->finishDeferredTasks(); + + if ($this->longRunning) { + $this + ->event(KernelEvent::RESETTING) + ->resetContainer() + ->event(KernelEvent::RESET); + } + + $this->event(KernelEvent::SHUTDOWN); } public function loadComposer(): self diff --git a/packages/core/src/Kernel.php b/packages/core/src/Kernel.php index 218a47ec0c..cd56b42ad5 100644 --- a/packages/core/src/Kernel.php +++ b/packages/core/src/Kernel.php @@ -24,6 +24,4 @@ public static function boot( ): self; public function shutdown(): void; - - public function reset(): void; } diff --git a/packages/core/src/Tempest.php b/packages/core/src/Tempest.php index 370eacb6ef..256cd9d4fd 100644 --- a/packages/core/src/Tempest.php +++ b/packages/core/src/Tempest.php @@ -9,12 +9,17 @@ final readonly class Tempest { /** @param \Tempest\Discovery\DiscoveryLocation[] $discoveryLocations */ - public static function boot(?string $root = null, array $discoveryLocations = [], ?string $internalStorage = null): Container - { + public static function boot( + ?string $root = null, + array $discoveryLocations = [], + ?string $internalStorage = null, + bool $longRunning = false, + ): Container { $kernel = FrameworkKernel::boot( root: $root ?? getcwd(), discoveryLocations: $discoveryLocations, internalStorage: $internalStorage, + longRunning: $longRunning, ); return $kernel->container; diff --git a/packages/router/src/Exceptions/HttpExceptionHandler.php b/packages/router/src/Exceptions/HttpExceptionHandler.php index d23920f39b..da55626515 100644 --- a/packages/router/src/Exceptions/HttpExceptionHandler.php +++ b/packages/router/src/Exceptions/HttpExceptionHandler.php @@ -25,7 +25,7 @@ public function __construct( private RouteConfig $routeConfig, ) {} - public function handle(Throwable $throwable): never + public function handle(Throwable $throwable): void { $request = $this->container->get(Request::class); @@ -34,8 +34,6 @@ public function handle(Throwable $throwable): never $this->responseSender->send($this->renderResponse($request, $throwable)); } finally { $this->kernel->shutdown(); - - exit(); } } diff --git a/packages/router/src/WorkerModeApplication.php b/packages/router/src/WorkerModeApplication.php index 533af571ba..827200f48e 100644 --- a/packages/router/src/WorkerModeApplication.php +++ b/packages/router/src/WorkerModeApplication.php @@ -21,7 +21,11 @@ public function __construct( /** @param \Tempest\Discovery\DiscoveryLocation[] $discoveryLocations */ public static function boot(string $root, array $discoveryLocations = []): self { - return Tempest::boot($root, $discoveryLocations)->get(WorkerModeApplication::class); + return Tempest::boot( + root: $root, + discoveryLocations: $discoveryLocations, + longRunning: true, + )->get(WorkerModeApplication::class); } public function run(): void @@ -35,7 +39,7 @@ public function run(): void ); $kernel = $this->container->get(Kernel::class); + $kernel->shutdown(); - $kernel->reset(); } } diff --git a/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php b/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php index bcfbf2a7a5..22dcacfb4a 100644 --- a/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php +++ b/packages/upgrade/src/Tempest312/UpdateKernelImplementationsRector.php @@ -2,7 +2,6 @@ namespace Tempest\Upgrade\Tempest312; -use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Identifier; @@ -45,16 +44,6 @@ public function refactor(Node $node): ?Node $hasChanged = true; } - if (! $node->getMethod('reset') instanceof ClassMethod) { - $node->stmts[] = new ClassMethod('reset', [ - 'flags' => Modifiers::PUBLIC, - 'returnType' => new Identifier('void'), - 'stmts' => [], - ]); - - $hasChanged = true; - } - return $hasChanged ? $node : null; } diff --git a/packages/upgrade/tests/Tempest312/Fixtures/ExistingImplementations.input.php b/packages/upgrade/tests/Tempest312/Fixtures/ExistingImplementations.input.php index b93672f942..1ad4fbcb27 100644 --- a/packages/upgrade/tests/Tempest312/Fixtures/ExistingImplementations.input.php +++ b/packages/upgrade/tests/Tempest312/Fixtures/ExistingImplementations.input.php @@ -32,11 +32,6 @@ public function shutdown(): void { $this->wasShutDown = true; } - - public function reset(): void - { - $this->wasReset = true; - } } final class ExistingConnection implements Connection diff --git a/packages/upgrade/tests/Tempest312/Tempest312RectorTest.php b/packages/upgrade/tests/Tempest312/Tempest312RectorTest.php index 0bed3424ec..f649f1444c 100644 --- a/packages/upgrade/tests/Tempest312/Tempest312RectorTest.php +++ b/packages/upgrade/tests/Tempest312/Tempest312RectorTest.php @@ -28,7 +28,6 @@ public function test_kernel_implementation_is_updated(): void $this->rector ->runFixture(__DIR__ . '/Fixtures/KernelImplementation.input.php') ->assertContains('public function shutdown(): void') - ->assertContains('public function reset(): void') ->assertContains('return;') ->assertNotContains('return $this;') ->assertNotContains('public function shutdown(): self'); @@ -38,7 +37,6 @@ public function test_aliased_interface_implementations_are_updated(): void { $this->rector ->runFixture(__DIR__ . '/Fixtures/AliasedImplementations.input.php') - ->assertContains('public function reset(): void') ->assertContains('public function inTransaction(): bool') ->assertContains('public function ping(): bool') ->assertContains('public function reconnect(): void'); diff --git a/tests/Integration/Http/Exceptions/ExceptionRendererTest.php b/tests/Integration/Http/Exceptions/ExceptionRendererTest.php index b42ee0f529..b39c7239f6 100644 --- a/tests/Integration/Http/Exceptions/ExceptionRendererTest.php +++ b/tests/Integration/Http/Exceptions/ExceptionRendererTest.php @@ -56,12 +56,10 @@ public static function boot(string $root, array $discoveryLocations = [], ?Conta return Kernel::boot($root, $discoveryLocations, $container, $internalStorage); // @phpstan-ignore-line } - public function shutdown(int|string $status = ''): never + public function shutdown(): never { throw new Exception('Shutdown.'); } - - public function reset(): void {} }); $this->container->singleton(ResponseSender::class, fn () => new class($this) implements ResponseSender { diff --git a/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php b/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php index 1cb5f31ca7..92f779ec0a 100644 --- a/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php +++ b/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php @@ -58,12 +58,10 @@ public static function boot(string $root, array $discoveryLocations = [], ?Conta return Kernel::boot($root, $discoveryLocations, $container, $internalStorage); // @phpstan-ignore-line } - public function shutdown(int|string $status = ''): never + public function shutdown(): never { throw new Exception('Shutdown.'); } - - public function reset(): void {} }, ); From ab063ecc280f64cff6c962c81f878ecb37784044 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 19 Jun 2026 08:43:17 +0200 Subject: [PATCH 23/30] Improve kernel implementation --- packages/console/src/ConsoleApplication.php | 6 ++---- packages/console/src/Exceptions/ConsoleExceptionHandler.php | 6 ++---- packages/core/src/FrameworkKernel.php | 6 +++++- packages/core/src/Kernel.php | 2 +- packages/router/src/HttpApplication.php | 2 -- .../Tempest312/Fixtures/AliasedImplementations.input.php | 2 +- .../Tempest312/Fixtures/ExistingImplementations.input.php | 2 +- .../Tempest312/Fixtures/KernelImplementation.input.php | 2 +- packages/upgrade/tests/Tempest312/Tempest312RectorTest.php | 2 +- 9 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/console/src/ConsoleApplication.php b/packages/console/src/ConsoleApplication.php index 946579b0ce..fed1dfdf72 100644 --- a/packages/console/src/ConsoleApplication.php +++ b/packages/console/src/ConsoleApplication.php @@ -48,7 +48,7 @@ public static function boot( return $container->get(ConsoleApplication::class); } - public function run(): never + public function run(): void { $exitCode = $this->container->get(ExecuteConsoleCommand::class)($this->argumentBag->getCommandName()); @@ -58,8 +58,6 @@ public function run(): never throw new ExitCodeWasInvalid($exitCode); } - $this->container->get(Kernel::class)->shutdown(); - - exit($exitCode); + $this->container->get(Kernel::class)->shutdown($exitCode); } } diff --git a/packages/console/src/Exceptions/ConsoleExceptionHandler.php b/packages/console/src/Exceptions/ConsoleExceptionHandler.php index 0e55d9a05e..3c9d20f9fe 100644 --- a/packages/console/src/Exceptions/ConsoleExceptionHandler.php +++ b/packages/console/src/Exceptions/ConsoleExceptionHandler.php @@ -31,7 +31,7 @@ public function __construct( private ExceptionProcessor $exceptionProcessor, ) {} - public function handle(Throwable $throwable): never + public function handle(Throwable $throwable): void { try { $this->exceptionProcessor->process($throwable); @@ -70,9 +70,7 @@ public function handle(Throwable $throwable): never ? $throwable->getExitCode() : ExitCode::ERROR; - $this->kernel->shutdown(); - - exit($exitCode->value); + $this->kernel->shutdown($exitCode->value); } } diff --git a/packages/core/src/FrameworkKernel.php b/packages/core/src/FrameworkKernel.php index 2eb1892345..9f43adb6e2 100644 --- a/packages/core/src/FrameworkKernel.php +++ b/packages/core/src/FrameworkKernel.php @@ -101,7 +101,7 @@ public function validateRoot(): self return $this; } - public function shutdown(): void + public function shutdown(int|string $status = ''): void { $this->event(KernelEvent::SHUTTING_DOWN) ->finishDeferredTasks(); @@ -114,6 +114,10 @@ public function shutdown(): void } $this->event(KernelEvent::SHUTDOWN); + + if (! $this->longRunning) { + exit($status); + } } public function loadComposer(): self diff --git a/packages/core/src/Kernel.php b/packages/core/src/Kernel.php index cd56b42ad5..3c098a4d49 100644 --- a/packages/core/src/Kernel.php +++ b/packages/core/src/Kernel.php @@ -23,5 +23,5 @@ public static function boot( ?string $internalStorage = null, ): self; - public function shutdown(): void; + public function shutdown(int|string $status = ''): void; } diff --git a/packages/router/src/HttpApplication.php b/packages/router/src/HttpApplication.php index 4c48cc06fb..965655a34a 100644 --- a/packages/router/src/HttpApplication.php +++ b/packages/router/src/HttpApplication.php @@ -35,7 +35,5 @@ public function run(): never ); $this->container->get(Kernel::class)->shutdown(); - - exit(); } } diff --git a/packages/upgrade/tests/Tempest312/Fixtures/AliasedImplementations.input.php b/packages/upgrade/tests/Tempest312/Fixtures/AliasedImplementations.input.php index 7c2d761dfd..2285f52a47 100644 --- a/packages/upgrade/tests/Tempest312/Fixtures/AliasedImplementations.input.php +++ b/packages/upgrade/tests/Tempest312/Fixtures/AliasedImplementations.input.php @@ -24,7 +24,7 @@ public static function boot( return new self(); } - public function shutdown(): self + public function shutdown(int|string $status = ''): void { return $this; } diff --git a/packages/upgrade/tests/Tempest312/Fixtures/ExistingImplementations.input.php b/packages/upgrade/tests/Tempest312/Fixtures/ExistingImplementations.input.php index 1ad4fbcb27..7eff8dc4dc 100644 --- a/packages/upgrade/tests/Tempest312/Fixtures/ExistingImplementations.input.php +++ b/packages/upgrade/tests/Tempest312/Fixtures/ExistingImplementations.input.php @@ -28,7 +28,7 @@ public static function boot( return new self(); } - public function shutdown(): void + public function shutdown(int|string $status = ''): void { $this->wasShutDown = true; } diff --git a/packages/upgrade/tests/Tempest312/Fixtures/KernelImplementation.input.php b/packages/upgrade/tests/Tempest312/Fixtures/KernelImplementation.input.php index 9b81a01b74..9f59c126d8 100644 --- a/packages/upgrade/tests/Tempest312/Fixtures/KernelImplementation.input.php +++ b/packages/upgrade/tests/Tempest312/Fixtures/KernelImplementation.input.php @@ -22,7 +22,7 @@ public static function boot( return new self(); } - public function shutdown(): self + public function shutdown(int|string $status = ''): self { return $this; } diff --git a/packages/upgrade/tests/Tempest312/Tempest312RectorTest.php b/packages/upgrade/tests/Tempest312/Tempest312RectorTest.php index f649f1444c..da71245447 100644 --- a/packages/upgrade/tests/Tempest312/Tempest312RectorTest.php +++ b/packages/upgrade/tests/Tempest312/Tempest312RectorTest.php @@ -27,7 +27,7 @@ public function test_kernel_implementation_is_updated(): void { $this->rector ->runFixture(__DIR__ . '/Fixtures/KernelImplementation.input.php') - ->assertContains('public function shutdown(): void') + ->assertContains('public function shutdown(int|string $status = \'\'): void') ->assertContains('return;') ->assertNotContains('return $this;') ->assertNotContains('public function shutdown(): self'); From f5af31b824d8800719b2b0d3a9e3537eae9be640 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 19 Jun 2026 08:45:55 +0200 Subject: [PATCH 24/30] Fix tests --- tests/Integration/Http/Exceptions/ExceptionRendererTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Http/Exceptions/ExceptionRendererTest.php b/tests/Integration/Http/Exceptions/ExceptionRendererTest.php index b39c7239f6..a932440ded 100644 --- a/tests/Integration/Http/Exceptions/ExceptionRendererTest.php +++ b/tests/Integration/Http/Exceptions/ExceptionRendererTest.php @@ -56,7 +56,7 @@ public static function boot(string $root, array $discoveryLocations = [], ?Conta return Kernel::boot($root, $discoveryLocations, $container, $internalStorage); // @phpstan-ignore-line } - public function shutdown(): never + public function shutdown(int|string $status = ''): never { throw new Exception('Shutdown.'); } From cf16d3c23df000936b7e66e696f2d98d0348aa48 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 19 Jun 2026 08:47:31 +0200 Subject: [PATCH 25/30] Prevent stale connections --- .../src/Connection/ConnectionInitializer.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/database/src/Connection/ConnectionInitializer.php b/packages/database/src/Connection/ConnectionInitializer.php index 247f0e7f7d..edc8fd28ed 100644 --- a/packages/database/src/Connection/ConnectionInitializer.php +++ b/packages/database/src/Connection/ConnectionInitializer.php @@ -11,25 +11,37 @@ final class ConnectionInitializer implements Initializer { - private static ?Connection $connection = null; + /** @var Connection[] */ + private static array $connections = []; #[Singleton] public function initialize(Container $container): Connection { $config = $container->get(DatabaseConfig::class); + $connectionKey = $this->getConnectionKey($config); $connection = $config->usePersistentConnection - ? self::$connection + ? self::$connections[$connectionKey] ?? null : null; if (! $connection instanceof Connection) { $connection = new PDOConnection($config); $connection->connect(); - self::$connection = $connection; + self::$connections[$connectionKey] = $connection; } elseif ($connection->ping() === false) { $connection->reconnect(); } return $connection; } + + private function getConnectionKey(DatabaseConfig $config): string + { + return hash('xxh128', serialize([ + $config->dsn, + $config->username, + $config->options, + $config->password, + ])); + } } From 16fb5076a18d30408eef486339dc5b18775cdb49 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 19 Jun 2026 08:52:17 +0200 Subject: [PATCH 26/30] Fix --- .../src/Connection/ConnectionReset.php | 6 +++++- .../Connection/ConnectionResetTest.php | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/database/src/Connection/ConnectionReset.php b/packages/database/src/Connection/ConnectionReset.php index 157bfde253..bb6f72df94 100644 --- a/packages/database/src/Connection/ConnectionReset.php +++ b/packages/database/src/Connection/ConnectionReset.php @@ -15,11 +15,15 @@ public function __construct( public function reset(): void { + // Manually looping over the connection singletons so that we can check whether they still have an active transaction if ($this->container instanceof GenericContainer) { - /** @var Connection[] $connections */ $connections = $this->container->getSingletons(Connection::class); foreach ($connections as $connection) { + if (! $connection instanceof Connection) { + continue; + } + if ($connection->inTransaction()) { throw new CouldNotResetConnection("There's still an active transaction, make sure to close it before ending the request"); } diff --git a/tests/Integration/Database/Connection/ConnectionResetTest.php b/tests/Integration/Database/Connection/ConnectionResetTest.php index 1332badb42..26966cfb27 100644 --- a/tests/Integration/Database/Connection/ConnectionResetTest.php +++ b/tests/Integration/Database/Connection/ConnectionResetTest.php @@ -2,6 +2,7 @@ namespace Tests\Tempest\Integration\Database\Connection; +use Exception; use PHPUnit\Framework\Attributes\Test; use Tempest\Database\Connection\Connection; use Tempest\Database\Exceptions\CouldNotResetConnection; @@ -38,4 +39,22 @@ public function test_properly_closed_transaction_can_reset_connection(): void $newConnection = $this->container->get(Connection::class); $this->assertNotSame($connection, $newConnection); } + + #[Test] + public function test_reset_with_uninstantiated_singletons(): void + { + $this->container->singleton( + Connection::class, + fn () => throw new Exception('Should not happen'), + tag: 'other', + ); + + /** @var Connection $connection */ + $connection = $this->container->get(Connection::class); + + $this->container->reset(); + + $newConnection = $this->container->get(Connection::class); + $this->assertNotSame($connection, $newConnection); + } } From a5e550ec029890ac6ebbaf60755d940b0ec87753 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 19 Jun 2026 08:55:42 +0200 Subject: [PATCH 27/30] QA --- packages/router/src/HttpApplication.php | 2 +- tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/router/src/HttpApplication.php b/packages/router/src/HttpApplication.php index 965655a34a..a9e5cc2de5 100644 --- a/packages/router/src/HttpApplication.php +++ b/packages/router/src/HttpApplication.php @@ -24,7 +24,7 @@ public static function boot(string $root, array $discoveryLocations = []): self return Tempest::boot($root, $discoveryLocations)->get(HttpApplication::class); } - public function run(): never + public function run(): void { $router = $this->container->get(Router::class); $psrRequest = $this->container->get(RequestFactory::class)->make(); diff --git a/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php b/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php index 92f779ec0a..7ad74cca00 100644 --- a/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php +++ b/tests/Integration/Http/Exceptions/HttpExceptionHandlerTest.php @@ -58,7 +58,7 @@ public static function boot(string $root, array $discoveryLocations = [], ?Conta return Kernel::boot($root, $discoveryLocations, $container, $internalStorage); // @phpstan-ignore-line } - public function shutdown(): never + public function shutdown(int|string $status = ''): never { throw new Exception('Shutdown.'); } From 1141cbbafc2c269fd064b05f56c8546f091bf3df Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 19 Jun 2026 09:00:58 +0200 Subject: [PATCH 28/30] QA --- packages/database/src/Connection/ConnectionInitializer.php | 1 + packages/database/src/DatabaseInitializer.php | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/database/src/Connection/ConnectionInitializer.php b/packages/database/src/Connection/ConnectionInitializer.php index edc8fd28ed..bf36c0bba9 100644 --- a/packages/database/src/Connection/ConnectionInitializer.php +++ b/packages/database/src/Connection/ConnectionInitializer.php @@ -42,6 +42,7 @@ private function getConnectionKey(DatabaseConfig $config): string $config->username, $config->options, $config->password, + $config->tag, ])); } } diff --git a/packages/database/src/DatabaseInitializer.php b/packages/database/src/DatabaseInitializer.php index d32474da35..f0eb6bad39 100644 --- a/packages/database/src/DatabaseInitializer.php +++ b/packages/database/src/DatabaseInitializer.php @@ -65,6 +65,7 @@ private function getConnectionKey(DatabaseConfig $config): string $config->username, $config->options, $config->password, + $config->tag, ])); } } From c12790d2aaceecbe490ee61a51fab63c5f734726 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 19 Jun 2026 09:12:36 +0200 Subject: [PATCH 29/30] QA --- src/Tempest/Framework/Testing/IntegrationTest.php | 1 + tests/Integration/Router/WorkerModeApplicationTest.php | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tempest/Framework/Testing/IntegrationTest.php b/src/Tempest/Framework/Testing/IntegrationTest.php index a51b95fb21..60d40d09d7 100644 --- a/src/Tempest/Framework/Testing/IntegrationTest.php +++ b/src/Tempest/Framework/Testing/IntegrationTest.php @@ -156,6 +156,7 @@ protected function setupKernel(): self root: $this->root, discoveryLocations: $discoveryLocations, internalStorage: $this->internalStorage, + longRunning: true, ); /** @var GenericContainer $container */ diff --git a/tests/Integration/Router/WorkerModeApplicationTest.php b/tests/Integration/Router/WorkerModeApplicationTest.php index ce9420c34c..f94a39c0b5 100644 --- a/tests/Integration/Router/WorkerModeApplicationTest.php +++ b/tests/Integration/Router/WorkerModeApplicationTest.php @@ -12,7 +12,6 @@ final class WorkerModeApplicationTest extends FrameworkIntegrationTestCase #[Test] public function test_shutdown_and_reset_are_called(): void { - $this->http->get('/'); $this->eventBus->preventEventHandling(); $application = new WorkerModeApplication($this->container); From e11dedfbc3264fc0d70efdc86fbee11714a61322 Mon Sep 17 00:00:00 2001 From: brendt Date: Fri, 19 Jun 2026 09:16:23 +0200 Subject: [PATCH 30/30] QA --- .../Framework/Testing/IntegrationTest.php | 1 - .../Router/WorkerModeApplicationTest.php | 23 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Tempest/Framework/Testing/IntegrationTest.php b/src/Tempest/Framework/Testing/IntegrationTest.php index 60d40d09d7..a51b95fb21 100644 --- a/src/Tempest/Framework/Testing/IntegrationTest.php +++ b/src/Tempest/Framework/Testing/IntegrationTest.php @@ -156,7 +156,6 @@ protected function setupKernel(): self root: $this->root, discoveryLocations: $discoveryLocations, internalStorage: $this->internalStorage, - longRunning: true, ); /** @var GenericContainer $container */ diff --git a/tests/Integration/Router/WorkerModeApplicationTest.php b/tests/Integration/Router/WorkerModeApplicationTest.php index f94a39c0b5..b338b7c6de 100644 --- a/tests/Integration/Router/WorkerModeApplicationTest.php +++ b/tests/Integration/Router/WorkerModeApplicationTest.php @@ -3,7 +3,14 @@ namespace Integration\Router; use PHPUnit\Framework\Attributes\Test; +use Tempest\Container\GenericContainer; +use Tempest\Core\FrameworkKernel; +use Tempest\Core\Kernel; use Tempest\Core\KernelEvent; +use Tempest\EventBus\EventBus; +use Tempest\Http\RequestFactory; +use Tempest\Router\ResponseSender; +use Tempest\Router\Router; use Tempest\Router\WorkerModeApplication; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; @@ -14,7 +21,21 @@ public function test_shutdown_and_reset_are_called(): void { $this->eventBus->preventEventHandling(); - $application = new WorkerModeApplication($this->container); + $container = new GenericContainer(); + + $container->singleton(EventBus::class, $this->container->get(EventBus::class)); + $container->singleton(Router::class, $this->container->get(Router::class)); + $container->singleton(RequestFactory::class, $this->container->get(RequestFactory::class)); + $container->singleton(ResponseSender::class, $this->container->get(ResponseSender::class)); + + $container->singleton(Kernel::class, new FrameworkKernel( + root: $this->kernel->root, + discoveryLocations: $this->discoveryLocations, + container: $container, + longRunning: true, + )); + + $application = new WorkerModeApplication($container); ob_start(); $application->run();