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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 15 additions & 4 deletions
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

Lines changed: 26 additions & 10 deletions
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

Lines changed: 47 additions & 1 deletion
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

Lines changed: 41 additions & 1 deletion
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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 14 additions & 1 deletion
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)
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