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

byerikas/eloquent-query-cache

Eloquent Query Cache is a Laravel package for caching Eloquent query results, reducing repeated database hits and improving performance in read-heavy apps. It plugs into Laravel’s cache system to store and retrieve query results with minimal code changes.

View on GitHub
Deep Wiki
Context7

Getting Started

This release (v1.2.0) is a feature release on top of v1.1.0 — no breaking changes, no deprecations, so upgrading is a composer update byerikas/eloquent-query-cache away. It adds two things worth knowing on day one:

  1. exists() support — existence checks now go through the caching builder instead of silently bypassing or erroring.
  2. Property/function passthrough on builder creation — extra property reads and method calls are forwarded when the cached builder instance is constructed, which matters if you use custom EloquentBuilder subclasses, builder macros, or custom properties on your builder.

Where to look first:

  • Publish and read the config: php artisan vendor:publish --tag=... (check the package README for the exact tag) — TTL, cache store, and key prefix live there.
  • Find the trait the package ships (the thing you add to a model, typically a QueryCacheable-style trait) and read what it overrides — usually newEloquentBuilder() plus model event hooks for invalidation.
  • Check which cache store you're on. Tag-based invalidation needs Redis or Memcached; the file/database drivers will not give you selective flushing.

First use case: put the trait on one read-heavy, rarely-written model (a categories table, a countries/settings lookup, a CMS taxonomy) and cache a list query. Confirm the hit path with Cache::getStore() + a debug query log before rolling it out to anything with a write path.

// typical shape — exact trait name per package docs
class Category extends Model
{
    use QueryCacheable;

    // optional per-model overrides
    public $cacheFor = 3600;
}

Implementation Patterns

Trait-on-the-model, cache-on-the-query. The package's core value is that you don't change call sites: Category::where('active', 1)->get() is cached transparently. That means adoption is incremental — add the trait to a model, verify invalidation, move to the next model. Don't add it to all models at once.

Cache the read-heavy, write-light models. Lookups, taxonomies, menu trees, config-ish tables, dashboards aggregating over reference data. Avoid it on models that are written in the same request that reads them — you pay key generation and invalidation for nothing.

Use exists() deliberately now. With v1.2.0, Model::where(...)->exists() is a cached call. This is a real win for guard-style checks in middleware/policies (if (Permission::where(...)->exists())), which otherwise hit the DB several times per request. But it also means an exists() result is only as fresh as your invalidation — for authorization-critical checks, either bypass the cache for that query or make sure the write path flushes.

Rely on model-event invalidation, and know its boundary. The package flushes on saved/deleted events. That covers $model->save(), create(), delete(), and Eloquent-level update(). It does not cover Model::where(...)->update([...]), DB::table(...)->update(), raw SQL, or writes from another service. Pattern: for bulk writes, wrap them and fire the invalidation manually (or touch the models first). Where another application writes to the same tables, prefer short TTLs over expecting correctness.

Custom builders and macros are the point of the v1.2.0 passthrough. If you have your own builder class via newEloquentBuilder() or macros registered on Illuminate\Database\Eloquent\Builder, this release is what makes the cached builder behave like the real one for those extra property/function accesses. Pattern:

// custom builder macro — now reachable through the cached builder
Category::macro('activeWithCounts', fn () => $this->where('active', 1)->withCount('posts'));

Test these explicitly after upgrading; passthrough of some properties doesn't guarantee every exotic magic access is covered.

Combine with eager loading, not instead of it. Cache the query result and keep with() in the query — the eager loads are part of the cached payload, so you avoid the N+1 and the base query on a hit.

Per-query opt-out. Anything user-specific (dashboards filtered by auth()->id(), search results, paginated admin tables) should bypass the cache. Either use the package's disable mechanism for that call or, cleaner, keep sensitive models untraited and use Cache::remember() explicitly there with a key that includes the user ID.

Testing integration. Flush the cache in setUp() (or use RefreshDatabase plus a cache clear) — otherwise cached rows leak between tests, and a "passing" test can be reading data from three tests ago. Assert invalidation explicitly: create → read → update → assert the read changed.

Gotchas and Tips

Cache keys are derived from SQL + bindings. Two queries that look different but produce the same SQL and bindings share a key (usually fine), and two that look the same but differ in connection, with() clauses, or global scopes may or may not collide depending on how the key is built. If you use global scopes (multi-tenancy, soft deletes), verify the tenant key is part of the cache key — a multi-tenant app with a shared cache and tenant-agnostic keys is a data-leak bug, not a performance bug. Test this before deploying.

Mass updates silently break freshness. Post::where('category_id', 3)->update(['published' => true]) fires no model events. Cached lists referencing category 3 keep lying until TTL expiry. Mitigations: shorter TTL on those models, explicit flush in the command/action, or route writes through Eloquent.

Transactions. A read inside an open transaction that then rolls back can populate the cache with data that never existed. If you read-then-write in one transaction, defer the cached read until after commit, or bypass the cache inside transactional code.

exists() after upgrade. This is the sharpest new edge in v1.2.0. Behaviour that previously always hit the DB now may return a cached boolean. Grep for ->exists() on newly-traited models — especially in policies, middleware, and uniqueness validation paths — and decide per call site whether staleness is acceptable. Unique-email validation using a cached exists() is the kind of thing that produces a duplicate row in production.

Passthrough is not full delegation. v1.2.0 forwards extra properties and method calls at builder construction. That covers most custom-builder and macro usage, but if you rely on a builder that does nontrivial work in its constructor or holds per-instance state, test it. If something misbehaves, the extension point is your own newEloquentBuilder() returning a subclass that composes the package's caching behaviour rather than fighting the passthrough.

Aggregates and iteration. count(), sum(), avg() and get() are the natural cache targets. cursor(), lazy(), chunk() and each() stream rows and generally shouldn't be expected to be cached — treat them as uncached and don't build correctness assumptions (or performance budgets) around them.

Tag support depends on the store. Flushing an individual model's entries requires a taggable store. On the file driver you'll be flushing everything or waiting for TTL. Set the store explicitly in config rather than inheriting CACHE_DRIVER from a default that differs between local, CI, and production — this is the single most common "works locally, stale in prod" cause with cache packages.

Debugging. When a value looks stale: (a) confirm you're on the store you think you are, (b) log the generated cache key and check whether the write path produced a different key, (c) check whether the write actually went through Eloquent, (d) check TTL. Nine times out of ten it's (c).

Config quirks. TTL as 0/null often means "forever" rather than "disabled" — read the config comments, don't assume. Likewise a false-y toggle is not always the same as an absent key. Set values explicitly for each environment.

Upgrade advice. v1.2.0 is additive: bump the constraint, run your test suite with a cache flush in setup, then specifically exercise (i) any exists() calls on traited models and (ii) any custom builder macros/properties. If both behave, no other migration work is needed.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource