mikebronner/laravel-model-caching

Automatic caching for Eloquent models.

Maintainers

Package info

github.com/mikebronner/laravel-model-caching

pkg:composer/mikebronner/laravel-model-caching

Statistics

Installs: 112 728

Dependents: 3

Suggesters: 0

Stars: 2 348

Open Issues: 1

13.0.2 2026-03-02 15:50 UTC

This package is auto-updated.

Last update: 2026-03-02 15:52:50 UTC


README

Laravel Package Packagist GitHub license PHP Version Laravel Latest Stable Version GitHub Stars codecov Tests

Model Caching for Laravel masthead image

πŸ—‚οΈ Table of Contents

πŸ“– Summary

Automatic, self-invalidating Eloquent model and relationship caching. Add a trait to your models and all query results are cached automatically β€” no manual cache keys, no forgetting to invalidate. When a model is created, updated, or deleted the relevant cache entries are flushed for you.

⚑ Typical performance improvements range from 100–900% reduction in database queries on read-heavy pages. πŸ§ͺ Backed by 335+ integration tests across PHP 8.2–8.5 and Laravel 11–13.

Use this package when your application makes many repeated Eloquent queries and you want a drop-in caching layer that stays in sync with your data without any manual bookkeeping.

πŸ”„ Before & After

❌ Without this package β€” manual cache keys, manual invalidation:

$posts = Cache::remember('posts:active:page:1', 3600, function () {
    return Post::where('active', true)->with('comments')->paginate();
});

// And in every observer or event listener…
Cache::forget('posts:active:page:1');
// Hope you remembered every key variant! πŸ˜…

βœ… With this package β€” add the trait, query normally:

// Just query. Caching and invalidation happen automatically. ✨
$posts = Post::where('active', true)->with('comments')->paginate();

βœ… What Gets Cached

  • Model queries (get, first, find, all, paginate, pluck, value, exists)
  • Aggregations (count, sum, avg, min, max)
  • Eager-loaded relationships (via with())

🚫 What Does Not Get Cached

  • Lazy-loaded relationships β€” only eager-loaded (with()) relationships are cached. Use with() to benefit from caching.
  • Queries using select() clauses β€” custom column selections bypass the cache.
  • Queries inside transactions β€” cache is not automatically flushed when a transaction commits; call flushCache() manually if needed.
  • inRandomOrder() queries β€” caching is automatically disabled since results should differ each time.

πŸ’Ύ Cache Drivers

Driver Supported
Redis βœ… (recommended)
Memcached βœ…
APC βœ…
Array ❌
File ❌
Database ❌
DynamoDB ❌

πŸ“‹ Requirements

  • PHP 8.2+
  • Laravel 11, 12, or 13

πŸ“¦ Installation

composer require genealabs/laravel-model-caching

✨ The service provider is auto-discovered. No additional setup is required.

πŸš€ Getting Started

Add the Cachable trait to your models. The recommended approach is a base model that all other models extend:

<?php

namespace App\Models;

use GeneaLabs\LaravelModelCaching\Traits\Cachable;
use Illuminate\Database\Eloquent\Model;

abstract class BaseModel extends Model
{
    use Cachable;
}

Alternatively, extend the included CachedModel directly:

<?php

namespace App\Models;

use GeneaLabs\LaravelModelCaching\CachedModel;

class Post extends CachedModel
{
    // ...
}

πŸŽ‰ That's it β€” all Eloquent queries and eager-loaded relationships on these models are now cached and automatically invalidated.

⚠️ Note: You can cache the User model β€” the Cachable trait does not conflict with Laravel's authentication. Just avoid using cache cool-down periods on it, and ensure user updates always go through Eloquent (not raw DB::table() queries) so cache invalidation fires correctly.

🌍 Real-World Example

Consider a blog with posts, comments, and tags:

class Post extends BaseModel
{
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}

// All cached automatically β€” the query, the eager loads, everything. πŸͺ„
$posts = Post::with('comments', 'tags')
    ->where('published', true)
    ->latest()
    ->paginate(15);

When a new comment is created, the cache for Post and Comment queries is automatically invalidated β€” no manual Cache::forget() calls needed. 🧹

βš™οΈ Configuration

Publish the config file:

php artisan modelCache:publish --config

This creates config/laravel-model-caching.php:

return [
    'cache-prefix'         => '',
    'enabled'              => env('MODEL_CACHE_ENABLED', true),
    'use-database-keying'  => env('MODEL_CACHE_USE_DATABASE_KEYING', true),
    'store'                => env('MODEL_CACHE_STORE'),
    'fallback-to-database' => env('MODEL_CACHE_FALLBACK_TO_DB', false),
];

πŸ”§ Environment Variables

Variable Default Description
MODEL_CACHE_ENABLED true βœ… Enable or disable caching globally.
MODEL_CACHE_STORE null πŸ’Ύ Cache store name from config/cache.php. Uses the default store when not set.
MODEL_CACHE_USE_DATABASE_KEYING true πŸ”‘ Include database connection and name in cache keys. Important for multi-tenant or multi-database apps.
MODEL_CACHE_FALLBACK_TO_DB false πŸ›‘οΈ When true, falls back to direct database queries if the cache backend is unavailable (e.g. Redis is down) instead of throwing an exception.

πŸ“ Note: The cache-prefix option is set directly in the config file (not via an environment variable). For dynamic prefixes (e.g. multi-tenant), use the per-model $cachePrefix property shown below.

πŸ’Ύ Custom Cache Store

