Skip to content

Commit ba82917

Browse files
[Cache] Enable namespace-based invalidation by prefixing keys with backend-native namespace separators
1 parent ca6f399 commit ba82917

25 files changed

+476
-44
lines changed

composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"psr/http-message": "^1.0|^2.0",
4848
"psr/link": "^1.1|^2.0",
4949
"psr/log": "^1|^2|^3",
50-
"symfony/contracts": "^3.5",
50+
"symfony/contracts": "^3.6",
5151
"symfony/polyfill-ctype": "~1.8",
5252
"symfony/polyfill-intl-grapheme": "~1.0",
5353
"symfony/polyfill-intl-icu": "~1.0",
@@ -217,7 +217,7 @@
217217
"url": "src/Symfony/Contracts",
218218
"options": {
219219
"versions": {
220-
"symfony/contracts": "3.5.x-dev"
220+
"symfony/contracts": "3.6.x-dev"
221221
}
222222
}
223223
},

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

+5
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@
207207
use Symfony\Component\Yaml\Yaml;
208208
use Symfony\Contracts\Cache\CacheInterface;
209209
use Symfony\Contracts\Cache\CallbackInterface;
210+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
210211
use Symfony\Contracts\Cache\TagAwareCacheInterface;
211212
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
212213
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -2568,6 +2569,10 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
25682569
$container->registerAliasForArgument($tagAwareId, TagAwareCacheInterface::class, $pool['name'] ?? $name);
25692570
$container->registerAliasForArgument($name, CacheInterface::class, $pool['name'] ?? $name);
25702571
$container->registerAliasForArgument($name, CacheItemPoolInterface::class, $pool['name'] ?? $name);
2572+
2573+
if (interface_exists(NamespacedPoolInterface::class)) {
2574+
$container->registerAliasForArgument($name, NamespacedPoolInterface::class, $pool['name'] ?? $name);
2575+
}
25712576
}
25722577

25732578
$definition->setPublic($pool['public']);

src/Symfony/Component/Cache/Adapter/AbstractAdapter.php

+15-4
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
2020
use Symfony\Component\Cache\Traits\ContractsTrait;
2121
use Symfony\Contracts\Cache\CacheInterface;
22+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
2223

2324
/**
2425
* @author Nicolas Grekas <p@tchwork.com>
2526
*/
26-
abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
27+
abstract class AbstractAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
2728
{
2829
use AbstractAdapterTrait;
2930
use ContractsTrait;
@@ -37,7 +38,17 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
3738

3839
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
3940
{
40-
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
41+
if ('' !== $namespace) {
42+
if (str_contains($namespace, static::NS_SEPARATOR)) {
43+
if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) {
44+
throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace));
45+
}
46+
$namespace = str_replace(static::NS_SEPARATOR, '', $namespace);
47+
}
48+
$this->namespace = CacheItem::validateKey($namespace).static::NS_SEPARATOR;
49+
}
50+
$this->rootNamespace = $namespace;
51+
4152
$this->defaultLifetime = $defaultLifetime;
4253
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
4354
throw new InvalidArgumentException(\sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
@@ -159,7 +170,7 @@ public function commit(): bool
159170
$v = $values[$id];
160171
$type = get_debug_type($v);
161172
$message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
162-
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
173+
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
163174
}
164175
} else {
165176
foreach ($values as $id => $v) {
@@ -182,7 +193,7 @@ public function commit(): bool
182193
$ok = false;
183194
$type = get_debug_type($v);
184195
$message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
185-
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
196+
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
186197
}
187198
}
188199

src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php

+26-10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Cache\ResettableInterface;
1818
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
1919
use Symfony\Component\Cache\Traits\ContractsTrait;
20+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
2021
use Symfony\Contracts\Cache\TagAwareCacheInterface;
2122

