Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Namespaced Cache Laravel Package

cache/namespaced-cache

A PSR-6 decorator that scopes a shared cache pool to a single namespace, so clear() invalidates only that namespace. Includes tag support, generation-key safety, and | hierarchical key deletion.

View on GitHub
Deep Wiki
Context7

Getting Started

cache/namespaced-cache is not a Laravel package. It has no service provider, no facade, no config/cache.php driver, and no Cache::namespace() method. It is a ~100-line PSR-6 decorator from the php-cache organisation. Anything you read claiming it drops into Cache::store('namespaced_redis') is wrong — plan your integration around PSR-6 or you will burn a day on it.

What it actually gives you:

$inner      = new \Cache\Adapter\Redis\RedisCachePool($redisClient); // implements HierarchicalPoolInterface
$tenantPool = new \Cache\Adapter\Namespaced\NamespacedCachePool($inner, 'tenant:42');

$item = $tenantPool->getItem('profile.1');   // stored as tenant:42/profile.1

First steps:

  1. composer require cache/namespaced-cache plus a concrete adapter (cache/redis-adapter, cache/array-adapter for tests).
  2. Read NamespacedCachePool itself — it is short and is the whole documentation. Then read PhpCache\Cache\HierarchicalPoolInterface and HierarchicalCachePoolTrait.
  3. Bind a factory (not a pool) in a service provider, because the namespace is per-request/per-tenant:
$this->app->singleton(TenantCache::class, fn ($app) => new TenantCache($app->make('cache.psr6.inner')));

class TenantCache
{
    public function __construct(private HierarchicalPoolInterface $inner) {}

    public function for(string $namespace): NamespacedCachePool
    {
        return new NamespacedCachePool($this->inner, $namespace);
    }
}

Good first use case: a secondary cache pool (separate Redis DB index or connection) used only for tenant-scoped data, so Laravel's sessions, queues, rate limiters and lock store keep using the native driver untouched.

Implementation Patterns

Namespace as a factory, not a store. Resolve ->for($tenantId) at the edge (middleware, job handle(), controller), pass the pool down. Avoid injecting NamespacedCachePool as a singleton — the namespace is immutable once constructed.

You must write remember() yourself. PSR-6 has no such helper and Laravel's Cache::remember() won't touch your pool:

public function remember(NamespacedCachePool $pool, string $key, \DateInterval|int $ttl, callable $cb)
{
    $item = $pool->getItem($key);
    if ($item->isHit()) {
        return $item->get();
    }
    $value = $cb();
    $pool->save($item->set($value)->expiresAfter($ttl));
    return $value;
}

Keep stampede protection in Laravel. PSR-6 has no locks. Wrap the closure with Cache::lock("ns:{$ns}:{$key}") (Laravel's store) so concurrency control stays on a primitive that supports it.

Invalidate by versioning the namespace, not by flushing. Since clear() on the namespaced pool delegates to the underlying pool's clear() (i.e. it nukes every namespace), the multi-tenant pattern is tenant:42:v7. Bump the version number to invalidate and let TTLs evict the orphans. Store the current version in the database or a tiny Laravel cache key.

Coexistence layout. Typical production arrangement: Laravel redis store for sessions/queues/locks/rate limits, php-cache Redis adapter on a second connection (different DB index) for namespaced domain data. Same Redis, zero key collisions, no shared flush() risk.

Migration path. Read-through fallback while cutting over: try the namespaced pool, on miss check the legacy Laravel key, then backfill the namespaced entry. Delete the legacy key only once you're confident.

Testing. cache/array-adapter's ArrayCachePool implements the hierarchical interface — new pool per test, assert tenant:1 and tenant:2 don't see each other. This is the one place the package is genuinely pleasant.

Integration point if you must reuse Laravel's store. Write a small class implementing HierarchicalPoolInterface via HierarchicalCachePoolTrait that delegates getItem/save/deleteItem to an injected Illuminate\Contracts\Cache\Repository. It's ~40 lines and avoids running a second Redis client.

Gotchas and Tips

  • clear() is global. Never expose a "clear my tenant's cache" button mapped to NamespacedCachePool::clear(). Namespace versioning is the only safe invalidation.
  • The constructor typehint is HierarchicalPoolInterface, not CacheItemPoolInterface. A Symfony Psr6Cache bridge or a generic PSR-6 pool will throw a TypeError. You cannot just wrap an arbitrary pool, and there's no generic "Laravel → PSR-6" shim that also satisfies the hierarchy interface.
  • Always isHit(). PSR-6 returns a miss item with null, not false. $item->get() on a miss is silent null — a classic source of "cache is broken" bug reports.
  • Reserved key characters. PSR-6 forbids {}()/\@: in user keys, and the package joins namespace and key with a separator internally. Use dots or dashes in your own keys; passing user:1:profile risks an InvalidArgumentException.
  • No increment/decrement, no getMany guarantees, no tags. Counters, leaderboards, dedupe windows — keep those on the native Laravel store.
  • Serialization. The adapter serializes values. Eloquent models will round-trip but you get detached instances; prefer arrays/DTOs. Closures and resources fail.
  • TTLs are DateTimeInterface or seconds/DateInterval. expiresAfter(3600) works, but expiresAt(new DateTimeImmutable('+1 hour')) is clearer and avoids "is this seconds or minutes" ambiguity in code review.
  • saveDeferred()/commit() exist. If a teammate uses them, items don't persist until commit(). Rarely worth it outside bulk warmup.
  • Debugging. Log $item->getKey() (it includes the namespace) and verify in Redis with SCAN 0 MATCH "tenant:42/*" COUNT 1000 — never KEYS * on a shared instance.
  • Octane. Pool objects are cheap but hold a client; register them as singleton and add them to Octane's flush list, or rebuild per request.
  • Abandonment risk is low, blast radius is smaller. The repo is [READ-ONLY] (split from the cache/cache monorepo), 14 stars, last tagged 1.x. But the decorator is ~100 lines — vendoring it or forking for a fix is a half-hour job, not a migration. Pin the minor version and don't build architecture that assumes upstream maintenance.
  • Use it where it's actually strong: tenant/feature/module isolation over a pool you control. Don't try to retrofit it onto sessions, queues, or anything that needs tags, atomicity, or bulk scoped deletion — Laravel's own Cache::tags() or plain key prefixes will serve you better there.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel