From eedac0384e3b1ae668fc745a8dc09494d49360f1 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Tue, 24 Dec 2024 12:27:11 +0100 Subject: [PATCH] refactor(database): wrap `PDO` so it can be closed --- .../Database/src/DatabaseInitializer.php | 1 - .../src/Exceptions/ConnectionClosed.php | 9 +++++ src/Tempest/Database/src/GenericDatabase.php | 9 ++--- src/Tempest/Database/src/PDO.php | 35 +++++++++++++++++++ src/Tempest/Database/src/PDOInitializer.php | 25 ++++++------- .../GenericTransactionManager.php | 8 ++--- .../TransactionManagerInitializer.php | 2 +- .../Database/tests/GenericDatabaseTest.php | 19 ++++++---- .../tests/GenericTransactionManagerTest.php | 27 +++++++------- .../Console/Components/TaskComponentTest.php | 3 ++ .../Database/GenericDatabaseTest.php | 15 ++++++++ 11 files changed, 108 insertions(+), 45 deletions(-) create mode 100644 src/Tempest/Database/src/Exceptions/ConnectionClosed.php create mode 100644 src/Tempest/Database/src/PDO.php diff --git a/src/Tempest/Database/src/DatabaseInitializer.php b/src/Tempest/Database/src/DatabaseInitializer.php index a37763c81..17edfcb37 100644 --- a/src/Tempest/Database/src/DatabaseInitializer.php +++ b/src/Tempest/Database/src/DatabaseInitializer.php @@ -4,7 +4,6 @@ namespace Tempest\Database; -use PDO; use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Singleton; diff --git a/src/Tempest/Database/src/Exceptions/ConnectionClosed.php b/src/Tempest/Database/src/Exceptions/ConnectionClosed.php new file mode 100644 index 000000000..03bfe946d --- /dev/null +++ b/src/Tempest/Database/src/Exceptions/ConnectionClosed.php @@ -0,0 +1,9 @@ +pdo + ->getPdo() ->prepare($query->getSql()) ->execute($bindings); } catch (PDOException $pdoException) { @@ -35,16 +36,16 @@ public function execute(Query $query): void public function getLastInsertId(): Id { - return new Id($this->pdo->lastInsertId()); + return new Id($this->pdo->getPdo()->lastInsertId()); } public function fetch(Query $query): array { - $pdoQuery = $this->pdo->prepare($query->getSql()); + $pdoQuery = $this->pdo->getPdo()->prepare($query->getSql()); $pdoQuery->execute($this->resolveBindings($query)); - return $pdoQuery->fetchAll(PDO::FETCH_NAMED); + return $pdoQuery->fetchAll(GlobalPDO::FETCH_NAMED); } public function fetchFirst(Query $query): ?array diff --git a/src/Tempest/Database/src/PDO.php b/src/Tempest/Database/src/PDO.php new file mode 100644 index 000000000..bf613e968 --- /dev/null +++ b/src/Tempest/Database/src/PDO.php @@ -0,0 +1,35 @@ +pdo === null) { + throw new ConnectionClosed('The database connection is closed.'); + } + + return $this->pdo; + } + + public function close(): void + { + $this->pdo = null; + } +} diff --git a/src/Tempest/Database/src/PDOInitializer.php b/src/Tempest/Database/src/PDOInitializer.php index 3d854f9e0..b2d250561 100644 --- a/src/Tempest/Database/src/PDOInitializer.php +++ b/src/Tempest/Database/src/PDOInitializer.php @@ -4,32 +4,27 @@ namespace Tempest\Database; -use PDO; use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Singleton; final class PDOInitializer implements Initializer { - private static ?PDO $pdo = null; - #[Singleton] public function initialize(Container $container): PDO { - // Prevent multiple PDO connections to live on in memory while running tests - // TODO: need to improve - if (self::$pdo === null) { - $databaseConfig = $container->get(DatabaseConfig::class); + if ($container->has(PDO::class)) { + return $container->get(PDO::class); + } - $connection = $databaseConfig->connection(); + $databaseConfig = $container->get(DatabaseConfig::class); - self::$pdo = new PDO( - $connection->getDsn(), - $connection->getUsername(), - $connection->getPassword(), - ); - } + $connection = $databaseConfig->connection(); - return self::$pdo; + return PDO::create( + $connection->getDsn(), + $connection->getUsername(), + $connection->getPassword(), + ); } } diff --git a/src/Tempest/Database/src/Transactions/GenericTransactionManager.php b/src/Tempest/Database/src/Transactions/GenericTransactionManager.php index a6196da51..bfba807b2 100644 --- a/src/Tempest/Database/src/Transactions/GenericTransactionManager.php +++ b/src/Tempest/Database/src/Transactions/GenericTransactionManager.php @@ -4,10 +4,10 @@ namespace Tempest\Database\Transactions; -use PDO; use Tempest\Database\Exceptions\CouldNotBeginTransaction; use Tempest\Database\Exceptions\CouldNotCommitTransaction; use Tempest\Database\Exceptions\CouldNotRollbackTransaction; +use Tempest\Database\PDO; final class GenericTransactionManager implements TransactionManager { @@ -17,7 +17,7 @@ public function __construct(private PDO $pdo) public function begin(): void { - $transactionBegun = $this->pdo->beginTransaction(); + $transactionBegun = $this->pdo->getPdo()->beginTransaction(); if (! $transactionBegun) { throw new CouldNotBeginTransaction(); @@ -26,7 +26,7 @@ public function begin(): void public function commit(): void { - $transactionCommitted = $this->pdo->commit(); + $transactionCommitted = $this->pdo->getPdo()->commit(); if (! $transactionCommitted) { throw new CouldNotCommitTransaction(); @@ -35,7 +35,7 @@ public function commit(): void public function rollback(): void { - $transactionRolledBack = $this->pdo->rollBack(); + $transactionRolledBack = $this->pdo->getPdo()->rollBack(); if (! $transactionRolledBack) { throw new CouldNotRollbackTransaction(); diff --git a/src/Tempest/Database/src/Transactions/TransactionManagerInitializer.php b/src/Tempest/Database/src/Transactions/TransactionManagerInitializer.php index 8cb5fbd1b..55d763b8d 100644 --- a/src/Tempest/Database/src/Transactions/TransactionManagerInitializer.php +++ b/src/Tempest/Database/src/Transactions/TransactionManagerInitializer.php @@ -4,10 +4,10 @@ namespace Tempest\Database\Transactions; -use PDO; use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Singleton; +use Tempest\Database\PDO; final readonly class TransactionManagerInitializer implements Initializer { diff --git a/src/Tempest/Database/tests/GenericDatabaseTest.php b/src/Tempest/Database/tests/GenericDatabaseTest.php index 7a942004d..6cf886727 100644 --- a/src/Tempest/Database/tests/GenericDatabaseTest.php +++ b/src/Tempest/Database/tests/GenericDatabaseTest.php @@ -5,9 +5,10 @@ namespace Tempest\Database\Tests; use Exception; -use PDO; +use PDO as GlobalPDO; use PHPUnit\Framework\TestCase; use Tempest\Database\GenericDatabase; +use Tempest\Database\PDO; use Tempest\Database\Transactions\GenericTransactionManager; /** @@ -17,16 +18,18 @@ final class GenericDatabaseTest extends TestCase { public function test_it_executes_transactions(): void { - $pdo = $this->createMock(PDO::class); - $pdo->expects($this->once()) + $globalPdo = $this->createMock(GlobalPDO::class); + $globalPdo->expects($this->once()) ->method('beginTransaction') ->withAnyParameters() ->willReturn(true); - $pdo->expects($this->once()) + $globalPdo->expects($this->once()) ->method('commit') ->withAnyParameters() ->willReturn(true); + $pdo = new PDO($globalPdo); + $database = new GenericDatabase( $pdo, new GenericTransactionManager($pdo), @@ -41,16 +44,18 @@ public function test_it_executes_transactions(): void public function test_it_rolls_back_transactions_on_failure(): void { - $pdo = $this->createMock(PDO::class); - $pdo->expects($this->once()) + $globalPdo = $this->createMock(GlobalPDO::class); + $globalPdo->expects($this->once()) ->method('beginTransaction') ->withAnyParameters() ->willReturn(true); - $pdo->expects($this->once()) + $globalPdo->expects($this->once()) ->method('rollback') ->withAnyParameters() ->willReturn(true); + $pdo = new PDO($globalPdo); + $database = new GenericDatabase( $pdo, new GenericTransactionManager($pdo), diff --git a/src/Tempest/Database/tests/GenericTransactionManagerTest.php b/src/Tempest/Database/tests/GenericTransactionManagerTest.php index b04b7ba52..3f4fa8135 100644 --- a/src/Tempest/Database/tests/GenericTransactionManagerTest.php +++ b/src/Tempest/Database/tests/GenericTransactionManagerTest.php @@ -4,11 +4,12 @@ namespace Tempest\Database\Tests; -use PDO; +use PDO as GlobalPDO; use PHPUnit\Framework\TestCase; use Tempest\Database\Exceptions\CouldNotBeginTransaction; use Tempest\Database\Exceptions\CouldNotCommitTransaction; use Tempest\Database\Exceptions\CouldNotRollbackTransaction; +use Tempest\Database\PDO; use Tempest\Database\Transactions\GenericTransactionManager; /** @@ -18,20 +19,20 @@ final class GenericTransactionManagerTest extends TestCase { public function test_it_calls_being_transactions(): void { - $pdo = $this->createMock(PDO::class); + $pdo = $this->createMock(GlobalPDO::class); $pdo->expects($this->once()) ->method('beginTransaction') ->withAnyParameters() ->willReturn(true); - $manager = new GenericTransactionManager($pdo); + $manager = new GenericTransactionManager(new PDO($pdo)); $manager->begin(); } public function test_it_throws_an_exception_when_transaction_cannot_begin(): void { - $pdo = $this->createMock(PDO::class); + $pdo = $this->createMock(GlobalPDO::class); $pdo->expects($this->once()) ->method('beginTransaction') ->withAnyParameters() @@ -39,27 +40,27 @@ public function test_it_throws_an_exception_when_transaction_cannot_begin(): voi $this->expectException(CouldNotBeginTransaction::class); - $manager = new GenericTransactionManager($pdo); + $manager = new GenericTransactionManager(new PDO($pdo)); $manager->begin(); } public function test_it_calls_commit_transactions(): void { - $pdo = $this->createMock(PDO::class); + $pdo = $this->createMock(GlobalPDO::class); $pdo->expects($this->once()) ->method('commit') ->withAnyParameters() ->willReturn(true); - $manager = new GenericTransactionManager($pdo); + $manager = new GenericTransactionManager(new PDO($pdo)); $manager->commit(); } public function test_it_throws_an_exception_when_transaction_cannot_commit(): void { - $pdo = $this->createMock(PDO::class); + $pdo = $this->createMock(GlobalPDO::class); $pdo->expects($this->once()) ->method('commit') ->withAnyParameters() @@ -67,27 +68,27 @@ public function test_it_throws_an_exception_when_transaction_cannot_commit(): vo $this->expectException(CouldNotCommitTransaction::class); - $manager = new GenericTransactionManager($pdo); + $manager = new GenericTransactionManager(new PDO($pdo)); $manager->commit(); } public function test_it_calls_rollback_transactions(): void { - $pdo = $this->createMock(PDO::class); + $pdo = $this->createMock(GlobalPDO::class); $pdo->expects($this->once()) ->method('rollBack') ->withAnyParameters() ->willReturn(true); - $manager = new GenericTransactionManager($pdo); + $manager = new GenericTransactionManager(new PDO($pdo)); $manager->rollback(); } public function test_it_throws_an_exception_when_transaction_cannot_rollback(): void { - $pdo = $this->createMock(PDO::class); + $pdo = $this->createMock(GlobalPDO::class); $pdo->expects($this->once()) ->method('rollBack') ->withAnyParameters() @@ -95,7 +96,7 @@ public function test_it_throws_an_exception_when_transaction_cannot_rollback(): $this->expectException(CouldNotRollbackTransaction::class); - $manager = new GenericTransactionManager($pdo); + $manager = new GenericTransactionManager(new PDO($pdo)); $manager->rollback(); } diff --git a/tests/Integration/Console/Components/TaskComponentTest.php b/tests/Integration/Console/Components/TaskComponentTest.php index cd72be28b..e8ff3d01b 100644 --- a/tests/Integration/Console/Components/TaskComponentTest.php +++ b/tests/Integration/Console/Components/TaskComponentTest.php @@ -9,6 +9,7 @@ use Tempest\Console\Components\Interactive\TaskComponent; use Tempest\Console\Console; use Tempest\Console\Terminal\Terminal; +use Tempest\Database\PDO; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; /** @@ -20,6 +21,8 @@ protected function setUp(): void { parent::setUp(); + $this->container->get(PDO::class)->close(); + if (PHP_OS_FAMILY === 'Windows') { $this->markTestSkipped('These tests require the pcntl extension, which is not available on Windows.'); } diff --git a/tests/Integration/Database/GenericDatabaseTest.php b/tests/Integration/Database/GenericDatabaseTest.php index 7995915b6..c31d71c7a 100644 --- a/tests/Integration/Database/GenericDatabaseTest.php +++ b/tests/Integration/Database/GenericDatabaseTest.php @@ -5,9 +5,12 @@ namespace Tests\Tempest\Integration\Database; use Exception; +use PDO; use Tempest\Database\Database; +use Tempest\Database\Exceptions\ConnectionClosed; use Tempest\Database\Migrations\CreateMigrationsTable; use Tempest\Database\Migrations\Migration; +use Tempest\Database\PDO as DatabasePDO; use Tests\Tempest\Fixtures\Migrations\CreateAuthorTable; use Tests\Tempest\Fixtures\Modules\Books\Models\Author; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; @@ -43,4 +46,16 @@ public function test_execute_with_fail_works_correctly(): void $this->assertCount(0, Author::all()); } + + public function test_pdo_close(): void + { + $this->expectException(ConnectionClosed::class); + + $pdo = $this->container->get(DatabasePDO::class); + + $this->assertInstanceOf(PDO::class, $pdo->getPdo()); + + $pdo->close(); + $pdo->getPdo(); // this throws + } }