beste/in-memory-cache
A PSR-6 in-memory cache for PHP that works as a sensible default implementation and in tests. It supports PSR-20 clocks, including frozen clocks, so you can test time-based expiry without a real cache server. Simple, fast, and dependency-free. Install with composer require beste/in-memory-cache.
Architecture fit — adjacent, not plug-in. The package implements PSR-6 (Psr\Cache\CacheItemPoolInterface) only. Laravel's cache layer is not PSR-6; it is built on Illuminate\Contracts\Cache\Store with a PSR-16-shaped surface (get/put/forget). There is no first-party Laravel bridge that lets Cache::store('in_memory') resolve a PSR-6 pool. Any claim that this "aligns seamlessly with the Cache facade" should be rejected in review — the integration is a deliberate adapter, not a config line.
Where it genuinely fits:
CacheItemPoolInterface (Symfony-derived libraries, Geocoder, PSR-6 consumers pulled in transitively). Here it is a drop-in with zero glue code.Mockery or Carbon::setTestNow() against a real store.Store fake rather than a PSR-6 pool.Integration feasibility — low effort, but the effort is in the adapter, not the package. composer require is one line and there are no transitive dependencies. The API surface is four methods. Two credible paths:
InMemoryCache in tests, inject it where a PSR-6 pool is accepted. No Laravel wiring, no config changes.Cache::store() (days). Requires writing and owning a Store implementation that maps get/put/forget/flush onto the PSR-6 pool, handling CacheItem misses, TTL translation from seconds to DateInterval, and Laravel's flush/forever semantics. That is roughly 100–150 LOC plus its own test suite — and it becomes code you maintain forever.Path 2 deserves scrutiny: Laravel already ships the array store, which is in-memory, per-request, TTL-aware, and supported by the framework. For Laravel-native code, array covers most of the same ground. The honest differentiators are only (a) PSR-6 interface compliance for third-party libraries and (b) the injected clock. A TPM should anchor the business case on those two and explicitly retire the "cheap Redis fallback" narrative.
Technical risk:
composer why-not beste/in-memory-cache 1.5.Key questions for the team:
Cache::store() integration, who owns the adapter and its tests, and what is the sunset criterion?array store already satisfy the test requirement? If yes, the marginal value collapses to the frozen-clock TTL testing only.Stack fit.
| Context | Fit | Notes |
|---|---|---|
| Laravel 11/12, PHP 8.3/8.4/8.5 | Test-only: strong. Runtime: adapter required | No native PSR-6 consumption in the framework |
| Laravel 10, PHP 8.1/8.2 | Pin 1.4.x | 1.5.0 drops < 8.3 |
| Symfony / PSR-6-native code | Strong | Direct construction, no glue |
| Plain PHP libraries with PSR-6 seams | Strong | Zero dependencies |
| Any FPM-served production request path | Weak | Per-request lifetime nullifies the fallback value |
Migration path. Three sources of existing pain, in order of likely payoff:
Mockery::mock(CacheItemPoolInterface::class) proliferation → replace with a real InMemoryCache. Removes expectation-drift bugs and lets tests assert on actual cache state (isHit(), get()) rather than on method-call choreography.array-based PSR-6 fakes → delete and depend on the package. This is the cleanest "build vs. buy" win; the replacement is a one-line constructor change.symfony/cache ArrayAdapter used purely as a test double → optional swap. Low urgency; ArrayAdapter also implements PSR-6 and is already in-tree. Do not migrate without a specific reason.Explicitly do not migrate production cache reads/writes onto this pool. If a fallback story is genuinely required, the correct design is Laravel's failover cache driver (Laravel 11+) pointing at a durable secondary, not an in-process pool.
Compatibility. Pin with a caret constraint and test both the floor and ceiling of your PHP matrix in CI. Check composer why-not before any framework upgrade; the PHP floor moving to 8.3 will be the recurring breakage vector. Watch for the PSR-20 clock being injected only in tests — never resolve a frozen clock in a production container binding.
Sequencing.
CacheItemPoolInterface and PSR-6 usage. If the count is zero outside tests, scope the decision to "test dependency" and stop.CacheFactory trait) so no test constructs the pool ad hoc. Document the boundary: "test doubles only; not a runtime driver."If the adapter is pursued, the shape is a thin Illuminate\Contracts\Cache\Store implementation delegating to the pool: get($key) → pool->getItem($key) returning the value or null; put($key, $value, $seconds) → getItem()->set()->expiresAfter($seconds) then save(); forever() → set() with no expiry; flush() → pool->clear(). Budget for the ugly parts: Laravel's many()/putMany() batching, TTL-null semantics, and Cache::increment/decrement, which have no PSR-6 equivalent and must be emulated read-modify-write.
Maintenance. Near-zero surface area, semver discipline, published changelogs, and a small codebase — the code itself will not surprise you. The maintenance burden is entirely policy: version floors that track PHP releases (1.3 → 8.4, 1.4 → 8.5, 1.5 → drop < 8.3) mean an upgrade cadence obligation roughly once per PHP release cycle. Assign an owner and put the pin in the dependency-upgrade runbook.
Support. No vendor SLA. Single maintainer, low star count, zero dependents means community answers will be scarce — your team is the support tier. Mitigate with the fork escape hatch: at under ~1k LOC and MIT licensing, a hard fork is an afternoon, not a quarter. Record this explicitly in the risk register so it is a decision, not a surprise.
Scaling. Treat this as process-local state with a request-sized lifetime. On FPM it is naturally bounded — it dies with the request. On Octane, Swoole, or long-lived queue workers it persists and therefore must be bounded by policy: every write carries an explicit TTL, the key space is finite and enumerated, and the pool is cleared at worker lifecycle boundaries (e.g., Octane::tick or a job-middleware reset). Horizontal scaling is a non-goal — each process gets its own island, and any code that assumes a warm shared cache will behave differently in production than in a single-process test.
Failure modes to design against:
Store bridge is built, increment/decrement/many emulation gaps produce count drift that only appears under concurrency.Ramp-up. One to two hours for a developer already comfortable with PSR-6 — the README is sufficient and the package is small enough to read end to end. The non-obvious learning is conceptual, not syntactic: understanding that PSR-6 pools and Laravel's cache are different abstractions, and that in-memory means per-process. Both belong in the team's testing guide as a short section, not a wiki page. Budget a half-day of tech-lead time for the decision record and the boundary documentation, and treat that as the actual cost of adoption.
Recommendation. Adopt as a require-dev dependency for PSR-6 test seams and deterministic TTL testing; do not adopt as a production cache driver. Revisit only if the PSR-6 usage audit surfaces real production demand, at which point the adapter is a scoped, owned work item rather than an assumption.
How can I help you explore Laravel packages today?