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

Prefixed Cache Laravel Package

cache/prefixed-cache

Decorates any PSR-6 or PSR-16 cache so every key automatically carries a fixed prefix. PrefixedCachePool::create() also supports native tags, and prefixes are safely encoded before joining to keys.

View on GitHub
Deep Wiki
Context7

Getting Started

What it is: a ~50-line decorator that implements Psr\Cache\CacheItemPoolInterface, wraps any other PSR-6 pool, and concatenates a string onto every key before delegating. That's the entire feature set. There is no Laravel service provider, no facade, no config file, and no integration with Illuminate\Contracts\Cache\Repository.

Install

composer require cache/prefixed-cache
# you also need an actual PSR-6 pool — Laravel's cache stores are NOT PSR-6
composer require cache/redis-cache          # or symfony/cache, cache/array-adapter, ...

Minimal use (verbatim from the README, note the missing separator):

use Cache\Prefixed\PrefixedCachePool;

$anyPSR6Pool  = new RedisCachePool($client);
$prefixedPool = new PrefixedCachePool($anyPSR6Pool, 'acme');   // key "user" becomes "acmeuser"

Do this first — a 60-second smoke test with the array adapter. Do not wire it into Redis until you've seen the key names and the clear() behaviour with your own eyes:

$inner    = new Cache\Adapter\PHPArray\ArrayCachePool();
$prefixed = new Cache\Prefixed\PrefixedCachePool($inner, 'shop.v1.');

$prefixed->save($prefixed->getItem('products')->set('x'));

var_dump(array_keys($inner->getValues()));   // ["shop.v1.products"] — confirms concatenation
$prefixed->clear();
var_dump(count($inner->getValues()));        // 0 — confirms clear() nukes the WHOLE inner pool

Where to look first

  1. src/PrefixedCachePool.php in the repo — read the whole file, it's shorter than this section. Everything you need to know is there.
  2. Changelog.md — the release notes on Packagist just say "See change log", so that file is the only real changelog.
  3. The shared docs at php-cache.com — the README defers all feature documentation (tagging, hierarchy) there; none of those features exist in this package, but the docs explain the ecosystem it plugs into.

Best first use case in a Laravel app: prefix-versioned cache rollover, not multi-tenancy. Include a version segment in the prefix ("acme.{$env}.v{$cacheVersion}.") so a deploy that bumps CACHE_VERSION instantly invalidates everything without ever calling clear(). That gives you a safe, zero-risk way to try the package on a real store, and it sidesteps the package's single biggest footgun (see below).


Implementation Patterns

1. Scoping a third-party library that demands PSR-6

This is the only pattern where the package is unambiguously the right tool. Some library in your dependency tree typehints Psr\Cache\CacheItemPoolInterface (HTTP cache middleware, serializer caches, schema/metadata caches). You hand it a prefixed pool and it can never collide with your application keys:

$this->app->singleton(SomeLibrary::class, function ($app) {
    $pool = new PrefixedCachePool(
        $app->make(RedisCachePool::class),
        'lib.somelibrary.'
    );

    return new SomeLibrary($pool);   // library writes to lib.somelibrary.*, nothing else
});

Keep your own reference to the base pool in the container if you ever need to bypass the prefix — there is no documented accessor to reach the inner pool from the wrapper.

2. Prefix-versioned deploys (blue/green caches)

// config/cache.php
'pool_prefix' => sprintf(
    '%s.%s.v%s.',
    Str::slug(config('app.name')),
    config('app.env'),
    config('app.cache_version', '1')      // bump on release / cache-breaking change
),
$pool = new PrefixedCachePool($redisPool, config('cache.pool_prefix'));

Old keys age out via TTL; no flush, no downtime, trivial rollback (revert the env var). Pair this with SCAN MATCH 'acme.production.v*' in your ops runbook for cleanup.

3. Per-tenant / per-request namespacing

Use a scoped binding so the prefix is resolved per request, not frozen at boot:

// AppServiceProvider::register()
$this->app->scoped(Psr\Cache\CacheItemPoolInterface::class, function ($app) {
    $tenant = tenant('id') ?? 'shared';

    return new PrefixedCachePool(
        $app->make(RedisCachePool::class),
        "tenant.{$tenant}.api."
    );
});

scoped() is important — under Octane a singleton() would capture tenant #1's prefix and serve it to every subsequent tenant in that worker. That is a data-leak-shaped bug, not a performance bug.

For a finer granularity, use contextual binding so only one consumer gets the tenant-scoped pool:

$this->app->when(ReportingService::class)
    ->needs(Psr\Cache\CacheItemPoolInterface::class)
    ->give(fn () => new PrefixedCachePool($base, 'tenant.'.tenant('id').'.reports.'));

4. Test isolation

Cheap and genuinely useful in a suite that shares one Redis instance:

protected function setUp(): void
{
    parent::setUp();

    $this->app->instance(Psr\Cache\CacheItemPoolInterface::class,
        new PrefixedCachePool(
            new ArrayCachePool(),                          // per-test, no Redis at all
            'test.'.Str::random(8).'.'
        )
    );
}

Swap in a real Redis pool with a random prefix when you deliberately want cache hits across a single test.

5. Composing decorators

PrefixedCachePool is a plain decorator, so it stacks. Order determines the final key:

$pool = new PrefixedCachePool(
    new PrefixedCachePool($redisPool, 'app.'),   // inner prefix
    'acme.'                                       // outer prefix
);
// final key: acme.app.user_123

