Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Model Caching Laravel Package

genealabs/laravel-model-caching

A Laravel package that adds automatic query caching to Eloquent models. Use the provided trait to cache model queries, with cached results invalidated automatically when records are created, updated, or deleted, reducing database load with minimal code changes.

View on GitHub
Deep Wiki
Context7

Getting Started

This package sits between Eloquent and your cache store: any query run through a model that uses the Cachable trait is cached under a key derived from the model, connection, SQL and bindings, and that cache is invalidated automatically from model events (created, updated, deleted, restored, …). The mental model is "query-result cache with event-driven invalidation", not a replacement for HTTP/response caching or for designing fast queries.

Minimal setup (13.2.0 requires PHP 8.3+; on 8.2 Composer will hold you at 13.1.11):

composer require genealabs/laravel-model-caching
php artisan vendor:publish   # pick the package's provider, then read config/laravel-model-caching.php
php artisan modelCache:clear # run once after deploy on upgrade, see Gotchas

The service provider is auto-discovered. Before anything else, set the cache store to one that supports tags — Redis or Memcached (CACHE_STORE=redis). Tag support is what makes invalidation precise; the package's own test suite runs against Redis for the same reason. If you are on file or database, fix that first.

Then opt in one model at a time:

use GeneaLabs\LaravelModelCaching\Traits\Cachable;

class Category extends Model
{
    use Cachable;
}

If the model is new, you can extend GeneaLabs\LaravelModelCaching\CachedModel instead. If you already extend an application base model, use the trait — PHP gives you one parent only.

First use case: a read-heavy, write-light model — categories, settings, countries, a catalog. Warm it once, then watch the query count in Telescope/Debugbar drop:

$categories = Category::where('active', true)->orderBy('name')->get(); // cached
Category::create(['name' => 'New']);                                   // invalidates

Where to look first when something surprises you: the published config (prefix, store, tag toggles, cooldown, TTL defaults), the Cachable trait and the caching query builder it returns, and the README's "What Does Not Get Cached" list — it was corrected in 13.2.0 and is now a better map of the weak spots than it used to be. Then look in Redis with SCAN <prefix>* and see whether the key you expect exists.

Implementation Patterns

Adopt by read/write ratio, not by table count. Cache lookup and reference tables and expensive aggregate reads. Do not put the trait on audit logs, counters, session-like tables or anything written per request — you pay invalidation on every write and rarely get a hit.

Route hot reads through one canonical scope. The cache key is the query. Two call sites with slightly different select, orderBy or eager loads produce two keys and half the hit rate:

// app/Models/Product.php
public function scopeVisible(Builder $query): Builder
{
    return $query->where('published', true)->with('category')->orderBy('name');
}

// everywhere
Product::visible()->get();

Use TTLs for data you can tolerate being slightly old, and rely on event invalidation for correctness. 13.2.0 adds first-class TTL support (global config plus per-model), and TTLs are now resolved through eager loads too — so a relation loaded via with() inherits the intended window instead of falling back to "forever":

Product::visible()->cache(10)->get();   // confirm the TTL unit in the builder for your version

Set a global default in the published config rather than sprinkling cache() at call sites; use the per-model override for the few models that genuinely need a shorter or longer window.

Bypass deliberately. For reads that must reflect a write that just happened, or when debugging a suspected stale result:

$fresh = Order::disableCache()->findOrFail($id);

Invalidate manually after writes that skip Eloquent events. This is the single most common source of stale data:

DB::table('products')->where('legacy', true)->update(['imported_at' => now()]);

Product::clearCache(); // no model events fired above

Wrap such writes in a small service method so the invalidation is not forgotten at one of five call sites.

Invalidate after commit, not inside the transaction. Model events fire before the transaction commits. A concurrent reader can miss the cache, read the pre-commit row, and repopulate the cache with stale data that then survives. Defer the invalidation:

DB::transaction(function () use ($product) {
    $product->update([...]);

    DB::afterCommit(fn () => Product::clearCache());
});

Many-to-many is now handled, but keep the pivot in mind. 13.2.0 tags eager-loaded belongsToMany/morphToMany queries with their pivot table and flushes those tags when the pivot row is written — including eager loads that don't join the pivot (with('stores')). If you had workarounds for stale pivot data (manual clearCache() after attach()/sync()), re-test them; you may be able to delete them.

Warm cold caches after deploys and flushes. A version upgrade or a flush leaves every consumer cold; a warm-up job that runs the canonical scopes prevents a thundering herd on the primary database. This matters more on this release, since the key format changes make the first read after deploy a guaranteed miss.

Plan for multi-tenancy. Keys are built from the query and its bindings. If tenant isolation comes from a global scope, the tenant ID is in the bindings and keys differ — verify that, and verify it still holds for any query built with withoutGlobalScope. Where it does not, put the tenant in the prefix. Note that 13.2.0 changed keying for closure where(), morphTo eager loads and withoutGlobalScopesExcept() against scoped models, so re-verify your isolation assumptions after upgrading rather than assuming the old behaviour.

