ymigval/laravel-model-cache
Transparently caches Eloquent query results with zero syntax changes, auto-invalidating on create, update, delete, and restore. Configurable TTL, stores, and tags for read-heavy Laravel apps.
Architecture fit
The package uses a trait (HasCachedQueries) that overrides newEloquentBuilder() to return a CacheableBuilder, making every get()/first() on that model cache-aware with no call-site changes. This is a shallow-footprint, deep-semantics design: the code change is one use statement per model, but the behavioral change is global to that model's reads. For a read-heavy Laravel app on Redis, that's an attractive ratio. For a TPM, the important framing is that this is implicit caching — the default state of the system becomes "reads may be stale," and that is a product/correctness decision, not just a performance one.
Cache keys derived from SQL + bindings + model class are the architecture's load-bearing element. They give you correctness for free on simple where x = ? queries and become the primary scaling liability on dynamic queries (whereIn with variable-length arrays, pagination offsets, user-scoped filters, sort permutations). There is no documented key-cardinality bound, no stampede/lock protection, and no hit/miss instrumentation — all three are things the adopting team will have to build or size around.
Invalidation is event-based (created, updated, deleted, restored) plus a ModelRelationships trait for pivot operations. That covers idiomatic single-model Eloquent writes. It does not cover writes that bypass model events — Model::where(...)->update(), upsert(), query-builder inserts, seeders/imports, raw SQL, or another service writing to the same database. Those are the silent-staleness paths and they are common in mature codebases.
Integration feasibility
Low friction mechanically: composer require, optional config publish, add the trait. The hard dependency is a tag-capable cache store (Redis/Memcached/database). File and Array drivers "work" but fall back to clearing the entire cache on invalidation — acceptable in dev, not in production. Provisioning a dedicated Redis store and prefix is effectively a prerequisite, not a nicety.
Technical risk
CacheableBuilder, which is doable but couples your builder hierarchy to a third-party class.Illuminate\Database\Eloquent\Builder is not a stable interface across five majors. Claiming Laravel 13 support is a forward-compatibility claim that must be verified per release, and a broken override fails loudly at query time.get() returns hydrated models; if those are stored serialized, a deploy that changes a model class can invalidate readability of live cache entries. Deploy-time flush becomes mandatory, not optional.Key questions to put to the maintainer / spike
chunk(), cursor(), lazy(), paginate(), and aggregate methods (count(), exists()) cache, pass through, or behave inconsistently?withoutCache()?Stack fit Best fit: Laravel 8–12, PHP 8.x, Redis already in the stack, read-heavy domain models with low write rates and tolerant freshness windows (CMS content, catalogs, taxonomy, feature/config lookups, public profile pages). Poor fit: money, inventory, permissions, or anything where a stale read is a customer-visible defect; and apps whose writes largely bypass Eloquent events.
Migration path
Opt-in per model is the key property — this is an incremental rollout, not a cutover. The enabled config toggle gives a global kill switch, which is the rollback mechanism. Exit cost is also low: removing the package means deleting one trait and, where needed, replacing implicit caching with explicit Cache::remember — the coupling is concentrated in model declarations.
Compatibility gates before rollout
update()/upsert()/raw SQL on those models without a compensating flush.flush collateral damage.Sequencing
cache_key_prefix, distinct from the default store. Size memory and pick an eviction policy deliberately (allkeys-lru can evict hot keys; noeviction throws).enabled = false. Run the full test suite; establish a baseline for query counts and p95 latency on the target models.flushModelCache() calls or the mcache:flush command to importers, bulk jobs, admin tools, and any non-Eloquent writer. This is the step most likely to be skipped and most likely to cause an incident.php artisan mcache:flush (scoped per model where possible) to the deploy pipeline; full flush is mandatory after model/schema changes because of serialized cached models.remember() TTLs per model over the 60-minute default — different write rates need different windows.Maintenance
Single-maintainer package with no dependents. Treat it as fork-ready: the surface is small (one builder, two traits, one command), it is MIT, and it is vendorable. Budget for owning it — specifically for tracking Eloquent\Builder signature changes on every Laravel major. Without that ownership assumption, an abandoned package becomes a hard blocker on framework upgrades.
Support Community-only via GitHub issues; no SLA, no commercial backstop. Internal documentation should capture the caching semantics, the flush command, and the "which models are cached and why" inventory, because the failure mode is invisible by design.
Scaling The scaling constraint is cache key cardinality, not the cache backend. Every distinct SQL+bindings combination becomes a key; attacker- or user-controlled filter parameters can inflate this without bound, which is both a memory cost and a mild DoS vector. Second constraint is stampede: a hot key expiring under load pushes a burst to the database at exactly the moment the cache is least helpful. TTL jitter and request-level de-duplication are mitigations you would own. Third is cold-start after a flush — the README suggests cache warming but ships none.
Failure modes, ranked by severity
Ramp-up
Developer cost is low (one trait, syntax unchanged), but the conceptual cost is real: engineers must internalize that Post::where(...)->get() may return stale rows and that non-Eloquent writes do not invalidate. The README is unusually thorough for a package this size — implicit vs. explicit caching, withoutCache(), scopes, and flush commands are all documented — but it is silent on multi-tenancy, transactions, key cardinality, and failure behavior, which are precisely the operational questions that matter. Plan a short internal onboarding note with a rule of thumb: cache the model only if you can enumerate every writer to it.
How can I help you explore Laravel packages today?