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 Cache Laravel Package

codewithdennis/laravel-model-cache

Cache Eloquent model queries in Laravel with a single trait. Cached results are automatically invalidated when models are created, updated, or deleted, cutting database load and speeding up repeated reads with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Pin the beta explicitly — this is v1.0.0-beta.1, so avoid ^1.0-style constraints and treat the API as provisional:
    composer require codewithdennis/laravel-model-cache:"1.0.0-beta.1"
    
  2. Publish the config (adjust the tag if it differs in your install; check php artisan vendor:publish output):
    php artisan vendor:publish --tag="model-cache-config"
    
    Open config/model-cache.php and note three things before writing any code: the default cache store, the default TTL, and the key prefix. These determine correctness more than anything else in the package.
  3. Confirm your cache backend. Point CACHE_STORE at redis (or memcached) in staging and production. On file, database, or array, tag-based invalidation degrades or silently no-ops, which is the single most common way this package "works" locally and goes stale in production.
  4. Apply the trait to exactly one low-risk model:
    use CodeWithDennis\LaravelModelCache\HasModelCache;
    
    class Category extends Model
    {
        use HasModelCache;
    }
    
  5. First use case: a read-mostly reference table — categories, settings, plans, countries — hit on nearly every request and written to rarely. Wrap the hot query and verify the second call skips the database:
    $categories = Category::where('active', true)->orderBy('name')->get();
    
    Check with Laravel Telescope's cache and query panels: on request two, the query should disappear and a cache hit should appear.

Where to look first: the trait source (which builder methods are actually intercepted — this defines what is and isn't cacheable), the config file defaults, and the test suite, which is the most reliable usage documentation for a package with thin prose docs.

Implementation Patterns

Opt in per model; never globally. Applying the trait via a base model is tempting and wrong for a beta dependency. Choose models by read/write ratio: reference data and content reads are ideal, transactional tables (orders, inventory, balances) are not.

Query-level caching as the default posture. Rather than relying on blanket model caching you haven't fully characterized, be explicit per query:

Category::where('active', true)->cache(ttl: 600)->get();
Post::with('author', 'tags')->cache()->paginate(20);

This makes the cache surface reviewable in code review — a real advantage over a trait that silently changes every read on the model.

Warm the cache deliberately. The package's headline feature is pre-warming heavy queries once and holding them indefinitely. Use a scheduled command for expensive aggregates rather than letting the first unlucky user pay the cost:

// app/Console/Commands/WarmModelCache.php
public function handle(): void
{
    Category::where('active', true)->cache(forever: true)->get();
    Plan::with('features')->cache(forever: true)->get();
}

Register it in routes/console.php or the scheduler, and re-run it after deploys or cache flushes — otherwise you get a cold-start thundering herd exactly when traffic is highest.

Keep writes going through Eloquent instances. Invalidation is driven by model events (saved, deleted, restored, forceDeleted). Any write path that skips those events leaves stale data behind:

$post->update(['title' => 'New']);            // invalidates
Post::where('id', $id)->update([...]);        // mass update — see Gotchas
DB::table('posts')->where(...)->update([]);  // bypasses everything

Compose with scopes and relationships. Because interception happens at the builder level, local scopes chain naturally:

public function scopePublished($query)
{
    return $query->where('published_at', '<=', now());
}

Post::published()->latest()->cache(300)->get();

Cache eager-loaded relations on their own models rather than assuming the parent's entry covers them.

Neutralize the cache in tests. Do it in TestCase::setUp() so no test can forget:

protected function setUp(): void
{
    parent::setUp();
    config(['model-cache.store' => 'array']);
}

Plus one integration test that does exercise Redis, asserting invalidation on update and delete — that's the test that catches a broken upgrade.

Wrap it behind repository or scope methods. Given beta instability, funnel cached queries through a small number of methods so a rename in 1.0.0 touches one file instead of forty.

Instrument before and after. Log query counts per route (Telescope, or DB::listen counters in a middleware). A cache hit ratio below ~50% on a model means the trait is adding serialization cost without buying anything — remove it.

Gotchas and Tips

  • Mass updates and deletes bypass events. Post::where(...)->update([]) and ->delete() fire no model events, so cached entries go stale until TTL expiry. Either iterate models, or explicitly forget the affected keys afterwards. Audit every mass write in your codebase before trusting the trait.
  • TTL is not correctness. A TTL bounds staleness, it doesn't prevent it. If invalidation is broken, a 10-minute TTL means ten minutes of wrong data — not an error anyone will notice. Confirm event-based invalidation empirically with a test, not by assumption.
  • Watch the transaction boundary. Invalidation firing inside a transaction, before commit, lets a concurrent read repopulate the cache from pre-commit state. After any write in a transaction, re-fetch through an uncached path or forget the key after commit (DB::afterCommit).
  • Tag support varies by driver. file and database drivers ignore or over-approximate tags. If invalidation depends on tags, Redis/Memcached is a prerequisite, not a preference. Read the trait's forget logic to confirm what it actually does.
  • Read-after-write paths need bypassing. $model->refresh(), fresh(), and immediate re-queries can return stale data if the read path is cached. For payments, inventory, and permission checks, do not read through the cache.
  • Multi-tenancy is a hard blocker until verified. Check that the cache key incorporates tenant/auth scope; a missing scope returns one tenant's rows to another, and that failure is silent and catastrophic. Test with two tenants before shipping.
  • Key prefixes across environments. If staging and production share a Redis instance, identical prefixes cause cross-environment staleness. Suffix the prefix with app()->environment().
  • Disable in local and testing. Set MODEL_CACHE_ENABLED=false (or your config equivalent) so N+1s and missing indexes stay visible while developing — caching hides exactly the performance problems you want to find early.
  • Debugging technique: use Cache::tags([...])->flush() and Cache::get() manually in tinker to see the exact keys and payloads the package writes. If a query still appears in the query log on repeat requests, your call isn't intercepted — most often it's a raw DB::table() query or an aggregate method the trait doesn't wrap.
  • Octane and long-lived workers. Verify behavior under Octane before deploying; static or per-request cache state carried across requests is a classic failure mode in model-layer packages.
  • Extension points: the trait's model event listeners are where you hook additional invalidation — for pivot table changes, related-model updates, or writes performed by queue workers and third-party syncs that the package doesn't observe.
  • Pin the exact version and assign an owner. Single maintainer, pre-1.0, no documented upgrade path. Keep the code surface small enough that vendoring or forking is realistic, and write a one-page runbook covering how to flush selectively and how to disable the package entirely during an incident.
  • Not a fit for high-write tables. For write-heavy tables with complex reads, a read model or materialized view is more predictable than per-model query caching. This package earns its keep on 90/10 read/write data.
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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment