diff --git a/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php b/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php index 73f0ea6bcc480..ab46a5894e684 100644 --- a/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php @@ -150,32 +150,33 @@ protected function doFetch(array $ids): iterable $now = time(); $expired = []; - $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN (?)"; - $result = $this->conn->executeQuery($sql, [ - $now, - $ids, - ], [ - ParameterType::INTEGER, - Connection::PARAM_STR_ARRAY, - ])->iterateNumeric(); + $sql = str_pad('', (\count($ids) << 1) - 1, '?,'); + $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue($i = 1, $now, ParameterType::INTEGER); + foreach ($ids as $id) { + $stmt->bindValue(++$i, $id, $this->getIdColumnType()); + } + $result = $stmt->executeQuery()->iterateNumeric(); foreach ($result as $row) { + $id = \is_resource($row[0]) ? stream_get_contents($row[0]) : $row[0]; if (null === $row[1]) { - $expired[] = $row[0]; + $expired[] = $id; } else { - yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]); + yield $id => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]); } } if ($expired) { - $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN (?)"; - $this->conn->executeStatement($sql, [ - $now, - $expired, - ], [ - ParameterType::INTEGER, - Connection::PARAM_STR_ARRAY, - ]); + $sql = str_pad('', (\count($expired) << 1) - 1, '?,'); + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)"; + $stmt = $this->conn->prepare($sql); + $stmt->bindValue($i = 1, $now, ParameterType::INTEGER); + foreach ($expired as $id) { + $stmt->bindValue(++$i, $id, $this->getIdColumnType()); + } + $stmt->executeQuery(); } } @@ -189,7 +190,7 @@ protected function doHave(string $id): bool $id, time(), ], [ - ParameterType::STRING, + $this->getIdColumnType(), ParameterType::INTEGER, ]); @@ -224,9 +225,16 @@ protected function doClear(string $namespace): bool */ protected function doDelete(array $ids): bool { - $sql = "DELETE FROM $this->table WHERE $this->idCol IN (?)"; + $ids = array_values($ids); + $sql = str_pad('', (\count($ids) << 1) - 1, '?,'); + $sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)"; try { - $this->conn->executeStatement($sql, [array_values($ids)], [Connection::PARAM_STR_ARRAY]); + $stmt = $this->conn->prepare($sql); + $i = 0; + foreach ($ids as $id) { + $stmt->bindValue(++$i, $id, $this->getIdColumnType()); + } + $stmt->executeQuery(); } catch (TableNotFoundException $e) { } @@ -296,7 +304,7 @@ protected function doSave(array $values, int $lifetime) $stmt->bindValue(7, $lifetime, ParameterType::INTEGER); $stmt->bindValue(8, $now, ParameterType::INTEGER); } elseif (null !== $platformName) { - $stmt->bindParam(1, $id); + $stmt->bindParam(1, $id, $this->getIdColumnType()); $stmt->bindParam(2, $data, ParameterType::LARGE_OBJECT); $stmt->bindValue(3, $lifetime, ParameterType::INTEGER); $stmt->bindValue(4, $now, ParameterType::INTEGER); @@ -384,6 +392,7 @@ private function addTableToSchema(Schema $schema): void { $types = [ 'mysql' => 'binary', + 'pgsql' => 'binary', 'sqlite' => 'text', ]; @@ -394,4 +403,9 @@ private function addTableToSchema(Schema $schema): void $table->addColumn($this->timeCol, 'integer', ['unsigned' => true]); $table->setPrimaryKey([$this->idCol]); } + + private function getIdColumnType(): int + { + return 'pgsql' === $this->getPlatformName() ? ParameterType::BINARY : ParameterType::STRING; + } } diff --git a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php index 5d107244312e7..1765aea5f241f 100644 --- a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php @@ -294,7 +294,7 @@ public function createTable() $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; break; case 'pgsql': - $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + $sql = "CREATE TABLE $this->table ($this->idCol BYTEA NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; break; case 'oci': $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; @@ -370,7 +370,7 @@ protected function doFetch(array $ids) $stmt = $connection->prepare($sql); $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT); foreach ($ids as $id) { - $stmt->bindValue(++$i, $id); + $stmt->bindValue(++$i, $id, $this->getIdColumnType()); } $result = $stmt->execute(); @@ -382,10 +382,11 @@ protected function doFetch(array $ids) } foreach ($result as $row) { + $id = \is_resource($row[0]) ? stream_get_contents($row[0]) : $row[0]; if (null === $row[1]) { - $expired[] = $row[0]; + $expired[] = $id; } else { - yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]); + yield $id => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]); } } @@ -395,7 +396,7 @@ protected function doFetch(array $ids) $stmt = $connection->prepare($sql); $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT); foreach ($expired as $id) { - $stmt->bindValue(++$i, $id); + $stmt->bindValue(++$i, $id, $this->getIdColumnType()); } $stmt->execute(); } @@ -411,7 +412,7 @@ protected function doHave(string $id) $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)"; $stmt = $connection->prepare($sql); - $stmt->bindValue(':id', $id); + $stmt->bindValue(':id', $id, $this->getIdColumnType()); $stmt->bindValue(':time', time(), \PDO::PARAM_INT); $stmt->execute(); @@ -452,7 +453,11 @@ protected function doDelete(array $ids) $sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)"; try { $stmt = $this->getConnection()->prepare($sql); - $stmt->execute(array_values($ids)); + $i = 0; + foreach (array_values($ids) as $id) { + $stmt->bindValue(++$i, $id, $this->getIdColumnType()); + } + $stmt->execute(); } catch (\PDOException $e) { } @@ -524,7 +529,7 @@ protected function doSave(array $values, int $lifetime) $stmt->bindValue(7, $lifetime, \PDO::PARAM_INT); $stmt->bindValue(8, $now, \PDO::PARAM_INT); } else { - $stmt->bindParam(':id', $id); + $stmt->bindParam(':id', $id, $this->getIdColumnType()); $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); $stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT); $stmt->bindValue(':time', $now, \PDO::PARAM_INT); @@ -580,4 +585,9 @@ private function getServerVersion(): string return $this->serverVersion; } + + private function getIdColumnType(): int + { + return 'pgsql' === $this->driver ? \PDO::PARAM_LOB : \PDO::PARAM_STR; + } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterAndDoctrineDbalAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterAndDoctrineDbalAdapterTest.php new file mode 100644 index 0000000000000..cd93e61062260 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterAndDoctrineDbalAdapterTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use Doctrine\DBAL\DriverManager; +use PHPUnit\Framework\SkippedTestSuiteError; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; + +/** + * @group time-sensitive + */ +class TagAwareAdapterAndDoctrineDbalAdapterTest extends TagAwareAdapterTestCase +{ + protected static $dbFile; + + public static function setUpBeforeClass(): void + { + if (!\extension_loaded('pdo_sqlite')) { + throw new SkippedTestSuiteError('Extension pdo_sqlite required.'); + } + + self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); + } + + public static function tearDownAfterClass(): void + { + @unlink(self::$dbFile); + } + + public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterface + { + return new TagAwareAdapter(new DoctrineDbalAdapter(DriverManager::getConnection(['driver' => 'pdo_sqlite', 'path' => self::$dbFile]), '', $defaultLifetime)); + } + + protected function createCacheAdapter(): AbstractAdapter + { + return new DoctrineDbalAdapter(DriverManager::getConnection(['driver' => 'pdo_sqlite', 'path' => self::$dbFile])); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterAndPdoAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterAndPdoAdapterTest.php new file mode 100644 index 0000000000000..a1f81039e0c4e --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterAndPdoAdapterTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use PHPUnit\Framework\SkippedTestSuiteError; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\PdoAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; + +/** + * @group time-sensitive + */ +class TagAwareAdapterAndPdoAdapterTest extends TagAwareAdapterTestCase +{ + protected static $dbFile; + + public static function setUpBeforeClass(): void + { + if (!\extension_loaded('pdo_sqlite')) { + throw new SkippedTestSuiteError('Extension pdo_sqlite required.'); + } + + self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_cache'); + } + + public static function tearDownAfterClass(): void + { + @unlink(self::$dbFile); + } + + public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterface + { + return new TagAwareAdapter(new PdoAdapter('sqlite:'.self::$dbFile, '', $defaultLifetime)); + } + + protected function createCacheAdapter(): AbstractAdapter + { + return new PdoAdapter('sqlite:'.self::$dbFile, '', 0); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php index 4346272e59e3e..630b12b2db45e 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php @@ -11,23 +11,17 @@ namespace Symfony\Component\Cache\Tests\Adapter; -use PHPUnit\Framework\MockObject\MockObject; use Psr\Cache\CacheItemPoolInterface; -use Symfony\Component\Cache\Adapter\AdapterInterface; -use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\TagAwareAdapter; -use Symfony\Component\Cache\PruneableInterface; -use Symfony\Component\Cache\Tests\Fixtures\PrunableAdapter; use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive */ -class TagAwareAdapterTest extends AdapterTestCase +class TagAwareAdapterTest extends TagAwareAdapterTestCase { - use TagAwareTestTrait; - public function createCachePool($defaultLifetime = 0): CacheItemPoolInterface { return new TagAwareAdapter(new FilesystemAdapter('', $defaultLifetime)); @@ -38,282 +32,8 @@ public static function tearDownAfterClass(): void (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } - public function testPrune() - { - $cache = new TagAwareAdapter($this->getPruneableMock()); - $this->assertTrue($cache->prune()); - - $cache = new TagAwareAdapter($this->getNonPruneableMock()); - $this->assertFalse($cache->prune()); - - $cache = new TagAwareAdapter($this->getFailingPruneableMock()); - $this->assertFalse($cache->prune()); - } - - public function testKnownTagVersionsTtl() - { - $itemsPool = new FilesystemAdapter('', 10); - $tagsPool = new ArrayAdapter(); - - $pool = new TagAwareAdapter($itemsPool, $tagsPool, 10); - - $item = $pool->getItem('foo'); - $item->tag(['baz']); - $item->expiresAfter(100); - - $tag = $tagsPool->getItem('baz'.TagAwareAdapter::TAGS_PREFIX); - $tagsPool->save($tag->set(10)); - - $pool->save($item); - $this->assertTrue($pool->getItem('foo')->isHit()); - $this->assertTrue($pool->getItem('foo')->isHit()); - - sleep(20); - - $this->assertTrue($pool->getItem('foo')->isHit()); - - sleep(5); - - $this->assertTrue($pool->getItem('foo')->isHit()); - } - - public function testTagEntryIsCreatedForItemWithoutTags() - { - $pool = $this->createCachePool(); - - $itemKey = 'foo'; - $item = $pool->getItem($itemKey); - $pool->save($item); - - $adapter = new FilesystemAdapter(); - $this->assertTrue($adapter->hasItem(TagAwareAdapter::TAGS_PREFIX.$itemKey)); - } - - public function testHasItemReturnsFalseWhenPoolDoesNotHaveItemTags() - { - $pool = $this->createCachePool(); - - $itemKey = 'foo'; - $item = $pool->getItem($itemKey); - $pool->save($item); - - $anotherPool = $this->createCachePool(); - - $adapter = new FilesystemAdapter(); - $adapter->deleteItem(TagAwareAdapter::TAGS_PREFIX.$itemKey); // simulate item losing tags pair - - $this->assertFalse($anotherPool->hasItem($itemKey)); - } - - public function testGetItemReturnsCacheMissWhenPoolDoesNotHaveItemTags() - { - $pool = $this->createCachePool(); - - $itemKey = 'foo'; - $item = $pool->getItem($itemKey); - $pool->save($item); - - $anotherPool = $this->createCachePool(); - - $adapter = new FilesystemAdapter(); - $adapter->deleteItem(TagAwareAdapter::TAGS_PREFIX.$itemKey); // simulate item losing tags pair - - $item = $anotherPool->getItem($itemKey); - $this->assertFalse($item->isHit()); - } - - public function testHasItemReturnsFalseWhenPoolDoesNotHaveItemAndOnlyHasTags() - { - $pool = $this->createCachePool(); - - $itemKey = 'foo'; - $item = $pool->getItem($itemKey); - $pool->save($item); - - $anotherPool = $this->createCachePool(); - - $adapter = new FilesystemAdapter(); - $adapter->deleteItem($itemKey); // simulate losing item but keeping tags - - $this->assertFalse($anotherPool->hasItem($itemKey)); - } - - public function testInvalidateTagsWithArrayAdapter() - { - $adapter = new TagAwareAdapter(new ArrayAdapter()); - - $item = $adapter->getItem('foo'); - - $this->assertFalse($item->isHit()); - - $item->tag('bar'); - $item->expiresAfter(100); - $adapter->save($item); - - $this->assertTrue($adapter->getItem('foo')->isHit()); - - $adapter->invalidateTags(['bar']); - - $this->assertFalse($adapter->getItem('foo')->isHit()); - } - - public function testGetItemReturnsCacheMissWhenPoolDoesNotHaveItemAndOnlyHasTags() - { - $pool = $this->createCachePool(); - - $itemKey = 'foo'; - $item = $pool->getItem($itemKey); - $pool->save($item); - - $anotherPool = $this->createCachePool(); - - $adapter = new FilesystemAdapter(); - $adapter->deleteItem($itemKey); // simulate losing item but keeping tags - - $item = $anotherPool->getItem($itemKey); - $this->assertFalse($item->isHit()); - } - - /** - * @return PruneableInterface&MockObject - */ - private function getPruneableMock(): PruneableInterface - { - $pruneable = $this->createMock(PrunableAdapter::class); - - $pruneable - ->expects($this->atLeastOnce()) - ->method('prune') - ->willReturn(true); - - return $pruneable; - } - - /** - * @return PruneableInterface&MockObject - */ - private function getFailingPruneableMock(): PruneableInterface - { - $pruneable = $this->createMock(PrunableAdapter::class); - - $pruneable - ->expects($this->atLeastOnce()) - ->method('prune') - ->willReturn(false); - - return $pruneable; - } - - /** - * @return AdapterInterface&MockObject - */ - private function getNonPruneableMock(): AdapterInterface - { - return $this->createMock(AdapterInterface::class); - } - - /** - * @doesNotPerformAssertions - */ - public function testToleranceForStringsAsTagVersionsCase1() + protected function createCacheAdapter(): AbstractAdapter { - $pool = $this->createCachePool(); - $adapter = new FilesystemAdapter(); - - $itemKey = 'foo'; - $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); - $adapter->save($tag->set("\x00abc\xff")); - $item = $pool->getItem($itemKey); - $pool->save($item->tag('bar')); - $pool->hasItem($itemKey); - $pool->getItem($itemKey); - } - - /** - * @doesNotPerformAssertions - */ - public function testToleranceForStringsAsTagVersionsCase2() - { - $pool = $this->createCachePool(); - $adapter = new FilesystemAdapter(); - - $itemKey = 'foo'; - $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); - $adapter->save($tag->set("\x00abc\xff")); - $item = $pool->getItem($itemKey); - $pool->save($item->tag('bar')); - sleep(100); - $pool->getItem($itemKey); - $pool->hasItem($itemKey); - } - - /** - * @doesNotPerformAssertions - */ - public function testToleranceForStringsAsTagVersionsCase3() - { - $pool = $this->createCachePool(); - $adapter = new FilesystemAdapter(); - - $itemKey = 'foo'; - $adapter->deleteItem('bar'.TagAwareAdapter::TAGS_PREFIX); - $item = $pool->getItem($itemKey); - $pool->save($item->tag('bar')); - $pool->getItem($itemKey); - - $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); - $adapter->save($tag->set("\x00abc\xff")); - - $pool->hasItem($itemKey); - $pool->getItem($itemKey); - sleep(100); - $pool->getItem($itemKey); - $pool->hasItem($itemKey); - } - - /** - * @doesNotPerformAssertions - */ - public function testToleranceForStringsAsTagVersionsCase4() - { - $pool = $this->createCachePool(); - $adapter = new FilesystemAdapter(); - - $itemKey = 'foo'; - $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); - $adapter->save($tag->set('abcABC')); - - $item = $pool->getItem($itemKey); - $pool->save($item->tag('bar')); - - $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); - $adapter->save($tag->set('001122')); - - $pool->invalidateTags(['bar']); - $pool->getItem($itemKey); - } - - /** - * @doesNotPerformAssertions - */ - public function testToleranceForStringsAsTagVersionsCase5() - { - $pool = $this->createCachePool(); - $pool2 = $this->createCachePool(); - $adapter = new FilesystemAdapter(); - - $itemKey1 = 'foo'; - $item = $pool->getItem($itemKey1); - $pool->save($item->tag('bar')); - - $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); - $adapter->save($tag->set('abcABC')); - - $itemKey2 = 'baz'; - $item = $pool2->getItem($itemKey2); - $pool2->save($item->tag('bar')); - foreach ($pool->getItems([$itemKey1, $itemKey2]) as $item) { - // run generator - } + return new FilesystemAdapter(); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTestCase.php new file mode 100644 index 0000000000000..a54028427d07b --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTestCase.php @@ -0,0 +1,307 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Adapter; + +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Tests\Fixtures\PrunableAdapter; + +abstract class TagAwareAdapterTestCase extends AdapterTestCase +{ + use TagAwareTestTrait; + + abstract protected function createCacheAdapter(): AbstractAdapter; + + public function testPrune() + { + $cache = new TagAwareAdapter($this->getPruneableMock()); + $this->assertTrue($cache->prune()); + + $cache = new TagAwareAdapter($this->getNonPruneableMock()); + $this->assertFalse($cache->prune()); + + $cache = new TagAwareAdapter($this->getFailingPruneableMock()); + $this->assertFalse($cache->prune()); + } + + public function testKnownTagVersionsTtl() + { + $itemsPool = new FilesystemAdapter('', 10); + $tagsPool = new ArrayAdapter(); + + $pool = new TagAwareAdapter($itemsPool, $tagsPool, 10); + + $item = $pool->getItem('foo'); + $item->tag(['baz']); + $item->expiresAfter(100); + + $tag = $tagsPool->getItem('baz'.TagAwareAdapter::TAGS_PREFIX); + $tagsPool->save($tag->set(10)); + + $pool->save($item); + $this->assertTrue($pool->getItem('foo')->isHit()); + $this->assertTrue($pool->getItem('foo')->isHit()); + + sleep(20); + + $this->assertTrue($pool->getItem('foo')->isHit()); + + sleep(5); + + $this->assertTrue($pool->getItem('foo')->isHit()); + } + + public function testTagEntryIsCreatedForItemWithoutTags() + { + $pool = $this->createCachePool(); + + $itemKey = 'foo'; + $item = $pool->getItem($itemKey); + $pool->save($item); + + $adapter = $this->createCacheAdapter(); + $this->assertTrue($adapter->hasItem(TagAwareAdapter::TAGS_PREFIX.$itemKey)); + } + + public function testHasItemReturnsFalseWhenPoolDoesNotHaveItemTags() + { + $pool = $this->createCachePool(); + + $itemKey = 'foo'; + $item = $pool->getItem($itemKey); + $pool->save($item); + + $anotherPool = $this->createCachePool(); + + $adapter = $this->createCacheAdapter(); + $adapter->deleteItem(TagAwareAdapter::TAGS_PREFIX.$itemKey); // simulate item losing tags pair + + $this->assertFalse($anotherPool->hasItem($itemKey)); + } + + public function testGetItemReturnsCacheMissWhenPoolDoesNotHaveItemTags() + { + $pool = $this->createCachePool(); + + $itemKey = 'foo'; + $item = $pool->getItem($itemKey); + $pool->save($item); + + $anotherPool = $this->createCachePool(); + + $adapter = $this->createCacheAdapter(); + $adapter->deleteItem(TagAwareAdapter::TAGS_PREFIX.$itemKey); // simulate item losing tags pair + + $item = $anotherPool->getItem($itemKey); + $this->assertFalse($item->isHit()); + } + + public function testHasItemReturnsFalseWhenPoolDoesNotHaveItemAndOnlyHasTags() + { + $pool = $this->createCachePool(); + + $itemKey = 'foo'; + $item = $pool->getItem($itemKey); + $pool->save($item); + + $anotherPool = $this->createCachePool(); + + $adapter = $this->createCacheAdapter(); + $adapter->deleteItem($itemKey); // simulate losing item but keeping tags + + $this->assertFalse($anotherPool->hasItem($itemKey)); + } + + public function testInvalidateTagsWithArrayAdapter() + { + $adapter = new TagAwareAdapter(new ArrayAdapter()); + + $item = $adapter->getItem('foo'); + + $this->assertFalse($item->isHit()); + + $item->tag('bar'); + $item->expiresAfter(100); + $adapter->save($item); + + $this->assertTrue($adapter->getItem('foo')->isHit()); + + $adapter->invalidateTags(['bar']); + + $this->assertFalse($adapter->getItem('foo')->isHit()); + } + + public function testGetItemReturnsCacheMissWhenPoolDoesNotHaveItemAndOnlyHasTags() + { + $pool = $this->createCachePool(); + + $itemKey = 'foo'; + $item = $pool->getItem($itemKey); + $pool->save($item); + + $anotherPool = $this->createCachePool(); + + $adapter = $this->createCacheAdapter(); + $adapter->deleteItem($itemKey); // simulate losing item but keeping tags + + $item = $anotherPool->getItem($itemKey); + $this->assertFalse($item->isHit()); + } + + /** + * @return PruneableInterface&MockObject + */ + private function getPruneableMock(): PruneableInterface + { + $pruneable = $this->createMock(PrunableAdapter::class); + + $pruneable + ->expects($this->atLeastOnce()) + ->method('prune') + ->willReturn(true); + + return $pruneable; + } + + /** + * @return PruneableInterface&MockObject + */ + private function getFailingPruneableMock(): PruneableInterface + { + $pruneable = $this->createMock(PrunableAdapter::class); + + $pruneable + ->expects($this->atLeastOnce()) + ->method('prune') + ->willReturn(false); + + return $pruneable; + } + + /** + * @return AdapterInterface&MockObject + */ + private function getNonPruneableMock(): AdapterInterface + { + return $this->createMock(AdapterInterface::class); + } + + /** + * @doesNotPerformAssertions + */ + public function testToleranceForStringsAsTagVersionsCase1() + { + $pool = $this->createCachePool(); + $adapter = $this->createCacheAdapter(); + + $itemKey = 'foo'; + $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); + $adapter->save($tag->set("\x00abc\xff")); + $item = $pool->getItem($itemKey); + $pool->save($item->tag('bar')); + $pool->hasItem($itemKey); + $pool->getItem($itemKey); + } + + /** + * @doesNotPerformAssertions + */ + public function testToleranceForStringsAsTagVersionsCase2() + { + $pool = $this->createCachePool(); + $adapter = $this->createCacheAdapter(); + + $itemKey = 'foo'; + $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); + $adapter->save($tag->set("\x00abc\xff")); + $item = $pool->getItem($itemKey); + $pool->save($item->tag('bar')); + sleep(100); + $pool->getItem($itemKey); + $pool->hasItem($itemKey); + } + + /** + * @doesNotPerformAssertions + */ + public function testToleranceForStringsAsTagVersionsCase3() + { + $pool = $this->createCachePool(); + $adapter = $this->createCacheAdapter(); + + $itemKey = 'foo'; + $adapter->deleteItem('bar'.TagAwareAdapter::TAGS_PREFIX); + $item = $pool->getItem($itemKey); + $pool->save($item->tag('bar')); + $pool->getItem($itemKey); + + $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); + $adapter->save($tag->set("\x00abc\xff")); + + $pool->hasItem($itemKey); + $pool->getItem($itemKey); + sleep(100); + $pool->getItem($itemKey); + $pool->hasItem($itemKey); + } + + /** + * @doesNotPerformAssertions + */ + public function testToleranceForStringsAsTagVersionsCase4() + { + $pool = $this->createCachePool(); + $adapter = $this->createCacheAdapter(); + + $itemKey = 'foo'; + $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); + $adapter->save($tag->set('abcABC')); + + $item = $pool->getItem($itemKey); + $pool->save($item->tag('bar')); + + $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); + $adapter->save($tag->set('001122')); + + $pool->invalidateTags(['bar']); + $pool->getItem($itemKey); + } + + /** + * @doesNotPerformAssertions + */ + public function testToleranceForStringsAsTagVersionsCase5() + { + $pool = $this->createCachePool(); + $pool2 = $this->createCachePool(); + $adapter = $this->createCacheAdapter(); + + $itemKey1 = 'foo'; + $item = $pool->getItem($itemKey1); + $pool->save($item->tag('bar')); + + $tag = $adapter->getItem('bar'.TagAwareAdapter::TAGS_PREFIX); + $adapter->save($tag->set('abcABC')); + + $itemKey2 = 'baz'; + $item = $pool2->getItem($itemKey2); + $pool2->save($item->tag('bar')); + foreach ($pool->getItems([$itemKey1, $itemKey2]) as $item) { + // run generator + } + } +}