From daa7e0c7559749246cf0047c93204689cbe64fba Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Thu, 3 Aug 2023 17:46:21 +0300 Subject: [PATCH] draft implementation --- .github/workflows/build.yml | 5 +- src/CacheItem.php | 106 +++++++++++++++++++++- src/CacheItemPool.php | 167 ++++++++++++++++++++++++++++++++++- tests/CacheItemPoolTest.php | 169 ++++++++++++++++++++++++++++++++++++ tests/TestCase.php | 8 +- 5 files changed, 450 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 11a2323..4425e61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,10 +20,13 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: mbstring + extensions: mbstring, pdo, sqlite, pdo_sqlite, memcached tools: composer:v2 coverage: none + - name: Install Memcached. + uses: niden/actions-memcached@v7 + - name: Install dependencies run: | composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi diff --git a/src/CacheItem.php b/src/CacheItem.php index 2939f20..e32300d 100644 --- a/src/CacheItem.php +++ b/src/CacheItem.php @@ -3,12 +3,116 @@ namespace yii1tech\psr\cache; use CComponent; +use Psr\Cache\CacheItemInterface; /** * @author Paul Klimov * @since 1.0 */ -class CacheItem extends CComponent +class CacheItem extends CComponent implements CacheItemInterface { + /** + * @var string cache item key (ID). + */ + private $_key; + /** + * @var mixed cache item value. + */ + private $_value; + + /** + * @var int|null cache item expire. + */ + private $_expire; + + /** + * Sets the key for the current cache item. + * + * @param string $key the key string for this cache item. + * @return static self reference. + */ + public function setKey(string $key): self + { + $this->_key = $key; + + return $this; + } + + /** + * @return int|null cache item expiration in seconds. + */ + public function getExpire() + { + return $this->_expire; + } + + /** + * {@inheritdoc} + */ + public function getKey(): string + { + return $this->_key; + } + + /** + * {@inheritdoc} + */ + public function get(): mixed + { + if ($this->_value === false) { + return null; + } + + return $this->_value; + } + + /** + * {@inheritdoc} + */ + public function isHit(): bool + { + return $this->_value !== false; + } + + /** + * {@inheritdoc} + */ + public function set($value): static + { + $this->_value = $value; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function expiresAt($expiration): static + { + if ($expiration === null) { + $this->_expire = null; + } else { + $this->_expire = $expiration->getTimestamp() - time(); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function expiresAfter($time): static + { + if ($time === null) { + $this->_expire = null; + } elseif ($time instanceof \DateInterval) { + $timestamp = (new \DateTime())->add($time)->getTimestamp(); + $this->_expire = $timestamp - time(); + } else { + $this->_expire = (int) $time; + } + + return $this; + } } \ No newline at end of file diff --git a/src/CacheItemPool.php b/src/CacheItemPool.php index 3bd31ed..a6096b4 100644 --- a/src/CacheItemPool.php +++ b/src/CacheItemPool.php @@ -3,20 +3,45 @@ namespace yii1tech\psr\cache; use CApplicationComponent; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; use Yii; /** * @author Paul Klimov * @since 1.0 */ -class CacheItemPool extends CApplicationComponent +class CacheItemPool extends CApplicationComponent implements CacheItemPoolInterface { /** - * @var \ICache|array|string + * @var bool whether to automatically commit all deferred items on object destruction. + */ + public $autocommit = true; + + /** + * @var \ICache|array|string wrapped Yii cache component. */ private $_cache = 'cache'; /** + * @var \Psr\Cache\CacheItemInterface[] deferred cache items. + */ + private $_deferredItems = []; + + /** + * Destructor. + * Commits deferred cache items, if {@see $autocommit} is enabled. + */ + public function __destruct() + { + if ($this->autocommit) { + $this->commit(); + } + } + + /** + * Returns wrapped Yii cache component instance. + * * @return \ICache Yii cache component instance. */ public function getCache() @@ -33,6 +58,8 @@ public function getCache() } /** + * Sets the Yii cache component to be used for cache items storage. + * * @param \ICache|array|string $cache cache component instance, application component ID or array configuration. * @return static self reference. */ @@ -43,5 +70,141 @@ public function setCache($cache): self return $this; } + /** + * @return \Psr\Cache\CacheItemInterface[] deferred cache items. + */ + public function getDeferredItems(): array + { + return $this->_deferredItems; + } + + /** + * Instantiates cache item. + * + * @param string $key cache item key. + * @param mixed $value cache item value. + * @return \Psr\Cache\CacheItemInterface cache item instance. + */ + protected function createCacheItem($key, $value): CacheItemInterface + { + $item = new CacheItem(); + $item->setKey($key); + $item->set($value); + + return $item; + } + + /** + * {@inheritdoc} + */ + public function getItem($key): CacheItemInterface + { + if (isset($this->_deferredItems[$key])) { + return $this->_deferredItems[$key]; + } + + $value = $this->getCache()->get($key); + + return $this->createCacheItem($key, $value); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []): iterable + { + $items = []; + foreach ($this->getCache()->mget($keys) as $key => $value) { + $items[$key] = $this->createCacheItem($key, $value); + } + + return $items; + } + + /** + * {@inheritdoc} + */ + public function hasItem($key): bool + { + return $this->getCache()->get($key) !== false; + } + + /** + * {@inheritdoc} + */ + public function clear(): bool + { + return $this->getCache()->flush(); + } + + /** + * {@inheritdoc} + */ + public function deleteItem($key): bool + { + return $this->getCache()->delete($key); + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys): bool + { + $cache = $this->getCache(); + + $result = true; + + foreach ($keys as $key) { + if (!$cache->delete($key)) { + $result = false; + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function save(CacheItemInterface $item): bool + { + if (!$item instanceof CacheItem) { + return false; + } + + return $this->getCache()->set( + $item->getKey(), + $item->get(), + $item->getExpire() + // @todo dependency + ); + } + /** + * {@inheritdoc} + */ + public function saveDeferred(CacheItemInterface $item): bool + { + $this->_deferredItems[$item->getKey()] = $item; + + return true; + } + + /** + * {@inheritdoc} + */ + public function commit(): bool + { + $result = true; + + foreach ($this->_deferredItems as $key => $item) { + if ($this->save($item)) { + unset($this->_deferredItems[$key]); + } else { + $result = false; + } + } + + return $result; + } } \ No newline at end of file diff --git a/tests/CacheItemPoolTest.php b/tests/CacheItemPoolTest.php index 08461a1..2abbdf4 100644 --- a/tests/CacheItemPoolTest.php +++ b/tests/CacheItemPoolTest.php @@ -3,6 +3,7 @@ namespace yii1tech\psr\cache\test; use CDummyCache; +use DateInterval; use ICache; use yii1tech\psr\cache\CacheItemPool; @@ -31,4 +32,172 @@ public function testGetDefaultCache(): void $cache = $pool->getCache(); $this->assertTrue($cache instanceof ICache); } + + public function testSave(): void + { + $pool = new CacheItemPool(); + + $key = 'test'; + $value = 'test-value'; + + $item = $pool->getItem($key); + $item->set($value); + $item->expiresAfter(DateInterval::createFromDateString('1 hour')); + + $this->assertTrue($pool->save($item)); + + $this->assertTrue($pool->hasItem($key)); + + $item = $pool->getItem($key); + $this->assertTrue($item->isHit()); + $this->assertEquals($value, $item->get()); + } + + /** + * @depends testSave + */ + public function testGetBatch(): void + { + $pool = new CacheItemPool(); + + $key = 'test'; + $value = 'test-value'; + + $item = $pool->getItem($key); + $item->set($value); + $item->expiresAfter(DateInterval::createFromDateString('1 hour')); + + $this->assertTrue($pool->save($item)); + + $items = $pool->getItems([$key]); + $this->assertArrayHasKey($key, $items); + + $item = $items[$key]; + + $this->assertTrue($item->isHit()); + $this->assertEquals($value, $item->get()); + } + + /** + * @depends testSave + */ + public function testDelete(): void + { + $pool = new CacheItemPool(); + + $key = 'test'; + $value = 'test-value'; + + $item = $pool->getItem($key); + $item->set($value); + $item->expiresAfter(DateInterval::createFromDateString('1 hour')); + + $this->assertTrue($pool->save($item)); + + $this->assertTrue($pool->deleteItem($key)); + + $this->assertFalse($pool->hasItem($key)); + + $item = $pool->getItem($key); + $this->assertFalse($item->isHit()); + } + + /** + * @depends testDelete + */ + public function testDeleteBatch(): void + { + $pool = new CacheItemPool(); + + $key = 'test'; + $value = 'test-value'; + + $item = $pool->getItem($key); + $item->set($value); + $item->expiresAfter(DateInterval::createFromDateString('1 hour')); + + $this->assertTrue($pool->save($item)); + + $this->assertTrue($pool->deleteItems([$key])); + + $this->assertFalse($pool->hasItem($key)); + + $item = $pool->getItem($key); + $this->assertFalse($item->isHit()); + } + + /** + * @depends testSave + */ + public function testClear(): void + { + $pool = new CacheItemPool(); + + $key = 'test'; + $value = 'test-value'; + + $item = $pool->getItem($key); + $item->set($value); + $item->expiresAfter(DateInterval::createFromDateString('1 hour')); + + $this->assertTrue($pool->save($item)); + + $this->assertTrue($pool->clear()); + + $this->assertFalse($pool->hasItem($key)); + + $item = $pool->getItem($key); + $this->assertFalse($item->isHit()); + } + + /** + * @depends testSave + */ + public function testSaveDeferred(): void + { + $pool = new CacheItemPool(); + + $key = 'test'; + $value = 'test-value'; + + $item = $pool->getItem($key); + $item->set($value); + $item->expiresAfter(DateInterval::createFromDateString('1 hour')); + + $this->assertTrue($pool->saveDeferred($item)); + + $this->assertEmpty($pool->getCache()->get($key)); + + $deferredItems = $pool->getDeferredItems(); + $this->assertArrayHasKey($key, $deferredItems); + + $pool->commit(); + $this->assertSame($value, $pool->getCache()->get($key)); + + $this->assertEmpty($pool->getDeferredItems()); + } + + /** + * @depends testSaveDeferred + */ + public function testAutoCommit(): void + { + $pool = new CacheItemPool(); + + $key = 'test'; + $value = 'test-value'; + + $item = $pool->getItem($key); + $item->set($value); + $item->expiresAfter(DateInterval::createFromDateString('1 hour')); + + $this->assertTrue($pool->saveDeferred($item)); + + $cache = $pool->getCache(); + + $pool->autocommit = true; + unset($pool); + + $this->assertSame($value, $cache->get($key)); + } } \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 02305f5..00845ae 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -40,8 +40,14 @@ protected function mockApplication($config = [], $appClass = CConsoleApplication 'id' => 'testapp', 'basePath' => __DIR__, 'components' => [ + 'db' => [ + 'class' => \CDbConnection::class, + 'connectionString' => 'sqlite::memory:', + ], 'cache' => [ - 'class' => \CDummyCache::class, + 'class' => \CDbCache::class, + 'connectionID' => 'db', + 'autoCreateCacheTable' => true, ], ], ], $config));