cache/psr-6-doctrine-bridge
Adapts any PSR-6 cache pool to Doctrine's legacy Cache interface. Install via Composer, then use contains, fetch, save and delete as usual; falls back to SHA-256 keys when the pool rejects a key. Includes getCachePool access.
Architecture fit — weak, and shrinking. This package is a one-directional shim: it adapts any PSR-6 CacheItemPoolInterface so it satisfies Doctrine\Common\Cache\Cache. It only matters if the stack actually contains Doctrine, and in Laravel that means laravel-doctrine/orm or a ported legacy module — not the Eloquent default. More importantly, the interface being adapted was deprecated in doctrine/cache 1.11 and removed in 2.0, and Doctrine ORM 2.11+/3.x consumes PSR-6 pools natively. So the bridge's addressable use case is legacy-only and contracting. The signal set (15 stars, 0 dependents, opportunity 43, no consumers to observe) says this is a commodity ~200-LOC shim, not a strategic dependency. Treat it as "buy" only because it is nearly free; treat internalizing it as an equally valid option.
Integration feasibility — cheap to try, brittle to depend on. The mechanics are trivial: composer require cache/psr-6-doctrine-bridge, then new DoctrineCacheBridge($pool) wired into Doctrine's metadata_cache_driver / query_cache_driver / result_cache_driver. The gating risk is dependency resolution, not code. Two corrections to earlier assumptions in this evaluation chain matter for planning:
Illuminate\Contracts\Cache\Repository extends Psr\SimpleCache\CacheInterface), not PSR-6. There is no Cache::store('redis')->getPsr6Adapter(). You must supply a real PSR-6 pool (symfony/cache RedisAdapter, cache/adapter, etc.), which adds a second cache abstraction to the stack.doctrine/cache ^1.x (abandoned) and, in its 3.x line, likely psr/cache ^1.0. Modern Symfony/Laravel Doctrine components require psr/cache ^2|^3 and doctrine/cache ^2. If composer why-not shows a conflict, this evaluation ends here — no spike, no pilot.Technical risk — concentrated in semantics, not performance. The adapter surface hides four behaviours that bite in production:
| Risk | Why it matters | Severity |
|---|---|---|
| Key charset mismatch | Doctrine keys contain \, :, @ (e.g. App\Entity\User, dc2_...) which are reserved in PSR-6. Confirm the bridge hashes keys before handing them to the pool. |
High |
deleteAll() → $pool->clear() |
Doctrine's cache:clear / flush() would wipe the entire shared pool, including non-Doctrine entries. |
Critical |
| Namespace no-op | setNamespace() is likely inert; two apps in one Redis DB silently collide (staging serving prod metadata). |
High |
| TTL & stats semantics | Doctrine save($k,$v,0) = forever vs PSR-6 item defaults; getStats(), getNamespace() likely unsupported and throw or return empty. |
Medium |
Key questions (to close before any commitment):
composer require resolve against our actual Laravel/Symfony/Doctrine version set without downgrading psr/cache or doctrine/cache? (Owner: platform eng; 1 hour.)deleteAll() against a shared pool, and can we guarantee a dedicated pool/keyspace? (Owner: backend lead; blocking.)Stack fit — negative for a modern stack, narrow for a legacy one. The decision tree is short:
Doctrine\Common\Cache\Cache consumer present (older gedmo/*, older bundles, a vendored library typed against the legacy interface) → bridge is the cheapest unblock. Adopt narrowly.Where it does fit, always give it a dedicated PSR-6 pool mapping to a separate Redis database or key prefix from the application cache. Never share a pool with Illuminate\Cache; the clear() semantics make co-tenancy unsafe.
Migration path. Preferred sequence is "remove the need, then remove the shim":
Doctrine\Common\Cache\Cache typehint and every doctrine/cache 1.x requirement in composer.lock.App\Cache\DoctrinePool) that internally delegates to the bridge. Every future swap then touches one file.app/Support/Cache/ under our namespace, MIT-attributed, and drop the doctrine/cache 1.x transitive.Compatibility checks to run, in order: composer why-not psr/cache ^3.0; composer why-not doctrine/cache ^2.0; PHP version support in the bridge's composer.json; serialization behaviour with RedisAdapter vs FilesystemCache under EntityManager::clear(). Note that array and file Laravel drivers offer no operational upside here — the value of this bridge only materialises with a shared, external backend.
Sequencing (5–6 dev-days total, most of it validation, not code):
| Phase | Work | Exit criterion |
|---|---|---|
| 0. Gate (0.5d) | Composer resolution + why-not matrix against our real lockfile |
Resolves cleanly, or stop |
| 1. Spike (2d) | Wire metadata cache only; verify key charset, TTL, deleteAll() scope, cross-release read |
All four semantics confirmed or mitigated |
| 2. Benchmark (1d) | Bridge+Redis vs native FilesystemCache vs native PSR-6 |
No regression beyond noise; document numbers |
| 3. Pilot (1–2d) | Query + result cache in one non-critical service; staging soak 1 week | Hit rate within 5% of baseline; no incidents |
| 4. Rollout | Remaining Doctrine caches; remove legacy cache config | Legacy doctrine/cache 1.x out of lockfile |
Metadata cache first — it is write-once, small, and lowest blast radius. Result cache last; it is the one whose corruption produces wrong query results rather than slow ones.
Maintenance. Code maintenance is near-zero; dependency maintenance is the cost. You inherit an abandoned doctrine/cache 1.x transitively, which typically trips SCA/security review and dependency-upgrade automation. Budget an internal fork as the likely end state: it removes the abandonment risk at the cost of owning ~200 LOC that nobody outside the team reads. Whichever way you go, name a single owner in the repo's CODEOWNERS — a 15-star, zero-dependent package with no upstream SLA will not be maintained on our behalf.
Support. The on-call surface is small but non-obvious. Add three runbook entries: (a) how to flush Doctrine caches without wiping the application pool — this is the entry that prevents a 2 a.m. Redis-wide flush; (b) how to confirm keys are hashed and what a poisoned/colliding key looks like; (c) what serialization change looks like — post-deploy cache misses that never recover indicate stale-format entries, not a broken pool. Instrument hit/miss ratios per cache type (metadata, query, result) separately; a single blended metric hides result-cache regressions.
Scaling. The bridge itself is negligible — a few microseconds of array/CacheItem translation per operation, dwarfed by network RTT to Redis. The scaling story is entirely the pool: clustering, connection pooling, eviction. Watch two real hazards: cache stampede on query/result cache after deploy (PSR-6 has no locking or probabilistic early expiry in most adapters, and Doctrine's cold-cache warm-up can stampede the database), and hot keys in a small metadata keyspace. Mitigate stampede with a deploy-time warmup or jittered TTLs, not with this package.
Failure modes.
| Scenario | Impact | Detection | Mitigation |
|---|---|---|---|
deleteAll() clears shared pool |
App-wide cache miss storm; DB saturation | Redis FLUSHDB in logs; latency spike |
Dedicated pool/DB; deny cache:clear scope |
| Reserved-char key rejection | Cache writes throw under load | CacheException from pool |
Verify hashing in spike; otherwise wrap/escape |
| Namespace collision across envs | Wrong-schema metadata served in prod | Unexplained "column not found" errors | Isolate Redis DB per environment |
| Legacy transitive dep flagged | Blocks security sign-off / release | SCA gate | Internal fork; or avoid entirely |
| Upstream goes silent permanently | No PHP/Doctrine upgrades | — | Fork to internal namespace now |
| Serialization drift across releases | Permanent cache misses, not errors | Hit rate never recovers post-deploy | Flush on schema/model change |
Ramp-up. Half a day for a mid-level engineer to wire and test; effectively zero for anyone who never touches Doctrine, since the bridge stays behind the service container. Knowledge concentration is the risk: at 15 stars and 0 dependents there is no community to ask, so the person who wrote the spike owns it until the runbook and tests exist. Budget those 2–3 days explicitly.
TPM recommendation. Do not adopt this as a platform dependency. Fund a 0.5-day gate to answer whether our Doctrine version can consume PSR-6 natively; if yes, that is the whole initiative and the package is unnecessary. If a hard legacy constraint blocks it, adopt behind an internal adapter interface with a dedicated pool and a named owner, and schedule the fork-or-remove decision into the following quarter rather than letting it become permanent. Effort is small either way (5–6 dev-days); the real cost is the abandoned transitive dependency and the shared-pool flush hazard, both of which are avoidable but only if decided up front.
How can I help you explore Laravel packages today?