diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index a1f0de7dcaefd..fc6ebbd66c102 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -16,11 +16,13 @@ use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; /** * @author Nicolas Grekas */ -abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface +abstract class AbstractAdapter implements ContextAwareAdapterInterface, LoggerAwareInterface { use LoggerAwareTrait; @@ -32,9 +34,17 @@ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface private $createCacheItem; private $mergeByLifetime; + /** + * @var int|null The maximum length to enforce for identifiers or null when no limit applies + */ + protected $maxIdLength; + protected function __construct($namespace = '', $defaultLifetime = 0) { - $this->namespace = '' === $namespace ? '' : $this->getId($namespace); + $this->namespace = '' === $namespace ? '' : $this->getId($namespace).':'; + if (null !== $this->maxIdLength && strlen($namespace) > $this->maxIdLength - 24) { + throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, strlen($namespace), $namespace)); + } $this->createCacheItem = \Closure::bind( function ($key, $value, $isHit) use ($defaultLifetime) { $item = new CacheItem(); @@ -363,6 +373,22 @@ public function commit() return $ok; } + /** + * {@inheritdoc} + */ + public function withContext($context) + { + $fork = clone $this; + $fork->deferred = array(); + $fork->namespace .= $this->getId($context).':'; + + if (null !== $this->maxIdLength && strlen($fork->namespace) > $this->maxIdLength - 23) { + throw new CacheException(sprintf('Full namespace must be %d chars max, %d reached ("%s")', $this->maxIdLength - 24, strlen($fork->namespace), substr($fork->namespace, 0, -1))); + } + + return $fork; + } + public function __destruct() { if ($this->deferred) { @@ -401,7 +427,14 @@ private function getId($key) { CacheItem::validateKey($key); - return $this->namespace.$key; + if (null === $this->maxIdLength) { + return $this->namespace.$key; + } + if (strlen($id = $this->namespace.$key) > $this->maxIdLength) { + $id = $this->namespace.substr_replace(base64_encode(md5($key, true)), ':', -2); + } + + return $id; } private function generateItems($items, &$keys) diff --git a/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php b/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php index d0af11395700d..67afd5c72a89e 100644 --- a/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php @@ -37,9 +37,9 @@ public function __construct($namespace = '', $defaultLifetime = 0, $version = nu if (null !== $version) { CacheItem::validateKey($version); - if (!apcu_exists($version.':'.$namespace)) { + if (!apcu_exists($version.'@'.$namespace)) { $this->clear($namespace); - apcu_add($version.':'.$namespace, null); + apcu_add($version.'@'.$namespace, null); } } } diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index 2898ba50cdc9a..bf1af03dd2869 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -19,7 +19,7 @@ /** * @author Nicolas Grekas */ -class ArrayAdapter implements AdapterInterface, LoggerAwareInterface +class ArrayAdapter implements ContextAwareAdapterInterface, LoggerAwareInterface { use LoggerAwareTrait; @@ -27,6 +27,7 @@ class ArrayAdapter implements AdapterInterface, LoggerAwareInterface private $values = array(); private $expiries = array(); private $createCacheItem; + private $forks = array(); /** * @param int $defaultLifetime @@ -115,6 +116,9 @@ public function hasItem($key) public function clear() { $this->values = $this->expiries = array(); + foreach ($this->forks as $fork) { + $fork->clear(); + } return true; } @@ -197,6 +201,24 @@ public function commit() return true; } + /** + * {@inheritdoc} + */ + public function withContext($context) + { + CacheItem::validateKey($context); + + if (isset($this->forks[$context])) { + return $this->forks[$context]; + } + + $fork = clone $this; + $fork->values = $fork->expiries = array(); + $this->forks[$context] = $fork; + + return $fork; + } + private function generateItems(array $keys, $now) { $f = $this->createCacheItem; diff --git a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php index c731e47ddd808..4874e96824e4e 100644 --- a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php @@ -14,6 +14,7 @@ use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\CacheException; use Symfony\Component\Cache\Exception\InvalidArgumentException; /** @@ -24,7 +25,7 @@ * * @author Kévin Dunglas */ -class ChainAdapter implements AdapterInterface +class ChainAdapter implements ContextAwareAdapterInterface { private $adapters = array(); private $saveUp; @@ -223,4 +224,22 @@ public function commit() return $committed; } + + /** + * {@inheritdoc} + */ + public function withContext($context) + { + $fork = clone $this; + $fork->adapters = array(); + + foreach ($this->adapters as $adapter) { + if (!$adapter instanceof ContextAwareAdapterInterface) { + throw new CacheException(sprintf('%s does not implement ContextAwareAdapterInterface.', get_class($adapter))); + } + $fork->adapters[] = $adapter->withContext($context); + } + + return $fork; + } } diff --git a/src/Symfony/Component/Cache/Adapter/ContextAwareAdapterInterface.php b/src/Symfony/Component/Cache/Adapter/ContextAwareAdapterInterface.php new file mode 100644 index 0000000000000..f6ea3a655d0d3 --- /dev/null +++ b/src/Symfony/Component/Cache/Adapter/ContextAwareAdapterInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheException; +use Psr\Cache\InvalidArgumentException; + +/** + * Interface for creating contextualized key spaces from existing pools. + * + * @author Nicolas Grekas + */ +interface ContextAwareAdapterInterface extends AdapterInterface +{ + /** + * Derivates a new cache pool from an existing one by contextualizing its key space. + * + * Contexts add to each others. + * Clearing a parent also clears all its derivated + * pools (but not necessarily their deferred items). + * + * @param string $context A context identifier. + * + * @return self A clone of the current instance, where cache keys are isolated from its parent + * + * @throws InvalidArgumentException When $context contains invalid characters + * @throws CacheException When the adapter can't be forked + */ + public function withContext($context); +} diff --git a/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php b/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php index ed91bf56cd0e5..8197885a68514 100644 --- a/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/DoctrineAdapter.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Cache\Adapter; use Doctrine\Common\Cache\CacheProvider; +use Symfony\Component\Cache\Exception\CacheException; /** * @author Nicolas Grekas @@ -27,6 +28,14 @@ public function __construct(CacheProvider $provider, $namespace = '', $defaultLi $provider->setNamespace($namespace); } + /** + * @inheritdoc} + */ + public function withContext($context) + { + throw new CacheException(sprintf('%s does not implement ContextAwareAdapterInterface.', get_class($this))); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php b/src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php index 809ec15dfb0ae..f72dee95a82ac 100644 --- a/src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php +++ b/src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Cache\Adapter; +use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\Exception\InvalidArgumentException; /** @@ -79,6 +80,19 @@ protected function doDelete(array $ids) return $ok; } + /** + * {@inheritdoc} + */ + public function withContext($context) + { + CacheItem::validateKey($context); + + $fork = clone $this; + $fork->init($context, $this->directory); + + return $fork; + } + private function write($file, $data, $expiresAt = null) { if (false === @file_put_contents($this->tmp, $data)) { diff --git a/src/Symfony/Component/Cache/Adapter/NullAdapter.php b/src/Symfony/Component/Cache/Adapter/NullAdapter.php index f58f81e5b8960..4ed3639134528 100644 --- a/src/Symfony/Component/Cache/Adapter/NullAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/NullAdapter.php @@ -17,7 +17,7 @@ /** * @author Titouan Galopin */ -class NullAdapter implements AdapterInterface +class NullAdapter implements ContextAwareAdapterInterface { private $createCacheItem; @@ -110,6 +110,14 @@ public function commit() return false; } + /** + * {@inheritdoc} + */ + public function withContext($context) + { + return clone $this; + } + private function generateItems(array $keys) { $f = $this->createCacheItem; diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php index 6f349ab433ef8..dcb017d1b4741 100644 --- a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php @@ -14,11 +14,12 @@ use Psr\Cache\CacheItemInterface; use Psr\Cache\InvalidArgumentException; use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\CacheException; /** * @author Nicolas Grekas */ -class TagAwareAdapter implements TagAwareAdapterInterface +class TagAwareAdapter implements ContextAwareAdapterInterface, TagAwareAdapterInterface { const TAGS_PREFIX = "\0tags\0"; @@ -242,6 +243,22 @@ public function commit() return $this->itemsAdapter->commit() && $ok; } + /** + * {@inheritdoc} + */ + public function withContext($context) + { + if (!$this->itemsAdapter instanceof ContextAwareAdapterInterface) { + throw new CacheException(sprintf('%s does not implement ContextAwareAdapterInterface.', get_class($this->itemsAdapter))); + } + + $fork = clone $this; + $fork->itemsAdapter = $this->itemsAdapter->withContext($context); + $fork->deferred = array(); + + return $fork; + } + public function __destruct() { $this->commit(); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php index 4bc80ee30022e..d649b08581cb9 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 Symfony\Component\Cache\Adapter\ContextAwareAdapterInterface; abstract class AdapterTestCase extends CachePoolTest { @@ -71,6 +72,59 @@ public function testNotUnserializable() } $this->assertFalse($item->isHit()); } + + public function testContext() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + + return; + } + $cache = $this->createCachePool(); + + if (!$cache instanceof ContextAwareAdapterInterface) { + $this->markTestSkipped('ContextAwareAdapterInterface not implemented.'); + } + + $item = $cache->getItem('foo'); + $cache->save($item->set('foo')); + + $fork = $cache->withContext('ns'); + $item = $fork->getItem('foo'); + $this->assertFalse($item->isHit()); + + $fork->save($item->set('bar')); + $item = $cache->getItem('bar'); + $this->assertFalse($item->isHit()); + + $fork = $cache->withContext('ns'); + $item = $fork->getItem('foo'); + $this->assertTrue($item->isHit()); + + $cache->clear(); + $item = $fork->getItem('foo'); + $this->assertFalse($item->isHit()); + } + + /** + * @expectedException \Psr\Cache\InvalidArgumentException + * @dataProvider invalidKeys + */ + public function testBadContext($context) + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + + return; + } + $cache = $this->createCachePool(); + + if (!$cache instanceof ContextAwareAdapterInterface) { + $this->markTestSkipped('ContextAwareAdapterInterface not implemented.'); + } + + $cache->withContext($context); + } } class NotUnserializable implements \Serializable diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php index b80913c6e089c..d29d9f08df3a6 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/ChainAdapterTest.php @@ -22,6 +22,10 @@ */ class ChainAdapterTest extends AdapterTestCase { + protected $skippedTests = array( + 'testBadContext' => 'ContextAwareAdapterInterface not implemented by ExternalAdapter.', + ); + public function createCachePool($defaultLifetime = 0) { return new ChainAdapter(array(new ArrayAdapter($defaultLifetime), new ExternalAdapter(), new FilesystemAdapter('', $defaultLifetime)), $defaultLifetime); @@ -44,4 +48,13 @@ public function testInvalidAdapterException() { new ChainAdapter(array(new \stdClass())); } + + /** + * @expectedException Symfony\Component\Cache\Exception\CacheException + * @expectedExceptionMessage Symfony\Component\Cache\Adapter\ProxyAdapter does not implement ContextAwareAdapterInterface. + */ + public function testContext() + { + parent::testContext(); + } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php index 93ec9824388e1..ea6cdeea17203 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/DoctrineAdapterTest.php @@ -23,10 +23,20 @@ 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', + 'testBadContext' => 'ContextAwareAdapterInterface not implemented.', ); public function createCachePool($defaultLifetime = 0) { return new DoctrineAdapter(new ArrayCache($defaultLifetime), '', $defaultLifetime); } + + /** + * @expectedException Symfony\Component\Cache\Exception\CacheException + * @expectedExceptionMessage Symfony\Component\Cache\Adapter\DoctrineAdapter does not implement ContextAwareAdapterInterface. + */ + public function testContext() + { + parent::testContext(); + } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php index fc824ede58868..2b8335f1d27e1 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php @@ -97,4 +97,30 @@ public function testTagsAreCleanedOnDelete() $this->assertTrue($pool->getItem('k')->isHit()); } + + public function testTagsImpactSubContexts() + { + $pool = $this->createCachePool(); + $fork = $pool->withContext('ns'); + + $poolItem = $pool->getItem('i1'); + $forkItem = $fork->getItem('i2'); + + $poolItem->tag('foo'); + $forkItem->tag('foo'); + + $pool->save($poolItem); + $fork->save($forkItem); + + $this->assertTrue($pool->hasItem('i1')); + $this->assertTrue($fork->hasItem('i2')); + + $this->assertTrue($fork->invalidateTags('foo')); + + $this->assertFalse($pool->hasItem('i1')); + $this->assertFalse($fork->hasItem('i2')); + + $this->assertFalse($pool->getItem('i1')->isHit()); + $this->assertFalse($fork->getItem('i2')->isHit()); + } }