Pairity/tests/CachingTest.php
Funky Waddle 68f3c05868
Some checks are pending
CI / test (8.2) (push) Waiting to run
CI / test (8.3) (push) Waiting to run
Cache and some other things
2026-01-06 10:56:40 -06:00

177 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Pairity\Tests;
use PHPUnit\Framework\TestCase;
use Pairity\Database\ConnectionManager;
use Pairity\Model\AbstractDto;
use Pairity\Model\AbstractDao;
use Pairity\Orm\UnitOfWork;
use Psr\SimpleCache\CacheInterface;
class TestDto extends AbstractDto {}
final class CachingTest extends TestCase
{
private function conn()
{
return ConnectionManager::make(['driver' => 'sqlite', 'path' => ':memory:']);
}
private function mockCache()
{
return new class implements CacheInterface {
private array $store = [];
public $hits = 0;
public $sets = 0;
public function get(string $key, mixed $default = null): mixed {
if (isset($this->store[$key])) {
$this->hits++;
return unserialize($this->store[$key]);
}
return $default;
}
public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool {
$this->sets++;
$this->store[$key] = serialize($value);
return true;
}
public function delete(string $key): bool { unset($this->store[$key]); return true; }
public function clear(): bool { $this->store = []; return true; }
public function getMultiple(iterable $keys, mixed $default = null): iterable { return []; }
public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool { return true; }
public function deleteMultiple(iterable $keys): bool { return true; }
public function has(string $key): bool { return isset($this->store[$key]); }
};
}
public function testCachingFindById(): void
{
$conn = $this->conn();
$conn->execute('CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)');
$dao = new class($conn) extends AbstractDao {
public function getTable(): string { return 'items'; }
protected function dtoClass(): string { return TestDto::class; }
};
$cache = $this->mockCache();
$dao->setCache($cache);
$created = $dao->insert(['name' => 'Item 1']);
$id = $created->id;
// insert() calls findById() internally to return the fresh DTO, so it might already be cached.
$cache->hits = 0; // Reset hits for the test
// First find - should be a cache hit now because of insert()
$item1 = $dao->findById($id);
$this->assertNotNull($item1);
$this->assertEquals(1, $cache->hits);
// Second find - cache hit again
$item2 = $dao->findById($id);
$this->assertNotNull($item2);
$this->assertEquals(2, $cache->hits);
$this->assertEquals($item1->name, $item2->name);
// Note: item1 and item2 are different instances if not in UoW,
// because we serialize/unserialize in our mock cache.
$this->assertNotSame($item1, $item2);
// Update - should invalidate cache
$dao->update($id, ['name' => 'Updated']);
$item3 = $dao->findById($id);
// How many hits now?
// findById checks cache for ID (miss)
// findById calls findOneBy
// findOneBy checks cache for criteria (miss)
$this->assertEquals('Updated', $item3->name);
}
public function testIdentityMapIntegration(): void
{
$conn = $this->conn();
$conn->execute('CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)');
$dao = new class($conn) extends AbstractDao {
public function getTable(): string { return 'items'; }
protected function dtoClass(): string { return TestDto::class; }
};
$cache = $this->mockCache();
$dao->setCache($cache);
$created = $dao->insert(['name' => 'Item 1']);
$id = $created->id;
$cache->hits = 0;
UnitOfWork::run(function() use ($dao, $id, $cache) {
$item1 = $dao->findById($id); // Cache hit (from insert), attaches to identity map
$item2 = $dao->findById($id); // UoW lookup (no cache hit recorded)
$this->assertEquals(1, $cache->hits);
$this->assertSame($item1, $item2);
});
// Outside UoW
$item3 = $dao->findById($id); // Cache hit
$this->assertEquals(2, $cache->hits);
// Run another UoW, should hit cache but then return same instance from UoW
UnitOfWork::run(function() use ($dao, $id, $cache) {
$item4 = $dao->findById($id); // Cache hit, attached to UoW
$this->assertEquals(3, $cache->hits);
$item5 = $dao->findById($id); // UoW hit
$this->assertEquals(3, $cache->hits);
$this->assertSame($item4, $item5);
});
}
public function testFindAllCaching(): void
{
$conn = $this->conn();
$conn->execute('CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)');
$dao = new class($conn) extends AbstractDao {
public function getTable(): string { return 'items'; }
protected function dtoClass(): string { return TestDto::class; }
};
$cache = $this->mockCache();
$dao->setCache($cache);
$dao->insert(['name' => 'A']);
$dao->insert(['name' => 'B']);
$all1 = $dao->findAllBy([]);
$this->assertCount(2, $all1);
$this->assertEquals(0, $cache->hits);
$all2 = $dao->findAllBy([]);
$this->assertCount(2, $all2);
$this->assertEquals(1, $cache->hits);
// Test bulk invalidation
$dao->deleteBy(['name' => 'A']); // In our trait, this does nothing unless clear_all_on_bulk is true
// Let's configure it to clear all
$dao = new class($conn) extends AbstractDao {
public function getTable(): string { return 'items'; }
protected function dtoClass(): string { return TestDto::class; }
public function cacheConfig(): array {
return array_merge(parent::cacheConfig(), ['clear_all_on_bulk' => true]);
}
};
$dao->setCache($cache);
$dao->findAllBy([]); // missed again because it's a new DAO instance/key prefix
$dao->findAllBy([]); // hit
$this->assertGreaterThanOrEqual(1, $cache->hits);
$dao->deleteBy(['name' => 'A']);
$all3 = $dao->findAllBy([]); // miss (invalidated)
$this->assertCount(1, $all3);
}
}