Because save()/saveDeferred()/commit() pass straight through, always call commit() on the outermost wrapper if you defer anything.

6. Building a central key/prefix helper

The decorator does no normalisation. Put every prefix construction in one place so it is greppable and reviewable:

final class CacheNamespace
{
    public static function prefix(string $module, ?string $tenant = null): string
    {
        return Str::slug(implode('.', array_filter([
            config('app.name'), config('app.env'), $tenant, $module,
            'v'.config('app.cache_version', '1'),
        ]))).'.';
    }
}

Then new PrefixedCachePool($redisPool, CacheNamespace::prefix('billing', tenant('id'))).

7. If you specifically want it behind the Cache facade

You can't. Cache::extend() callbacks must return a Illuminate\Contracts\Cache\Repository, and Laravel's Store implementations are not PSR-6 pools — there is no getStore() you can hand to this class and have it work. Your options, in order of preference:

  1. Use Laravel's native per-store prefix option in config/cache.php (Redis/Memcached/database stores all support it). This covers almost every "shared Redis across apps" scenario with zero dependencies.
  2. Use cache tags (Cache::store('redis')->tags(['tenant:5'])->put(...)) if what you actually want is scoped invalidation — the tag namespace already isolates keys, and tags()->flush() only clears that tag, which this package cannot do.
  3. Write a ~60-line Illuminate\Contracts\Cache\Store adapter that translates get/put/forget/flush onto a PSR-6 pool. Only worth it if a PSR-6 pool is forced on you from elsewhere.

8. Migration sequencing (if you adopt)

Wrap one non-critical store first (sessions or a reporting cache), verify hit-rate and key names in Redis, then expand. Never wrap a pool in one service provider while other code writes to the same pool unprefixed — you'll end up with two key formats for the same logical data and an unflushable mess.


Gotchas and Tips

The package adds no separator. The README's own example (new PrefixedCachePool($pool, 'acme')) produces acmeuser, not acme:user. Always supply your own trailing delimiter in the prefix string. Forgetting it is the number-one way to get prefix collisions (tenant1 vs tenant12 → tenant1user vs tenant12user is fine, but acme vs acmeshop overlaps).

clear() flushes the entire underlying pool. The decorator has no way to enumerate prefixed keys through PSR-6, so it delegates straight through. On a shared Redis database this wipes every other app's cache. If you have any code path that can reach clear()/flush(), use a dedicated Redis database index per namespace group, or adopt the version-in-prefix pattern instead of flushing. This is the single most important thing to verify in your smoke test.

Delimiter choice is constrained by PSR-6. Keys must not contain the reserved characters { } ( ) / \ @ :. Symfony's cache adapters (CacheItem::validateKey) will throw InvalidArgumentException on a colon; php-cache's own adapters are more permissive, which makes this bug environment-specific and easy to miss. Use . or _ as the delimiter if there's any chance a Symfony pool ends up underneath.

Budget your key length. Prefix + key must fit the backend limit — 250 bytes for Memcached, 64 characters in several PSR-6 implementations. A UUID tenant id plus a module plus a version plus a hashed key blows past that in a hurry, and truncation means silent collisions. Hash long components (substr(sha1($tenantUuid), 0, 8)) before they enter the prefix.

getKey() returns the prefixed key. The CacheItemInterface you get back from getItem() is the inner pool's item; $item->getKey() gives you acme.user_123, not user_123. Anything that logs, tags, or branches on the item key must expect the prefix. Similarly, verify in a test whether getItems(['a','b']) is keyed by your original keys or by the prefixed ones before you index into its result by $keys.

Double-prefixing is silent. Reusing a key you got from getKey() and passing it back into getItem() produces acme.acme.user_123. No exception, just a permanent cache miss and a Redis instance full of garbage. Keep a rule: application code only ever sees unprefixed keys; the wrapper is the only place a prefix exists.

Wrong prefix = 100% miss rate, never an error. hasItem() just returns false. If you see hit rate drop to zero after a deploy, diff the prefix string before you debug anything else. Log the resolved prefix exactly once at boot (and in php artisan about-style diagnostics) so it's visible.

Check the psr/cache constraint before you commit. This package is a read-only split of the php-cache/cache monorepo and has historically targeted the psr/cache ^1.0 interface, whose methods have no return types. A pool/interface pinned to ^3.0 (Symfony 6+, many modern adapters) has typed signatures that a v1 implementor can't satisfy — you'll find out at composer update or with a fatal signature error. composer why psr/cache is the ten-second check.

No escape hatch, no config, no extension points. There's no getInnerPool(), no event, no middleware, no way to override key generation. If you need scoped clear(), key mapping back to originals, a clearPrefix() method, or an accessor to the inner pool, don't fork the package — copy its ~50 lines into your own App\Cache\PrefixedPool and own it. That is genuinely cheaper than a dependency with 2 stars, 0 dependents, and a READ-ONLY banner.

Treat it as a reference implementation, not a dependency. The decorator pattern it demonstrates is the valuable part. Laravel's own Illuminate\Cache\DecoratedCache and the native prefix config solve adjacent problems with people actively maintaining them. Adopt this only when a PSR-6 pool is non-negotiable and Laravel's native prefix/tags can't reach it.

Debugging recipe: reproduce with cache/array-adapter (its getValues() shows you the literal keys), confirm the exact concatenation, then move to Redis and inspect with SCAN 0 MATCH 'yourprefix.*' COUNT 100 — never KEYS * on a shared instance. Log $prefixed->getItem($k)->getKey() once in a test to pin down what your consumers will actually see.

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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log