2223
/**
@@ -30,16 +31,31 @@
3031
*
3132
* @internal
3233
*/
33-
abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface
34+
abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, AdapterInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
3435
{
3536
use AbstractAdapterTrait;
3637
use ContractsTrait;
3738

39+
/**
40+
* @internal
41+
*/
42+
protected const NS_SEPARATOR = ':';
43+
3844
private const TAGS_PREFIX = "\1tags\1";
3945

4046
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
4147
{
42-
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
48+
if ('' !== $namespace) {
49+
if (str_contains($namespace, static::NS_SEPARATOR)) {
50+
if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) {
51+
throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace));
52+
}
53+
$namespace = str_replace(static::NS_SEPARATOR, '', $namespace);
54+
}
55+
$this->namespace = CacheItem::validateKey($namespace).static::NS_SEPARATOR;
56+
}
57+
$this->rootNamespace = $this->namespace;
58+
4359
$this->defaultLifetime = $defaultLifetime;
4460
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
4561
throw new InvalidArgumentException(\sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
@@ -70,7 +86,7 @@ static function ($key, $value, $isHit) {
7086
CacheItem::class
7187
);
7288
self::$mergeByLifetime ??= \Closure::bind(
73-
static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) {
89+
static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime, $rootNamespace) {
7490
$byLifetime = [];
7591
$now = microtime(true);
7692
$expiredIds = [];
@@ -102,10 +118,10 @@ static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime)
102118
$value['tag-operations'] = ['add' => [], 'remove' => []];
103119
$oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
104120
foreach (array_diff_key($value['tags'], $oldTags) as $addedTag) {
105-
$value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
121+
$value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag, $rootNamespace);
106122
}
107123
foreach (array_diff_key($oldTags, $value['tags']) as $removedTag) {
108-
$value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
124+
$value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag, $rootNamespace);
109125
}
110126
$value['tags'] = array_keys($value['tags']);
111127

@@ -168,7 +184,7 @@ protected function doDeleteYieldTags(array $ids): iterable
168184
public function commit(): bool
169185
{
170186
$ok = true;
171-
$byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime);
187+
$byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime, $this->rootNamespace);
172188
$retry = $this->deferred = [];
173189

174190
if ($expiredIds) {
@@ -195,7 +211,7 @@ public function commit(): bool
195211
$v = $values[$id];
196212
$type = get_debug_type($v);
197213
$message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
198-
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
214+
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
199215
}
200216
} else {
201217
foreach ($values as $id => $v) {
@@ -219,7 +235,7 @@ public function commit(): bool
219235
$ok = false;
220236
$type = get_debug_type($v);
221237
$message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
222-
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
238+
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
223239
}
224240
}
225241

@@ -244,7 +260,7 @@ public function deleteItems(array $keys): bool
244260
try {
245261
foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
246262
foreach ($tags as $tag) {
247-
$tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
263+
$tagData[$this->getId(self::TAGS_PREFIX.$tag, $this->rootNamespace)][] = $id;
248264
}
249265
}
250266
} catch (\Exception) {
@@ -283,7 +299,7 @@ public function invalidateTags(array $tags): bool
283299

284300
$tagIds = [];
285301
foreach (array_unique($tags) as $tag) {
286-
$tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
302+
$tagIds[] = $this->getId(self::TAGS_PREFIX.$tag, $this->rootNamespace);
287303
}
288304

289305
try {

src/Symfony/Component/Cache/Adapter/ArrayAdapter.php

+47-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
use Symfony\Component\Cache\CacheItem;
1919
use Symfony\Component\Cache\Exception\InvalidArgumentException;
2020
use Symfony\Component\Cache\ResettableInterface;
21+
use Symfony\Component\Cache\Traits\NamespacedPoolTrait;
2122
use Symfony\Contracts\Cache\CacheInterface;
23+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
2224

2325
/**
2426
* An in-memory cache storage.
@@ -27,13 +29,23 @@
2729
*
2830
* @author Nicolas Grekas <p@tchwork.com>
2931
*/
30-
class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
32+
class ArrayAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
3133
{
3234
use LoggerAwareTrait;
35+
use NamespacedPoolTrait {
36+
pushNamespace as private doPushNamespace;
37+
popNamespace as private doPopNamespace;
38+
}
39+
40+
/**
41+
* @internal
42+
*/
43+
protected const NS_SEPARATOR = ":";
3344

3445
private array $values = [];
3546
private array $tags = [];
3647
private array $expiries = [];
48+
private array $stack = [];
3749

3850
private static \Closure $createCacheItem;
3951

@@ -231,11 +243,45 @@ public function clear(string $prefix = ''): bool
231243
}
232244
}
233245

246+
$stack = $this->stack;
247+
$this->stack = [];
248+
foreach ($stack as $ns => $pool) {
249+
if (str_starts_with($ns, $this->namespace)) {
250+
unset($stack[$ns]);
251+
$pool->clear($prefix);
252+
}
253+
}
254+
$this->stack = $stack ?: [];
255+
234256
$this->values = $this->tags = $this->expiries = [];
235257

236258
return true;
237259
}
238260

