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
+ */
+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 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());
+ }
}