diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 96c170675c..94ee06ee59 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -1,6 +1,11 @@ UPGRADE FROM 4.3 to 4.4 ======================= +Cache +----- + + * Added argument `$prefix` to `AdapterInterface::clear()` + DependencyInjection ------------------- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index bb6f87dd00..d385784c30 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -16,6 +16,7 @@ Cache * Removed `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead. * Removed all PSR-16 adapters, use `Psr16Cache` or `Symfony\Contracts\Cache\CacheInterface` implementations instead. * Removed `SimpleCacheAdapter`, use `Psr16Adapter` instead. + * Added argument `$prefix` to `AdapterInterface::clear()` Config ------ diff --git a/src/Symfony/Component/Cache/Adapter/AdapterInterface.php b/src/Symfony/Component/Cache/Adapter/AdapterInterface.php index 85fe07684f..6da364d97f 100644 --- a/src/Symfony/Component/Cache/Adapter/AdapterInterface.php +++ b/src/Symfony/Component/Cache/Adapter/AdapterInterface.php @@ -34,4 +34,11 @@ interface AdapterInterface extends CacheItemPoolInterface * @return \Traversable|CacheItem[] */ public function getItems(array $keys = []); + + /** + * {@inheritdoc} + * + * @param string $prefix + */ + public function clear(/*string $prefix = ''*/); } diff --git a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php index 80aa7c6d1b..6cafed8ffd 100644 --- a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -192,14 +192,21 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa /** * {@inheritdoc} + * + * @param string $prefix */ - public function clear() + public function clear(/*string $prefix = ''*/) { + $prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : ''; $cleared = true; $i = $this->adapterCount; while ($i--) { - $cleared = $this->adapters[$i]->clear() && $cleared; + if ($this->adapters[$i] instanceof AdapterInterface) { + $cleared = $this->adapters[$i]->clear($prefix) && $cleared; + } else { + $cleared = $this->adapters[$i]->clear() && $cleared; + } } return $cleared; diff --git a/src/Symfony/Component/Cache/Adapter/NullAdapter.php b/src/Symfony/Component/Cache/Adapter/NullAdapter.php index 54cd453565..159d43e2c9 100644 --- a/src/Symfony/Component/Cache/Adapter/NullAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/NullAdapter.php @@ -75,8 +75,10 @@ class NullAdapter implements AdapterInterface, CacheInterface /** * {@inheritdoc} + * + * @param string $prefix */ - public function clear() + public function clear(/*string $prefix = ''*/) { return true; } diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php index bccafcf47e..24f13832cb 100644 --- a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php @@ -147,9 +147,17 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa /** * {@inheritdoc} + * + * @param string $prefix */ - public function clear() + public function clear(/*string $prefix = ''*/) { + $prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : ''; + + if ($this->pool instanceof AdapterInterface) { + return $this->pool->clear($this->namespace.$prefix); + } + return $this->pool->clear(); } diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php index 5b08418fcc..02feca3f91 100644 --- a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php @@ -213,10 +213,26 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac /** * {@inheritdoc} + * + * @param string $prefix */ - public function clear() + public function clear(/*string $prefix = ''*/) { - $this->deferred = []; + $prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : ''; + + if ('' !== $prefix) { + foreach ($this->deferred as $key => $item) { + if (0 === strpos($key, $prefix)) { + unset($this->deferred[$key]); + } + } + } else { + $this->deferred = []; + } + + if ($this->pool instanceof AdapterInterface) { + return $this->pool->clear($prefix); + } return $this->pool->clear(); } diff --git a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php index 660acf54d7..1fe830ff2f 100644 --- a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php @@ -167,11 +167,18 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt /** * {@inheritdoc} + * + * @param string $prefix */ - public function clear() + public function clear(/*string $prefix = ''*/) { + $prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : ''; $event = $this->start(__FUNCTION__); try { + if ($this->pool instanceof AdapterInterface) { + return $event->result = $this->pool->clear($prefix); + } + return $event->result = $this->pool->clear(); } finally { $event->end = microtime(true); diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index 1d24a97ed8..136953fe27 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * added support for connecting to Redis Sentinel clusters + * added argument `$prefix` to `AdapterInterface::clear()` 4.3.0 ----- diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php index 93318ffd48..aa1a57318b 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php @@ -252,6 +252,26 @@ abstract class AdapterTestCase extends CachePoolTest $this->assertFalse($this->isPruned($cache, 'foo')); $this->assertTrue($this->isPruned($cache, 'qux')); } + + public function testClearPrefix() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $cache = $this->createCachePool(0, __FUNCTION__); + $cache->clear(); + + $item = $cache->getItem('foobar'); + $cache->save($item->set(1)); + + $item = $cache->getItem('barfoo'); + $cache->save($item->set(2)); + + $cache->clear('foo'); + $this->assertFalse($cache->hasItem('foobar')); + $this->assertTrue($cache->hasItem('barfoo')); + } } class NotUnserializable diff --git a/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php index 8f520cb59a..b5644cbc84 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php @@ -23,6 +23,7 @@ class DoctrineAdapterTest extends AdapterTestCase 'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayCache is not.', 'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayCache is not.', 'testNotUnserializable' => 'ArrayCache does not use serialize/unserialize', + 'testClearPrefix' => 'Doctrine cannot clear by prefix', ]; public function createCachePool($defaultLifetime = 0) diff --git a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php index 59f33f3aee..050f105bc9 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php @@ -19,6 +19,7 @@ class MemcachedAdapterTest extends AdapterTestCase protected $skippedTests = [ 'testHasItemReturnsFalseWhenDeferredItemIsExpired' => 'Testing expiration slows down the test suite', 'testDefaultLifeTime' => 'Testing expiration slows down the test suite', + 'testClearPrefix' => 'Memcached cannot clear by prefix', ]; protected static $client; diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php index 3a5904b107..b6adc3a44c 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php @@ -72,7 +72,7 @@ class PhpArrayAdapterTest extends AdapterTestCase public function createCachePool($defaultLifetime = 0, $testMethod = null) { - if ('testGetMetadata' === $testMethod) { + if ('testGetMetadata' === $testMethod || 'testClearPrefix' === $testMethod) { return new PhpArrayAdapter(self::$file, new FilesystemAdapter()); } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/Psr16AdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/Psr16AdapterTest.php index 09c55e60d0..1647a6d4cb 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/Psr16AdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/Psr16AdapterTest.php @@ -23,6 +23,7 @@ class Psr16AdapterTest extends AdapterTestCase { protected $skippedTests = [ 'testPrune' => 'Psr16adapter just proxies', + 'testClearPrefix' => 'SimpleCache cannot clear by prefix', ]; public function createCachePool($defaultLifetime = 0) diff --git a/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php index 8097e49cfd..c4ec9bf62e 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/SimpleCacheAdapterTest.php @@ -23,6 +23,7 @@ class SimpleCacheAdapterTest extends AdapterTestCase { protected $skippedTests = [ 'testPrune' => 'SimpleCache just proxies', + 'testClearPrefix' => 'SimpleCache cannot clear by prefix', ]; public function createCachePool($defaultLifetime = 0) diff --git a/src/Symfony/Component/Cache/Traits/AbstractTrait.php b/src/Symfony/Component/Cache/Traits/AbstractTrait.php index 7c8ccabfb0..07205758a8 100644 --- a/src/Symfony/Component/Cache/Traits/AbstractTrait.php +++ b/src/Symfony/Component/Cache/Traits/AbstractTrait.php @@ -102,9 +102,12 @@ trait AbstractTrait /** * {@inheritdoc} + * + * @param string $prefix */ - public function clear() + public function clear(/*string $prefix = ''*/) { + $prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : ''; $this->deferred = []; if ($cleared = $this->versioningIsEnabled) { $namespaceVersion = substr_replace(base64_encode(pack('V', mt_rand())), static::NS_SEPARATOR, 5); @@ -120,7 +123,7 @@ trait AbstractTrait } try { - return $this->doClear($this->namespace) || $cleared; + return $this->doClear($this->namespace.$prefix) || $cleared; } catch (\Exception $e) { CacheItem::log($this->logger, 'Failed to clear the cache: '.$e->getMessage(), ['exception' => $e]); diff --git a/src/Symfony/Component/Cache/Traits/ArrayTrait.php b/src/Symfony/Component/Cache/Traits/ArrayTrait.php index df7d238e2d..e1ce980bf5 100644 --- a/src/Symfony/Component/Cache/Traits/ArrayTrait.php +++ b/src/Symfony/Component/Cache/Traits/ArrayTrait.php @@ -66,10 +66,22 @@ trait ArrayTrait /** * {@inheritdoc} + * + * @param string $prefix */ - public function clear() + public function clear(/*string $prefix = ''*/) { - $this->values = $this->expiries = []; + $prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : ''; + + if ('' !== $prefix) { + foreach ($this->values as $key => $value) { + if (0 === strpos($key, $prefix)) { + unset($this->values[$key], $this->expiries[$key]); + } + } + } else { + $this->values = $this->expiries = []; + } return true; } diff --git a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php index 37e1fd1f06..97bc2600f1 100644 --- a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php +++ b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php @@ -56,6 +56,10 @@ trait FilesystemCommonTrait $ok = true; foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS)) as $file) { + if ('' !== $namespace && 0 !== strpos($this->getFileKey($file), $namespace)) { + continue; + } + $ok = ($file->isDir() || $this->doUnlink($file) || !file_exists($file)) && $ok; } @@ -114,6 +118,11 @@ trait FilesystemCommonTrait return $dir.substr($hash, 2, 20); } + private function getFileKey(string $file): string + { + return ''; + } + /** * @internal */ diff --git a/src/Symfony/Component/Cache/Traits/FilesystemTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemTrait.php index 9c444f9b04..31f0d74372 100644 --- a/src/Symfony/Component/Cache/Traits/FilesystemTrait.php +++ b/src/Symfony/Component/Cache/Traits/FilesystemTrait.php @@ -108,4 +108,17 @@ trait FilesystemTrait return $failed; } + + private function getFileKey(string $file): string + { + if (!$h = @fopen($file, 'rb')) { + return ''; + } + + fgets($h); // expiry + $encodedKey = fgets($h); + fclose($h); + + return rawurldecode(rtrim($encodedKey)); + } } diff --git a/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php index 4395de0252..6548ec115c 100644 --- a/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php +++ b/src/Symfony/Component/Cache/Traits/PhpArrayTrait.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Cache\Traits; +use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\VarExporter\VarExporter; @@ -121,13 +122,20 @@ EOF; /** * {@inheritdoc} + * + * @param string $prefix */ - public function clear() + public function clear(/*string $prefix = ''*/) { + $prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : ''; $this->keys = $this->values = []; $cleared = @unlink($this->file) || !file_exists($this->file); + if ($this->pool instanceof AdapterInterface) { + return $this->pool->clear($prefix) && $cleared; + } + return $this->pool->clear() && $cleared; } diff --git a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php index 5ed4d6023e..d7125bec9e 100644 --- a/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php +++ b/src/Symfony/Component/Cache/Traits/PhpFilesTrait.php @@ -211,17 +211,19 @@ trait PhpFilesTrait $value = var_export($value, true); } + $encodedKey = rawurlencode($key); + if (!$isStaticValue) { // We cannot use a closure here because of https://bugs.php.net/76982 $value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value); - $value = "files[$key] = $this->getFile($key, true); // Since OPcache only compiles files older than the script execution start, set the file's mtime in the past - $ok = $this->write($file, $value, self::$startTime - 10) && $ok; + $ok = $this->write($file, "