diff --git a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php index 7512472150631..c38949975d884 100644 --- a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -15,6 +15,7 @@ use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\PruneableInterface; /** * Chains several adapters together. @@ -24,7 +25,7 @@ * * @author Kévin Dunglas */ -class ChainAdapter implements AdapterInterface +class ChainAdapter implements AdapterInterface, PruneableInterface { private $adapters = array(); private $adapterCount; @@ -231,4 +232,20 @@ public function commit() return $committed; } + + /** + * {@inheritdoc} + */ + public function prune() + { + $pruned = true; + + foreach ($this->adapters as $adapter) { + if ($adapter instanceof PruneableInterface) { + $pruned = $adapter->prune() && $pruned; + } + } + + return $pruned; + } } diff --git a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php index 832185629b053..9db17805628de 100644 --- a/src/Symfony/Component/Cache/Adapter/PdoAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PdoAdapter.php @@ -11,9 +11,10 @@ namespace Symfony\Component\Cache\Adapter; +use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\Traits\PdoTrait; -class PdoAdapter extends AbstractAdapter +class PdoAdapter extends AbstractAdapter implements PruneableInterface { use PdoTrait; diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index e8172d94988d4..bd637f10df0b0 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -5,9 +5,9 @@ CHANGELOG ----- * added PruneableInterface so PSR-6 or PSR-16 cache implementations can declare support for manual stale cache pruning - * added FilesystemTrait::prune() and PhpFilesTrait::prune() implementations - * now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, and PhpFilesCache implement PruneableInterface and support - manual stale cache pruning + * added prune logic to FilesystemTrait, PhpFilesTrait, PdoTrait, and ChainTrait + * now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, PhpFilesCache, PdoAdapter, PdoCache, ChainAdapter, and + ChainCache implement PruneableInterface and support manual stale cache pruning 3.3.0 ----- diff --git a/src/Symfony/Component/Cache/PruneableInterface.php b/src/Symfony/Component/Cache/PruneableInterface.php index cd366adb55290..42615253689fe 100644 --- a/src/Symfony/Component/Cache/PruneableInterface.php +++ b/src/Symfony/Component/Cache/PruneableInterface.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Cache; /** - * Interface for adapters and simple cache implementations that allow pruning expired items. + * Interface extends psr-6 and psr-16 caches to allow for pruning (deletion) of all expired cache items. */ interface PruneableInterface { diff --git a/src/Symfony/Component/Cache/Simple/ChainCache.php b/src/Symfony/Component/Cache/Simple/ChainCache.php index 08bb4881b463f..8bb944fd4773f 100644 --- a/src/Symfony/Component/Cache/Simple/ChainCache.php +++ b/src/Symfony/Component/Cache/Simple/ChainCache.php @@ -13,6 +13,7 @@ use Psr\SimpleCache\CacheInterface; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\PruneableInterface; /** * Chains several caches together. @@ -22,7 +23,7 @@ * * @author Nicolas Grekas */ -class ChainCache implements CacheInterface +class ChainCache implements CacheInterface, PruneableInterface { private $miss; private $caches = array(); @@ -219,4 +220,20 @@ public function setMultiple($values, $ttl = null) return $saved; } + + /** + * {@inheritdoc} + */ + public function prune() + { + $pruned = true; + + foreach ($this->caches as $cache) { + if ($cache instanceof PruneableInterface) { + $pruned = $cache->prune() && $pruned; + } + } + + return $pruned; + } } diff --git a/src/Symfony/Component/Cache/Simple/PdoCache.php b/src/Symfony/Component/Cache/Simple/PdoCache.php index 3e698e2f952c8..41730cec57f47 100644 --- a/src/Symfony/Component/Cache/Simple/PdoCache.php +++ b/src/Symfony/Component/Cache/Simple/PdoCache.php @@ -11,9 +11,10 @@ namespace Symfony\Component\Cache\Simple; +use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\Traits\PdoTrait; -class PdoCache extends AbstractCache +class PdoCache extends AbstractCache implements PruneableInterface { use PdoTrait; diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php index 6ba02b0d158ff..27318a487fc05 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Cache\IntegrationTests\CachePoolTest; +use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\PruneableInterface; abstract class AdapterTestCase extends CachePoolTest @@ -83,6 +84,7 @@ public function testPrune() $this->fail('Test classes for pruneable caches must implement `isPruned($cache, $name)` method.'); } + /** @var PruneableInterface|CacheItemPoolInterface $cache */ $cache = $this->createCachePool(); $doSet = function ($name, $value, \DateInterval $expiresAfter = null) use ($cache) { @@ -96,6 +98,18 @@ public function testPrune() $cache->save($item); }; + $doSet('foo', 'foo-val', new \DateInterval('PT05S')); + $doSet('bar', 'bar-val', new \DateInterval('PT10S')); + $doSet('baz', 'baz-val', new \DateInterval('PT15S')); + $doSet('qux', 'qux-val', new \DateInterval('PT20S')); + + sleep(30); + $cache->prune(); + $this->assertTrue($this->isPruned($cache, 'foo')); + $this->assertTrue($this->isPruned($cache, 'bar')); + $this->assertTrue($this->isPruned($cache, 'baz')); + $this->assertTrue($this->isPruned($cache, 'qux')); + $doSet('foo', 'foo-val'); $doSet('bar', 'bar-val', new \DateInterval('PT20S')); $doSet('baz', 'baz-val', new \DateInterval('PT40S')); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php index b80913c6e089c..293a90cc86783 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php @@ -11,9 +11,11 @@ namespace Symfony\Component\Cache\Tests\Adapter; +use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter; +use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\Tests\Fixtures\ExternalAdapter; /** @@ -44,4 +46,73 @@ public function testInvalidAdapterException() { new ChainAdapter(array(new \stdClass())); } + + public function testPrune() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $cache = new ChainAdapter(array( + $this->getPruneableMock(), + $this->getNonPruneableMock(), + $this->getPruneableMock(), + )); + $this->assertTrue($cache->prune()); + + $cache = new ChainAdapter(array( + $this->getPruneableMock(), + $this->getFailingPruneableMock(), + $this->getPruneableMock(), + )); + $this->assertFalse($cache->prune()); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|PruneableCacheInterface + */ + private function getPruneableMock() + { + $pruneable = $this + ->getMockBuilder(PruneableCacheInterface::class) + ->getMock(); + + $pruneable + ->expects($this->atLeastOnce()) + ->method('prune') + ->will($this->returnValue(true)); + + return $pruneable; + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|PruneableCacheInterface + */ + private function getFailingPruneableMock() + { + $pruneable = $this + ->getMockBuilder(PruneableCacheInterface::class) + ->getMock(); + + $pruneable + ->expects($this->atLeastOnce()) + ->method('prune') + ->will($this->returnValue(false)); + + return $pruneable; + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|AdapterInterface + */ + private function getNonPruneableMock() + { + return $this + ->getMockBuilder(AdapterInterface::class) + ->getMock(); + } +} + +interface PruneableCacheInterface extends PruneableInterface, AdapterInterface +{ } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php index 90c9ece8bcc9d..24e3f9bbc9582 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php @@ -12,12 +12,15 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Symfony\Component\Cache\Adapter\PdoAdapter; +use Symfony\Component\Cache\Tests\Traits\PdoPruneableTrait; /** * @group time-sensitive */ class PdoAdapterTest extends AdapterTestCase { + use PdoPruneableTrait; + protected static $dbFile; public static function setupBeforeClass() diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PdoDbalAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PdoDbalAdapterTest.php index b9c396fdc59eb..1e8c6155bdd35 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PdoDbalAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PdoDbalAdapterTest.php @@ -13,12 +13,15 @@ use Doctrine\DBAL\DriverManager; use Symfony\Component\Cache\Adapter\PdoAdapter; +use Symfony\Component\Cache\Tests\Traits\PdoPruneableTrait; /** * @group time-sensitive */ class PdoDbalAdapterTest extends AdapterTestCase { + use PdoPruneableTrait; + protected static $dbFile; public static function setupBeforeClass() diff --git a/src/Symfony/Component/Cache/Tests/Simple/CacheTestCase.php b/src/Symfony/Component/Cache/Tests/Simple/CacheTestCase.php index 600cd338be540..48b972824c2c2 100644 --- a/src/Symfony/Component/Cache/Tests/Simple/CacheTestCase.php +++ b/src/Symfony/Component/Cache/Tests/Simple/CacheTestCase.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Cache\Tests\Simple; use Cache\IntegrationTests\SimpleCacheTest; +use Psr\SimpleCache\CacheInterface; use Symfony\Component\Cache\PruneableInterface; abstract class CacheTestCase extends SimpleCacheTest @@ -80,8 +81,21 @@ public function testPrune() $this->fail('Test classes for pruneable caches must implement `isPruned($cache, $name)` method.'); } + /** @var PruneableInterface|CacheInterface $cache */ $cache = $this->createSimpleCache(); + $cache->set('foo', 'foo-val', new \DateInterval('PT05S')); + $cache->set('bar', 'bar-val', new \DateInterval('PT10S')); + $cache->set('baz', 'baz-val', new \DateInterval('PT15S')); + $cache->set('qux', 'qux-val', new \DateInterval('PT20S')); + + sleep(30); + $cache->prune(); + $this->assertTrue($this->isPruned($cache, 'foo')); + $this->assertTrue($this->isPruned($cache, 'bar')); + $this->assertTrue($this->isPruned($cache, 'baz')); + $this->assertTrue($this->isPruned($cache, 'qux')); + $cache->set('foo', 'foo-val'); $cache->set('bar', 'bar-val', new \DateInterval('PT20S')); $cache->set('baz', 'baz-val', new \DateInterval('PT40S')); diff --git a/src/Symfony/Component/Cache/Tests/Simple/ChainCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/ChainCacheTest.php index 282bb62a6530e..ab28e3bce7b9b 100644 --- a/src/Symfony/Component/Cache/Tests/Simple/ChainCacheTest.php +++ b/src/Symfony/Component/Cache/Tests/Simple/ChainCacheTest.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Cache\Tests\Simple; +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\Simple\ArrayCache; use Symfony\Component\Cache\Simple\ChainCache; use Symfony\Component\Cache\Simple\FilesystemCache; @@ -40,6 +42,75 @@ public function testEmptyCachesException() */ public function testInvalidCacheException() { - new Chaincache(array(new \stdClass())); + new ChainCache(array(new \stdClass())); } + + public function testPrune() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $cache = new ChainCache(array( + $this->getPruneableMock(), + $this->getNonPruneableMock(), + $this->getPruneableMock(), + )); + $this->assertTrue($cache->prune()); + + $cache = new ChainCache(array( + $this->getPruneableMock(), + $this->getFailingPruneableMock(), + $this->getPruneableMock(), + )); + $this->assertFalse($cache->prune()); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|PruneableCacheInterface + */ + private function getPruneableMock() + { + $pruneable = $this + ->getMockBuilder(PruneableCacheInterface::class) + ->getMock(); + + $pruneable + ->expects($this->atLeastOnce()) + ->method('prune') + ->will($this->returnValue(true)); + + return $pruneable; + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|PruneableCacheInterface + */ + private function getFailingPruneableMock() + { + $pruneable = $this + ->getMockBuilder(PruneableCacheInterface::class) + ->getMock(); + + $pruneable + ->expects($this->atLeastOnce()) + ->method('prune') + ->will($this->returnValue(false)); + + return $pruneable; + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|CacheInterface + */ + private function getNonPruneableMock() + { + return $this + ->getMockBuilder(CacheInterface::class) + ->getMock(); + } +} + +interface PruneableCacheInterface extends PruneableInterface, CacheInterface +{ } diff --git a/src/Symfony/Component/Cache/Tests/Simple/PdoCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PdoCacheTest.php index 47c0ee52d99ac..cf5730952dd84 100644 --- a/src/Symfony/Component/Cache/Tests/Simple/PdoCacheTest.php +++ b/src/Symfony/Component/Cache/Tests/Simple/PdoCacheTest.php @@ -12,12 +12,15 @@ namespace Symfony\Component\Cache\Tests\Simple; use Symfony\Component\Cache\Simple\PdoCache; +use Symfony\Component\Cache\Tests\Traits\PdoPruneableTrait; /** * @group time-sensitive */ class PdoCacheTest extends CacheTestCase { + use PdoPruneableTrait; + protected static $dbFile; public static function setupBeforeClass() diff --git a/src/Symfony/Component/Cache/Tests/Simple/PdoDbalCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/PdoDbalCacheTest.php index 51a10af30663b..0c40c04a2cae7 100644 --- a/src/Symfony/Component/Cache/Tests/Simple/PdoDbalCacheTest.php +++ b/src/Symfony/Component/Cache/Tests/Simple/PdoDbalCacheTest.php @@ -13,12 +13,15 @@ use Doctrine\DBAL\DriverManager; use Symfony\Component\Cache\Simple\PdoCache; +use Symfony\Component\Cache\Tests\Traits\PdoPruneableTrait; /** * @group time-sensitive */ class PdoDbalCacheTest extends CacheTestCase { + use PdoPruneableTrait; + protected static $dbFile; public static function setupBeforeClass() diff --git a/src/Symfony/Component/Cache/Tests/Traits/PdoPruneableTrait.php b/src/Symfony/Component/Cache/Tests/Traits/PdoPruneableTrait.php new file mode 100644 index 0000000000000..a9c459fb87171 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Traits/PdoPruneableTrait.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Traits; + +trait PdoPruneableTrait +{ + protected function isPruned($cache, $name) + { + $o = new \ReflectionObject($cache); + + if (!$o->hasMethod('getConnection')) { + self::fail('Cache does not have "getConnection()" method.'); + } + + $getPdoConn = $o->getMethod('getConnection'); + $getPdoConn->setAccessible(true); + + /** @var \Doctrine\DBAL\Statement $select */ + $select = $getPdoConn->invoke($cache)->prepare('SELECT 1 FROM cache_items WHERE item_id LIKE :id'); + $select->bindValue(':id', sprintf('%%%s', $name)); + $select->execute(); + + return 0 === count($select->fetchAll(\PDO::FETCH_COLUMN)); + } +} diff --git a/src/Symfony/Component/Cache/Traits/PdoTrait.php b/src/Symfony/Component/Cache/Traits/PdoTrait.php index 20d1eee1bd1b6..dd8c97d8ab1b2 100644 --- a/src/Symfony/Component/Cache/Traits/PdoTrait.php +++ b/src/Symfony/Component/Cache/Traits/PdoTrait.php @@ -34,6 +34,7 @@ trait PdoTrait private $username = ''; private $password = ''; private $connectionOptions = array(); + private $namespace; private function init($connOrDsn, $namespace, $defaultLifetime, array $options) { @@ -63,6 +64,7 @@ private function init($connOrDsn, $namespace, $defaultLifetime, array $options) $this->username = isset($options['db_username']) ? $options['db_username'] : $this->username; $this->password = isset($options['db_password']) ? $options['db_password'] : $this->password; $this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions; + $this->namespace = $namespace; parent::__construct($namespace, $defaultLifetime); } @@ -137,6 +139,27 @@ public function createTable() $conn->exec($sql); } + /** + * {@inheritdoc} + */ + public function prune() + { + $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= :time"; + + if ('' !== $this->namespace) { + $deleteSql .= " AND $this->idCol LIKE :namespace"; + } + + $delete = $this->getConnection()->prepare($deleteSql); + $delete->bindValue(':time', time(), \PDO::PARAM_INT); + + if ('' !== $this->namespace) { + $delete->bindValue(':namespace', sprintf('%s%%', $this->namespace), \PDO::PARAM_STR); + } + + return $delete->execute(); + } + /** * {@inheritdoc} */