mike-bronner / laravel-model-caching
Automatic caching for Eloquent models.
Package info
github.com/mike-bronner/laravel-model-caching
pkg:composer/mike-bronner/laravel-model-caching
Requires
- php: >=8.2
- illuminate/cache: ^11.0|^12.0|^13.0
- illuminate/config: ^11.0|^12.0|^13.0
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/container: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- mikebronner/laravel-pivot-events: *
Requires (Dev)
- aws/aws-sdk-php: ^3.322.9
- barryvdh/laravel-debugbar: ^3.15
- barryvdh/laravel-ide-helper: ^3.5
- doctrine/dbal: ^3.3|^4.2
- fakerphp/faker: ^1.11
- larastan/larastan: ^2.0|^3.0
- laravel/boost: ^2.0
- laravel/pint: ^1.27
- mikebronner/development-settings: ^0.1.15
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpunit/phpunit: ^10.5|^11.5|^12.5
- slevomat/coding-standard: ^7.0|^8.14
- squizlabs/php_codesniffer: ^3.6|^4.0
- symfony/thanks: ^1.2
Suggests
- aws/aws-sdk-php: Required for DynamoDB cache store support (^3.322)
Replaces
- genealabs/laravel-model-caching: 13.1.0
- mikebronner/laravel-model-caching: 13.1.0
- dev-master
- 13.1.0
- 13.0.6
- 13.0.4
- 13.0.3
- 13.0.2
- 13.0.1
- 13.0.0
- 12.1.0-RC3
- 12.1.0-RC2
- 12.1.0-RC1
- 12.0.4
- 12.0.3
- 12.0.2
- 12.0.1
- 12.0.0
- 11.0.1
- 11.0.0
- 0.13.9
- 0.13.8
- 0.13.7
- 0.13.6
- 0.13.5
- 0.13.4
- 0.13.3
- 0.13.2
- 0.13.1
- 0.13.0
- 0.12.5
- 0.12.4
- 0.12.3
- 0.12.2
- 0.12.1
- 0.12.0
- 0.11.7
- 0.11.6
- 0.11.5
- 0.11.4
- 0.11.3
- 0.11.2
- 0.11.1
- 0.11.0
- 0.10.2
- 0.10.1
- 0.10.0
- 0.9.0
- 0.8.10
- 0.8.9
- 0.8.8
- 0.8.7
- 0.8.6
- 0.8.5
- 0.8.4
- 0.8.3
- 0.8.2
- 0.8.1
- 0.8.0
- 0.7.4
- 0.7.3
- 0.7.2
- 0.7.1
- 0.7.0
- 0.6.3
- 0.6.2
- 0.6.1
- 0.6.0
- 0.5.6
- 0.5.5
- 0.5.4
- 0.5.3
- 0.5.2
- 0.5.1
- 0.5.0
- 0.4.24
- 0.4.23
- 0.4.22
- 0.4.21
- 0.4.20
- 0.4.19
- 0.4.18
- 0.4.17
- 0.4.16
- 0.4.15
- 0.4.14
- 0.4.13
- 0.4.12
- 0.4.11
- 0.4.10
- 0.4.9
- 0.4.8
- 0.4.7
- 0.4.6
- 0.4.5
- 0.4.4
- 0.4.3
- 0.4.2
- 0.4.1
- 0.4.0
- 0.3.7
- 0.3.6
- 0.3.5
- 0.3.4
- 0.3.3
- 0.3.2
- 0.3.1
- 0.3.0
- 0.2.64
- 0.2.63
- 0.2.62
- 0.2.61
- 0.2.60
- 0.2.59
- 0.2.58
- 0.2.57
- 0.2.56
- 0.2.55
- 0.2.54
- 0.2.53
- 0.2.52
- 0.2.51
- 0.2.50
- 0.2.49
- 0.2.48
- 0.2.47
- 0.2.46
- 0.2.45
- 0.2.44
- 0.2.43
- 0.2.42
- 0.2.41
- 0.2.40
- 0.2.39
- 0.2.38
- 0.2.37
- 0.2.36
- 0.2.35
- 0.2.34
- 0.2.33
- 0.2.32
- 0.2.31
- 0.2.30
- 0.2.29
- 0.2.28
- 0.2.27
- 0.2.26
- 0.2.25
- 0.2.24
- 0.2.23
- 0.2.22
- 0.2.21
- 0.2.20
- 0.2.19
- 0.2.18
- 0.2.17
- 0.2.16
- 0.2.15
- 0.2.14
- 0.2.13
- 0.2.12
- 0.2.11
- 0.2.10
- 0.2.9
- 0.2.8
- 0.2.7
- 0.2.6
- 0.2.5
- 0.2.4
- 0.2.3
- 0.2.2
- 0.2.1
- 0.2.0
- 0.1.0
- dev-moe/588-cacheable-trait-serialize-for-serializable-classes
This package is auto-updated.
Last update: 2026-04-07 15:03:40 UTC
README
ποΈ Table of Contents
- π Summary
- π¦ Installation
- π Getting Started
- βοΈ Configuration
- π€ Contributing
- β¬οΈ Upgrading
- π Security
- π Further Reading
π 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. Usewith()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 | β |
| DynamoDB | β |
| Array | β |
| File | β |
| Database | β |
π 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
Usermodel β theCachabletrait 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 rawDB::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-prefixoption is set directly in the config file (not via an environment variable). For dynamic prefixes (e.g. multi-tenant), use the per-model$cachePrefixproperty 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
βοΈ DynamoDB Cache Store
DynamoDB is supported when your selected Laravel cache store uses the
dynamodb driver:
MODEL_CACHE_STORE=dynamodb-model AWS_ACCESS_KEY_ID=your-access-key AWS_SECRET_ACCESS_KEY=your-secret-key AWS_DEFAULT_REGION=us-east-1 AWS_DYNAMODB_CACHE_ENDPOINT= AWS_DYNAMODB_CACHE_TABLE=cache
Define the store in config/cache.php using the same fields Laravel documents
for the DynamoDB cache driver:
'stores' => [ 'dynamodb-model' => [ 'driver' => 'dynamodb', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'table' => env('AWS_DYNAMODB_CACHE_TABLE', 'cache'), 'endpoint' => env('AWS_DYNAMODB_CACHE_ENDPOINT'), 'attributes' => [ 'key' => 'key', 'value' => 'value', 'expiration' => 'expires_at', ], ], ],
If your application does not already require it, install the AWS SDK:
composer require aws/aws-sdk-php
Enable DynamoDB TTL on the table's expires_at attribute as described in the
Laravel cache docs.
How invalidation works on DynamoDB
Model invalidation on DynamoDB uses logical namespace versioning instead of native cache tags:
modelCache:clearrotates a package-wide namespace key.- Model and relationship invalidation rotate per-tag namespace keys.
- Cached query rows become unreachable immediately after the namespace changes.
- Old query rows are not deleted eagerly; DynamoDB removes them later through TTL.
This package does not issue table scans or destructive flushes on DynamoDB.
TTL guidance
Laravel's DynamoDB cache store writes forever() entries with a long-lived
expiration instead of a truly unbounded item. In current Laravel releases that
window is several years, which means stale DynamoDB rows are bounded but can
linger for a long time after invalidation.
Practical guidance:
- Always enable TTL on
expires_at. - Treat DynamoDB cache invalidation as logical invalidation first and physical cleanup later.
- Expect dead query rows to accumulate temporarily on write-heavy or high-churn models.
- If you need faster physical cleanup than Laravel's long-lived cache TTL allows, Redis is usually a better fit.
Operational notes
- Namespace control keys do not grow without bound. The package stores one global namespace key plus one key per normalized tag hash.
- Multi-tag invalidation is not atomic. Tags are rotated one at a time, so a crash in the middle of an invalidation can leave a partial namespace rotation. A later invalidation will still converge the cache to the latest version.
- Tag control keys hash the raw tag string, so long or punctuation-heavy tags are supported. Namespace collisions are limited to theoretical SHA-1 collisions.
- Cache cool-down metadata intentionally bypasses namespace versioning and stays on the raw cache store.
Troubleshooting
modelCache:cleardid not shrink the DynamoDB table: expected. The command makes old rows unreachable; it does not physically delete every row.- Stale rows are still visible in DynamoDB: expected until TTL removes them.
- Frequent invalidations increase table size: expected on high-churn models because stale rows remain until TTL cleanup.
- Connection failures during reads: enable
MODEL_CACHE_FALLBACK_TO_DB=trueif you want query paths to fall back to the database during cache outages. - Connection failures during
modelCache:clear: the command now returns a non-zero exit code and prints the cache error instead of silently succeeding.
When to use DynamoDB vs Redis
- Use DynamoDB when you are already operating in AWS-native or serverless environments, want a managed cache store without running Redis, or need a simple multi-AZ DynamoDB-backed cache layer.
- Use Redis when you need lower latency, higher write churn, native tag support, or faster physical cleanup of invalidated cache data.
π·οΈ 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.
π Static Analysis (Larastan / PHPStan)
The package is compatible with Larastan
at level 5 and above. Because the Cachable trait wraps Eloquent's builder,
PHPStan may report "undefined method" errors for methods like cache() or
flushCache() on your models. To resolve these, add a @mixin annotation to
your cached model:
use GeneaLabs\LaravelModelCaching\Traits\Cachable; use Illuminate\Database\Eloquent\Model; /** * @mixin \GeneaLabs\LaravelModelCaching\CachedBuilder<\Illuminate\Database\Eloquent\Model> */ class Post extends Model { use Cachable; }
If you use a custom Eloquent builder that gets wrapped by CachedBuilder,
PHPStan cannot infer the custom methods from the CachedBuilder return type.
Add a @return override annotation on your model's newEloquentBuilder()
method, or add @mixin YourCustomBuilder to the model class.
The package ships with a phpstan-baseline.neon that suppresses internal
analysis errors in the package's own test fixtures. These do not affect
consumer projects.
π€ 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!