diff --git a/typo3/sysext/core/Classes/Cache/Backend/AbstractBackend.php b/typo3/sysext/core/Classes/Cache/Backend/AbstractBackend.php index 6d80002a2c03ad98bb450a0d05802aa9a014b321..ee3f64181c4c9eb370da2fe3c223ea7dca5e073d 100644 --- a/typo3/sysext/core/Classes/Cache/Backend/AbstractBackend.php +++ b/typo3/sysext/core/Classes/Cache/Backend/AbstractBackend.php @@ -106,6 +106,22 @@ abstract class AbstractBackend implements \TYPO3\CMS\Core\Cache\Backend\BackendI $this->defaultLifetime = $defaultLifetime; } + /** + * Backwards compatibility safeguard since re-introducing flushByTags as API. + * See https://review.typo3.org/#/c/50537/ comments for patch set 14. + * + * The method is here even though it is only required for TaggableBackendInterface. + * We add it here to ensure third party cache backends do not fail but instead + * delegate to a less efficient linear flushing behavior. + * + * @param string[] $tags + * @api + */ + public function flushByTags(array $tags) + { + array_walk($tags, [$this, 'flushByTag']); + } + /** * Calculates the expiry time by the given lifetime. If no lifetime is * specified, the default lifetime is used. diff --git a/typo3/sysext/core/Classes/Cache/Backend/TaggableBackendInterface.php b/typo3/sysext/core/Classes/Cache/Backend/TaggableBackendInterface.php index d9f2949f692812ccbed75e74bf0113e7748178ff..8dd7db8a0c207eb0274f4a972bb4836a9faf9bb7 100644 --- a/typo3/sysext/core/Classes/Cache/Backend/TaggableBackendInterface.php +++ b/typo3/sysext/core/Classes/Cache/Backend/TaggableBackendInterface.php @@ -26,6 +26,15 @@ interface TaggableBackendInterface extends \TYPO3\CMS\Core\Cache\Backend\Backend */ public function flushByTag($tag); + /** + * Removes all cache entries of this cache which are tagged by any of the specified tags. + * + * @param string[] $tag List of tags + * @return void + * @api + */ + public function flushByTags(array $tags); + /** * Finds and returns all cache entry identifiers which are tagged by the * specified tag diff --git a/typo3/sysext/core/Classes/Cache/Backend/Typo3DatabaseBackend.php b/typo3/sysext/core/Classes/Cache/Backend/Typo3DatabaseBackend.php index d7f59f30a1508db18f24ca98b39bd85d04d819f3..817461ec14da8e5c54e4bd5935d5c8135e4df1ac 100644 --- a/typo3/sysext/core/Classes/Cache/Backend/Typo3DatabaseBackend.php +++ b/typo3/sysext/core/Classes/Cache/Backend/Typo3DatabaseBackend.php @@ -261,6 +261,69 @@ class Typo3DatabaseBackend extends AbstractBackend implements TaggableBackendInt ->truncate($this->tagsTable); } + /** + * Removes all entries tagged by any of the specified tags. Performs the SQL + * operation as a bulk query for better performance. + * + * @param string[] $tags + */ + public function flushByTags(array $tags) + { + $this->throwExceptionIfFrontendDoesNotExist(); + + if (empty($tags)) { + return; + } + + /** @var Connection $connection */ + $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable); + + // A large set of tags was detected. Process it in chunks to guard against exceeding + // maximum SQL query limits. + if (count($tags) > 100) { + array_walk(array_chunk($tags, 100), [$this, 'flushByTags']); + return; + } + // VERY simple quoting of tags is sufficient here for performance. Tags are already + // validated to not contain any bad characters, e.g. they are automatically generated + // inside this class and suffixed with a pure integer enforced by DB. + $quotedTagList = array_map(function ($value) { + return '\'' . $value . '\''; + }, $tags); + + if ($this->isConnectionMysql($connection)) { + // Use a optimized query on mysql ... don't use on your own + // * ansi sql does not know about multi table delete + // * doctrine query builder does not support join on delete() + $connection->executeQuery( + 'DELETE tags2, cache1' + . ' FROM ' . $this->tagsTable . ' AS tags1' + . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier' + . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier' + . ' WHERE tags1.tag IN (' . implode(',', $quotedTagList) . ')' + ); + } else { + $queryBuilder = $connection->createQueryBuilder(); + $result = $queryBuilder->select('identifier') + ->from($this->tagsTable) + ->where('tag IN (' . implode(',', $quotedTagList) . ')') + // group by is like DISTINCT and used here to suppress possible duplicate identifiers + ->groupBy('identifier') + ->execute(); + $cacheEntryIdentifiers = []; + while ($row = $result->fetch()) { + $cacheEntryIdentifiers[] = $row['identifier']; + } + $quotedIdentifiers = $queryBuilder->createNamedParameter($cacheEntryIdentifiers, Connection::PARAM_STR_ARRAY); + $queryBuilder->delete($this->cacheTable) + ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers)) + ->execute(); + $queryBuilder->delete($this->tagsTable) + ->where($queryBuilder->expr()->in('identifier', $quotedIdentifiers)) + ->execute(); + } + } + /** * Removes all cache entries of this cache which are tagged by the specified tag. * @@ -271,7 +334,15 @@ class Typo3DatabaseBackend extends AbstractBackend implements TaggableBackendInt { $this->throwExceptionIfFrontendDoesNotExist(); + if (empty($tag)) { + return; + } + + /** @var Connection $connection */ $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->cacheTable); + + $quotedTag = '\'' . $tag . '\''; + if ($this->isConnectionMysql($connection)) { // Use a optimized query on mysql ... don't use on your own // * ansi sql does not know about multi table delete @@ -281,14 +352,13 @@ class Typo3DatabaseBackend extends AbstractBackend implements TaggableBackendInt . ' FROM ' . $this->tagsTable . ' AS tags1' . ' JOIN ' . $this->tagsTable . ' AS tags2 ON tags1.identifier = tags2.identifier' . ' JOIN ' . $this->cacheTable . ' AS cache1 ON tags1.identifier = cache1.identifier' - . ' WHERE tags1.tag = ?', - [$tag] + . ' WHERE tags1.tag = ' . $quotedTag ); } else { $queryBuilder = $connection->createQueryBuilder(); $result = $queryBuilder->select('identifier') ->from($this->tagsTable) - ->where($queryBuilder->expr()->eq('tag', $queryBuilder->createNamedParameter($tag, \PDO::PARAM_STR))) + ->where('tag = ' . $quotedTag) // group by is like DISTINCT and used here to suppress possible duplicate identifiers ->groupBy('identifier') ->execute(); diff --git a/typo3/sysext/core/Classes/Cache/CacheManager.php b/typo3/sysext/core/Classes/Cache/CacheManager.php index c4b56e7a7ad7b7a48e53ad536691c7108442f501..bb6321e36dfc96c557dd155d46225feaf1c0d0fc 100644 --- a/typo3/sysext/core/Classes/Cache/CacheManager.php +++ b/typo3/sysext/core/Classes/Cache/CacheManager.php @@ -163,15 +163,14 @@ class CacheManager implements SingletonInterface public function flushCachesInGroup($groupIdentifier) { $this->createAllCaches(); - if (isset($this->cacheGroups[$groupIdentifier])) { - foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) { - if (isset($this->caches[$cacheIdentifier])) { - $this->caches[$cacheIdentifier]->flush(); - } - } - } else { + if (!isset($this->cacheGroups[$groupIdentifier])) { throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390334120); } + foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) { + if (isset($this->caches[$cacheIdentifier])) { + $this->caches[$cacheIdentifier]->flush(); + } + } } /** @@ -179,23 +178,51 @@ class CacheManager implements SingletonInterface * caches of a specific group. * * @param string $groupIdentifier - * @param string $tag Tag to search for + * @param string|array $tag Tag to search for * @return void * @throws NoSuchCacheGroupException * @api */ public function flushCachesInGroupByTag($groupIdentifier, $tag) { + if (empty($tag)) { + return; + } $this->createAllCaches(); - if (isset($this->cacheGroups[$groupIdentifier])) { - foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) { - if (isset($this->caches[$cacheIdentifier])) { - $this->caches[$cacheIdentifier]->flushByTag($tag); - } - } - } else { + if (!isset($this->cacheGroups[$groupIdentifier])) { throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390337129); } + foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) { + if (isset($this->caches[$cacheIdentifier])) { + $this->caches[$cacheIdentifier]->flushByTag($tag); + } + } + } + + /** + * Flushes entries tagged by any of the specified tags in all registered + * caches of a specific group. + * + * @param string $groupIdentifier + * @param string[] $tag Tags to search for + * @return void + * @throws NoSuchCacheGroupException + * @api + */ + public function flushCachesInGroupByTags($groupIdentifier, array $tags) + { + if (empty($tag)) { + return; + } + $this->createAllCaches(); + if (!isset($this->cacheGroups[$groupIdentifier])) { + throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390337130); + } + foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) { + if (isset($this->caches[$cacheIdentifier])) { + $this->caches[$cacheIdentifier]->flushByTags($tags); + } + } } /** @@ -214,6 +241,21 @@ class CacheManager implements SingletonInterface } } + /** + * Flushes entries tagged by any of the specified tags in all registered caches. + * + * @param string[] $tag Tags to search for + * @return void + * @api + */ + public function flushCachesByTags(array $tags) + { + $this->createAllCaches(); + foreach ($this->caches as $cache) { + $cache->flushByTags($tags); + } + } + /** * Instantiates all registered caches. * diff --git a/typo3/sysext/core/Classes/Cache/Frontend/AbstractFrontend.php b/typo3/sysext/core/Classes/Cache/Frontend/AbstractFrontend.php index cdddad469f8ed2e02d379358815794ba4edae507..b32fc92d1233bad53b136d31f6da67135c0000b9 100644 --- a/typo3/sysext/core/Classes/Cache/Frontend/AbstractFrontend.php +++ b/typo3/sysext/core/Classes/Cache/Frontend/AbstractFrontend.php @@ -121,6 +121,25 @@ abstract class AbstractFrontend implements FrontendInterface $this->backend->flush(); } + /** + * Removes all cache entries of this cache which are tagged by any of the specified tags. + * + * @param string[] $tags + * @return void + * @throws \InvalidArgumentException + */ + public function flushByTags(array $tags) + { + foreach ($tags as $tag) { + if (!$this->isValidTag($tag)) { + throw new \InvalidArgumentException('"' . $tag . '" is not a valid tag for a cache entry.', 1233057360); + } + } + if ($this->backend instanceof TaggableBackendInterface) { + $this->backend->flushByTags($tags); + } + } + /** * Removes all cache entries of this cache which are tagged by the specified tag. * @@ -171,12 +190,20 @@ abstract class AbstractFrontend implements FrontendInterface /** * Checks the validity of a tag. Returns TRUE if it's valid. * - * @param string $tag An identifier to be checked for validity + * @param string|array $tag An identifier to be checked for validity * @return bool * @api */ public function isValidTag($tag) { - return preg_match(self::PATTERN_TAG, $tag) === 1; + if (!is_array($tag)) { + return preg_match(self::PATTERN_TAG, $tag) === 1; + } + foreach ($tag as $tagValue) { + if (!$this->isValidTag($tagValue)) { + return false; + } + } + return true; } } diff --git a/typo3/sysext/core/Classes/Cache/Frontend/FrontendInterface.php b/typo3/sysext/core/Classes/Cache/Frontend/FrontendInterface.php index 43868c38f2073bf2cdfba9d79d3f93ce8c12bfbd..5863c619f22c0df5a127ff44ef6e02fef7699133 100644 --- a/typo3/sysext/core/Classes/Cache/Frontend/FrontendInterface.php +++ b/typo3/sysext/core/Classes/Cache/Frontend/FrontendInterface.php @@ -114,6 +114,15 @@ interface FrontendInterface */ public function flushByTag($tag); + /** + * Removes all cache entries of this cache which are tagged by any of the specified tags. + * + * @param string[] $tag List of tags + * @return void + * @api + */ + public function flushByTags(array $tags); + /** * Does garbage collection * diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index c5437d90fe22f57e25657cbd0c2c2b689067c703..24629cf2e0fa98e86c779a595471feac2bbb0dad 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -8113,9 +8113,7 @@ class DataHandler /** @var CacheManager $cacheManager */ $cacheManager = $this->getCacheManager(); - foreach ($tagsToClear as $tag => $_) { - $cacheManager->flushCachesInGroupByTag('pages', $tag); - } + $cacheManager->flushCachesInGroupByTags('pages', array_keys($tagsToClear)); // Execute collected clear cache commands from page TSConfig foreach ($clearCacheCommands as $command) { @@ -8360,9 +8358,7 @@ class DataHandler } // process caching framwork operations if (!empty($tagsToFlush)) { - foreach (array_unique($tagsToFlush) as $tag) { - $this->getCacheManager()->flushCachesInGroupByTag('pages', $tag); - } + $this->getCacheManager()->flushCachesInGroupByTags('pages', $tagsToFlush); } // Call post processing function for clear-cache: diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcBackendTest.php index cc3639352158261c78bd8da0410d6b641e03624a..24f63dbdaeee7896252367f17229fb4024321d42 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcBackendTest.php +++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcBackendTest.php @@ -207,6 +207,22 @@ class ApcBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase $this->assertTrue($backend->has('BackendAPCTest3'), 'BackendAPCTest3'); } + /** + * @test + */ + public function flushByTagsRemovesCacheEntriesWithSpecifiedTags() + { + $backend = $this->setUpBackend(); + $data = 'some data' . microtime(); + $backend->set('BackendAPCTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']); + $backend->set('BackendAPCTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']); + $backend->set('BackendAPCTest3', $data, ['UnitTestTag%test']); + $backend->flushByTags(['UnitTestTag%special', 'UnitTestTag%boring']); + $this->assertFalse($backend->has('BackendAPCTest1'), 'BackendAPCTest1'); + $this->assertFalse($backend->has('BackendAPCTest2'), 'BackendAPCTest2'); + $this->assertTrue($backend->has('BackendAPCTest3'), 'BackendAPCTest3'); + } + /** * @test */ diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcuBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcuBackendTest.php index 865ba7e436327b5849f339b8bccbeb88b64f18d7..1620d3c769f506ca3f01d7ba57998c7d76a91de4 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcuBackendTest.php +++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/ApcuBackendTest.php @@ -206,6 +206,22 @@ class ApcuBackendTest extends UnitTestCase $this->assertTrue($backend->has('BackendAPCUTest3')); } + /** + * @test + */ + public function flushByTagsRemovesCacheEntriesWithSpecifiedTags() + { + $backend = $this->setUpBackend(); + $data = 'some data' . microtime(); + $backend->set('BackendAPCUTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']); + $backend->set('BackendAPCUTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']); + $backend->set('BackendAPCUTest3', $data, ['UnitTestTag%test']); + $backend->flushByTags(['UnitTestTag%special', 'UnitTestTag%boring']); + $this->assertFalse($backend->has('BackendAPCUTest1'), 'BackendAPCTest1'); + $this->assertFalse($backend->has('BackendAPCUTest2'), 'BackendAPCTest2'); + $this->assertTrue($backend->has('BackendAPCUTest3'), 'BackendAPCTest3'); + } + /** * @test */ diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php index 61d1f2e87ac78536b65f5550de6341fc57d1d0dd..2509c93ee4ce129e92eed288af3f3e9444100aa3 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php +++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/MemcachedBackendTest.php @@ -192,6 +192,22 @@ class MemcachedBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase $this->assertTrue($backend->has('BackendMemcacheTest3'), 'BackendMemcacheTest3'); } + /** + * @test + */ + public function flushByTagsRemovesCacheEntriesWithSpecifiedTags() + { + $backend = $this->setUpBackend(); + $data = 'some data' . microtime(); + $backend->set('BackendMemcacheTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']); + $backend->set('BackendMemcacheTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']); + $backend->set('BackendMemcacheTest3', $data, ['UnitTestTag%test']); + $backend->flushByTags(['UnitTestTag%special', 'UnitTestTag%boring']); + $this->assertFalse($backend->has('BackendMemcacheTest1'), 'BackendMemcacheTest1'); + $this->assertFalse($backend->has('BackendMemcacheTest2'), 'BackendMemcacheTest2'); + $this->assertTrue($backend->has('BackendMemcacheTest3'), 'BackendMemcacheTest3'); + } + /** * @test */ diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/PdoBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/PdoBackendTest.php index 95151caf643960511c0080379fdcf114afe2e838..57c0c6e71c16fa8635ed8705478bd0b692396e2a 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/Backend/PdoBackendTest.php +++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/PdoBackendTest.php @@ -176,6 +176,22 @@ class PdoBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase $this->assertTrue($backend->has('PdoBackendTest3'), 'PdoBackendTest3'); } + /** + * @test + */ + public function flushByTagsRemovesCacheEntriesWithSpecifiedTags() + { + $backend = $this->setUpBackend(); + $data = 'some data' . microtime(); + $backend->set('PdoBackendTest1', $data, ['UnitTestTag%test', 'UnitTestTags%boring']); + $backend->set('PdoBackendTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']); + $backend->set('PdoBackendTest3', $data, ['UnitTestTag%test']); + $backend->flushByTags(['UnitTestTag%special', 'UnitTestTags%boring']); + $this->assertFalse($backend->has('PdoBackendTest1'), 'PdoBackendTest1'); + $this->assertFalse($backend->has('PdoBackendTest2'), 'PdoBackendTest2'); + $this->assertTrue($backend->has('PdoBackendTest3'), 'PdoBackendTest3'); + } + /** * @test */ diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/RedisBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/RedisBackendTest.php index c44f0b01d456c84567ddc83a6eab704101203a7d..40039f96c4d77719ebc9bc9c3e433b6c9acc64f3 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/Backend/RedisBackendTest.php +++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/RedisBackendTest.php @@ -762,6 +762,28 @@ class RedisBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase $this->assertSame($expectedResult, $actualResult); } + /** + * @test Functional + */ + public function flushByTagsRemovesEntriesTaggedWithSpecifiedTags() + { + $this->setUpBackend(); + $identifier = $this->getUniqueId('identifier'); + $this->backend->set($identifier . 'A', 'data', ['tag1']); + $this->backend->set($identifier . 'B', 'data', ['tag2']); + $this->backend->set($identifier . 'C', 'data', ['tag1', 'tag2']); + $this->backend->set($identifier . 'D', 'data', ['tag3']); + $this->backend->flushByTags(['tag1', 'tag2']); + $expectedResult = [false, false, false, true]; + $actualResult = [ + $this->backend->has($identifier . 'A'), + $this->backend->has($identifier . 'B'), + $this->backend->has($identifier . 'C'), + $this->backend->has($identifier . 'D') + ]; + $this->assertSame($expectedResult, $actualResult); + } + /** * @test Implementation */ diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/TransientMemoryBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/TransientMemoryBackendTest.php index 26b61b61bec21d10b64007021711fd6333525b39..0e2ee1e4ef71d262fe3f2cdb78c14313ab5179f9 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/Backend/TransientMemoryBackendTest.php +++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/TransientMemoryBackendTest.php @@ -159,6 +159,24 @@ class TransientMemoryBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase $this->assertTrue($backend->has('TransientMemoryBackendTest3'), 'TransientMemoryBackendTest3'); } + /** + * @test + */ + public function flushByTagsRemovesCacheEntriesWithSpecifiedTags() + { + $cache = $this->createMock(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface::class); + $backend = new \TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend('Testing'); + $backend->setCache($cache); + $data = 'some data' . microtime(); + $backend->set('TransientMemoryBackendTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']); + $backend->set('TransientMemoryBackendTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']); + $backend->set('TransientMemoryBackendTest3', $data, ['UnitTestTag%test']); + $backend->flushByTags(['UnitTestTag%special', 'UnitTestTag%boring']); + $this->assertFalse($backend->has('TransientMemoryBackendTest1'), 'TransientMemoryBackendTest1'); + $this->assertFalse($backend->has('TransientMemoryBackendTest2'), 'TransientMemoryBackendTest2'); + $this->assertTrue($backend->has('TransientMemoryBackendTest3'), 'TransientMemoryBackendTest3'); + } + /** * @test */ diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/Typo3DatabaseBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/Typo3DatabaseBackendTest.php index a15798d911411cc112405e2f6bc85835ebc7b6e3..474c9ef978270904298286530e7e79a5fe9dcea8 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/Backend/Typo3DatabaseBackendTest.php +++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/Typo3DatabaseBackendTest.php @@ -176,6 +176,50 @@ class Typo3DatabaseBackendTest extends UnitTestCase $subject->flush(); } + public function flushByTagCallsDeleteOnConnection() + { + $frontendProphecy = $this->prophesize(FrontendInterface::class); + $frontendProphecy->getIdentifier()->willReturn('cache_test'); + + $subject = new Typo3DatabaseBackend('Testing'); + $subject->setCache($frontendProphecy->reveal()); + + $connectionProphet = $this->prophesize(Connection::class); + $connectionProphet->delete('cf_cache_test')->shouldBeCalled()->willReturn(0); + $connectionProphet->delete('cf_cache_test_tags')->shouldBeCalled()->willReturn(0); + + $connectionPoolProphet = $this->prophesize(ConnectionPool::class); + $connectionPoolProphet->getConnectionForTable(Argument::cetera())->willReturn($connectionProphet->reveal()); + + // Two instances are required as there are different tables being cleared + GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal()); + GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal()); + + $subject->flushByTag('Tag'); + } + + public function flushByTagsCallsDeleteOnConnection() + { + $frontendProphecy = $this->prophesize(FrontendInterface::class); + $frontendProphecy->getIdentifier()->willReturn('cache_test'); + + $subject = new Typo3DatabaseBackend('Testing'); + $subject->setCache($frontendProphecy->reveal()); + + $connectionProphet = $this->prophesize(Connection::class); + $connectionProphet->delete('cf_cache_test')->shouldBeCalled()->willReturn(0); + $connectionProphet->delete('cf_cache_test_tags')->shouldBeCalled()->willReturn(0); + + $connectionPoolProphet = $this->prophesize(ConnectionPool::class); + $connectionPoolProphet->getConnectionForTable(Argument::cetera())->willReturn($connectionProphet->reveal()); + + // Two instances are required as there are different tables being cleared + GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal()); + GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal()); + + $subject->flushByTag(['Tag1', 'Tag2']); + } + /** * @test */ @@ -184,6 +228,16 @@ class Typo3DatabaseBackendTest extends UnitTestCase $subject = new Typo3DatabaseBackend('Testing'); $this->expectException(Exception::class); $this->expectExceptionCode(1236518288); - $subject->flushByTag([]); + $subject->flushByTag('Tag'); + } + /** + * @test + */ + public function flushByTagsThrowsExceptionIfFrontendWasNotSet() + { + $subject = new Typo3DatabaseBackend('Testing'); + $this->expectException(Exception::class); + $this->expectExceptionCode(1236518288); + $subject->flushByTags([]); } } diff --git a/typo3/sysext/core/Tests/Unit/Cache/Backend/WincacheBackendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Backend/WincacheBackendTest.php index f31fbe198bd647467e8bc58fed686a5dda55133f..c6e0868bd814f464eb03b51c3796d7a4cbcb389d 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/Backend/WincacheBackendTest.php +++ b/typo3/sysext/core/Tests/Unit/Cache/Backend/WincacheBackendTest.php @@ -169,6 +169,22 @@ class WincacheBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase $this->assertTrue($backend->has('BackendWincacheTest3'), 'BackendWincacheTest3'); } + /** + * @test + */ + public function flushByTagsRemovesCacheEntriesWithSpecifiedTags() + { + $backend = $this->setUpBackend(); + $data = 'some data' . microtime(); + $backend->set('BackendWincacheTest1', $data, ['UnitTestTag%test', 'UnitTestTag%boring']); + $backend->set('BackendWincacheTest2', $data, ['UnitTestTag%test', 'UnitTestTag%special']); + $backend->set('BackendWincacheTest3', $data, ['UnitTestTag%test']); + $backend->flushByTag('UnitTestTag%special', 'UnitTestTag%boring'); + $this->assertTrue($backend->has('BackendWincacheTest1'), 'BackendWincacheTest1'); + $this->assertFalse($backend->has('BackendWincacheTest2'), 'BackendWincacheTest2'); + $this->assertTrue($backend->has('BackendWincacheTest3'), 'BackendWincacheTest3'); + } + /** * @test */ diff --git a/typo3/sysext/core/Tests/Unit/Cache/CacheManagerTest.php b/typo3/sysext/core/Tests/Unit/Cache/CacheManagerTest.php index c8410122c9f2ec04e2a60619e51ef4924ac93086..a03fe9b4908d7ad3e67752f2fa0d8e3f64c1c007 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/CacheManagerTest.php +++ b/typo3/sysext/core/Tests/Unit/Cache/CacheManagerTest.php @@ -138,6 +138,28 @@ class CacheManagerTest extends UnitTestCase $manager->flushCachesByTag('theTag'); } + /** + * @test + */ + public function flushCachesByTagsCallsTheFlushByTagsMethodOfAllRegisteredCaches() + { + $manager = new CacheManager(); + $cache1 = $this->getMockBuilder(AbstractFrontend::class) + ->setMethods(['getIdentifier', 'set', 'get', 'getByTag', 'has', 'remove', 'flush', 'flushByTags']) + ->disableOriginalConstructor() + ->getMock(); + $cache1->expects($this->atLeastOnce())->method('getIdentifier')->will($this->returnValue('cache1')); + $cache1->expects($this->once())->method('flushByTags')->with($this->equalTo(['theTag'])); + $manager->registerCache($cache1); + $cache2 = $this->getMockBuilder(AbstractFrontend::class) + ->setMethods(['getIdentifier', 'set', 'get', 'getByTag', 'has', 'remove', 'flush', 'flushByTags']) + ->disableOriginalConstructor() + ->getMock(); + $cache2->expects($this->once())->method('flushByTags')->with($this->equalTo(['theTag'])); + $manager->registerCache($cache2); + $manager->flushCachesByTags(['theTag']); + } + /** * @test */ diff --git a/typo3/sysext/core/Tests/Unit/Cache/Fixtures/FrontendFixture.php b/typo3/sysext/core/Tests/Unit/Cache/Fixtures/FrontendFixture.php index 6a861c9f5b8bf4de267b38ac7f16bee9e868aa63..4f6cf54b0a98b8bcaf68fad4eab797154053a7c2 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/Fixtures/FrontendFixture.php +++ b/typo3/sysext/core/Tests/Unit/Cache/Fixtures/FrontendFixture.php @@ -65,6 +65,10 @@ class FrontendFixture implements FrontendInterface { } + public function flushByTags(array $tags) + { + } + public function collectGarbage() { } diff --git a/typo3/sysext/core/Tests/Unit/Cache/Frontend/AbstractFrontendTest.php b/typo3/sysext/core/Tests/Unit/Cache/Frontend/AbstractFrontendTest.php index 227abfe6a7b117112971371a068fd9278b512f43..a4fd5bea268211ae302ceea4bf1c3e6aa15743dd 100644 --- a/typo3/sysext/core/Tests/Unit/Cache/Frontend/AbstractFrontendTest.php +++ b/typo3/sysext/core/Tests/Unit/Cache/Frontend/AbstractFrontendTest.php @@ -103,7 +103,7 @@ class AbstractFrontendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase $tag = 'sometag'; $identifier = 'someCacheIdentifier'; $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface::class) - ->setMethods(['setCache', 'get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'collectGarbage']) + ->setMethods(['setCache', 'get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'flushByTags', 'collectGarbage']) ->disableOriginalConstructor() ->getMock(); $backend->expects($this->once())->method('flushByTag')->with($tag); @@ -114,6 +114,25 @@ class AbstractFrontendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase $cache->flushByTag($tag); } + /** + * @test + */ + public function flushByTagsCallsBackendIfItIsATaggableBackend() + { + $tag = 'sometag'; + $identifier = 'someCacheIdentifier'; + $backend = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface::class) + ->setMethods(['setCache', 'get', 'set', 'has', 'remove', 'findIdentifiersByTag', 'flush', 'flushByTag', 'flushByTags', 'collectGarbage']) + ->disableOriginalConstructor() + ->getMock(); + $backend->expects($this->once())->method('flushByTags')->with([$tag]); + $cache = $this->getMockBuilder(\TYPO3\CMS\Core\Cache\Frontend\StringFrontend::class) + ->setMethods(['__construct', 'get', 'set', 'has', 'remove', 'getByTag']) + ->setConstructorArgs([$identifier, $backend]) + ->getMock(); + $cache->flushByTags([$tag]); + } + /** * @test */ diff --git a/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php b/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php index 209b72c6beb701c52207667cd8595edc54ce73a0..e264eb38aadba9bd64a9a9a76cf00e0d8602e7fa 100644 --- a/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php +++ b/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php @@ -16,6 +16,7 @@ namespace TYPO3\CMS\Core\Tests\Unit\DataHandler; use Prophecy\Argument; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools; use TYPO3\CMS\Core\DataHandling\DataHandler; use TYPO3\CMS\Core\Tests\AccessibleObjectInterface; @@ -349,7 +350,18 @@ class DataHandlerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase /** @var $subject DataHandler|\PHPUnit_Framework_MockObject_MockObject */ $subject = $this->getMockBuilder(DataHandler::class) - ->setMethods(['newlog', 'checkModifyAccessList', 'tableReadOnly', 'checkRecordUpdateAccess', 'recordInfo']) + ->setMethods([ + 'newlog', + 'checkModifyAccessList', + 'tableReadOnly', + 'checkRecordUpdateAccess', + 'recordInfo', + 'getCacheManager', + 'registerElementsToBeDeleted', + 'unsetElementsToBeDeleted', + 'resetElementsToBeDeleted' + ]) + ->disableOriginalConstructor() ->getMock(); $subject->bypassWorkspaceRestrictions = false; @@ -360,10 +372,18 @@ class DataHandlerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase ] ] ]; + + $cacheManagerMock = $this->getMockBuilder(CacheManager::class) + ->setMethods(['flushCachesInGroupByTags']) + ->getMock(); + $cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTags')->with('pages', []); + + $subject->expects($this->once())->method('getCacheManager')->willReturn($cacheManagerMock); $subject->expects($this->once())->method('recordInfo')->will($this->returnValue(null)); $subject->expects($this->once())->method('checkModifyAccessList')->with('pages')->will($this->returnValue(true)); $subject->expects($this->once())->method('tableReadOnly')->with('pages')->will($this->returnValue(false)); $subject->expects($this->once())->method('checkRecordUpdateAccess')->will($this->returnValue(true)); + $subject->expects($this->once())->method('unsetElementsToBeDeleted')->willReturnArgument(0); /** @var BackendUserAuthentication|\PHPUnit_Framework_MockObject_MockObject $backEndUser */ $backEndUser = $this->createMock(BackendUserAuthentication::class); diff --git a/typo3/sysext/extbase/Classes/Service/CacheService.php b/typo3/sysext/extbase/Classes/Service/CacheService.php index 04cb590779791911fc1ceec90135e34bec35cc0a..b1c2911ba05fe43d99228a264088bc3b69b201f1 100644 --- a/typo3/sysext/extbase/Classes/Service/CacheService.php +++ b/typo3/sysext/extbase/Classes/Service/CacheService.php @@ -67,9 +67,10 @@ class CacheService implements \TYPO3\CMS\Core\SingletonInterface if (!is_array($pageIdsToClear)) { $pageIdsToClear = [(int)$pageIdsToClear]; } - foreach ($pageIdsToClear as $pageId) { - $this->cacheManager->flushCachesInGroupByTag('pages', 'pageId_' . $pageId); - } + $tags = array_map(function ($item) { + return 'pageId_' . $item; + }, $pageIdsToClear); + $this->cacheManager->flushCachesInGroupByTags('pages', $tags); } } diff --git a/typo3/sysext/extbase/Tests/Unit/Service/CacheServiceTest.php b/typo3/sysext/extbase/Tests/Unit/Service/CacheServiceTest.php index 305788a73c4ca9b0cb465c33f7b05d95e0e35a0b..c9cc1e69c4251343a36ea0b9d50f6657d29fcc5b 100644 --- a/typo3/sysext/extbase/Tests/Unit/Service/CacheServiceTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Service/CacheServiceTest.php @@ -41,7 +41,7 @@ class CacheServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase */ public function clearPageCacheConvertsPageIdsToArray() { - $this->cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTag')->with('pages', 'pageId_123'); + $this->cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTags')->with('pages', ['pageId_123']); $this->cacheService->clearPageCache(123); } @@ -50,7 +50,7 @@ class CacheServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase */ public function clearPageCacheConvertsPageIdsToNumericArray() { - $this->cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTag')->with('pages', 'pageId_0'); + $this->cacheManagerMock->expects($this->once())->method('flushCachesInGroupByTags')->with('pages', ['pageId_0']); $this->cacheService->clearPageCache('Foo'); } @@ -68,9 +68,7 @@ class CacheServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase */ public function clearPageCacheUsesCacheManagerToFlushCacheOfSpecifiedPages() { - $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTag')->with('pages', 'pageId_1'); - $this->cacheManagerMock->expects($this->at(1))->method('flushCachesInGroupByTag')->with('pages', 'pageId_2'); - $this->cacheManagerMock->expects($this->at(2))->method('flushCachesInGroupByTag')->with('pages', 'pageId_3'); + $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTags')->with('pages', ['pageId_1', 'pageId_2', 'pageId_3']); $this->cacheService->clearPageCache([1, 2, 3]); } @@ -79,9 +77,7 @@ class CacheServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase */ public function clearsCachesOfRegisteredPageIds() { - $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTag')->with('pages', 'pageId_2'); - $this->cacheManagerMock->expects($this->at(1))->method('flushCachesInGroupByTag')->with('pages', 'pageId_15'); - $this->cacheManagerMock->expects($this->at(2))->method('flushCachesInGroupByTag')->with('pages', 'pageId_8'); + $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTags')->with('pages', ['pageId_2', 'pageId_15', 'pageId_8']); $this->cacheService->getPageIdStack()->push(8); $this->cacheService->getPageIdStack()->push(15); @@ -95,10 +91,7 @@ class CacheServiceTest extends \TYPO3\CMS\Core\Tests\UnitTestCase */ public function clearsCachesOfDuplicateRegisteredPageIdsOnlyOnce() { - $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTag')->with('pages', 'pageId_2'); - $this->cacheManagerMock->expects($this->at(1))->method('flushCachesInGroupByTag')->with('pages', 'pageId_15'); - $this->cacheManagerMock->expects($this->at(2))->method('flushCachesInGroupByTag')->with('pages', 'pageId_8'); - $this->cacheManagerMock->expects($this->exactly(3))->method('flushCachesInGroupByTag'); + $this->cacheManagerMock->expects($this->at(0))->method('flushCachesInGroupByTags')->with('pages', ['pageId_2', 'pageId_15', 'pageId_8']); $this->cacheService->getPageIdStack()->push(8); $this->cacheService->getPageIdStack()->push(15);