Measure. Compare DB::getQueryLog()/Telescope query counts and Redis hit counts before and after enabling the trait on one model. If hits are near zero, the model is being written too often, the keys are fragmenting, or the query fell into the "runs uncached" bucket described below.

Gotchas and Tips

  • Upgrade ritual for 13.2.0. Run php artisan modelCache:clear after deploying (php artisan cache:clear too, if any script relied on the old behaviour). Some changed queries keep their old key text, so until the next write to that table they could read an entry written by the old version.
  • Type $isCachable as bool. The trait now declares protected bool $isCachable; a model or builder that redeclares it untyped is a fatal error at class load. Grep your app models before upgrading.
  • Delete trait rules naming newBaseQueryBuilder. It was added to Cachable in 13.1.8, collided with CTE-style traits, and is gone in 13.2.0. Cachable::newBaseQueryBuilder as … / insteadof … now fail at class load. Other::newBaseQueryBuilder insteadof Cachable still works.
  • modelCache:clear no longer wipes the store. On Redis it used to empty the app's cache, or the whole Redis database when the prefix was empty; it now deletes only the package's own entries. Any deploy/CI script that used it as a blunt "clear everything" needs an explicit cache:clear added.
  • Some queries now run uncached instead of risking a wrong hit. Values that can't be keyed reliably (objects whose serialize() is rejected, resources), eager-load constraints with afterQuery() callbacks, relations eager-loading a non-Cachable model, and macro/custom builder calls receiving closures or non-encodable arrays all bypass the cache in 13.2.0. Correct behaviour, but it shows up as an unexplained miss — check whether your query is in that set before assuming the package is broken.
  • Macro and custom builder arguments are keyed as JSON now. null/false/"" and 1/"1" used to share a key; they no longer do. Expect a one-time cold read, and expect more distinct keys if you call macros with varied arguments in a loop.
  • Anything that bypasses model events bypasses invalidation: DB::table() writes, Model::insert(), upsert(), builder-level ->update()/->delete(), raw SQL, imports, pivot writes done through the query builder rather than the relation, and changes made by another application against the same database. Each needs explicit clearCache().
  • Cache keys and tags are a contract, not an implementation detail. This release changed keying for closure where/orWhere/whereNot on scoped models, morphTo eager loads, withoutGlobalScopesExcept(), eager-load constraints that do more than add where, most belongsToMany/morphToMany shapes, fromSub()/union bindings holding Stringable objects, through-relations using with(), and cooldown keys on models declaring $cachePrefix. Never assert exact key strings in your own tests — assert behaviour (query count, freshness) instead.
  • Tag support is the precision mechanism. Redis/Memcached/array tag; file and database do not, and you end up with coarser invalidation. Check CACHE_STORE before blaming the package for stale reads.
  • Odd bindings can produce surprising keys. The package has had key-building bugs around unusual binding types (for example DateTimeImmutable); where() values of arbitrary type and subquery from clauses were fixed in this line. If a query with date, enum or subquery bindings never seems to hit, inspect the key/bindings rather than assuming the query is uncacheable.
  • Cached results are serialized models. Anything you append or mutate on the models before they are cached can be served back to other requests. Keep $appends deterministic and request-independent on cached models.
  • Not every builder method is cached (the README's list is now accurate — lazy loads and select() are not the blanket exceptions people assumed). Streaming/chunked reads (cursor(), chunk(), lazy()) and some ordering clauses remain weak spots. If a call must be cached, verify the key in Redis after running it.
  • High-write models cause stampedes. Every write invalidates; every invalidation sends concurrent readers to the database. Enable the cooldown setting where supported (cooldown keys were reworked in 13.2.0 — a window that is open at upgrade ends early once), add a lock around the hottest queries, or drop the trait for that model.
  • Tests need cache hygiene. Use the array store (taggable) in phpunit.xml, and flush between tests (Cache::flush() or Model::clearCache() in setUp), otherwise cached rows leak across tests and assertions on query counts drift. Consider disabling caching for the suite if you mostly test write paths. On 13.2.0, also pin the package version in CI and run the PHP 8.3 matrix.
  • exists() and count() are cached too. Duplicate checks, uniqueness validations and rate-limit-style logic should use disableCache() unless a small staleness window is acceptable.
  • Debugging ritual: see a stale row → confirm the write path fires events (or clearCache() was called) → confirm the driver supports tags → SCAN the prefix in Redis to see whether the key exists and when it was written → check whether the query is on the uncached-by-design list. In local development, disableCache() while stepping through is faster than guessing.
  • Trait vs. base class: pick one. Adding both to the same model or mixing CachedModel with a custom base class gives confusing builder behaviour.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo