From bc4ededa60c26f1ee20c319e71ab3ffba4a6e2cd Mon Sep 17 00:00:00 2001 From: sonypradana Date: Sat, 7 Mar 2026 22:27:21 +0700 Subject: [PATCH 1/8] feat: add PDO cache storage driver with unit and integration tests - Implement PdoStorage with raw SQL for portability - Add PdoStorageTest for unit testing (SQLite memory) - Add PdoStorageRealConnectionTest for integration testing - Apply project-wide coding standard fixes via php-cs-fixer --- src/System/Cache/Storage/PdoStorage.php | 172 ++++++++++++++++++ .../Storage/PdoStorageRealConnectionTest.php | 84 +++++++++ tests/Cache/Storage/PdoStorageTest.php | 98 ++++++++++ 3 files changed, 354 insertions(+) create mode 100644 src/System/Cache/Storage/PdoStorage.php create mode 100644 tests/Cache/Storage/PdoStorageRealConnectionTest.php create mode 100644 tests/Cache/Storage/PdoStorageTest.php diff --git a/src/System/Cache/Storage/PdoStorage.php b/src/System/Cache/Storage/PdoStorage.php new file mode 100644 index 00000000..d79c517b --- /dev/null +++ b/src/System/Cache/Storage/PdoStorage.php @@ -0,0 +1,172 @@ +pdo->prepare("SELECT value, expiration FROM {$this->table} WHERE key = :key"); + $stmt->execute(['key' => $key]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (false === $row) { + return $default; + } + + if (time() >= (int) $row['expiration']) { + $this->delete($key); + + return $default; + } + + return unserialize($row['value']); + } + + public function set(string $key, mixed $value, int|\DateInterval|null $ttl = null): bool + { + $expiration = $this->calculateExpirationTimestamp($ttl); + $serialized = serialize($value); + + $stmt = $this->pdo->prepare("DELETE FROM {$this->table} WHERE key = :key"); + $stmt->execute(['key' => $key]); + + $stmt = $this->pdo->prepare("INSERT INTO {$this->table} (key, value, expiration) VALUES (:key, :value, :expiration)"); + + return $stmt->execute([ + 'key' => $key, + 'value' => $serialized, + 'expiration' => $expiration, + ]); + } + + public function delete(string $key): bool + { + $stmt = $this->pdo->prepare("DELETE FROM {$this->table} WHERE key = :key"); + + return $stmt->execute(['key' => $key]); + } + + public function clear(): bool + { + $stmt = $this->pdo->prepare("DELETE FROM {$this->table}"); + + return $stmt->execute(); + } + + public function getMultiple(iterable $keys, mixed $default = null): iterable + { + $result = []; + foreach ($keys as $key) { + $result[$key] = $this->get($key, $default); + } + + return $result; + } + + public function setMultiple(iterable $values, int|\DateInterval|null $ttl = null): bool + { + $success = true; + foreach ($values as $key => $value) { + if (false === $this->set($key, $value, $ttl)) { + $success = false; + } + } + + return $success; + } + + public function deleteMultiple(iterable $keys): bool + { + $success = true; + foreach ($keys as $key) { + if (false === $this->delete($key)) { + $success = false; + } + } + + return $success; + } + + public function has(string $key): bool + { + $stmt = $this->pdo->prepare("SELECT 1 FROM {$this->table} WHERE key = :key AND expiration > :now"); + $stmt->execute([ + 'key' => $key, + 'now' => time(), + ]); + + return false !== $stmt->fetchColumn(); + } + + public function increment(string $key, int $value): int + { + $stmt = $this->pdo->prepare("SELECT value, expiration FROM {$this->table} WHERE key = :key"); + $stmt->execute(['key' => $key]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (false === $row || time() >= (int) $row['expiration']) { + $this->set($key, $value); + + return $value; + } + + $current = unserialize($row['value']); + $expiration = (int) $row['expiration']; + + if (false === is_int($current)) { + throw new \InvalidArgumentException('Value increment must be integer.'); + } + + $new = $current + $value; + $ttl = $expiration - time(); + + $this->set($key, $new, max(0, $ttl)); + + return $new; + } + + public function decrement(string $key, int $value): int + { + return $this->increment($key, $value * -1); + } + + public function remember(string $key, int|\DateInterval|null $ttl, \Closure $callback): mixed + { + $value = $this->get($key); + + if (null !== $value) { + return $value; + } + + $this->set($key, $value = $callback(), $ttl); + + return $value; + } + + private function calculateExpirationTimestamp(int|\DateInterval|\DateTimeInterface|null $ttl): int + { + if ($ttl instanceof \DateInterval) { + return (new \DateTimeImmutable())->add($ttl)->getTimestamp(); + } + + if ($ttl instanceof \DateTimeInterface) { + return $ttl->getTimestamp(); + } + + $ttl ??= $this->defaultTTL; + + return time() + $ttl; + } +} diff --git a/tests/Cache/Storage/PdoStorageRealConnectionTest.php b/tests/Cache/Storage/PdoStorageRealConnectionTest.php new file mode 100644 index 00000000..df5cac69 --- /dev/null +++ b/tests/Cache/Storage/PdoStorageRealConnectionTest.php @@ -0,0 +1,84 @@ +createConnection(); + $this->createCacheTable(); + + $reflection = new \ReflectionClass($this->pdo); + $property = $reflection->getProperty('dbh'); + $property->setAccessible(true); + $dbh = $property->getValue($this->pdo); + + $this->storage = new PdoStorage($dbh, 'cache', 60); + } + + protected function tearDown(): void + { + $this->dropCacheTable(); + } + + private function createCacheTable(): void + { + $this->pdo->query(' + CREATE TABLE cache ( + key VARCHAR(255) PRIMARY KEY, + value TEXT, + expiration INT + ) + ')->execute(); + } + + private function dropCacheTable(): void + { + $this->pdo->query('DROP TABLE IF EXISTS cache')->execute(); + } + + public function testRealConnectionGetAndSet() + { + $this->assertTrue($this->storage->set('real_key', ['complex' => 'data', 'number' => 123])); + $result = $this->storage->get('real_key'); + + $this->assertIsArray($result); + $this->assertEquals('data', $result['complex']); + $this->assertEquals(123, $result['number']); + } + + public function testRealConnectionExpiration() + { + $this->storage->set('expired_soon', 'bye', 1); + $this->assertEquals('bye', $this->storage->get('expired_soon')); + + // Wait for expiration + sleep(2); + + $this->assertNull($this->storage->get('expired_soon')); + } + + public function testRealConnectionIncrement() + { + $this->storage->set('counter', 10, 10); + $this->assertEquals(15, $this->storage->increment('counter', 5)); + $this->assertEquals(15, $this->storage->get('counter')); + } + + public function testRealConnectionClear() + { + $this->storage->set('a', 1); + $this->storage->set('b', 2); + $this->assertTrue($this->storage->clear()); + $this->assertFalse($this->storage->has('a')); + $this->assertFalse($this->storage->has('b')); + } +} diff --git a/tests/Cache/Storage/PdoStorageTest.php b/tests/Cache/Storage/PdoStorageTest.php new file mode 100644 index 00000000..8b26cf04 --- /dev/null +++ b/tests/Cache/Storage/PdoStorageTest.php @@ -0,0 +1,98 @@ +pdo = new \PDO('sqlite::memory:'); + $this->pdo->exec(' + CREATE TABLE cache ( + key VARCHAR(255) PRIMARY KEY, + value TEXT, + expiration INT + ) + '); + + $this->storage = new PdoStorage($this->pdo, 'cache', 60); + } + + public function testGetAndSet() + { + $this->assertTrue($this->storage->set('foo', 'bar')); + $this->assertEquals('bar', $this->storage->get('foo')); + } + + public function testGetExpired() + { + $this->storage->set('foo', 'bar', -1); + $this->assertNull($this->storage->get('foo')); + } + + public function testDelete() + { + $this->storage->set('foo', 'bar'); + $this->assertTrue($this->storage->delete('foo')); + $this->assertNull($this->storage->get('foo')); + } + + public function testClear() + { + $this->storage->set('foo', 'bar'); + $this->storage->set('baz', 'qux'); + $this->assertTrue($this->storage->clear()); + $this->assertNull($this->storage->get('foo')); + $this->assertNull($this->storage->get('baz')); + } + + public function testHas() + { + $this->storage->set('foo', 'bar'); + $this->assertTrue($this->storage->has('foo')); + $this->assertFalse($this->storage->has('not_found')); + } + + public function testIncrement() + { + $this->storage->set('num', 1); + $this->assertEquals(2, $this->storage->increment('num', 1)); + $this->assertEquals(2, $this->storage->get('num')); + } + + public function testDecrement() + { + $this->storage->set('num', 10); + $this->assertEquals(7, $this->storage->decrement('num', 3)); + $this->assertEquals(7, $this->storage->get('num')); + } + + public function testRemember() + { + $this->assertNull($this->storage->get('rem')); + $value = $this->storage->remember('rem', 60, fn () => 'remembered'); + $this->assertEquals('remembered', $value); + $this->assertEquals('remembered', $this->storage->get('rem')); + } + + public function testMultiple() + { + $values = ['a' => 1, 'b' => 2]; + $this->assertTrue($this->storage->setMultiple($values)); + + $get = $this->storage->getMultiple(['a', 'b', 'c'], 'default'); + $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 'default'], (array) $get); + + $this->assertTrue($this->storage->deleteMultiple(['a', 'b'])); + $this->assertNull($this->storage->get('a')); + $this->assertNull($this->storage->get('b')); + } +} From cf64bd8307f2b31ce6b147578d385a6b2c88136d Mon Sep 17 00:00:00 2001 From: SonyPradana Date: Mon, 9 Mar 2026 14:27:13 +0800 Subject: [PATCH 2/8] test --- .github/workflows/database.yml | 6 +- src/System/Cache/Storage/PdoStorage.php | 44 ++++++++-- .../Storage/PdoStorageRealConnectionTest.php | 87 ++++++++++++++----- tests/Cache/Storage/PdoStorageTest.php | 77 +++++++++++++--- 4 files changed, 170 insertions(+), 44 deletions(-) diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 96b7a616..47500005 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -53,7 +53,7 @@ jobs: command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - name: Execute tests - run: vendor/bin/phpunit tests/DataBase --exclude-group not-for-mysql5.7 + run: vendor/bin/phpunit tests/DataBase tests/Cache/Storage/PdoStorageRealConnectionTest.php tests/Cache/Storage/PdoStorageRealConnectionTest.php --exclude-group not-for-mysql5.7 env: DB_CONNECTION: mysql DB_USERNAME: root @@ -96,7 +96,7 @@ jobs: command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - name: Execute tests - run: vendor/bin/phpunit tests/DataBase + run: vendor/bin/phpunit tests/DataBase tests/Cache/Storage/PdoStorageRealConnectionTest.php env: DB_CONNECTION: mysql DB_USERNAME: root @@ -139,6 +139,6 @@ jobs: command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - name: Execute tests - run: vendor/bin/phpunit tests/DataBase + run: vendor/bin/phpunit tests/DataBase tests/Cache/Storage/PdoStorageRealConnectionTest.php env: DB_CONNECTION: mariadb diff --git a/src/System/Cache/Storage/PdoStorage.php b/src/System/Cache/Storage/PdoStorage.php index d79c517b..f3ca7a18 100644 --- a/src/System/Cache/Storage/PdoStorage.php +++ b/src/System/Cache/Storage/PdoStorage.php @@ -17,7 +17,10 @@ public function __construct( public function get(string $key, mixed $default = null): mixed { - $stmt = $this->pdo->prepare("SELECT value, expiration FROM {$this->table} WHERE key = :key"); + $table = $this->quoteIdentifier($this->table); + $key_column = $this->quoteIdentifier('key'); + + $stmt = $this->pdo->prepare("SELECT value, expiration FROM {$table} WHERE {$key_column} = :key"); $stmt->execute(['key' => $key]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); @@ -39,10 +42,16 @@ public function set(string $key, mixed $value, int|\DateInterval|null $ttl = nul $expiration = $this->calculateExpirationTimestamp($ttl); $serialized = serialize($value); - $stmt = $this->pdo->prepare("DELETE FROM {$this->table} WHERE key = :key"); + $table = $this->quoteIdentifier($this->table); + $key_column = $this->quoteIdentifier('key'); + + $stmt = $this->pdo->prepare("DELETE FROM {$table} WHERE {$key_column} = :key"); $stmt->execute(['key' => $key]); - $stmt = $this->pdo->prepare("INSERT INTO {$this->table} (key, value, expiration) VALUES (:key, :value, :expiration)"); + $value_column = $this->quoteIdentifier('value'); + $exp_column = $this->quoteIdentifier('expiration'); + + $stmt = $this->pdo->prepare("INSERT INTO {$table} ({$key_column}, {$value_column}, {$exp_column}) VALUES (:key, :value, :expiration)"); return $stmt->execute([ 'key' => $key, @@ -53,14 +62,18 @@ public function set(string $key, mixed $value, int|\DateInterval|null $ttl = nul public function delete(string $key): bool { - $stmt = $this->pdo->prepare("DELETE FROM {$this->table} WHERE key = :key"); + $table = $this->quoteIdentifier($this->table); + $key_column = $this->quoteIdentifier('key'); + + $stmt = $this->pdo->prepare("DELETE FROM {$table} WHERE {$key_column} = :key"); return $stmt->execute(['key' => $key]); } public function clear(): bool { - $stmt = $this->pdo->prepare("DELETE FROM {$this->table}"); + $table = $this->quoteIdentifier($this->table); + $stmt = $this->pdo->prepare("DELETE FROM {$table}"); return $stmt->execute(); } @@ -101,7 +114,11 @@ public function deleteMultiple(iterable $keys): bool public function has(string $key): bool { - $stmt = $this->pdo->prepare("SELECT 1 FROM {$this->table} WHERE key = :key AND expiration > :now"); + $table = $this->quoteIdentifier($this->table); + $key_column = $this->quoteIdentifier('key'); + $exp_column = $this->quoteIdentifier('expiration'); + + $stmt = $this->pdo->prepare("SELECT 1 FROM {$table} WHERE {$key_column} = :key AND {$exp_column} > :now"); $stmt->execute([ 'key' => $key, 'now' => time(), @@ -112,7 +129,10 @@ public function has(string $key): bool public function increment(string $key, int $value): int { - $stmt = $this->pdo->prepare("SELECT value, expiration FROM {$this->table} WHERE key = :key"); + $table = $this->quoteIdentifier($this->table); + $key_column = $this->quoteIdentifier('key'); + + $stmt = $this->pdo->prepare("SELECT value, expiration FROM {$table} WHERE {$key_column} = :key"); $stmt->execute(['key' => $key]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); @@ -155,6 +175,16 @@ public function remember(string $key, int|\DateInterval|null $ttl, \Closure $cal return $value; } + private function quoteIdentifier(string $identifier): string + { + $driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + + return match ($driver) { + 'mysql', 'mariadb' => "`{$identifier}`", + default => "\"{$identifier}\"", + }; + } + private function calculateExpirationTimestamp(int|\DateInterval|\DateTimeInterface|null $ttl): int { if ($ttl instanceof \DateInterval) { diff --git a/tests/Cache/Storage/PdoStorageRealConnectionTest.php b/tests/Cache/Storage/PdoStorageRealConnectionTest.php index df5cac69..20c8cafe 100644 --- a/tests/Cache/Storage/PdoStorageRealConnectionTest.php +++ b/tests/Cache/Storage/PdoStorageRealConnectionTest.php @@ -4,48 +4,76 @@ namespace System\Test\Cache\Storage; +use PHPUnit\Framework\TestCase; use System\Cache\Storage\PdoStorage; -use System\Test\Database\TestDatabase; -class PdoStorageRealConnectionTest extends TestDatabase +/** + * @group database + * @covers \System\Cache\Storage\PdoStorage + */ +class PdoStorageRealConnectionTest extends TestCase { + private \PDO $pdo; private PdoStorage $storage; + private string $driver; protected function setUp(): void { - $this->createConnection(); - $this->createCacheTable(); - - $reflection = new \ReflectionClass($this->pdo); - $property = $reflection->getProperty('dbh'); - $property->setAccessible(true); - $dbh = $property->getValue($this->pdo); + $this->driver = $_ENV['DB_CONNECTION'] ?? 'mysql'; + $host = $_ENV['DB_HOST'] ?? '127.0.0.1'; + $db = $_ENV['DB_DATABASE'] ?? 'forge'; + $user = $_ENV['DB_USERNAME'] ?? 'root'; + $pass = $_ENV['DB_PASSWORD'] ?? ''; + $port = $_ENV['DB_PORT'] ?? '3306'; + + try { + $dsn = "{$this->driver}:host={$host};port={$port};dbname={$db};charset=utf8mb4"; + $this->pdo = new \PDO($dsn, $user, $pass, [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + ]); + } catch (\PDOException $e) { + $this->markTestSkipped('Database connection failed: ' . $e->getMessage()); + } - $this->storage = new PdoStorage($dbh, 'cache', 60); + $this->createCacheTable(); + $this->storage = new PdoStorage($this->pdo, 'cache', 60); } protected function tearDown(): void { - $this->dropCacheTable(); + if (isset($this->pdo)) { + $this->dropCacheTable(); + } } private function createCacheTable(): void { - $this->pdo->query(' - CREATE TABLE cache ( - key VARCHAR(255) PRIMARY KEY, - value TEXT, - expiration INT + $quote = match ($this->driver) { + 'mysql', 'mariadb' => '`', + default => '"', + }; + + $this->pdo->exec(" + CREATE TABLE IF NOT EXISTS cache ( + {$quote}key{$quote} VARCHAR(255) PRIMARY KEY, + {$quote}value{$quote} TEXT, + {$quote}expiration{$quote} INT ) - ')->execute(); + "); } private function dropCacheTable(): void { - $this->pdo->query('DROP TABLE IF EXISTS cache')->execute(); + $this->pdo->exec('DROP TABLE IF EXISTS cache'); } - public function testRealConnectionGetAndSet() + /** + * @test + * @testdox it can get and set cache on real connection + * @covers \System\Cache\Storage\PdoStorage::get + * @covers \System\Cache\Storage\PdoStorage::set + */ + public function it_can_get_and_set_cache_on_real_connection() { $this->assertTrue($this->storage->set('real_key', ['complex' => 'data', 'number' => 123])); $result = $this->storage->get('real_key'); @@ -55,7 +83,12 @@ public function testRealConnectionGetAndSet() $this->assertEquals(123, $result['number']); } - public function testRealConnectionExpiration() + /** + * @test + * @testdox it should handle cache expiration on real connection + * @covers \System\Cache\Storage\PdoStorage::get + */ + public function it_should_handle_cache_expiration_on_real_connection() { $this->storage->set('expired_soon', 'bye', 1); $this->assertEquals('bye', $this->storage->get('expired_soon')); @@ -66,14 +99,24 @@ public function testRealConnectionExpiration() $this->assertNull($this->storage->get('expired_soon')); } - public function testRealConnectionIncrement() + /** + * @test + * @testdox it can increment cache on real connection + * @covers \System\Cache\Storage\PdoStorage::increment + */ + public function it_can_increment_cache_on_real_connection() { $this->storage->set('counter', 10, 10); $this->assertEquals(15, $this->storage->increment('counter', 5)); $this->assertEquals(15, $this->storage->get('counter')); } - public function testRealConnectionClear() + /** + * @test + * @testdox it can clear cache on real connection + * @covers \System\Cache\Storage\PdoStorage::clear + */ + public function it_can_clear_cache_on_real_connection() { $this->storage->set('a', 1); $this->storage->set('b', 2); diff --git a/tests/Cache/Storage/PdoStorageTest.php b/tests/Cache/Storage/PdoStorageTest.php index 8b26cf04..17e62ade 100644 --- a/tests/Cache/Storage/PdoStorageTest.php +++ b/tests/Cache/Storage/PdoStorageTest.php @@ -7,6 +7,10 @@ use PHPUnit\Framework\TestCase; use System\Cache\Storage\PdoStorage; +/** + * @group database + * @covers \System\Cache\Storage\PdoStorage + */ class PdoStorageTest extends TestCase { private \PDO $pdo; @@ -17,35 +21,57 @@ protected function setUp(): void $this->pdo = new \PDO('sqlite::memory:'); $this->pdo->exec(' CREATE TABLE cache ( - key VARCHAR(255) PRIMARY KEY, - value TEXT, - expiration INT + "key" VARCHAR(255) PRIMARY KEY, + "value" TEXT, + "expiration" INT ) '); $this->storage = new PdoStorage($this->pdo, 'cache', 60); } - public function testGetAndSet() + /** + * @test + * @testdox it can get and set cache + * @covers \System\Cache\Storage\PdoStorage::get + * @covers \System\Cache\Storage\PdoStorage::set + * @covers \System\Cache\Storage\PdoStorage::quoteIdentifier + */ + public function it_can_get_and_set_cache() { $this->assertTrue($this->storage->set('foo', 'bar')); $this->assertEquals('bar', $this->storage->get('foo')); } - public function testGetExpired() + /** + * @test + * @testdox it should return null if cache expired + * @covers \System\Cache\Storage\PdoStorage::get + */ + public function it_should_return_null_if_cache_expired() { $this->storage->set('foo', 'bar', -1); $this->assertNull($this->storage->get('foo')); } - public function testDelete() + /** + * @test + * @testdox it can delete cache + * @covers \System\Cache\Storage\PdoStorage::delete + */ + public function it_can_delete_cache() { $this->storage->set('foo', 'bar'); $this->assertTrue($this->storage->delete('foo')); $this->assertNull($this->storage->get('foo')); } - public function testClear() + /** + * @test + * @testdox it can clear all cache + * @covers \System\Cache\Storage\PdoStorage::clear + */ + public function it_can_clear_all_cache() { $this->storage->set('foo', 'bar'); $this->storage->set('baz', 'qux'); @@ -54,28 +80,48 @@ public function testClear() $this->assertNull($this->storage->get('baz')); } - public function testHas() + /** + * @test + * @testdox it can check if cache exists + * @covers \System\Cache\Storage\PdoStorage::has + */ + public function it_can_check_if_cache_exists() { $this->storage->set('foo', 'bar'); $this->assertTrue($this->storage->has('foo')); $this->assertFalse($this->storage->has('not_found')); } - public function testIncrement() + /** + * @test + * @testdox it can increment cache value + * @covers \System\Cache\Storage\PdoStorage::increment + */ + public function it_can_increment_cache_value() { $this->storage->set('num', 1); $this->assertEquals(2, $this->storage->increment('num', 1)); $this->assertEquals(2, $this->storage->get('num')); } - public function testDecrement() + /** + * @test + * @testdox it can decrement cache value + * @covers \System\Cache\Storage\PdoStorage::decrement + */ + public function it_can_decrement_cache_value() { $this->storage->set('num', 10); $this->assertEquals(7, $this->storage->decrement('num', 3)); $this->assertEquals(7, $this->storage->get('num')); } - public function testRemember() + /** + * @test + * @testdox it can remember cache value + * @covers \System\Cache\Storage\PdoStorage::remember + */ + public function it_can_remember_cache_value() { $this->assertNull($this->storage->get('rem')); $value = $this->storage->remember('rem', 60, fn () => 'remembered'); @@ -83,7 +129,14 @@ public function testRemember() $this->assertEquals('remembered', $this->storage->get('rem')); } - public function testMultiple() + /** + * @test + * @testdox it can handle multiple cache operations + * @covers \System\Cache\Storage\PdoStorage::getMultiple + * @covers \System\Cache\Storage\PdoStorage::setMultiple + * @covers \System\Cache\Storage\PdoStorage::deleteMultiple + */ + public function it_can_handle_multiple_cache_operations() { $values = ['a' => 1, 'b' => 2]; $this->assertTrue($this->storage->setMultiple($values)); From f689f1b0d0de155c86919b6a4deb158d6fca64ec Mon Sep 17 00:00:00 2001 From: SonyPradana Date: Mon, 9 Mar 2026 14:27:56 +0800 Subject: [PATCH 3/8] formatting --- src/System/Cache/Storage/PdoStorage.php | 20 +++++----- .../Storage/PdoStorageRealConnectionTest.php | 19 +++++++--- tests/Cache/Storage/PdoStorageTest.php | 37 ++++++++++++++----- 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/System/Cache/Storage/PdoStorage.php b/src/System/Cache/Storage/PdoStorage.php index f3ca7a18..b6f5bf48 100644 --- a/src/System/Cache/Storage/PdoStorage.php +++ b/src/System/Cache/Storage/PdoStorage.php @@ -17,9 +17,9 @@ public function __construct( public function get(string $key, mixed $default = null): mixed { - $table = $this->quoteIdentifier($this->table); + $table = $this->quoteIdentifier($this->table); $key_column = $this->quoteIdentifier('key'); - + $stmt = $this->pdo->prepare("SELECT value, expiration FROM {$table} WHERE {$key_column} = :key"); $stmt->execute(['key' => $key]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); @@ -42,15 +42,15 @@ public function set(string $key, mixed $value, int|\DateInterval|null $ttl = nul $expiration = $this->calculateExpirationTimestamp($ttl); $serialized = serialize($value); - $table = $this->quoteIdentifier($this->table); + $table = $this->quoteIdentifier($this->table); $key_column = $this->quoteIdentifier('key'); $stmt = $this->pdo->prepare("DELETE FROM {$table} WHERE {$key_column} = :key"); $stmt->execute(['key' => $key]); $value_column = $this->quoteIdentifier('value'); - $exp_column = $this->quoteIdentifier('expiration'); - + $exp_column = $this->quoteIdentifier('expiration'); + $stmt = $this->pdo->prepare("INSERT INTO {$table} ({$key_column}, {$value_column}, {$exp_column}) VALUES (:key, :value, :expiration)"); return $stmt->execute([ @@ -62,9 +62,9 @@ public function set(string $key, mixed $value, int|\DateInterval|null $ttl = nul public function delete(string $key): bool { - $table = $this->quoteIdentifier($this->table); + $table = $this->quoteIdentifier($this->table); $key_column = $this->quoteIdentifier('key'); - + $stmt = $this->pdo->prepare("DELETE FROM {$table} WHERE {$key_column} = :key"); return $stmt->execute(['key' => $key]); @@ -73,7 +73,7 @@ public function delete(string $key): bool public function clear(): bool { $table = $this->quoteIdentifier($this->table); - $stmt = $this->pdo->prepare("DELETE FROM {$table}"); + $stmt = $this->pdo->prepare("DELETE FROM {$table}"); return $stmt->execute(); } @@ -114,7 +114,7 @@ public function deleteMultiple(iterable $keys): bool public function has(string $key): bool { - $table = $this->quoteIdentifier($this->table); + $table = $this->quoteIdentifier($this->table); $key_column = $this->quoteIdentifier('key'); $exp_column = $this->quoteIdentifier('expiration'); @@ -129,7 +129,7 @@ public function has(string $key): bool public function increment(string $key, int $value): int { - $table = $this->quoteIdentifier($this->table); + $table = $this->quoteIdentifier($this->table); $key_column = $this->quoteIdentifier('key'); $stmt = $this->pdo->prepare("SELECT value, expiration FROM {$table} WHERE {$key_column} = :key"); diff --git a/tests/Cache/Storage/PdoStorageRealConnectionTest.php b/tests/Cache/Storage/PdoStorageRealConnectionTest.php index 20c8cafe..a3b45bb8 100644 --- a/tests/Cache/Storage/PdoStorageRealConnectionTest.php +++ b/tests/Cache/Storage/PdoStorageRealConnectionTest.php @@ -9,6 +9,7 @@ /** * @group database + * * @covers \System\Cache\Storage\PdoStorage */ class PdoStorageRealConnectionTest extends TestCase @@ -27,7 +28,7 @@ protected function setUp(): void $port = $_ENV['DB_PORT'] ?? '3306'; try { - $dsn = "{$this->driver}:host={$host};port={$port};dbname={$db};charset=utf8mb4"; + $dsn = "{$this->driver}:host={$host};port={$port};dbname={$db};charset=utf8mb4"; $this->pdo = new \PDO($dsn, $user, $pass, [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, ]); @@ -69,11 +70,13 @@ private function dropCacheTable(): void /** * @test + * * @testdox it can get and set cache on real connection + * * @covers \System\Cache\Storage\PdoStorage::get * @covers \System\Cache\Storage\PdoStorage::set */ - public function it_can_get_and_set_cache_on_real_connection() + public function itCanGetAndSetCacheOnRealConnection() { $this->assertTrue($this->storage->set('real_key', ['complex' => 'data', 'number' => 123])); $result = $this->storage->get('real_key'); @@ -85,10 +88,12 @@ public function it_can_get_and_set_cache_on_real_connection() /** * @test + * * @testdox it should handle cache expiration on real connection + * * @covers \System\Cache\Storage\PdoStorage::get */ - public function it_should_handle_cache_expiration_on_real_connection() + public function itShouldHandleCacheExpirationOnRealConnection() { $this->storage->set('expired_soon', 'bye', 1); $this->assertEquals('bye', $this->storage->get('expired_soon')); @@ -101,10 +106,12 @@ public function it_should_handle_cache_expiration_on_real_connection() /** * @test + * * @testdox it can increment cache on real connection + * * @covers \System\Cache\Storage\PdoStorage::increment */ - public function it_can_increment_cache_on_real_connection() + public function itCanIncrementCacheOnRealConnection() { $this->storage->set('counter', 10, 10); $this->assertEquals(15, $this->storage->increment('counter', 5)); @@ -113,10 +120,12 @@ public function it_can_increment_cache_on_real_connection() /** * @test + * * @testdox it can clear cache on real connection + * * @covers \System\Cache\Storage\PdoStorage::clear */ - public function it_can_clear_cache_on_real_connection() + public function itCanClearCacheOnRealConnection() { $this->storage->set('a', 1); $this->storage->set('b', 2); diff --git a/tests/Cache/Storage/PdoStorageTest.php b/tests/Cache/Storage/PdoStorageTest.php index 17e62ade..a7ee8d60 100644 --- a/tests/Cache/Storage/PdoStorageTest.php +++ b/tests/Cache/Storage/PdoStorageTest.php @@ -9,6 +9,7 @@ /** * @group database + * * @covers \System\Cache\Storage\PdoStorage */ class PdoStorageTest extends TestCase @@ -32,12 +33,14 @@ protected function setUp(): void /** * @test + * * @testdox it can get and set cache + * * @covers \System\Cache\Storage\PdoStorage::get * @covers \System\Cache\Storage\PdoStorage::set * @covers \System\Cache\Storage\PdoStorage::quoteIdentifier */ - public function it_can_get_and_set_cache() + public function itCanGetAndSetCache() { $this->assertTrue($this->storage->set('foo', 'bar')); $this->assertEquals('bar', $this->storage->get('foo')); @@ -45,10 +48,12 @@ public function it_can_get_and_set_cache() /** * @test + * * @testdox it should return null if cache expired + * * @covers \System\Cache\Storage\PdoStorage::get */ - public function it_should_return_null_if_cache_expired() + public function itShouldReturnNullIfCacheExpired() { $this->storage->set('foo', 'bar', -1); $this->assertNull($this->storage->get('foo')); @@ -56,10 +61,12 @@ public function it_should_return_null_if_cache_expired() /** * @test + * * @testdox it can delete cache + * * @covers \System\Cache\Storage\PdoStorage::delete */ - public function it_can_delete_cache() + public function itCanDeleteCache() { $this->storage->set('foo', 'bar'); $this->assertTrue($this->storage->delete('foo')); @@ -68,10 +75,12 @@ public function it_can_delete_cache() /** * @test + * * @testdox it can clear all cache + * * @covers \System\Cache\Storage\PdoStorage::clear */ - public function it_can_clear_all_cache() + public function itCanClearAllCache() { $this->storage->set('foo', 'bar'); $this->storage->set('baz', 'qux'); @@ -82,10 +91,12 @@ public function it_can_clear_all_cache() /** * @test + * * @testdox it can check if cache exists + * * @covers \System\Cache\Storage\PdoStorage::has */ - public function it_can_check_if_cache_exists() + public function itCanCheckIfCacheExists() { $this->storage->set('foo', 'bar'); $this->assertTrue($this->storage->has('foo')); @@ -94,10 +105,12 @@ public function it_can_check_if_cache_exists() /** * @test + * * @testdox it can increment cache value + * * @covers \System\Cache\Storage\PdoStorage::increment */ - public function it_can_increment_cache_value() + public function itCanIncrementCacheValue() { $this->storage->set('num', 1); $this->assertEquals(2, $this->storage->increment('num', 1)); @@ -106,10 +119,12 @@ public function it_can_increment_cache_value() /** * @test + * * @testdox it can decrement cache value + * * @covers \System\Cache\Storage\PdoStorage::decrement */ - public function it_can_decrement_cache_value() + public function itCanDecrementCacheValue() { $this->storage->set('num', 10); $this->assertEquals(7, $this->storage->decrement('num', 3)); @@ -118,10 +133,12 @@ public function it_can_decrement_cache_value() /** * @test + * * @testdox it can remember cache value + * * @covers \System\Cache\Storage\PdoStorage::remember */ - public function it_can_remember_cache_value() + public function itCanRememberCacheValue() { $this->assertNull($this->storage->get('rem')); $value = $this->storage->remember('rem', 60, fn () => 'remembered'); @@ -131,12 +148,14 @@ public function it_can_remember_cache_value() /** * @test + * * @testdox it can handle multiple cache operations + * * @covers \System\Cache\Storage\PdoStorage::getMultiple * @covers \System\Cache\Storage\PdoStorage::setMultiple * @covers \System\Cache\Storage\PdoStorage::deleteMultiple */ - public function it_can_handle_multiple_cache_operations() + public function itCanHandleMultipleCacheOperations() { $values = ['a' => 1, 'b' => 2]; $this->assertTrue($this->storage->setMultiple($values)); From 60dd9cf6fcafa5bf61be414c2a08c818524a020d Mon Sep 17 00:00:00 2001 From: SonyPradana Date: Mon, 9 Mar 2026 14:47:07 +0800 Subject: [PATCH 4/8] use group test "database" --- .github/workflows/database.yml | 10 +++++++--- .phpactor.json | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .phpactor.json diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 47500005..2f65797e 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -6,6 +6,8 @@ on: - ".github/workflows/database.yml" - "src/System/Database/**" - "tests/DataBase/**" + - "src/System/Cache/Storage/PdoStorage.php" + - "tests/Cache/Storage/PdoStorageRealConnectionTest.php" branches: - master pull_request: @@ -13,6 +15,8 @@ on: - ".github/workflows/database.yml" - "src/System/Database/**" - "tests/DataBase/**" + - "src/System/Cache/Storage/PdoStorage.php" + - "tests/Cache/Storage/PdoStorageRealConnectionTest.php" jobs: mysql_57: @@ -53,7 +57,7 @@ jobs: command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - name: Execute tests - run: vendor/bin/phpunit tests/DataBase tests/Cache/Storage/PdoStorageRealConnectionTest.php tests/Cache/Storage/PdoStorageRealConnectionTest.php --exclude-group not-for-mysql5.7 + run: vendor/bin/phpunit --group database --exclude-group not-for-mysql5.7 env: DB_CONNECTION: mysql DB_USERNAME: root @@ -96,7 +100,7 @@ jobs: command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - name: Execute tests - run: vendor/bin/phpunit tests/DataBase tests/Cache/Storage/PdoStorageRealConnectionTest.php + run: vendor/bin/phpunit --group database env: DB_CONNECTION: mysql DB_USERNAME: root @@ -139,6 +143,6 @@ jobs: command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress - name: Execute tests - run: vendor/bin/phpunit tests/DataBase tests/Cache/Storage/PdoStorageRealConnectionTest.php + run: vendor/bin/phpunit --group database env: DB_CONNECTION: mariadb diff --git a/.phpactor.json b/.phpactor.json new file mode 100644 index 00000000..c38fd493 --- /dev/null +++ b/.phpactor.json @@ -0,0 +1,5 @@ +{ + "$schema": "/phpactor.schema.json", + "language_server_phpstan.enabled": true, + "language_server_php_cs_fixer.enabled": true +} \ No newline at end of file From d7148ffc3b7575dbad5c4afa6172ca07dc0f1212 Mon Sep 17 00:00:00 2001 From: SonyPradana Date: Mon, 9 Mar 2026 15:00:26 +0800 Subject: [PATCH 5/8] refactor: use optional dependency --- composer.json | 1 + src/System/Cache/Storage/PdoStorage.php | 12 +++++++++++- src/System/Cache/composer.json | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c6a9b1d6..fddf29d3 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "rector/rector": "^1.2" }, "suggest": { + "ext-pdo": "Required to use the PDO cache driver.", "ext-redis": "Required to use the phpredis connector." }, "autoload": { diff --git a/src/System/Cache/Storage/PdoStorage.php b/src/System/Cache/Storage/PdoStorage.php index b6f5bf48..d4a58876 100644 --- a/src/System/Cache/Storage/PdoStorage.php +++ b/src/System/Cache/Storage/PdoStorage.php @@ -8,11 +8,21 @@ class PdoStorage implements CacheInterface { + /** + * @param \PDO $pdo PDO instance + */ public function __construct( - private \PDO $pdo, + private $pdo, private string $table = 'cache', private int $defaultTTL = 3_600, ) { + if (false === extension_loaded('pdo') || !class_exists('\PDO')) { + throw new \RuntimeException('The PDO extension is required to use PdoStorage.'); + } + + if (false === ($this->pdo instanceof \PDO)) { + throw new \InvalidArgumentException('The pdo must be an instance of \PDO.'); + } } public function get(string $key, mixed $default = null): mixed diff --git a/src/System/Cache/composer.json b/src/System/Cache/composer.json index 480a44fc..ee4881ac 100644 --- a/src/System/Cache/composer.json +++ b/src/System/Cache/composer.json @@ -10,6 +10,9 @@ "require": { "php": "^8.0" }, + "suggest": { + "ext-pdo": "Required to use the PDO cache driver.", + }, "autoload": { "psr-4": { "System\\Cache\\": "" From bfa11f0cbff04528ea7484115e3d9ee36f963cfb Mon Sep 17 00:00:00 2001 From: SonyPradana Date: Mon, 9 Mar 2026 15:02:27 +0800 Subject: [PATCH 6/8] revert --- .phpactor.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .phpactor.json diff --git a/.phpactor.json b/.phpactor.json deleted file mode 100644 index c38fd493..00000000 --- a/.phpactor.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "/phpactor.schema.json", - "language_server_phpstan.enabled": true, - "language_server_php_cs_fixer.enabled": true -} \ No newline at end of file From 72a112b033f64d0d5852a0d79178a0bfecaa9c54 Mon Sep 17 00:00:00 2001 From: SonyPradana Date: Mon, 9 Mar 2026 16:38:46 +0800 Subject: [PATCH 7/8] formatting --- src/System/Cache/Storage/PdoStorage.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/System/Cache/Storage/PdoStorage.php b/src/System/Cache/Storage/PdoStorage.php index d4a58876..1fd2a817 100644 --- a/src/System/Cache/Storage/PdoStorage.php +++ b/src/System/Cache/Storage/PdoStorage.php @@ -9,18 +9,20 @@ class PdoStorage implements CacheInterface { /** - * @param \PDO $pdo PDO instance + * @param \PDO $pdo PDO instance */ public function __construct( - private $pdo, + private object $pdo, private string $table = 'cache', private int $defaultTTL = 3_600, ) { - if (false === extension_loaded('pdo') || !class_exists('\PDO')) { + /** @var class-string $class */ + $class = '\PDO'; + if (false === extension_loaded('pdo') || false === class_exists($class)) { throw new \RuntimeException('The PDO extension is required to use PdoStorage.'); } - if (false === ($this->pdo instanceof \PDO)) { + if (false === ($this->pdo instanceof $class)) { throw new \InvalidArgumentException('The pdo must be an instance of \PDO.'); } } From 5287bf21b8632c1f7511701f421124e4a4fdc9db Mon Sep 17 00:00:00 2001 From: Angger Pradana Date: Mon, 9 Mar 2026 16:23:37 +0700 Subject: [PATCH 8/8] formatting --- src/System/Cache/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System/Cache/composer.json b/src/System/Cache/composer.json index ee4881ac..6a303c11 100644 --- a/src/System/Cache/composer.json +++ b/src/System/Cache/composer.json @@ -11,7 +11,7 @@ "php": "^8.0" }, "suggest": { - "ext-pdo": "Required to use the PDO cache driver.", + "ext-pdo": "Required to use the PDO cache driver." }, "autoload": { "psr-4": {