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.
Architecture fit
Single-class adapter, MIT, split out of the php-cache monorepo and explicitly marked [READ-ONLY] — all issues and PRs route to a separate upstream tracker. It does exactly one thing: present an existing Psr\Cache\CacheItemPoolInterface as a Psr\SimpleCache\CacheInterface. There is no service provider, no config surface, no facade, and no Laravel coupling. It is a library, not an integration, and that is the correct framing for the decision.
The fit is narrow and conditional: it is justified only when we already own a PSR-6 pool we cannot replace — a vendor library handing us a CacheItemPoolInterface, a legacy Symfony/php-cache adapter, or a store whose only implementation is PSR-6 — and a consumer type-hints PSR-16. For everything else it is an extra layer with no product value.
One correction to carry into the decision record: Laravel natively adapts its own stores to both PSR-6 (Illuminate\Cache\Psr6Cache) and PSR-16 (Illuminate\Cache\Psr16Cache), but it does not convert an arbitrary third-party PSR-6 pool into PSR-16. So the package is redundant for Laravel-native stores and genuinely fills a gap for external pools — a gap symfony/cache's Psr16Cache also fills, with a far larger install base. The real architectural question is not "bridge or no bridge" but "which bridge, and can we eliminate the need for one."
Integration feasibility
Low effort, low ceremony, no data migration. composer require cache/simple-cache-bridge:^2.0, construct with a pool, bind the resulting CacheInterface as a singleton, inject it. Realistic estimate: half a day for a conformance spike plus a thin internal adapter; one to two days end-to-end including config plumbing and a feature-flagged rollout. Version 2 targets psr/cache 3 and supports psr/simple-cache 2 and 3, which aligns with current Laravel-era dependency ranges.
Two feasibility traps to validate before committing:
Cache::extend() is the wrong hook. The callback registered with Laravel's cache manager must return an Illuminate\Contracts\Cache\Store, not a PSR-16 instance. Registering this bridge as a custom CACHE_DRIVER is not a supported path and will fail at resolution time. The supported pattern is a container binding for Psr\SimpleCache\CacheInterface plus constructor injection into the services that need it.expiresAfter(), no getItems() batching, no CAS. Everything downstream of the bridge gets the lowest common denominator, and tag-based invalidation in particular does not survive the crossing.Technical risk Medium-to-high, and almost entirely supply-side rather than engineering-side:
Psr\Cache\InvalidArgumentException/CacheException; PSR-16 requires the Psr\SimpleCache equivalents. Whether the bridge translates these is a concrete, testable question — a leak here turns a cache fault into an uncaught 500 instead of a graceful miss.null as "use default" and 0/negative as "delete"; PSR-6 uses expiresAfter(). Misalignment silently shortens or extends item lifetimes, surfacing as inexplicable hit-ratio drift rather than an error.setMultiple/getMultiple/deleteMultiple are near-certainly loops over single-key PSR-6 calls, multiplying network round-trips against Redis/Memcached.Key questions
symfony/cache's Psr16Cache, or a Laravel store)?Psr\SimpleCache\CacheInterface rather than Laravel's Illuminate\Contracts\Cache\Repository? If the answer is "none," this is optional dependency debt.default_ttl behave across set()/setMultiple() when the underlying pool carries its own default lifetime?getItems()? If so, the bridge is disqualified and we need a different seam.Stack fit
A thin internal wrapper is the right shape: bind Psr\SimpleCache\CacheInterface to a singleton built from our existing pool, and let Laravel's repository remain the default for all new code. Do not attempt to expose this as a CACHE_DRIVER, and do not wire it through Cache::extend(). Keep the vendor class behind exactly one internal factory so no application code names it directly.
Migration path Non-destructive by construction — the bridge delegates to the pool we already have, so keys, TTLs, and stored values are untouched. That means zero backfill, zero dual-write, and a trivially reversible switch: remove the binding and consumers revert to whatever they used before. This reversibility is the package's strongest product argument and should be the basis of the go/no-go.
Compatibility
Pin to an exact version, commit the lockfile, and build a compatibility matrix in our own suite (supported PHP versions × production cache backend × hit/miss/expiry/exception paths). Treat upstream as frozen: no ^ ranges, no automated dependency bumps. If symfony/cache can serve the same pool, prefer it — same abstraction, vastly larger install base — and reserve this package for cases where its pool expectations are already proven in our environment.
Sequencing
get/set/delete/clear, the *Multiple methods, TTL edge cases (null, 0, negative), and exception translation against a deliberately failing backend.SimpleCacheFactory plus container binding, so replacement later is a one-file change.Maintenance
Near-zero code churn, non-trivial dependency governance. The package is a few hundred lines of delegation logic; if upstream disappears, vendoring or a ~150-line in-house reimplementation is a day's work. Budget that day in the plan rather than treating it as an emergency. The [READ-ONLY] split repo means bug reports cross a repository boundary — assign a named internal owner, because nobody upstream will triage on our timeline.
Support No SLA, community channel only, no vendor behind it. The realistic support model is "we own it the moment we ship it." Mitigate by keeping the vendor class behind a single internal class so support questions never become archaeology across the codebase, and by documenting the "use this only if…" rule in the ADR.
Scaling
No material CPU or memory overhead — one extra method call per operation. The scaling risk is amplification, not unit cost: if setMultiple/getMultiple fan out to N single-key PSR-6 calls, a 500-key batch becomes 500 round-trips where a native implementation would pipeline. Measure before using batch APIs in hot paths; prefer per-key access with application-side concurrency, or bypass the bridge for bulk jobs entirely.
Failure modes
Ramp-up Low technically — half a day for any engineer fluent in PSR interfaces, and the README is sufficient. The real ramp-up cost is organisational: teaching the team when to reach for the bridge (interfacing with a legacy PSR-6 pool) versus when to use Laravel's native repository (almost everything else). Left undocumented, this becomes a second, parallel caching idiom in the codebase — a larger long-term cost than the dependency itself.
How can I help you explore Laravel packages today?