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

Eloquent Cache Laravel Package

authentik/eloquent-cache

Eloquent Cache adds automatic query caching to Laravel models, storing results in your configured cache driver and invalidating them on create, update, and delete. Speed up repeated database reads with almost no code changes.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and enable a tag-capable cache store

composer require authentik/eloquent-cache

Before writing a single line of model code, confirm your cache driver supports tags (redis, memcached, array, dynamodb). The file and database drivers do not, and any tagged call will throw BadMethodCallException: This cache store does not support tagging. Set CACHE_STORE=redis in .env and add a dedicated connection/prefix so you do not collide with your app's other cached data.

Publish the package config if the repo ships one:

php artisan vendor:publish --tag=eloquent-cache

Where to look first

  • README.md — the trait names and the builder method (the public surface is small; the README is the contract).
  • src/ — expect a Cacheable trait, a CacheableAttributes (or similarly named) trait, a custom query Builder, and a ServiceProvider. Read the Builder class first: it tells you exactly which query methods are overridden and which silently fall through to uncached Eloquent.
  • tests/ — the fastest way to learn the intended call signatures for the 2023 release, since examples in the README sometimes lag the API.

First use case: a hot slug lookup

use Authentik\EloquentCache\Cacheable;

class Post extends Model
{
    use Cacheable;
}

Then, in a controller or repository:

$post = Post::cache()->where('slug', $slug)->first();

That single change is the lowest-risk win: read-heavy, rarely-changing, keyed by a unique column. Run it behind a feature flag, watch hit rate for a day, then expand to list endpoints. Do not start by wrapping your admin CRUD or anything that writes frequently — cache invalidation is where this package (and every cache layer) earns its keep.

Implementation Patterns

Read-through caching on hot lookups

// Detail page
$post = Post::cache()->findOrFail($id);

// Home page list
$latest = Post::cache()
    ->where('published', true)
    ->latest()
    ->limit(10)
    ->get();

Keep a wrapper in a repository class so the cache call lives in one place:

class PostRepository
{
    public function findBySlug(string $slug): ?Post
    {
        return Post::cache()->where('slug', $slug)->first();
    }
}

This makes it trivial to remove the package later, or to swap it for Cache::remember() if you outgrow it.

Invalidate on write — and be explicit about it

The package typically hooks saved/deleted events to flush the model's tag, but you must verify this against the source. If it only exposes a manual flushCache(), drive it from a model observer or a booted() hook:

protected static function booted(): void
{
    static::saved(fn (Post $post) => $post->flushCache());
    static::deleted(fn (Post $post) => $post->flushCache());
}

Also flush on restored if you use soft deletes, and consider flushing related models' tags when a post changes (e.g. Cache::tags(['posts', 'home'])->flush()).

Tag strategy: per-model and per-record

Two levels of tags cover most needs:

  • posts — flushed when any post changes (list pages, feeds).
  • post:{id} — flushed when that post changes (detail pages).

Detail pages use the narrow tag; lists and aggregates use the broad tag. This keeps a single edit from nuking the whole app's cache.

Cache expensive computed attributes

If your model exposes a getStatsAttribute() that hits another table or an external API, cache the attribute rather than the whole row. Use the attribute trait the package provides and define which attributes are worth caching:

use Authentik\EloquentCache\Cacheable;
use Authentik\EloquentCache\CacheableAttributes;

class Product extends Model
{
    use Cacheable, CacheableAttributes;

    protected array $cacheableAttributes = ['stats', 'price_history'];
}

Only list deterministic, serializable values here. Anything depending on auth()->user() or the current request must not be cached.

TTL layering

Give volatile data a short TTL and stable reference data a long one. Override the package's TTL method per model:

public function cacheTtl(): int
{
    return 300; // 5 minutes for a product detail
}

Pair a short TTL with tag flushing on write — TTL is a safety net, not the invalidation mechanism.

Testing

Use the array driver in phpunit.xml so each test starts cold, and assert both the cached and uncached path:

protected function setUp(): void
{
    parent::setUp();
    config(['cache.default' => 'array']);
    Cache::flush();
}

Write a test that updates a model and asserts the next read reflects the change. That is the only test that reliably catches broken invalidation.

Gotchas and Tips

The license is GPL-3.0. This is the biggest risk for a commercial Laravel app. GPL is copyleft: if you distribute the application (SaaS is generally fine; shipping on-prem or a desktop/CLI binary is not), the GPL obligations may extend to the whole work. Get legal sign-off before adopting, or plan to reimplement the ~200 lines yourself. With 0 dependents and 57 stars, you are also the only maintainer of a fork if the upstream stalls.

Tag support is non-negotiable. Every cache() call goes through the tagged store. Redis or Memcached in production; array in tests. Never file or database.

Query-builder writes bypass events. Post::where('slug', $slug)->update([...]) fires no model events, so no automatic flush happens. Wrap bulk writes with an explicit Cache::tags(['posts', "post:{$id}"])->flush() or use the package's flush helper immediately after. This is the #1 source of "the cache is lying" bugs.

Serialization limits. Cached models are serialized and stored. Custom casts that produce non-serializable objects (closures, PDO handles, Guzzle clients, some Carbon-like value objects with resources) will throw on write or come back corrupted. Test with a model that has your most exotic cast.

Eager-loaded relations bloat the cache. If you cache Post::cache()->with('comments')->first(), every comment is baked into the cached payload. It never reflects new comments unless you flush the post's tag on comment creation. Either cache the model without relations and fetch relations live, or add a comments tag and flush it in the Comment observer.

Pagination is not cache-friendly. paginate() on a cacheable builder may cache a paginator whose total goes stale, or skip caching entirely. Cache the data (IDs or a get() of a page) and construct the paginator around it, or accept uncached pagination for list pages.

Use findMany over N× find. Repeated Post::cache()->find($id) inside a loop still makes N cache round-trips. Batch with a single whereIn and rely on the package (or a Cache::many) to reduce Redis chatter.

Debugging stale reads. Log the cache key you expect ($post->cacheKey() if exposed, or reconstruct the tag name) and hit Redis directly:

redis-cli --scan --pattern '*post*' | head

In development, register a cache listener to see hits/misses and the key that was requested:

Cache::listen(fn ($event) => logger()->debug(class_basename($event), (array) $event));

Version compatibility. Last release was April 2023. Verify it installs cleanly against Laravel 11/12 (composer why-not will tell you fast). If the constraint blocks you, pin it with a composer.json replace or fork — the codebase is small enough that a fork is usually cheaper than fighting version constraints.

Cache stampedes. A popular key expiring at the same moment causes thundering-herd spikes on your database. For genuinely hot keys, wrap the fetch in Cache::lock() or use a short jittered TTL to spread expiry.

Extension points to lean on. Override cacheKey(), cacheTags(), and cacheTtl() on the model to control behavior without touching the package. That is where you encode domain rules like "a post's detail cache is valid until its updated_at changes" or "reference data lives for a day."

Repeated reads within one request still round-trip to Redis unless the package memoizes per-instance. If you are calling .cache() several times in a single request for the same record, hoist the result into a local variable — cheap insurance.

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