diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml
index f7162adb1c701..d0e596ab83338 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml
@@ -15,6 +15,10 @@
*/ -abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface, ResettableInterface +abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface { use AbstractTrait; + use GetTrait; private static $apcuSupported; private static $phpFilesSupported; diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index fee7ed6d906d5..17f2beaf0fe75 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -13,16 +13,19 @@ use Psr\Cache\CacheItemInterface; use Psr\Log\LoggerAwareInterface; +use Symfony\Component\Cache\CacheInterface; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Cache\Traits\ArrayTrait; +use Symfony\Component\Cache\Traits\GetTrait; /** * @author Nicolas Grekas
*/
-class ArrayAdapter implements AdapterInterface, LoggerAwareInterface, ResettableInterface
+class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
use ArrayTrait;
+ use GetTrait;
private $createCacheItem;
diff --git a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php
index 98b0cc24693b4..ea0af87d9f238 100644
--- a/src/Symfony/Component/Cache/Adapter/ChainAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/ChainAdapter.php
@@ -13,10 +13,12 @@
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
+use Symfony\Component\Cache\CacheInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
+use Symfony\Component\Cache\Traits\GetTrait;
/**
* Chains several adapters together.
@@ -26,8 +28,10 @@
*
* @author Kévin Dunglas
*/
-class PhpArrayAdapter implements AdapterInterface, PruneableInterface, ResettableInterface
+class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
use PhpArrayTrait;
+ use GetTrait;
private $createCacheItem;
@@ -77,6 +80,31 @@ public static function create($file, CacheItemPoolInterface $fallbackPool)
return $fallbackPool;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function get(string $key, callable $callback)
+ {
+ if (null === $this->values) {
+ $this->initialize();
+ }
+ if (null === $value = $this->values[$key] ?? null) {
+ if ($this->pool instanceof CacheInterface) {
+ return $this->pool->get($key, $callback);
+ }
+
+ return $this->doGet($this->pool, $key, $callback);
+ }
+ if ('N;' === $value) {
+ return null;
+ }
+ if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
+ return unserialize($value);
+ }
+
+ return $value;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php
index da286dbf173f7..b9981f5e64c0c 100644
--- a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php
@@ -13,17 +13,20 @@
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
+use Symfony\Component\Cache\CacheInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
+use Symfony\Component\Cache\Traits\GetTrait;
use Symfony\Component\Cache\Traits\ProxyTrait;
/**
* @author Nicolas Grekas
*/
-class ProxyAdapter implements AdapterInterface, PruneableInterface, ResettableInterface
+class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
use ProxyTrait;
+ use GetTrait;
private $namespace;
private $namespaceLen;
@@ -54,6 +57,20 @@ function ($key, $innerItem) use ($defaultLifetime, $poolHash) {
);
}
+ /**
+ * {@inheritdoc}
+ */
+ public function get(string $key, callable $callback)
+ {
+ if (!$this->pool instanceof CacheInterface) {
+ return $this->doGet($this->pool, $key, $callback);
+ }
+
+ return $this->pool->get($this->getId($key), function ($innerItem) use ($key, $callback) {
+ return $callback(($this->createCacheItem)($key, $innerItem));
+ });
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php
index 62f815e0171ad..257e404e1c43e 100644
--- a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php
@@ -16,16 +16,19 @@
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
+use Symfony\Component\Cache\TaggableCacheInterface;
+use Symfony\Component\Cache\Traits\GetTrait;
use Symfony\Component\Cache\Traits\ProxyTrait;
/**
* @author Nicolas Grekas
*/
-class TagAwareAdapter implements TagAwareAdapterInterface, PruneableInterface, ResettableInterface
+class TagAwareAdapter implements TagAwareAdapterInterface, TaggableCacheInterface, PruneableInterface, ResettableInterface
{
const TAGS_PREFIX = "\0tags\0";
use ProxyTrait;
+ use GetTrait;
private $deferred = array();
private $createCacheItem;
@@ -58,6 +61,7 @@ function ($key, $value, CacheItem $protoItem) {
);
$this->setCacheItemTags = \Closure::bind(
function (CacheItem $item, $key, array &$itemTags) {
+ $item->isTaggable = true;
if (!$item->isHit) {
return $item;
}
diff --git a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php
index 98d0e526933b9..a0df682d92b69 100644
--- a/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/TraceableAdapter.php
@@ -12,6 +12,8 @@
namespace Symfony\Component\Cache\Adapter;
use Psr\Cache\CacheItemInterface;
+use Symfony\Component\Cache\CacheInterface;
+use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
@@ -22,7 +24,7 @@
* @author Tobias Nyholm
*/
-class TraceableAdapter implements AdapterInterface, PruneableInterface, ResettableInterface
+class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
protected $pool;
private $calls = array();
@@ -32,6 +34,38 @@ public function __construct(AdapterInterface $pool)
$this->pool = $pool;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function get(string $key, callable $callback)
+ {
+ if (!$this->pool instanceof CacheInterface) {
+ throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_class($this->pool), CacheInterface::class));
+ }
+
+ $isHit = true;
+ $callback = function (CacheItem $item) use ($callback, &$isHit) {
+ $isHit = $item->isHit();
+
+ return $callback($item);
+ };
+
+ $event = $this->start(__FUNCTION__);
+ try {
+ $value = $this->pool->get($key, $callback);
+ $event->result[$key] = \is_object($value) ? \get_class($value) : gettype($value);
+ } finally {
+ $event->end = microtime(true);
+ }
+ if ($isHit) {
+ ++$event->hits;
+ } else {
+ ++$event->misses;
+ }
+
+ return $value;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php
index de68955d8e56d..2fda8b360240e 100644
--- a/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/TraceableTagAwareAdapter.php
@@ -11,10 +11,12 @@
namespace Symfony\Component\Cache\Adapter;
+use Symfony\Component\Cache\TaggableCacheInterface;
+
/**
* @author Robin Chalas
+ */
+interface CacheInterface
+{
+ /**
+ * @param callable(CacheItemInterface):mixed $callback Should return the computed value for the given key/item
+ *
+ * @return mixed The value corresponding to the provided key
+ */
+ public function get(string $key, callable $callback);
+}
diff --git a/src/Symfony/Component/Cache/CacheItem.php b/src/Symfony/Component/Cache/CacheItem.php
index cecaa126d9129..82ad9df68262c 100644
--- a/src/Symfony/Component/Cache/CacheItem.php
+++ b/src/Symfony/Component/Cache/CacheItem.php
@@ -14,6 +14,7 @@
use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\Exception\LogicException;
/**
* @author Nicolas Grekas
@@ -29,6 +30,7 @@ final class CacheItem implements CacheItemInterface
protected $prevTags = array();
protected $innerItem;
protected $poolHash;
+ protected $isTaggable = false;
/**
* {@inheritdoc}
@@ -109,7 +111,10 @@ public function expiresAfter($time)
*/
public function tag($tags)
{
- if (!\is_array($tags)) {
+ if (!$this->isTaggable) {
+ throw new LogicException(sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key));
+ }
+ if (!\is_iterable($tags)) {
$tags = array($tags);
}
foreach ($tags as $tag) {
diff --git a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php
index 91763e5a9f33b..5f29bfe5f2769 100644
--- a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php
+++ b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php
@@ -121,7 +121,15 @@ private function calculateStatistics(): array
foreach ($calls as $call) {
++$statistics[$name]['calls'];
$statistics[$name]['time'] += $call->end - $call->start;
- if ('getItem' === $call->name) {
+ if ('get' === $call->name) {
+ ++$statistics[$name]['reads'];
+ if ($call->hits) {
+ ++$statistics[$name]['hits'];
+ } else {
+ ++$statistics[$name]['misses'];
+ ++$statistics[$name]['writes'];
+ }
+ } elseif ('getItem' === $call->name) {
++$statistics[$name]['reads'];
if ($call->hits) {
++$statistics[$name]['hits'];
diff --git a/src/Symfony/Component/Cache/Exception/LogicException.php b/src/Symfony/Component/Cache/Exception/LogicException.php
new file mode 100644
index 0000000000000..042f73e6a5040
--- /dev/null
+++ b/src/Symfony/Component/Cache/Exception/LogicException.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Exception;
+
+use Psr\Cache\InvalidArgumentException as Psr6CacheInterface;
+use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInterface;
+
+class LogicException extends \LogicException implements Psr6CacheInterface, SimpleCacheInterface
+{
+}
diff --git a/src/Symfony/Component/Cache/TaggableCacheInterface.php b/src/Symfony/Component/Cache/TaggableCacheInterface.php
new file mode 100644
index 0000000000000..c112e72586d74
--- /dev/null
+++ b/src/Symfony/Component/Cache/TaggableCacheInterface.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache;
+
+/**
+ * Gets and stores items from a tag-aware cache.
+ *
+ * On cache misses, a callback is called that should return the missing value.
+ * It is given two arguments:
+ * - the missing cache key
+ * - the corresponding Symfony CacheItem object,
+ * allowing time-based *and* tags-based expiration control
+ *
+ * If you don't need tags-based invalidation, use CacheInterface instead.
+ *
+ * @author Nicolas Grekas
+ */
+interface TaggableCacheInterface extends CacheInterface
+{
+ /**
+ * @param callable(CacheItem):mixed $callback Should return the computed value for the given key/item
+ *
+ * @return mixed The value corresponding to the provided key
+ */
+ public function get(string $key, callable $callback);
+}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php
index 018d149467482..3c96b731cc565 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php
@@ -13,6 +13,7 @@
use Cache\IntegrationTests\CachePoolTest;
use Psr\Cache\CacheItemPoolInterface;
+use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
abstract class AdapterTestCase extends CachePoolTest
@@ -26,6 +27,26 @@ protected function setUp()
}
}
+ public function testGet()
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $cache = $this->createCachePool();
+
+ $value = mt_rand();
+
+ $this->assertSame($value, $cache->get('foo', function (CacheItem $item) use ($value) {
+ $this->assertSame('foo', $item->getKey());
+
+ return $value;
+ }));
+
+ $item = $cache->getItem('foo');
+ $this->assertSame($value, $item->get());
+ }
+
public function testDefaultLifeTime()
{
if (isset($this->skippedTests[__FUNCTION__])) {
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php
index 14b61263c5892..8630b52cf30c9 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php
@@ -21,6 +21,7 @@
class PhpArrayAdapterTest extends AdapterTestCase
{
protected $skippedTests = array(
+ 'testGet' => 'PhpArrayAdapter is read-only.',
'testBasicUsage' => 'PhpArrayAdapter is read-only.',
'testBasicUsageWithLongKey' => 'PhpArrayAdapter is read-only.',
'testClear' => 'PhpArrayAdapter is read-only.',
diff --git a/src/Symfony/Component/Cache/Tests/CacheItemTest.php b/src/Symfony/Component/Cache/Tests/CacheItemTest.php
index daca925fd5b78..3a0ea098ad7c1 100644
--- a/src/Symfony/Component/Cache/Tests/CacheItemTest.php
+++ b/src/Symfony/Component/Cache/Tests/CacheItemTest.php
@@ -55,6 +55,9 @@ public function provideInvalidKey()
public function testTag()
{
$item = new CacheItem();
+ $r = new \ReflectionProperty($item, 'isTaggable');
+ $r->setAccessible(true);
+ $r->setValue($item, true);
$this->assertSame($item, $item->tag('foo'));
$this->assertSame($item, $item->tag(array('bar', 'baz')));
@@ -72,6 +75,24 @@ public function testTag()
public function testInvalidTag($tag)
{
$item = new CacheItem();
+ $r = new \ReflectionProperty($item, 'isTaggable');
+ $r->setAccessible(true);
+ $r->setValue($item, true);
+
$item->tag($tag);
}
+
+ /**
+ * @expectedException \Symfony\Component\Cache\Exception\LogicException
+ * @expectedExceptionMessage Cache item "foo" comes from a non tag-aware pool: you cannot tag it.
+ */
+ public function testNonTaggableItem()
+ {
+ $item = new CacheItem();
+ $r = new \ReflectionProperty($item, 'key');
+ $r->setAccessible(true);
+ $r->setValue($item, 'foo');
+
+ $item->tag(array());
+ }
}
diff --git a/src/Symfony/Component/Cache/Traits/GetTrait.php b/src/Symfony/Component/Cache/Traits/GetTrait.php
new file mode 100644
index 0000000000000..d2a5f92da2e53
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/GetTrait.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits;
+
+use Psr\Cache\CacheItemPoolInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+trait GetTrait
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function get(string $key, callable $callback)
+ {
+ return $this->doGet($this, $key, $callback);
+ }
+
+ private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback)
+ {
+ $item = $pool->getItem($key);
+
+ if ($item->isHit()) {
+ return $item->get();
+ }
+
+ $pool->save($item->set($value = $callback($item)));
+
+ return $value;
+ }
+}