To use a dedicated cache store for model caching, define one in config/cache.php and reference it:

MODEL_CACHE_STORE=model-cache

🏷️ Cache Key Prefix

For multi-tenant applications you can isolate cache entries per tenant. Set the prefix globally in config:

'cache-prefix' => 'tenant-123',

Or per-model via a property:

<?php

namespace App\Models;

use GeneaLabs\LaravelModelCaching\Traits\Cachable;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use Cachable;

    protected $cachePrefix = 'tenant-123';
}

πŸ”Œ Multiple Database Connections

When use-database-keying is enabled (the default), cache keys automatically include the database connection and name. This keeps cache entries separate across connections without any extra configuration.

🚫 Disabling Cache

There are three ways to bypass caching:

1. Per-query (only affects this query chain, not subsequent queries):

$results = MyModel::disableCache()->where('active', true)->get();

2. Globally via environment:

MODEL_CACHE_ENABLED=false

3. For a block of code:

$result = app('model-cache')->runDisabled(function () {
    return MyModel::get();
});

// or via the Facade
use GeneaLabs\LaravelModelCaching\Facades\ModelCache;

ModelCache::runDisabled(function () {
    return MyModel::get();
});

πŸ’‘ Tip: Use option 1 in seeders to avoid pulling stale cached data during reseeds.

❄️ Cache Cool-Down Period

In high-traffic scenarios (e.g. frequent comment submissions) you may want to prevent every write from immediately flushing the cache. Cool-down requires two steps:

Declare the default duration on the model (this alone does nothing β€” it just sets the value):

<?php

namespace App\Models;

use GeneaLabs\LaravelModelCaching\Traits\Cachable;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    use Cachable;

    protected $cacheCooldownSeconds = 300; // 5 minutes ⏱️
}

Activate the cool-down by calling withCacheCooldownSeconds() in your query. This writes the cool-down window into the cache store:

// Activate using the model's default (300 seconds)
Comment::withCacheCooldownSeconds()->get();

// Or override with a specific duration
Comment::withCacheCooldownSeconds(30)->get();

Once activated, writes during the cool-down window will not flush the cache. After the window expires, the next write triggers a flush and re-warms the cache. πŸ”„

πŸ›‘οΈ Graceful Fallback

When enabled, if the cache backend (e.g. Redis) is unavailable the package logs a warning and falls back to querying the database directly β€” your application continues to function without caching rather than throwing an exception.

MODEL_CACHE_FALLBACK_TO_DB=true

🧹 Cache Invalidation

Cache is automatically flushed when:

Trigger Behavior
Model created Flush model cache
Model updated/saved Flush model cache
Model deleted Flush only if rows were actually deleted
Model force-deleted Flush only if rows were actually deleted
Pivot attach / detach / sync / updateExistingPivot Flush relationship cache
increment / decrement Flush model cache
insert / update (builder) Flush model cache
truncate Flush model cache

Cache tags are generated for the primary model, each eager-loaded relationship, joined tables, and morph-to target types, so only the relevant entries are invalidated. 🎯

πŸ”— BelongsToMany with Custom Pivot Models

Cache invalidation works for BelongsToMany relationships using custom pivot models (->using(CustomPivot::class)) as long as either the parent or the related model uses the Cachable trait.

🧹 Manual Cache Flushing

Artisan command β€” single model:

php artisan modelCache:clear --model='App\Models\Post'

Artisan command β€” all models:

php artisan modelCache:clear

πŸ”§ Programmatic via Facade:

use GeneaLabs\LaravelModelCaching\Facades\ModelCache;

// Single model
ModelCache::invalidate(App\Models\Post::class);

// Multiple models
ModelCache::invalidate([
    App\Models\Post::class,
    App\Models\Comment::class,
]);

⏰ Cache Expiration (TTL)

Cached queries are stored indefinitely (rememberForever) and rely on automatic invalidation (see above) to stay fresh. There is no per-query TTL option. If you need time-based expiry, use the cool-down period feature or flush the cache on a schedule via the Artisan command.

πŸ§ͺ Testing

In your test suite you can either disable model caching entirely or use the array cache driver:

🚫 Disable caching in tests:

// In your TestCase setUp() or phpunit.xml
config(['laravel-model-caching.enabled' => false]);

βœ… Use the array driver (useful for testing cache behavior itself):

config(['cache.stores.model-test' => ['driver' => 'array']]);
config(['laravel-model-caching.store' => 'model-test']);

πŸ‘· Queue Workers

The package has no special queue or Horizon integration. Cached queries inside queued jobs work the same as in HTTP requests. Cache invalidation triggered in a web request is immediately visible to queue workers (assuming a shared cache store like Redis). No additional configuration is needed.

🀝 Contributing

Contributions are welcome! πŸŽ‰ Please review the Contribution Guidelines and observe the Code of Conduct before submitting a pull request.

⬆️ Upgrading

For breaking changes and upgrade instructions between versions, see the Releases page on GitHub.

πŸ” Security

Please review the Security Policy for information on supported versions and how to report vulnerabilities.

πŸ“š Further Reading

The test suite serves as living documentation β€” browse it for detailed examples of every supported query type, relationship pattern, and edge case. πŸ“–

Built with ❀️ for the Laravel community using lots of β˜•οΈ by Mike Bronner.

This is an MIT-licensed open-source project. Its continued development is made possible by the community. If you find it useful, please consider πŸ’– becoming a sponsor and ⭐ing it on GitHub.

πŸ™ Thank you to all contributors who have helped make this package better!