Looking to hire Laravel developers? Try LaraJobs

laravel-model-caching maintained by ghostcompiler

Description
Relationship-aware Eloquent model caching for Laravel with dependency tracking, cache tags, pagination-safe keys, and opt-in model APIs.
Author
Ghost Compiler
Last update
2026/06/01 22:49 (dev-main)
License
Links
Downloads
1

Comments
comments powered by Disqus

Laravel Model Caching

Relationship-aware Eloquent model caching for Laravel.

This package is designed for applications where cached parent queries must be invalidated when loaded child models change. It is not only a query cache. It stores deterministic query results and records a lightweight dependency index from model instances to cache keys.

Features

  • Opt-in model caching through HasModelCaching
  • remember(), rememberForever(), dontCache(), and optional auto_remember for trait-enabled models
  • Cached read operations: get, first, firstOrFail, find, findOrFail, and pagination methods
  • Nested eager-load aware cache keys
  • Pagination-safe keys for paginate(), simplePaginate(), and cursorPaginate()
  • Morph map versioning in cache keys
  • Dependency index for precise invalidation on child updates
  • Structured cache payloads for models and collections (safe for morph relations and eager loads)
  • Cache tags when the selected Laravel cache driver supports them
  • Model observer invalidation for saved, deleted, restored, and force deleted events
  • Artisan commands for warming, inspecting, and flushing tracked entries

Installation

composer require ghostcompiler/laravel-model-caching

Publish the config:

php artisan vendor:publish --tag=model-cache-config

Redis is recommended for production because the dependency index can use native Redis sets.

Quick Start

Add the trait to models that should be cacheable:

use GhostCompiler\LaravelModelCaching\Concerns\HasModelCaching;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use HasModelCaching;
}

The trait alone does not cache queries. Either call remember() on the chain or enable auto_remember in config.

Use remember() on normal Eloquent queries:

$users = User::with('posts.comments')
    ->where('active', true)
    ->remember(60)
    ->get();

$tenant = User::whereHas('brand.domains', fn ($q) => $q->where('domain', $host))
    ->with('brand')
    ->remember(300)
    ->firstOrFail();

Enable automatic caching for all read queries on trait-enabled models:

// config/model-cache.php
'auto_remember' => true,

Then User::where('active', true)->first() is cached with default_ttl. Use dontCache() on chains that must always hit the database.

Use the convenience APIs:

$user = User::findCached(1);

$user->loadCached('posts.comments');

Disable caching on a query chain:

$users = User::remember(600)
    ->dontCache()
    ->where('active', true)
    ->get();

Cache Key Safety

Each key includes:

  • Model class, table, and connection
  • SQL and bindings
  • Selected columns
  • Limit, offset, groups, and orders
  • Eager loads, including nested relation names
  • Closure constraint source fingerprints when available
  • Pagination page, page name, cursor, and per-page values
  • Morph map and configured morph map version
  • Optional auth and tenant context

These queries generate different keys:

User::with('posts')->remember(60)->get();
User::with('posts.comments')->remember(60)->get();
User::with(['posts', 'roles'])->remember(60)->get();

Dependency Invalidation

On a cache miss, the package stores the query result and walks the returned models plus loaded relations. It records only model primary keys in a dependency index:

model-cache:dependency:user:1 -> [cache-key-a]
model-cache:dependency:post:5 -> [cache-key-a, cache-key-b]
model-cache:dependency:comment:9 -> [cache-key-b]

When Post #5 is saved, deleted, or force deleted, only keys listed under model-cache:dependency:post:5 are forgotten. No full application cache flush is required.

When a new Post is created (or soft-deleted row is restored), every cached query rooted on that model class is forgotten too — for example paginated user lists — so new rows can appear on the next request without waiting for TTL expiry:

model-cache:dependency:user:{class-hash}:_class -> [page-1-key, page-2-key, ...]

Updates to an existing row still invalidate only that row's dependencies plus any list page that included that instance.

Cache tags are used as an extra layer when supported. Per-entry tag metadata is stored separately so entries can include result-specific tags such as post:5 while still being readable later from a deterministic query key.

Morph Relations

Polymorphic relations are supported through the actual related models returned by Eloquent and through morph context in the cache key. If you change Laravel's morph map aliases, bump:

'morph_map_version' => 2,

This prevents old entries from colliding with new alias meanings.

Pagination

Pagination methods are cached at the top-level builder operation:

$users = User::with('posts')->remember(300)->paginate(10);

The key includes the page name, current page, per-page value, and cursor state where applicable.

Configuration

return [
    'enabled' => true,
    'default_ttl' => 3600,
    'auto_remember' => false,
    'store' => null,
    'prefix' => 'model-cache',
    'cache_tags' => true,
    'dependency_ttl' => 604800,
    'use_redis_sets' => true,
    'auto_observe_models' => true,
    'include_auth_id' => false,
    'context_callbacks' => [],
    'morph_map_version' => 1,
    'debug' => false,
];

For multi-tenant apps, add a context callback:

'context_callbacks' => [
    'tenant' => fn () => tenant('id'),
],

Commands

Warm a query:

php artisan model-cache:warm "App\Models\User" --with=posts --with=posts.comments --ttl=600 --limit=1000

Inspect keys depending on a model instance:

php artisan model-cache:inspect "App\Models\Post" 5

Flush keys depending on a model instance:

php artisan model-cache:flush "App\Models\Post" 5

Flush every key known to the package dependency index:

php artisan model-cache:flush --known

Production Notes

  • Treat auto_remember carefully: incidental reads on cacheable models are cached until TTL or model invalidation. Use dontCache() in admin or debug paths.
  • Prefer Redis for high traffic APIs.
  • Keep cacheable queries deterministic.
  • Include tenant/auth context when query results differ by user or tenant.
  • Use eager loading intentionally. The package tracks loaded relations, not unloaded graph possibilities.
  • Bump morph_map_version after morph map changes.
  • Use rememberForever() only for data that is always invalidated by model events.

Testing

composer install
composer test

Development And Build Environment

This package was developed using ServBay as the local development environment.

Development Tool Used

ServBay your development friend

Testing And Build Machine

  • Tested on: Mac M4
  • Built on: Mac M4

License

MIT