mike-bronner/laravel-model-caching
Automatic query caching for Laravel Eloquent models. Cache query results and relationships, then invalidate cached data automatically when models change, reducing database load and speeding up repeated reads with minimal code changes.
Architecture fit. Unchanged in kind: mike-bronner/laravel-model-caching remains a drop-in Eloquent optimization, not an architectural component. Two things in 13.2.0 move the boundary of where it fits, though. First, global and per-model TTL caching (PR #637) removes one of the previous disqualifiers — "anything where per-query TTL control matters" — because a model can now be given a bounded staleness window that acts as a backstop under the event-based invalidation. That widens eligibility from "read-heavy, low-churn reference data" to "read-heavy models with a defensible staleness SLA," but it does not make write-heavy models or read-after-write-consistent paths suitable. Second, the invalidation fixes (pivot-table tagging, where-subquery joined-table tagging, morphTo memoization, macro-argument keying) materially improve correctness on relationship-heavy models, which were previously the fragile case. Design-review posture stays the same: a latency/DB-load lever, admitted per model, never a blanket default.
Integration feasibility. Install effort is still low (one Composer dependency, one trait per model); upgrade effort is now a real line item rather than a version bump, and 13.2.0 is a good demonstration of why. Four upgrade hazards are worth naming:
protected bool $isCachable. Any model or builder redeclaring this untyped now fatals at class load. Loud and immediate, which is the good failure mode — but it must be grep'd across the codebase before deploying.newBaseQueryBuilder() removed from the trait. This is the collision class we flagged as a secondary risk, and it materialized: a method added in 13.1.8 collided with CTE-style traits and was pulled two patches later. Any local trait rule naming Cachable::newBaseQueryBuilder now fatals. Teams that patched around the collision must revert their workaround.modelCache:clear no longer empties the whole cache store. Blast radius is reduced (and this closes a genuine footgun where an empty prefix wiped an entire Redis database), but any operational script using it as a general flush must add php artisan cache:clear.
Additionally, the release changes many cache keys and tags, so a one-time cold cache for those query shapes should be assumed. That is a deploy-day capacity event, not a code risk, and belongs in the rollout runbook.Technical risk. The headline risk has shifted from "coupling to internals" in the abstract to demonstrated churn in builder internals: a method was introduced in 13.1.8 and removed in 13.2.0, with regressions shipped in 13.1.11 and fixed across #640/#651. Treat exact-version pinning and staged upgrades as mandatory controls, not hygiene. Separately, several fixes in this release close silent wrong-row bugs, not merely staleness: macro and custom-builder-method arguments previously shared cache keys across null/false/"" and 1/"1", and several query shapes reused keys they should not have. That means versions below 13.2.0 could return incorrect results, not just old ones, on codebases using macros, closure where() on globally-scoped models, or eager-loaded many-to-many relations. Recommendation: adopt 13.2.0 as the floor, and treat any existing deployment on 13.1.x as carrying a correctness defect rather than a performance-tuning gap. Residual risks are unchanged: builder decoration conflict with other packages, unbounded key cardinality in Redis, and coarse per-model invalidation. Note the release notes also state that the README's "What Does Not Get Cached" section was wrong about lazy loads and select() — so documentation is not a reliable specification; verify caching behaviour per code path against the code.
Key questions for the team.
$isCachable, or any trait rules reference Cachable::newBaseQueryBuilder? Both now fatal at class load.modelCache:clear flushing the entire store?where() under global scopes, or eager-loaded many-to-many relations? If so, we are currently exposed to key-collision bugs and must upgrade before, not after, piloting.Stack fit. Laravel with Eloquent, PHP 8.3+, and a tag-capable cache backend — Redis (phpredis/predis) preferred, Memcached viable with key-size and eviction caveats. Compatible with horizontal app scaling and queue workers provided invalidation shares the cache namespace. Confirm Octane behaviour explicitly if the platform runs it. The CTE/trait-collision conflict is resolved by this release, but verify against whatever else decorates the Eloquent builder in our tree — Scout, auditing, soft-delete extensions — since trait ordering still matters.
Migration path. Incremental and reversible, but now front-loaded with an upgrade rather than starting from a clean install:
$isCachable declarations, trait rules naming Cachable::newBaseQueryBuilder, and scripts relying on modelCache:clear clearing everything.disableCache() per query, or a repository-level Cache::remember) so teams are never blocked by package behaviour.Compatibility. Rebuild the version matrix from scratch: PHP 8.3/8.4, Laravel major, cache driver, and every package touching the Eloquent builder. Test trait ordering where multiple traits apply to one model. Explicitly exercise the historically fragile paths — soft deletes, global scopes, whereHas subqueries, inRandomOrder, cursor()/chunk(), pagination counts, belongsToMany/morphToMany with withPivot()/withTimestamps()/custom as(), macro calls with arguments, and fromSub()/union bindings — because several of these changed key text or moved to uncached in this release.
Sequencing. (1) Runtime upgrade to PHP 8.3 and package upgrade to 13.2.0 on a branch, with the grep checklist and a full test pass; (2) one-sprint PoC on a single model with load-test comparison and a TTL set; (3) cache infrastructure and observability — hit rate, miss rate, invalidation rate, Redis memory; (4) production pilot behind a feature flag, deployed with php artisan modelCache:clear followed by deliberate cache warming, since the key changes guarantee a cold start; (5) broaden with per-model staleness sign-off; (6) publish an internal standard on eligible models, TTL conventions, bypass mechanism, forbidden write patterns, and the upgrade/pinning policy. Do not sequence this before the cache store is production-hardened.
Maintenance. The maintenance picture is modestly better than the prior assessment: an active release with many substantive contributor PRs, a first-time contributor, and a maintainer resolving detailed review feedback within the cycle. That is a healthy signal. It is, however, a double-edged one — the velocity also produced a method added and removed inside a minor version, plus regressions that shipped and were then corrected. The correct operational response is to pin to an exact patch, upgrade deliberately with the checklist above, and retain the internal capability to fork and patch. Assign a named owner; packages with subtle invalidation semantics rot silently when ownership is diffuse.
Support. Still no vendor SLA; GitHub issues and community write-ups are the support surface. Responsiveness in this release cycle is encouraging, but community support cannot be scheduled against an incident. Keep the internal fork contingency budgeted.
Scaling. DB read reduction on the right models remains the payoff, and TTL support improves the economics: bounded staleness, a natural cap on how long a bad key can survive, and a safety net for any write path we missed. Costs still shift to Redis — key cardinality grows with query diversity, not data size — and TTL adds expiry churn without reducing cardinality. Coarse per-model invalidation is unchanged: one write to a hot table flushes the whole working set and can stampede the database. Mitigations: cache warming, TTL jitter, request coalescing on the hottest paths, and no trait on bursty-write models. New scaling consideration: every upgrade that changes key text creates a synchronized cold cache across the fleet — plan upgrades as capacity events, not just deploys.
Failure modes to design for.
$isCachable or a stale Cachable::newBaseQueryBuilder trait rule. High-severity but immediate and obvious; catch it in CI by loading every model class.where(), and many-to-many eager loads. Treat as a correctness defect in current deployments and remediate by upgrading.modelCache:clear is scoped to package entries; audit any runbook that assumed a full-store wipe.Ramp-up. Developer ramp-up is still near zero (one trait line), which remains the trap. The onboarding item now has three clauses instead of one: "any write outside Eloquent invalidates nothing," "every cached model carries an explicit TTL and a named staleness owner," and "upgrades are pinned, staged, and include a cache clear plus warm." One to two days for a senior engineer to be productive; the real investment is standing ownership, the upgrade runbook, and the fork contingency. Net: adoptable as a targeted optimization with clear per-model admission criteria — and now, with TTL and the correctness fixes in place, a more defensible bet than the prior release line, provided 13.2.0 is the floor and PHP 8.3 is on the roadmap.
How can I help you explore Laravel packages today?