261+
public function pushNamespace(string $namespace): static
262+
{
263+
$stack = &$this->stack;
264+
$clone = $this->doPushNamespace($namespace);
265+
266+
if (isset($stack[$clone->namespace])) {
267+
return $stack[$clone->namespace];
268+
}
269+
270+
$stack[''] ??= $this;
271+
$clone->clear();
272+
273+
return $stack[$clone->namespace] = $clone;
274+
}
275+
276+
public function popNamespace(?string &$namespace = null): static
277+
{
278+
$stack = &$this->stack;
279+
$stack[''] ??= $this;
280+
$clone = $this->doPopNamespace($namespace);
281+
282+
return $stack[$clone->namespace];
283+
}
284+
239285
/**
240286
* Returns all cached values, with cache miss as null.
241287
*/

src/Symfony/Component/Cache/Adapter/ChainAdapter.php

+41-1
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
use Psr\Cache\CacheItemPoolInterface;
1616
use Symfony\Component\Cache\CacheItem;
1717
use Symfony\Component\Cache\Exception\InvalidArgumentException;
18+
use Symfony\Component\Cache\Exception\LogicException;
1819
use Symfony\Component\Cache\PruneableInterface;
1920
use Symfony\Component\Cache\ResettableInterface;
2021
use Symfony\Component\Cache\Traits\ContractsTrait;
2122
use Symfony\Contracts\Cache\CacheInterface;
23+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
2224
use Symfony\Contracts\Service\ResetInterface;
2325

2426
/**
@@ -29,7 +31,7 @@
2931
*
3032
* @author Kévin Dunglas <dunglas@gmail.com>
3133
*/
32-
class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
34+
class ChainAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface
3335
{
3436
use ContractsTrait;
3537

@@ -280,6 +282,44 @@ public function prune(): bool
280282
return $pruned;
281283
}
282284

285+
public function pushNamespace(string $namespace): static
286+
{
287+
$clone = clone $this;
288+
$adapters = [];
289+
290+
foreach ($this->adapters as $adapter) {
291+
if (!$adapter instanceof NamespacedPoolInterface) {
292+
throw new LogicException('All adapters must implement NamespacedPoolInterface to support namespaces.');
293+
}
294+
295+
$adapters[] = $adapter->pushNamespace($namespace);
296+
}
297+
$clone->adapters = $adapters;
298+
299+
return $clone;
300+
}
301+
302+
public function popNamespace(?string &$namespace = null): static
303+
{
304+
$clone = clone $this;
305+
$namespace = null;
306+
307+
foreach ($this->adapters as $adapter) {
308+
if (!$adapter instanceof NamespacedPoolInterface) {
309+
throw new LogicException('All adapters should implement NamespacedPoolInterface to support namespaces.');
310+
}
311+
312+
$adapters[] = $adapter->popNamespace($ns);
313+
314+
if ($ns !== ($namespace ??= $ns)) {
315+
throw new LogicException('All adapters should pop the same namespace.');
316+
}
317+
}
318+
$clone->adapters = $adapters;
319+
320+
return $clone;
321+
}
322+
283323
public function reset(): void
284324
{
285325
foreach ($this->adapters as $adapter) {

src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -335,17 +335,17 @@ protected function doSave(array $values, int $lifetime): array|bool
335335
/**
336336
* @internal
337337
*/
338-
protected function getId(mixed $key): string
338+
protected function getId(mixed $key, ?string $namespace = null): string
339339
{
340340
if ('pgsql' !== $this->platformName ??= $this->getPlatformName()) {
341-
return parent::getId($key);
341+
return parent::getId($key, $namespace);
342342
}
343343

344344
if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
345345
$key = rawurlencode($key);
346346
}
347347

348-
return parent::getId($key);
348+
return parent::getId($key, $namespace);
349349
}
350350

351351
private function getPlatformName(): string

src/Symfony/Component/Cache/Adapter/NullAdapter.php

+14-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
use Psr\Cache\CacheItemInterface;
1515
use Symfony\Component\Cache\CacheItem;
1616
use Symfony\Contracts\Cache\CacheInterface;
17+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
1718

1819
/**
1920
* @author Titouan Galopin <galopintitouan@gmail.com>
2021
*/
21-
class NullAdapter implements AdapterInterface, CacheInterface
22+
class NullAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface
2223
{
2324
private static \Closure $createCacheItem;
2425

@@ -94,6 +95,18 @@ public function delete(string $key): bool
9495
return $this->deleteItem($key);
9596
}
9697

98+
public function pushNamespace(string $namespace): static
99+
{
100+
return $this;
101+
}
102+
103+
public function popNamespace(?string &$namespace = null): static
104+
{
105+
$namespace = '';
106+
107+
return $this;
108+
}
109+
97110
private function generateItems(array $keys): \Generator
98111
{
99112
$f = self::$createCacheItem;

0 commit comments

Comments
 (0)