diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index f7fe9d3464ac1..7313d16d25c70 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -71,9 +71,10 @@
Doctrine\Common\Cache
Symfony\Component\Cache
Symfony\Component\Cache\Tests\Fixtures
- Symfony\Component\Cache\Traits
- Symfony\Component\Console
- Symfony\Component\HttpFoundation
+ Symfony\Component\Cache\Tests\Traits
+ Symfony\Component\Cache\Traits
+ Symfony\Component\Console
+ Symfony\Component\HttpFoundation
diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
index 011a239bf6ac1..e8fc564ddd1ab 100644
--- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
@@ -11,14 +11,13 @@
namespace Symfony\Component\Cache\Adapter;
-use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
-use Symfony\Component\Cache\Traits\AbstractTrait;
+use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;
@@ -27,15 +26,12 @@
*/
abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
- use AbstractTrait;
+ use AbstractAdapterTrait;
use ContractsTrait;
private static $apcuSupported;
private static $phpFilesSupported;
- private $createCacheItem;
- private $mergeByLifetime;
-
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
@@ -142,81 +138,6 @@ public static function createConnection($dsn, array $options = [])
throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
}
- /**
- * {@inheritdoc}
- */
- public function getItem($key)
- {
- if ($this->deferred) {
- $this->commit();
- }
- $id = $this->getId($key);
-
- $f = $this->createCacheItem;
- $isHit = false;
- $value = null;
-
- try {
- foreach ($this->doFetch([$id]) as $value) {
- $isHit = true;
- }
- } catch (\Exception $e) {
- CacheItem::log($this->logger, 'Failed to fetch key "{key}"', ['key' => $key, 'exception' => $e]);
- }
-
- return $f($key, $value, $isHit);
- }
-
- /**
- * {@inheritdoc}
- */
- public function getItems(array $keys = [])
- {
- if ($this->deferred) {
- $this->commit();
- }
- $ids = [];
-
- foreach ($keys as $key) {
- $ids[] = $this->getId($key);
- }
- try {
- $items = $this->doFetch($ids);
- } catch (\Exception $e) {
- CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => $keys, 'exception' => $e]);
- $items = [];
- }
- $ids = array_combine($ids, $keys);
-
- return $this->generateItems($items, $ids);
- }
-
- /**
- * {@inheritdoc}
- */
- public function save(CacheItemInterface $item)
- {
- if (!$item instanceof CacheItem) {
- return false;
- }
- $this->deferred[$item->getKey()] = $item;
-
- return $this->commit();
- }
-
- /**
- * {@inheritdoc}
- */
- public function saveDeferred(CacheItemInterface $item)
- {
- if (!$item instanceof CacheItem) {
- return false;
- }
- $this->deferred[$item->getKey()] = $item;
-
- return true;
- }
-
/**
* {@inheritdoc}
*/
@@ -271,33 +192,4 @@ public function commit()
return $ok;
}
-
- public function __destruct()
- {
- if ($this->deferred) {
- $this->commit();
- }
- }
-
- private function generateItems($items, &$keys)
- {
- $f = $this->createCacheItem;
-
- try {
- foreach ($items as $id => $value) {
- if (!isset($keys[$id])) {
- $id = key($keys);
- }
- $key = $keys[$id];
- unset($keys[$id]);
- yield $key => $f($key, $value, true);
- }
- } catch (\Exception $e) {
- CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => array_values($keys), 'exception' => $e]);
- }
-
- foreach ($keys as $key) {
- yield $key => $f($key, null, false);
- }
- }
}
diff --git a/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php
new file mode 100644
index 0000000000000..ddebdf19bbb07
--- /dev/null
+++ b/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php
@@ -0,0 +1,302 @@
+
+ *
+ * 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\Log\LoggerAwareInterface;
+use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Cache\ResettableInterface;
+use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
+use Symfony\Component\Cache\Traits\ContractsTrait;
+use Symfony\Contracts\Cache\TagAwareCacheInterface;
+
+/**
+ * Abstract for native TagAware adapters.
+ *
+ * To keep info on tags, the tags are both serialized as part of cache value and provided as tag ids
+ * to Adapters on operations when needed for storage to doSave(), doDelete() & doInvalidate().
+ *
+ * @author Nicolas Grekas
+ * @author André Rømcke
+ *
+ * @internal
+ * @experimental in 4.3
+ */
+abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface
+{
+ use AbstractAdapterTrait;
+ use ContractsTrait;
+
+ private const TAGS_PREFIX = "\0tags\0";
+
+ protected function __construct(string $namespace = '', int $defaultLifetime = 0)
+ {
+ $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($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(
+ static function ($key, $value, $isHit) use ($defaultLifetime) {
+ $item = new CacheItem();
+ $item->key = $key;
+ $item->defaultLifetime = $defaultLifetime;
+ $item->isTaggable = true;
+ // If structure does not match what we expect return item as is (no value and not a hit)
+ if (!\is_array($value) || !\array_key_exists('value', $value)) {
+ return $item;
+ }
+ $item->isHit = $isHit;
+ // Extract value, tags and meta data from the cache value
+ $item->value = $value['value'];
+ $item->metadata[CacheItem::METADATA_TAGS] = $value['tags'] ?? [];
+ if (isset($value['meta'])) {
+ // For compactness these values are packed, & expiry is offset to reduce size
+ $v = \unpack('Ve/Nc', $value['meta']);
+ $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
+ $item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
+ }
+
+ return $item;
+ },
+ null,
+ CacheItem::class
+ );
+ $getId = \Closure::fromCallable([$this, 'getId']);
+ $tagPrefix = self::TAGS_PREFIX;
+ $this->mergeByLifetime = \Closure::bind(
+ static function ($deferred, &$expiredIds) use ($getId, $tagPrefix) {
+ $byLifetime = [];
+ $now = microtime(true);
+ $expiredIds = [];
+
+ foreach ($deferred as $key => $item) {
+ $key = (string) $key;
+ if (null === $item->expiry) {
+ $ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0;
+ } elseif (0 >= $ttl = (int) ($item->expiry - $now)) {
+ $expiredIds[] = $getId($key);
+ continue;
+ }
+ // Store Value and Tags on the cache value
+ if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
+ $value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]];
+ unset($metadata[CacheItem::METADATA_TAGS]);
+ } else {
+ $value = ['value' => $item->value, 'tags' => []];
+ }
+
+ if ($metadata) {
+ // For compactness, expiry and creation duration are packed, using magic numbers as separators
+ $value['meta'] = \pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME]);
+ }
+
+ // Extract tag changes, these should be removed from values in doSave()
+ $value['tag-operations'] = ['add' => [], 'remove' => []];
+ $oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
+ foreach (\array_diff($value['tags'], $oldTags) as $addedTag) {
+ $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
+ }
+ foreach (\array_diff($oldTags, $value['tags']) as $removedTag) {
+ $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
+ }
+
+ $byLifetime[$ttl][$getId($key)] = $value;
+ }
+
+ return $byLifetime;
+ },
+ null,
+ CacheItem::class
+ );
+ }
+
+ /**
+ * Persists several cache items immediately.
+ *
+ * @param array $values The values to cache, indexed by their cache identifier
+ * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning
+ * @param array[] $addTagData Hash where key is tag id, and array value is list of cache id's to add to tag
+ * @param array[] $removeTagData Hash where key is tag id, and array value is list of cache id's to remove to tag
+ *
+ * @return array The identifiers that failed to be cached or a boolean stating if caching succeeded or not
+ */
+ abstract protected function doSave(array $values, ?int $lifetime, array $addTagData = [], array $removeTagData = []): array;
+
+ /**
+ * Removes multiple items from the pool and their corresponding tags.
+ *
+ * @param array $ids An array of identifiers that should be removed from the pool
+ * @param array $tagData Optional array of tag identifiers => key identifiers that should be removed from the pool
+ *
+ * @return bool True if the items were successfully removed, false otherwise
+ */
+ abstract protected function doDelete(array $ids, array $tagData = []): bool;
+
+ /**
+ * Invalidates cached items using tags.
+ *
+ * @param string[] $tagIds An array of tags to invalidate, key is tag and value is tag id
+ *
+ * @return bool True on success
+ */
+ abstract protected function doInvalidate(array $tagIds): bool;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function commit()
+ {
+ $ok = true;
+ $byLifetime = $this->mergeByLifetime;
+ $byLifetime = $byLifetime($this->deferred, $expiredIds);
+ $retry = $this->deferred = [];
+
+ if ($expiredIds) {
+ // Tags are not cleaned up in this case, however that is done on invalidateTags().
+ $this->doDelete($expiredIds);
+ }
+ foreach ($byLifetime as $lifetime => $values) {
+ try {
+ $values = $this->extractTagData($values, $addTagData, $removeTagData);
+ $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
+ } catch (\Exception $e) {
+ }
+ if (true === $e || [] === $e) {
+ continue;
+ }
+ if (\is_array($e) || 1 === \count($values)) {
+ foreach (\is_array($e) ? $e : array_keys($values) as $id) {
+ $ok = false;
+ $v = $values[$id];
+ $type = \is_object($v) ? \get_class($v) : \gettype($v);
+ CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]);
+ }
+ } else {
+ foreach ($values as $id => $v) {
+ $retry[$lifetime][] = $id;
+ }
+ }
+ }
+
+ // When bulk-save failed, retry each item individually
+ foreach ($retry as $lifetime => $ids) {
+ foreach ($ids as $id) {
+ try {
+ $v = $byLifetime[$lifetime][$id];
+ $values = $this->extractTagData([$id => $v], $addTagData, $removeTagData);
+ $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
+ } catch (\Exception $e) {
+ }
+ if (true === $e || [] === $e) {
+ continue;
+ }
+ $ok = false;
+ $type = \is_object($v) ? \get_class($v) : \gettype($v);
+ CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]);
+ }
+ }
+
+ return $ok;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Overloaded in order to deal with tags for adjusted doDelete() signature.
+ */
+ public function deleteItems(array $keys)
+ {
+ if (!$keys) {
+ return true;
+ }
+
+ $ids = [];
+ $tagData = [];
+
+ foreach ($keys as $key) {
+ $ids[$key] = $this->getId($key);
+ unset($this->deferred[$key]);
+ }
+
+ foreach ($this->doFetch($ids) as $id => $value) {
+ foreach ($value['tags'] ?? [] as $tag) {
+ $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
+ }
+ }
+
+ try {
+ if ($this->doDelete(\array_values($ids), $tagData)) {
+ return true;
+ }
+ } catch (\Exception $e) {
+ }
+
+ $ok = true;
+
+ // When bulk-delete failed, retry each item individually
+ foreach ($ids as $key => $id) {
+ try {
+ $e = null;
+ if ($this->doDelete([$id])) {
+ continue;
+ }
+ } catch (\Exception $e) {
+ }
+ CacheItem::log($this->logger, 'Failed to delete key "{key}"', ['key' => $key, 'exception' => $e]);
+ $ok = false;
+ }
+
+ return $ok;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function invalidateTags(array $tags)
+ {
+ if (empty($tags)) {
+ return false;
+ }
+
+ $tagIds = [];
+ foreach (\array_unique($tags) as $tag) {
+ $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
+ }
+
+ if ($this->doInvalidate($tagIds)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Extracts tags operation data from $values set in mergeByLifetime, and returns values without it.
+ */
+ private function extractTagData(array $values, ?array &$addTagData, ?array &$removeTagData): array
+ {
+ $addTagData = $removeTagData = [];
+ foreach ($values as $id => $value) {
+ foreach ($value['tag-operations']['add'] as $tag => $tagId) {
+ $addTagData[$tagId][] = $id;
+ }
+
+ foreach ($value['tag-operations']['remove'] as $tag => $tagId) {
+ $removeTagData[$tagId][] = $id;
+ }
+
+ unset($values[$id]['tag-operations']);
+ }
+
+ return $values;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php
new file mode 100644
index 0000000000000..f96c670ae92e8
--- /dev/null
+++ b/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php
@@ -0,0 +1,149 @@
+
+ *
+ * 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 Symfony\Component\Cache\Exception\LogicException;
+use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
+use Symfony\Component\Cache\Marshaller\MarshallerInterface;
+use Symfony\Component\Cache\PruneableInterface;
+use Symfony\Component\Cache\Traits\FilesystemTrait;
+use Symfony\Component\Filesystem\Filesystem;
+
+/**
+ * Stores tag id <> cache id relationship as a symlink, and lookup on invalidation calls.
+ *
+ * @author Nicolas Grekas
+ * @author André Rømcke
+ *
+ * @experimental in 4.3
+ */
+class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements PruneableInterface
+{
+ use FilesystemTrait {
+ doSave as doSaveCache;
+ doDelete as doDeleteCache;
+ }
+
+ /**
+ * Folder used for tag symlinks.
+ */
+ private const TAG_FOLDER = 'tags';
+
+ /**
+ * @var Filesystem|null
+ */
+ private $fs;
+
+ public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null)
+ {
+ $this->marshaller = $marshaller ?? new DefaultMarshaller();
+ parent::__construct('', $defaultLifetime);
+ $this->init($namespace, $directory);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doSave(array $values, ?int $lifetime, array $addTagData = [], array $removeTagData = []): array
+ {
+ $failed = $this->doSaveCache($values, $lifetime);
+
+ $fs = $this->getFilesystem();
+ // Add Tags as symlinks
+ foreach ($addTagData as $tagId => $ids) {
+ $tagFolder = $this->getTagFolder($tagId);
+ foreach ($ids as $id) {
+ if ($failed && \in_array($id, $failed, true)) {
+ continue;
+ }
+
+ $file = $this->getFile($id);
+ $fs->symlink($file, $this->getFile($id, true, $tagFolder));
+ }
+ }
+
+ // Unlink removed Tags
+ $files = [];
+ foreach ($removeTagData as $tagId => $ids) {
+ $tagFolder = $this->getTagFolder($tagId);
+ foreach ($ids as $id) {
+ if ($failed && \in_array($id, $failed, true)) {
+ continue;
+ }
+
+ $files[] = $this->getFile($id, false, $tagFolder);
+ }
+ }
+ $fs->remove($files);
+
+ return $failed;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDelete(array $ids, array $tagData = []): bool
+ {
+ $ok = $this->doDeleteCache($ids);
+
+ // Remove tags
+ $files = [];
+ $fs = $this->getFilesystem();
+ foreach ($tagData as $tagId => $idMap) {
+ $tagFolder = $this->getTagFolder($tagId);
+ foreach ($idMap as $id) {
+ $files[] = $this->getFile($id, false, $tagFolder);
+ }
+ }
+ $fs->remove($files);
+
+ return $ok;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doInvalidate(array $tagIds): bool
+ {
+ foreach ($tagIds as $tagId) {
+ $tagsFolder = $this->getTagFolder($tagId);
+ if (!file_exists($tagsFolder)) {
+ continue;
+ }
+
+ foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($tagsFolder, \FilesystemIterator::SKIP_DOTS)) as $itemLink) {
+ if (!$itemLink->isLink()) {
+ throw new LogicException('Expected a (sym)link when iterating over tag folder, non link found: '.$itemLink);
+ }
+
+ $valueFile = $itemLink->getRealPath();
+ if ($valueFile && \file_exists($valueFile)) {
+ @unlink($valueFile);
+ }
+
+ @unlink((string) $itemLink);
+ }
+ }
+
+ return true;
+ }
+
+ private function getFilesystem(): Filesystem
+ {
+ return $this->fs ?? $this->fs = new Filesystem();
+ }
+
+ private function getTagFolder(string $tagId): string
+ {
+ return $this->getFile($tagId, false, $this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php
new file mode 100644
index 0000000000000..d4ee186789a31
--- /dev/null
+++ b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php
@@ -0,0 +1,209 @@
+
+ *
+ * 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 Predis;
+use Predis\Connection\Aggregate\ClusterInterface;
+use Predis\Response\Status;
+use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\LogicException;
+use Symfony\Component\Cache\Marshaller\MarshallerInterface;
+use Symfony\Component\Cache\Traits\RedisTrait;
+
+/**
+ * Stores tag id <> cache id relationship as a Redis Set, lookup on invalidation using sPOP.
+ *
+ * Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even
+ * if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache
+ * relationship survives eviction (cache cleanup when Redis runs out of memory).
+ *
+ * Requirements:
+ * - Server: Redis 3.2+
+ * - Client: PHP Redis 3.1.3+ OR Predis
+ * - Redis Server(s) configured with any `volatile-*` eviction policy, OR `noeviction` if it will NEVER fill up memory
+ *
+ * Design limitations:
+ * - Max 2 billion cache keys per cache tag
+ * E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 2 billion cache items as well
+ *
+ * @see https://redis.io/topics/lru-cache#eviction-policies Documentation for Redis eviction policies.
+ * @see https://redis.io/topics/data-types#sets Documentation for Redis Set datatype.
+ * @see https://redis.io/commands/spop Documentation for sPOP operation, capable of retriving AND emptying a Set at once.
+ *
+ * @author Nicolas Grekas
+ * @author André Rømcke
+ *
+ * @experimental in 4.3
+ */
+class RedisTagAwareAdapter extends AbstractTagAwareAdapter
+{
+ use RedisTrait;
+
+ /**
+ * Redis "Set" can hold more than 4 billion members, here we limit ourselves to PHP's > 2 billion max int (32Bit).
+ */
+ private const POP_MAX_LIMIT = 2147483647 - 1;
+
+ /**
+ * Limits for how many keys are deleted in batch.
+ */
+ private const BULK_DELETE_LIMIT = 10000;
+
+ /**
+ * On cache items without a lifetime set, we set it to 100 days. This is to make sure cache items are
+ * preferred to be evicted over tag Sets, if eviction policy is configured according to requirements.
+ */
+ private const DEFAULT_CACHE_TTL = 8640000;
+
+ /**
+ * @var bool|null
+ */
+ private $redisServerSupportSPOP = null;
+
+ /**
+ * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient The redis client
+ * @param string $namespace The default namespace
+ * @param int $defaultLifetime The default lifetime
+ * @param MarshallerInterface|null $marshaller
+ *
+ * @throws \Symfony\Component\Cache\Exception\LogicException If phpredis with version lower than 3.1.3.
+ */
+ public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
+ {
+ $this->init($redisClient, $namespace, $defaultLifetime, $marshaller);
+
+ // Make sure php-redis is 3.1.3 or higher configured for Redis classes
+ if (!$this->redis instanceof Predis\Client && \version_compare(\phpversion('redis'), '3.1.3', '<')) {
+ throw new LogicException('RedisTagAwareAdapter requires php-redis 3.1.3 or higher, alternatively use predis/predis');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doSave(array $values, ?int $lifetime, array $addTagData = [], array $delTagData = []): array
+ {
+ // serialize values
+ if (!$serialized = $this->marshaller->marshall($values, $failed)) {
+ return $failed;
+ }
+
+ // While pipeline isn't supported on RedisCluster, other setups will at least benefit from doing this in one op
+ $results = $this->pipeline(static function () use ($serialized, $lifetime, $addTagData, $delTagData) {
+ // Store cache items, force a ttl if none is set, as there is no MSETEX we need to set each one
+ foreach ($serialized as $id => $value) {
+ yield 'setEx' => [
+ $id,
+ 0 >= $lifetime ? self::DEFAULT_CACHE_TTL : $lifetime,
+ $value,
+ ];
+ }
+
+ // Add and Remove Tags
+ foreach ($addTagData as $tagId => $ids) {
+ yield 'sAdd' => array_merge([$tagId], $ids);
+ }
+
+ foreach ($delTagData as $tagId => $ids) {
+ yield 'sRem' => array_merge([$tagId], $ids);
+ }
+ });
+
+ foreach ($results as $id => $result) {
+ // Skip results of SADD/SREM operations, they'll be 1 or 0 depending on if set value already existed or not
+ if (\is_numeric($result)) {
+ continue;
+ }
+ // setEx results
+ if (true !== $result && (!$result instanceof Status || $result !== Status::get('OK'))) {
+ $failed[] = $id;
+ }
+ }
+
+ return $failed;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDelete(array $ids, array $tagData = []): bool
+ {
+ if (!$ids) {
+ return true;
+ }
+
+ $predisCluster = $this->redis instanceof \Predis\Client && $this->redis->getConnection() instanceof ClusterInterface;
+ $this->pipeline(static function () use ($ids, $tagData, $predisCluster) {
+ if ($predisCluster) {
+ foreach ($ids as $id) {
+ yield 'del' => [$id];
+ }
+ } else {
+ yield 'del' => $ids;
+ }
+
+ foreach ($tagData as $tagId => $idList) {
+ yield 'sRem' => \array_merge([$tagId], $idList);
+ }
+ })->rewind();
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doInvalidate(array $tagIds): bool
+ {
+ if (!$this->redisServerSupportSPOP()) {
+ return false;
+ }
+
+ // Pop all tag info at once to avoid race conditions
+ $tagIdSets = $this->pipeline(static function () use ($tagIds) {
+ foreach ($tagIds as $tagId) {
+ // Client: Predis or PHP Redis 3.1.3+ (https://github.com/phpredis/phpredis/commit/d2e203a6)
+ // Server: Redis 3.2 or higher (https://redis.io/commands/spop)
+ yield 'sPop' => [$tagId, self::POP_MAX_LIMIT];
+ }
+ });
+
+ // Flatten generator result from pipeline, ignore keys (tag ids)
+ $ids = \array_unique(\array_merge(...\iterator_to_array($tagIdSets, false)));
+
+ // Delete cache in chunks to avoid overloading the connection
+ foreach (\array_chunk($ids, self::BULK_DELETE_LIMIT) as $chunkIds) {
+ $this->doDelete($chunkIds);
+ }
+
+ return true;
+ }
+
+ private function redisServerSupportSPOP(): bool
+ {
+ if (null !== $this->redisServerSupportSPOP) {
+ return $this->redisServerSupportSPOP;
+ }
+
+ foreach ($this->getHosts() as $host) {
+ $info = $host->info('Server');
+ $info = isset($info['Server']) ? $info['Server'] : $info;
+ if (version_compare($info['redis_version'], '3.2', '<')) {
+ CacheItem::log($this->logger, 'Redis server needs to be version 3.2 or higher, your Redis server was detected as {version}', ['version' => $info['redis_version']]);
+
+ return $this->redisServerSupportSPOP = false;
+ }
+ }
+
+ return $this->redisServerSupportSPOP = true;
+ }
+}
diff --git a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php
index 1c69e10c942a2..5d7a2369c22e6 100644
--- a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php
+++ b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php
@@ -68,14 +68,20 @@ public function process(ContainerBuilder $container)
if ($pool->isAbstract()) {
continue;
}
+ $class = $adapter->getClass();
while ($adapter instanceof ChildDefinition) {
$adapter = $container->findDefinition($adapter->getParent());
+ $class = $class ?: $adapter->getClass();
if ($t = $adapter->getTag($this->cachePoolTag)) {
$tags[0] += $t[0];
}
}
$name = $tags[0]['name'] ?? $id;
if (!isset($tags[0]['namespace'])) {
+ if (null !== $class) {
+ $seed .= '.'.$class;
+ }
+
$tags[0]['namespace'] = $this->getNamespace($seed, $name);
}
if (isset($tags[0]['clearer'])) {
diff --git a/src/Symfony/Component/Cache/LockRegistry.php b/src/Symfony/Component/Cache/LockRegistry.php
index eee7948aab0b0..676fba5dca3f7 100644
--- a/src/Symfony/Component/Cache/LockRegistry.php
+++ b/src/Symfony/Component/Cache/LockRegistry.php
@@ -34,12 +34,14 @@ final class LockRegistry
*/
private static $files = [
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractAdapter.php',
+ __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractTagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AdapterInterface.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ApcuAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php',
+ __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'MemcachedAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'NullAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PdoAdapter.php',
@@ -48,6 +50,7 @@ final class LockRegistry
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ProxyAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'Psr16Adapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisAdapter.php',
+ __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisTagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'SimpleCacheAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapterInterface.php',
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/FilesystemTagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/FilesystemTagAwareAdapterTest.php
new file mode 100644
index 0000000000000..83a7ea52ddad4
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Adapter/FilesystemTagAwareAdapterTest.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Adapter;
+
+use Symfony\Component\Cache\Adapter\FilesystemTagAwareAdapter;
+use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
+
+/**
+ * @group time-sensitive
+ */
+class FilesystemTagAwareAdapterTest extends FilesystemAdapterTest
+{
+ use TagAwareTestTrait;
+
+ public function createCachePool($defaultLifetime = 0)
+ {
+ return new FilesystemTagAwareAdapter('', $defaultLifetime);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareAdapterTest.php
new file mode 100644
index 0000000000000..e321a1c9b8c22
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareAdapterTest.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\Adapter;
+
+use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
+use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
+
+class PredisTagAwareAdapterTest extends PredisAdapterTest
+{
+ use TagAwareTestTrait;
+
+ protected function setUp()
+ {
+ parent::setUp();
+ $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite';
+ }
+
+ public function createCachePool($defaultLifetime = 0)
+ {
+ $this->assertInstanceOf(\Predis\Client::class, self::$redis);
+ $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+
+ return $adapter;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareClusterAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareClusterAdapterTest.php
new file mode 100644
index 0000000000000..a8a72e1de4ea2
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareClusterAdapterTest.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\Adapter;
+
+use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
+use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
+
+class PredisTagAwareClusterAdapterTest extends PredisClusterAdapterTest
+{
+ use TagAwareTestTrait;
+
+ protected function setUp()
+ {
+ parent::setUp();
+ $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite';
+ }
+
+ public function createCachePool($defaultLifetime = 0)
+ {
+ $this->assertInstanceOf(\Predis\Client::class, self::$redis);
+ $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+
+ return $adapter;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareRedisClusterAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareRedisClusterAdapterTest.php
new file mode 100644
index 0000000000000..5b82a80ecb324
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareRedisClusterAdapterTest.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\Adapter;
+
+use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
+use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
+
+class PredisTagAwareRedisClusterAdapterTest extends PredisRedisClusterAdapterTest
+{
+ use TagAwareTestTrait;
+
+ protected function setUp()
+ {
+ parent::setUp();
+ $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite';
+ }
+
+ public function createCachePool($defaultLifetime = 0)
+ {
+ $this->assertInstanceOf(\Predis\Client::class, self::$redis);
+ $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+
+ return $adapter;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareAdapterTest.php
new file mode 100644
index 0000000000000..95e5fe7e3a9ed
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareAdapterTest.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\Tests\Adapter;
+
+use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
+use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
+use Symfony\Component\Cache\Traits\RedisProxy;
+
+class RedisTagAwareAdapterTest extends RedisAdapterTest
+{
+ use TagAwareTestTrait;
+
+ protected function setUp()
+ {
+ parent::setUp();
+ $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite';
+ }
+
+ public function createCachePool($defaultLifetime = 0)
+ {
+ $this->assertInstanceOf(RedisProxy::class, self::$redis);
+ $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+
+ return $adapter;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareArrayAdapterTest.php
new file mode 100644
index 0000000000000..5855cc3adfc6c
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareArrayAdapterTest.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\Adapter;
+
+use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
+use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
+
+class RedisTagAwareArrayAdapterTest extends RedisArrayAdapterTest
+{
+ use TagAwareTestTrait;
+
+ protected function setUp()
+ {
+ parent::setUp();
+ $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite';
+ }
+
+ public function createCachePool($defaultLifetime = 0)
+ {
+ $this->assertInstanceOf(\RedisArray::class, self::$redis);
+ $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+
+ return $adapter;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareClusterAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareClusterAdapterTest.php
new file mode 100644
index 0000000000000..ef17c1d69e814
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareClusterAdapterTest.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\Tests\Adapter;
+
+use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
+use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
+use Symfony\Component\Cache\Traits\RedisClusterProxy;
+
+class RedisTagAwareClusterAdapterTest extends RedisClusterAdapterTest
+{
+ use TagAwareTestTrait;
+
+ protected function setUp()
+ {
+ parent::setUp();
+ $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite';
+ }
+
+ public function createCachePool($defaultLifetime = 0)
+ {
+ $this->assertInstanceOf(RedisClusterProxy::class, self::$redis);
+ $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime);
+
+ return $adapter;
+ }
+}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php
index 7b8895b70019c..a339790862432 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php
@@ -14,13 +14,15 @@
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
-use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
/**
* @group time-sensitive
*/
class TagAwareAdapterTest extends AdapterTestCase
{
+ use TagAwareTestTrait;
+
public function createCachePool($defaultLifetime = 0)
{
return new TagAwareAdapter(new FilesystemAdapter('', $defaultLifetime));
@@ -32,53 +34,9 @@ public static function tearDownAfterClass()
}
/**
- * @expectedException \Psr\Cache\InvalidArgumentException
+ * Test feature specific to TagAwareAdapter as it implicit needs to save deferred when also saving expiry info.
*/
- public function testInvalidTag()
- {
- $pool = $this->createCachePool();
- $item = $pool->getItem('foo');
- $item->tag(':');
- }
-
- public function testInvalidateTags()
- {
- $pool = $this->createCachePool();
-
- $i0 = $pool->getItem('i0');
- $i1 = $pool->getItem('i1');
- $i2 = $pool->getItem('i2');
- $i3 = $pool->getItem('i3');
- $foo = $pool->getItem('foo');
-
- $pool->save($i0->tag('bar'));
- $pool->save($i1->tag('foo'));
- $pool->save($i2->tag('foo')->tag('bar'));
- $pool->save($i3->tag('foo')->tag('baz'));
- $pool->save($foo);
-
- $pool->invalidateTags(['bar']);
-
- $this->assertFalse($pool->getItem('i0')->isHit());
- $this->assertTrue($pool->getItem('i1')->isHit());
- $this->assertFalse($pool->getItem('i2')->isHit());
- $this->assertTrue($pool->getItem('i3')->isHit());
- $this->assertTrue($pool->getItem('foo')->isHit());
-
- $pool->invalidateTags(['foo']);
-
- $this->assertFalse($pool->getItem('i1')->isHit());
- $this->assertFalse($pool->getItem('i3')->isHit());
- $this->assertTrue($pool->getItem('foo')->isHit());
-
- $anotherPoolInstance = $this->createCachePool();
-
- $this->assertFalse($anotherPoolInstance->getItem('i1')->isHit());
- $this->assertFalse($anotherPoolInstance->getItem('i3')->isHit());
- $this->assertTrue($anotherPoolInstance->getItem('foo')->isHit());
- }
-
- public function testInvalidateCommits()
+ public function testInvalidateCommitsSeperatePools()
{
$pool1 = $this->createCachePool();
@@ -94,76 +52,6 @@ public function testInvalidateCommits()
$this->assertTrue($foo->isHit());
}
- public function testTagsAreCleanedOnSave()
- {
- $pool = $this->createCachePool();
-
- $i = $pool->getItem('k');
- $pool->save($i->tag('foo'));
-
- $i = $pool->getItem('k');
- $pool->save($i->tag('bar'));
-
- $pool->invalidateTags(['foo']);
- $this->assertTrue($pool->getItem('k')->isHit());
- }
-
- public function testTagsAreCleanedOnDelete()
- {
- $pool = $this->createCachePool();
-
- $i = $pool->getItem('k');
- $pool->save($i->tag('foo'));
- $pool->deleteItem('k');
-
- $pool->save($pool->getItem('k'));
- $pool->invalidateTags(['foo']);
-
- $this->assertTrue($pool->getItem('k')->isHit());
- }
-
- public function testTagItemExpiry()
- {
- $pool = $this->createCachePool(10);
-
- $item = $pool->getItem('foo');
- $item->tag(['baz']);
- $item->expiresAfter(100);
-
- $pool->save($item);
- $pool->invalidateTags(['baz']);
- $this->assertFalse($pool->getItem('foo')->isHit());
-
- sleep(20);
-
- $this->assertFalse($pool->getItem('foo')->isHit());
- }
-
- /**
- * @group legacy
- */
- public function testGetPreviousTags()
- {
- $pool = $this->createCachePool();
-
- $i = $pool->getItem('k');
- $pool->save($i->tag('foo'));
-
- $i = $pool->getItem('k');
- $this->assertSame(['foo' => 'foo'], $i->getPreviousTags());
- }
-
- public function testGetMetadata()
- {
- $pool = $this->createCachePool();
-
- $i = $pool->getItem('k');
- $pool->save($i->tag('foo'));
-
- $i = $pool->getItem('k');
- $this->assertSame(['foo' => 'foo'], $i->getMetadata()[CacheItem::METADATA_TAGS]);
- }
-
public function testPrune()
{
$cache = new TagAwareAdapter($this->getPruneableMock());
diff --git a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php
index f307aa5386b0c..4681b3dc8197a 100644
--- a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php
+++ b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php
@@ -13,6 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
+use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Cache\DependencyInjection\CachePoolPass;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -48,6 +49,27 @@ public function testNamespaceArgumentIsReplaced()
$this->assertSame('z3X945Jbf5', $cachePool->getArgument(0));
}
+ public function testNamespaceArgumentIsSeededWithAdapterClassName()
+ {
+ $container = new ContainerBuilder();
+ $container->setParameter('kernel.container_class', 'app');
+ $container->setParameter('kernel.project_dir', 'foo');
+ $adapter = new Definition();
+ $adapter->setAbstract(true);
+ $adapter->addTag('cache.pool');
+ $adapter->setClass(RedisAdapter::class);
+ $container->setDefinition('app.cache_adapter', $adapter);
+ $container->setAlias('app.cache_adapter_alias', 'app.cache_adapter');
+ $cachePool = new ChildDefinition('app.cache_adapter_alias');
+ $cachePool->addArgument(null);
+ $cachePool->addTag('cache.pool');
+ $container->setDefinition('app.cache_pool', $cachePool);
+
+ $this->cachePoolPass->process($container);
+
+ $this->assertSame('xmOJ8gqF-Y', $cachePool->getArgument(0));
+ }
+
public function testNamespaceArgumentIsNotReplacedIfArrayAdapterIsUsed()
{
$container = new ContainerBuilder();
diff --git a/src/Symfony/Component/Cache/Tests/Traits/TagAwareTestTrait.php b/src/Symfony/Component/Cache/Tests/Traits/TagAwareTestTrait.php
new file mode 100644
index 0000000000000..38cc4dc9cc990
--- /dev/null
+++ b/src/Symfony/Component/Cache/Tests/Traits/TagAwareTestTrait.php
@@ -0,0 +1,160 @@
+
+ *
+ * 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;
+
+use Symfony\Component\Cache\CacheItem;
+
+/**
+ * Common assertions for TagAware adapters.
+ *
+ * @method \Symfony\Component\Cache\Adapter\TagAwareAdapterInterface createCachePool() Must be implemented by TestCase
+ */
+trait TagAwareTestTrait
+{
+ /**
+ * @expectedException \Psr\Cache\InvalidArgumentException
+ */
+ public function testInvalidTag()
+ {
+ $pool = $this->createCachePool();
+ $item = $pool->getItem('foo');
+ $item->tag(':');
+ }
+
+ public function testInvalidateTags()
+ {
+ $pool = $this->createCachePool();
+
+ $i0 = $pool->getItem('i0');
+ $i1 = $pool->getItem('i1');
+ $i2 = $pool->getItem('i2');
+ $i3 = $pool->getItem('i3');
+ $foo = $pool->getItem('foo');
+
+ $pool->save($i0->tag('bar'));
+ $pool->save($i1->tag('foo'));
+ $pool->save($i2->tag('foo')->tag('bar'));
+ $pool->save($i3->tag('foo')->tag('baz'));
+ $pool->save($foo);
+
+ $pool->invalidateTags(['bar']);
+
+ $this->assertFalse($pool->getItem('i0')->isHit());
+ $this->assertTrue($pool->getItem('i1')->isHit());
+ $this->assertFalse($pool->getItem('i2')->isHit());
+ $this->assertTrue($pool->getItem('i3')->isHit());
+ $this->assertTrue($pool->getItem('foo')->isHit());
+
+ $pool->invalidateTags(['foo']);
+
+ $this->assertFalse($pool->getItem('i1')->isHit());
+ $this->assertFalse($pool->getItem('i3')->isHit());
+ $this->assertTrue($pool->getItem('foo')->isHit());
+
+ $anotherPoolInstance = $this->createCachePool();
+
+ $this->assertFalse($anotherPoolInstance->getItem('i1')->isHit());
+ $this->assertFalse($anotherPoolInstance->getItem('i3')->isHit());
+ $this->assertTrue($anotherPoolInstance->getItem('foo')->isHit());
+ }
+
+ public function testInvalidateCommits()
+ {
+ $pool = $this->createCachePool();
+
+ $foo = $pool->getItem('foo');
+ $foo->tag('tag');
+
+ $pool->saveDeferred($foo->set('foo'));
+ $pool->invalidateTags(['tag']);
+
+ // ??: This seems to contradict a bit logic in deleteItems, where it does unset($this->deferred[$key]); on key matches
+
+ $foo = $pool->getItem('foo');
+
+ $this->assertTrue($foo->isHit());
+ }
+
+ public function testTagsAreCleanedOnSave()
+ {
+ $pool = $this->createCachePool();
+
+ $i = $pool->getItem('k');
+ $pool->save($i->tag('foo'));
+
+ $i = $pool->getItem('k');
+ $pool->save($i->tag('bar'));
+
+ $pool->invalidateTags(['foo']);
+ $this->assertTrue($pool->getItem('k')->isHit());
+ }
+
+ public function testTagsAreCleanedOnDelete()
+ {
+ $pool = $this->createCachePool();
+
+ $i = $pool->getItem('k');
+ $pool->save($i->tag('foo'));
+ $pool->deleteItem('k');
+
+ $pool->save($pool->getItem('k'));
+ $pool->invalidateTags(['foo']);
+
+ $this->assertTrue($pool->getItem('k')->isHit());
+ }
+
+ public function testTagItemExpiry()
+ {
+ if (isset($this->skippedTests[__FUNCTION__])) {
+ $this->markTestSkipped($this->skippedTests[__FUNCTION__]);
+ }
+
+ $pool = $this->createCachePool(10);
+
+ $item = $pool->getItem('foo');
+ $item->tag(['baz']);
+ $item->expiresAfter(100);
+
+ $pool->save($item);
+ $pool->invalidateTags(['baz']);
+ $this->assertFalse($pool->getItem('foo')->isHit());
+
+ sleep(20);
+
+ $this->assertFalse($pool->getItem('foo')->isHit());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testGetPreviousTags()
+ {
+ $pool = $this->createCachePool();
+
+ $i = $pool->getItem('k');
+ $pool->save($i->tag('foo'));
+
+ $i = $pool->getItem('k');
+ $this->assertSame(['foo' => 'foo'], $i->getPreviousTags());
+ }
+
+ public function testGetMetadata()
+ {
+ $pool = $this->createCachePool();
+
+ $i = $pool->getItem('k');
+ $pool->save($i->tag('foo'));
+
+ $i = $pool->getItem('k');
+ $this->assertSame(['foo' => 'foo'], $i->getMetadata()[CacheItem::METADATA_TAGS]);
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php b/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php
new file mode 100644
index 0000000000000..f1d97abf2d2f8
--- /dev/null
+++ b/src/Symfony/Component/Cache/Traits/AbstractAdapterTrait.php
@@ -0,0 +1,139 @@
+
+ *
+ * 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\CacheItemInterface;
+use Symfony\Component\Cache\CacheItem;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+trait AbstractAdapterTrait
+{
+ use AbstractTrait;
+
+ /**
+ * @var \Closure needs to be set by class, signature is function(string , mixed , bool )
+ */
+ private $createCacheItem;
+
+ /**
+ * @var \Closure needs to be set by class, signature is function(array , string , array <&expiredIds>)
+ */
+ private $mergeByLifetime;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItem($key)
+ {
+ if ($this->deferred) {
+ $this->commit();
+ }
+ $id = $this->getId($key);
+
+ $f = $this->createCacheItem;
+ $isHit = false;
+ $value = null;
+
+ try {
+ foreach ($this->doFetch([$id]) as $value) {
+ $isHit = true;
+ }
+ } catch (\Exception $e) {
+ CacheItem::log($this->logger, 'Failed to fetch key "{key}"', ['key' => $key, 'exception' => $e]);
+ }
+
+ return $f($key, $value, $isHit);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItems(array $keys = [])
+ {
+ if ($this->deferred) {
+ $this->commit();
+ }
+ $ids = [];
+
+ foreach ($keys as $key) {
+ $ids[] = $this->getId($key);
+ }
+ try {
+ $items = $this->doFetch($ids);
+ } catch (\Exception $e) {
+ CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => $keys, 'exception' => $e]);
+ $items = [];
+ }
+ $ids = array_combine($ids, $keys);
+
+ return $this->generateItems($items, $ids);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(CacheItemInterface $item)
+ {
+ if (!$item instanceof CacheItem) {
+ return false;
+ }
+ $this->deferred[$item->getKey()] = $item;
+
+ return $this->commit();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function saveDeferred(CacheItemInterface $item)
+ {
+ if (!$item instanceof CacheItem) {
+ return false;
+ }
+ $this->deferred[$item->getKey()] = $item;
+
+ return true;
+ }
+
+ public function __destruct()
+ {
+ if ($this->deferred) {
+ $this->commit();
+ }
+ }
+
+ private function generateItems($items, &$keys)
+ {
+ $f = $this->createCacheItem;
+
+ try {
+ foreach ($items as $id => $value) {
+ if (!isset($keys[$id])) {
+ $id = key($keys);
+ }
+ $key = $keys[$id];
+ unset($keys[$id]);
+ yield $key => $f($key, $value, true);
+ }
+ } catch (\Exception $e) {
+ CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => array_values($keys), 'exception' => $e]);
+ }
+
+ foreach ($keys as $key) {
+ yield $key => $f($key, null, false);
+ }
+ }
+}
diff --git a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php
index 3f684acd685d3..37e1fd1f06b3c 100644
--- a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php
+++ b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php
@@ -101,11 +101,11 @@ private function write($file, $data, $expiresAt = null)
}
}
- private function getFile($id, $mkdir = false)
+ private function getFile($id, $mkdir = false, string $directory = null)
{
// Use MD5 to favor speed over security, which is not an issue here
$hash = str_replace('/', '-', base64_encode(hash('md5', static::class.$id, true)));
- $dir = $this->directory.strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR);
+ $dir = ($directory ?? $this->directory).strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR);
if ($mkdir && !file_exists($dir)) {
@mkdir($dir, 0777, true);
diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php
index 0b79a7d1adb30..b2faca651d0d6 100644
--- a/src/Symfony/Component/Cache/Traits/RedisTrait.php
+++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php
@@ -321,33 +321,13 @@ protected function doHave($id)
protected function doClear($namespace)
{
$cleared = true;
- $hosts = [$this->redis];
- $evalArgs = [[$namespace], 0];
-
if ($this->redis instanceof \Predis\Client) {
$evalArgs = [0, $namespace];
-
- $connection = $this->redis->getConnection();
- if ($connection instanceof ClusterInterface && $connection instanceof \Traversable) {
- $hosts = [];
- foreach ($connection as $c) {
- $hosts[] = new \Predis\Client($c);
- }
- }
- } elseif ($this->redis instanceof \RedisArray) {
- $hosts = [];
- foreach ($this->redis->_hosts() as $host) {
- $hosts[] = $this->redis->_instance($host);
- }
- } elseif ($this->redis instanceof RedisClusterProxy || $this->redis instanceof \RedisCluster) {
- $hosts = [];
- foreach ($this->redis->_masters() as $host) {
- $hosts[] = $h = new \Redis();
- $h->connect($host[0], $host[1]);
- }
+ } else {
+ $evalArgs = [[$namespace], 0];
}
- foreach ($hosts as $host) {
+ foreach ($this->getHosts() as $host) {
if (!isset($namespace[0])) {
$cleared = $host->flushDb() && $cleared;
continue;
@@ -479,4 +459,31 @@ private function pipeline(\Closure $generator)
yield $id => $results[$k];
}
}
+
+ private function getHosts(): array
+ {
+ $hosts = [$this->redis];
+ if ($this->redis instanceof \Predis\Client) {
+ $connection = $this->redis->getConnection();
+ if ($connection instanceof ClusterInterface && $connection instanceof \Traversable) {
+ $hosts = [];
+ foreach ($connection as $c) {
+ $hosts[] = new \Predis\Client($c);
+ }
+ }
+ } elseif ($this->redis instanceof \RedisArray) {
+ $hosts = [];
+ foreach ($this->redis->_hosts() as $host) {
+ $hosts[] = $this->redis->_instance($host);
+ }
+ } elseif ($this->redis instanceof RedisClusterProxy || $this->redis instanceof \RedisCluster) {
+ $hosts = [];
+ foreach ($this->redis->_masters() as $host) {
+ $hosts[] = $h = new \Redis();
+ $h->connect($host[0], $host[1]);
+ }
+ }
+
+ return $hosts;
+ }
}
diff --git a/src/Symfony/Component/Cache/phpunit.xml.dist b/src/Symfony/Component/Cache/phpunit.xml.dist
index c35458ca44716..591046cf1c41c 100644
--- a/src/Symfony/Component/Cache/phpunit.xml.dist
+++ b/src/Symfony/Component/Cache/phpunit.xml.dist
@@ -40,7 +40,8 @@
Doctrine\Common\Cache
Symfony\Component\Cache
Symfony\Component\Cache\Tests\Fixtures
- Symfony\Component\Cache\Traits
+ Symfony\Component\Cache\Tests\Traits
+ Symfony\Component\Cache\Traits
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy