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

Simple Cache Bridge Laravel Package

cache/simple-cache-bridge

Exposes an existing PSR-6 cache pool through the PSR-16 CacheInterface, letting you reuse one pool for both standards. Install with composer require cache/simple-cache-bridge:^2.0. Supports psr/cache 3 and psr/simple-cache 2 and 3.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the version line. This is a v2 package; it targets psr/cache 3 and accepts psr/simple-cache 2 or 3. Pin it explicitly rather than floating:

composer require cache/simple-cache-bridge:^2.0
"cache/simple-cache-bridge": "2.0.*"

The repo is [READ-ONLY] and split from the php-cache monorepo — issues and PRs go upstream to php-cache/cache, not to this repository. Expect nothing to be triaged on your timeline.

Where to look first. The entire surface area is one class: Cache\Bridge\SimpleCache\SimpleCacheBridge. Open it before you write any integration code and read the method bodies — there is no config file, no service provider, no facade, and the README's seven lines are the complete documentation. What you actually need to read is the constructor signature and the set() TTL branch, because those two decisions determine whether the package behaves correctly for you.

Minimal working call. Construct it with any Psr\Cache\CacheItemPoolInterface:

use Cache\Adapter\PHPArray\ArrayCachePool;
use Cache\Bridge\SimpleCache\SimpleCacheBridge;

$pool  = new ArrayCachePool();
$cache = new SimpleCacheBridge($pool);

$cache->set('exchange_rates:usd', $rates, 300);
$rates = $cache->get('exchange_rates:usd', []);

First real use case. The bridge earns its keep in exactly one scenario: a vendor library or SDK type-hints Psr\SimpleCache\CacheInterface in its constructor, and the pool you already own (a legacy Symfony adapter, a php-cache pool, something a package handed you) is PSR-6. Start with that consumer, not with an existing Laravel cache call site — replacing Cache::get() with this bridge is a net loss, since you'd be trading a richer contract for a poorer one.

Bind it once in a service provider and inject the interface, never the vendor class:

// app/Providers/CacheBridgeServiceProvider.php
public function register(): void
{
    $this->app->singleton(SimpleCacheBridge::class, fn ($app) => new SimpleCacheBridge(
        $app->make(ExchangeRatePool::class)   // your existing PSR-6 pool
    ));

    $this->app->alias(SimpleCacheBridge::class, \Psr\SimpleCache\CacheInterface::class);

    $this->app->when(\Vendor\Sdk\RateClient::class)
        ->needs(\Psr\SimpleCache\CacheInterface::class)
        ->give(fn ($app) => $app->make(\Psr\SimpleCache\CacheInterface::class));
}

If more than one PSR-6 pool exists in the app, do not alias the interface globally — use when()->needs()->give() per consumer, or a named binding like simple-cache.rates. A single global alias is the fastest way to wire the wrong pool into the wrong client and get silent cross-contamination.

Implementation Patterns

Wrap, then delegate — never let application code name the bridge. The cheapest insurance against abandonment is one internal class that the rest of the app depends on:

namespace App\Cache;

use Psr\SimpleCache\CacheInterface;

final class SimpleCache implements CacheInterface
{
    public function __construct(private CacheInterface $inner) {}

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->inner->get($this->qualify($key), $default);
    }

    // ...set/has/delete/clear/getMultiple/setMultiple/deleteMultiple/deleteMultiple
    // delegates, plus key namespacing and any instrumentation
}

Bind SimpleCache::class to new SimpleCache(new SimpleCacheBridge($pool)). If upstream goes dormant or symfony/cache's Psr16Cache proves a better fit for the same pool, the swap is one line in one file. It also gives you the natural home for key prefixing, hit/miss counters, and exception normalisation.

Use it as a read-through layer for a vendor client. The most common day-to-day shape: the vendor asks for a CacheInterface, your data lives behind a PSR-6 pool, and the bridge is glue.

$client = new \Vendor\Sdk\Client(
    http: $http,
    cache: $app->make(\Psr\SimpleCache\CacheInterface::class),
);

The pool keeps its existing configuration, connection, serializer and eviction policy. Nothing is migrated, nothing is dual-written. That reversibility is the whole argument for adopting this: delete the binding and the consumer falls back to whatever it used before.

Prefer per-key access over the *Multiple methods. getMultiple/setMultiple/deleteMultiple are loops over single-key PSR-6 calls, so a 500-key batch becomes 500 round-trips against Redis where a native PSR-16 client would pipeline. In hot paths, batch at the application layer with Http::pool()/concurrent jobs, or bypass the bridge entirely for bulk warm-up jobs and talk to the pool directly.

Test against the contract, not the bridge. Write one conformance test that exercises your pool through the bridge and assert PSR-16 semantics explicitly — this is where you'll discover whether the bridge meets the spec for your particular pool:

