Namespace-based cache invalidation is a technique where cache keys are grouped under a logical namespace (e.g. per user, locale, or entity). Instead of deleting individual keys, you invalidate the entire namespace, effectively removing all related entries at once.
In Symfony 7.3, we're adding namespace-based cache invalidation to all cache
adapters in the Cache component. This is implemented via the new
Symfony\Contracts\Cache\NamespacedPoolInterface
, which defines the
withSubNamespace()
method:
1 2 3 4 5 6 7
$subCache = $cache->withSubNamespace('foo');
$subCache->get('my_cache_key', function (ItemInterface $item): string {
$item->expiresAfter(3600);
return '...';
});
In this example, the cache item uses the my_cache_key
key, but it's stored
internally under the foo
namespace. This is handled transparently, so you
don't need to manually prefix keys like foo.my_cache_key
. In practice,
this groups cache items logically into different sets:
1 2 3 4 5 6 7 8 9 10 11 12 13
$subCache = $cache->withSubNamespace('foo');
$bar1 = $cache->getItem('bar1');
$bar1->set(...); $cache->save();
$bar2 = $subCache->getItem('bar2');
$bar2->set(...); $subCache->save();
$cache->getItem('bar1')->isHit(); // true
$cache->getItem('bar2')->isHit(); // false
$subCache->getItem('bar1')->isHit(); // false
$subCache->getItem('bar2')->isHit(); // true
$subCache->getItem('foo.bar2')->isHit(); // false
Namespaces can be anything that makes sense in your application. For example, you can cache data per user, per locale, or per entity:
1 2 3
$userCache = $cache->withSubNamespace((string) $userId);
$localeCache = $cache->withSubNamespace($request->getLocale());
$productCache = $cache->withSubNamespace($productId);
This looks awesome! In this blog post and the documentation you write "This is handled transparently, so you don't need to manually prefix keys like foo.my_cache_key".
But there is no example what happens if you try to do it. Would it find an item in the subCache if you call
$cache->getItem('foo.bar2)->isHit()
on the main cache? Maybe you want to add this example to the docs and the blog post, thanks!@Rafael namespaced keys cannot be accessed from the main cache, because the storage is not done by prefixing the PSR-16 key with the namespace followed by a dot. It requires getting a subcache instance for the "foo" namespace. The cache key in the underlying storage uses a delimiter that is not allowed in valid PSR-16 cache keys (most adapters use the colon for that, but they are free to use the delimiter they want in case there is a common convention for that storage). The only exception right now is the case of the Psr16Adapter wrapping an arbitrary PSR-16 implementation (to adapt one that is not from symfony/cache), as it has to keep generating valid PSR-16 keys for the underlying PSR-16 storage.
This means that you cannot have key conflicts between a namespaced cache and its parent cache.
Does it effectively create a new cache pool? Is it possible to use the cache:pool:... cli commands to invalidate or delete items or the whole sub-namespace?
I’m sorry to say I’ve got more questions after reading the article than before. Once we have a sub-cache, how can we invalidate all the keys in it? Is there a “invalidateSubNamespace” method in NamespacedPoolInterface? Do we need to declare anywhere the sub-caches (I guess not since the whole premise is to generate dynamic pseudo-pools) or the possibility or necessity of using sub-caches for a pool? It could useful to declare that a pool must be used with sub-caches for example, to prevent confusion and improve DX. Are the sub-caches cleared when the pool is cleared?
How does this compare to cache tags? What are the benefits of using namespaced cache over that?
Folks, in this PR we've improved the docs about this feature:
https://github.com/symfony/symfony-docs/pull/20966
The docs will be online soon at:
https://symfony.com/doc/7.3/components/cache.html#creating-sub-namespaces