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 Cacheable Model Laravel Package

elipzis/laravel-cacheable-model

Add automatic select-query caching to any Laravel Eloquent model with a single trait. Configure TTL, prefixes, and unique identifiers per model, skip manual cache calls, and rely on automatic flushing for inserts, updates, and deletes. Works with Redis, memcached, and more.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require elipzis/laravel-cacheable-model
php artisan vendor:publish --tag="cacheable-model-config"

Check the version you land on before writing code: 0.6.x targets Laravel 13 / PHP 8.5, 0.5.x targets Laravel 12 / PHP 8.4, 0.4.x targets Laravel 11 / PHP 8.3. Pin it explicitly in composer.json rather than floating ^ if your app straddles a Laravel upgrade, since the package overrides the query builder and minor bumps can change builder behaviour.

Then add the trait to a single model:

use ElipZis\Cacheable\Models\Traits\Cacheable;

class Currency extends Model
{
    use Cacheable;
}

That is the whole integration. Existing Currency::query()->where('code', 'EUR')->first() calls now hit the cache on the second call.

Where to look first: config/cacheable-model.php (published) for ttl, prefix, identifier, and the logging block — turn logging on in staging to actually see hits/misses and understand the generated keys. Then the README's "Note on using caching" section, which is the honest warning: every query on a Cacheable model is intercepted, so this is a targeted tool, not a global switch.

First use case: pick a genuinely read-heavy, low-write lookup table — currencies, countries, categories, feature flags, static permission lists. Something read on nearly every request and written a handful of times a week. Benchmark one endpoint before/after (query count via Telescope or DB::listen) so you have a number to justify extending it to a second model.

Implementation Patterns

Per-model tuning via the trait hook. Global config is usually wrong for at least one model, so override locally rather than editing the published file:

public function getCacheableProperties(): array
{
    return [
        'ttl' => 3600,          // stable lookup data, long TTL
        'prefix' => 'ref.currency',
        'identifier' => 'code', // match the column your app filters on
    ];
}

Give every model a distinct prefix. Two models sharing the default cacheable prefix is the fastest way to serve the wrong rows.

Fresh reads on demand. After an admin save, a webhook handler, or anywhere you need read-after-write consistency inside the same request:

Currency::query()->withoutCache()->find($code);

A common pattern is a small repository/service method that takes a $fresh = false flag and appends withoutCache() when set — keeps the escape hatch out of controllers.

Explicit flushing after out-of-band writes. The trait hooks Eloquent inserts/updates/deletes. It cannot see DB::table('currencies')->update(...), raw SQL, upsert, or writes performed by a separate service against the same database. Wire those up yourself:

DB::table('currencies')->insert($rows);
Currency::query()->flushCache();

Same applies to artisan commands, seeders, and queue workers that mutate data outside the model. A CurrencyObserver or a saved/deleted event listener that calls flushCache() is a reasonable belt-and-braces layer if your team writes to the table from many places.

Cache-store choice matters. The package uses tagged cache entries internally (see the remove expired items from tagged cache fix in 0.5.0). Tags are supported by Redis and Memcached; the file and database drivers are not fully tag-capable. Configure a tag-capable store for anything you actually care about, and treat file caching in local dev as "works, but not representative".

Warm on deploy. Rather than letting the first production request eat the miss, run a small warmup step in your deploy pipeline or a scheduled command that touches the hot queries (Currency::query()->get(), etc.). This avoids a stampede on a cold cache after a release that clears Redis.

Testing. In feature tests with RefreshDatabase, cached results can leak between tests if the store outlives the transaction. Either force the cache store to array in phpunit.xml (<env name="CACHE_STORE" value="array"/>) or call flushCache() / Cache::flush() in setUp. Assert the cache path explicitly — one test that queries twice and asserts a single DB::listen entry is worth more than ten that just pass.

Gotchas and Tips

  • The overhead is real. Every query against a Cacheable model goes through an interception layer, including the ones you'd rather not cache (counts, exists(), pagination). If you apply the trait broadly "just in case", you pay serialisation and key-building cost on hot write paths. Apply it to two or three models and measure.
  • Writes bypassing Eloquent are invisible. This is the number one source of stale-data bug reports. If you see stale rows, check for DB::table(), raw statements, insertOrIgnore, or a second app sharing the database, before blaming the package.
  • identifier must match reality. The default is id. If your code queries by slug or code and you leave identifier as id, cache invalidation on update may not target the entries your application actually reads. Set it to the column your where clauses use.
  • Query keys are value-sensitive. whereIn('id', [1,2,3]) and whereIn('id', [3,2,1]) are distinct entries; user-scoped queries (where('user_id', $id)) can produce an unbounded key space. Do not make a model Cacheable if its queries are mostly per-user filters — you'll create a cache of thousands of near-useless entries.
  • Version-specific bugs to know about. whereNull threw an "Undefined array key 'values'" error before 0.6.0; whereIn with an Expression was broken before 0.4.1; expired tagged entries were not removed before 0.5.0. If you're on an older tag and hit odd behaviour with null comparisons or subqueries, upgrade first.
  • Debugging. Enable logging.enabled and point logging.channel at a dedicated channel (dynamic channel support landed in 0.5.0). Logs show key construction and hit/miss, which is usually faster than guessing why a query isn't cached.
  • Stale after related-model changes. The trait tracks the model it's on. Updating Category does not invalidate a cached Product query that joins or eager-loads it. If a query's result depends on another table, flush explicitly in that table's write path.
  • Adoption is low (159 stars, no dependent packages). That's not disqualifying for a small MIT package, but it does mean you're the QA team. Pin the version, read the changelog on every bump, and cover your hot query paths with tests before upgrading Laravel majors.
  • Extension point. If you need bespoke key formats or invalidation logic, the practical route is a thin repository class around the model that owns withoutCache() / flushCache() calls, rather than subclassing the builder. Keep the package's surface area small in your codebase so swapping it out later is a one-file change.
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.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table