it('treats a zero ttl as a delete', function () {
    $cache = new SimpleCacheBridge(new ArrayCachePool());
    $cache->set('k', 'v');
    $cache->set('k', 'v', 0);
    expect($cache->has('k'))->toBeFalse();
});

it('honours the default when ttl is null', function () {
    // assert whether null means "pool default" or "forever" for YOUR pool
});

Run that matrix across your production cache backend, not just ArrayCachePool. An array pool hides expiry and network behaviour that Redis/Memcached will not.

Keep Laravel's repository as the default. New application code should call Cache::remember(), Cache::tags() etc. The bridge is the interop seam for third-party consumers only. If you don't document that boundary in an ADR ("use this only when interfacing with a PSR-6 pool we can't replace"), you'll end up with two parallel caching idioms and no rule for choosing between them, which costs more long-term than the dependency.

Instrument it on day one. One hit/miss counter and a latency histogram tagged by operation is enough to prove the bridge isn't degrading the pool. Without it, hit-ratio drift from a TTL mismatch and latency spikes from batch amplification are both invisible.

Gotchas and Tips

Cache::extend() and CACHE_DRIVER are the wrong hooks. Laravel's cache manager expects the extension callback to return an Illuminate\Contracts\Cache\Store. Registering this bridge as a custom driver will fail at resolution time. The supported path is a container binding for Psr\SimpleCache\CacheInterface, as above. Note also that Laravel already adapts its own stores to both PSR-6 (Illuminate\Cache\Psr6Cache) and PSR-16 (Illuminate\Cache\Psr16Cache) — so if the pool in question is a Laravel store, this package adds a layer for nothing.

Exception translation is the highest-risk unknown. PSR-6 throws Psr\Cache\InvalidArgumentException / Psr\Cache\CacheException; PSR-16 callers catch Psr\SimpleCache\InvalidArgumentException / Psr\SimpleCache\CacheException. These are unrelated types (psr/simple-cache does not extend psr/cache). If the bridge lets the PSR-6 exception escape, a cache fault becomes an uncaught 500 instead of a graceful miss. Prove it with a deliberately failing pool:

$pool = new class implements CacheItemPoolInterface {
    public function getItem(string $key): CacheItemInterface { throw new \Psr\Cache\CacheException('down'); }
    // ...
};
expect(fn () => (new SimpleCacheBridge($pool))->get('x'))
    ->toThrow(\Psr\SimpleCache\CacheException::class); // or catch it yourself in SimpleCache

If it leaks, catch and rethrow inside your SimpleCache decorator — that's the extension point, and it's ~5 lines.

TTL semantics are lossy and silent. PSR-16 says null means "use the implementation default", 0 or negative means "delete". PSR-6 has no notion of a default TTL at the interface level — it uses expiresAfter(). If your pool has its own default lifetime, null may mean "pool default" or "forever" depending on implementation. Pin the behaviour down with a test on your actual backend, and prefer passing explicit integer TTLs at call sites so you're never relying on the ambiguous branch. A disagreement here shows up as inexplicable hit-ratio drift, not an error.

No tags, no getItems(), no CAS. Anything downstream of the bridge gets the lowest common denominator of PSR-16. Tag-based invalidation ("purge all product-page fragments") does not survive the crossing. If a consumer needs tagging, this bridge is disqualified — use the pool directly or pick a different seam.

Key validation may be inconsistent. PSR-16 forbids {}()/\@: in keys and requires InvalidArgumentException. PSR-6 pools enforce their own rules. A key that's legal for the pool but illegal per PSR-16 (or vice versa) will behave inconsistently. Namespace and sanitise keys in your decorator rather than trusting the bridge to reject them identically.

Pool scoping in long-running workers. If the pool is request-scoped, or constructed per-request behind a singleton binding, queue workers and Octane will hold a stale or disconnected pool — the classic long-running-process trap. Register the pool itself as a singleton bound to the connection, not to the request, and confirm clear() doesn't operate on a pool you thought was isolated.

Pin the version and commit the lockfile. ~42 stars, zero dependents, single-organisation stewardship, and release notes that are a pointer to a changelog. Practically unexercised in the wild, with no security or PHP-version-update guarantee. Treat upstream as frozen: no ^ ranges, no automated dependency bumps, and a named internal owner. Budget one engineer-day to vendor or reimplement it in-house — it's a few hundred lines of delegation — and put that day in the plan rather than treating dormancy as an emergency.

Check symfony/cache's Psr16Cache first. It solves the same problem for the same kind of pool with a vastly larger install base. Reserve this package for cases where its specific constructor and pool expectations are already proven, or where you're specifically avoiding a Symfony dependency.

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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log