morilog/infinity-cache
Infinity Cache for Laravel makes it easy to cache expensive Eloquent query results and keep them fresh: cached data is automatically invalidated when the underlying models change, so you get the speed of caching without stale reads or manual cache-busting code.
Install (Laravel 5.1/5.2 era — read the compatibility note below first):
composer require morilog/infinity-cache
There is no package auto-discovery (this predates Laravel 5.5). Register the provider manually in config/app.php:
'providers' => [
// ...
Morilog\InfinityCache\InfinityCacheServiceProvider::class,
],
Publish and review the config so you know which cache store and TTL it defaults to:
php artisan vendor:publish --provider="Morilog\InfinityCache\InfinityCacheServiceProvider"
Where to look first: read the package's README.md and its src/ directory before writing any code. The surface area is small — a trait, a service provider, a config file, and a query-builder wrapper — and the README is the only authoritative source for exact method names. Do not assume the API matches what you'd expect from a modern package.
First use case: pick a single hot read path — a homepage sidebar, a category listing, a find() on a detail page — and cache only that. The intended pattern is to opt a model in with the trait and route reads through a cache-aware entry point:
use Morilog\InfinityCache\InfinityCacheTrait;
class Post extends Model
{
use InfinityCacheTrait;
}
Then measure with and without caching before rolling it out further. This package's value proposition is infinite TTL plus automatic invalidation on model writes — so the first thing you want to prove is that a save()/delete() actually clears the key you just populated.
Cache the read, not the whole app. Treat this as a targeted optimization for specific expensive queries, not a global Eloquent decorator. A workable workflow:
Service-layer wrapping. Because the package is old and loosely documented, do not scatter its calls through controllers. Wrap it in your own repository/service so you can swap the implementation later:
class PostRepository
{
public function publishedForHomepage(int $limit = 10)
{
// cache-aware read path provided by the trait
return Post::cache()->where('status', 'published')
->orderByDesc('published_at')
->limit($limit)
->get();
}
}
That indirection is the single most valuable pattern here — it means the day you migrate off this package (which you should plan for), you change one class.
Write-path invalidation. The package hooks Eloquent model events. Keep that contract intact by never bypassing the model layer for writes on cached entities:
$post->update([...]), $post->save(), $post->delete()Post::where(...)->update([...]) — mass updates skip model events in Eloquent generallyDB::table('posts')->update(...) — bypasses everythingIf you legitimately need mass updates, wrap them with an explicit flush:
DB::transaction(function () {
Post::where('status', 'draft')->update(['status' => 'archived']);
// explicitly clear this model's cache namespace afterwards
});
Pre-warming. Infinite-TTL caches are cold on deploy and after a flush. Add a small artisan command or a post-deploy step that renders the hot pages (or calls your repository methods once) so the first real user isn't the one paying the query cost.
Grouped invalidation. If the package's store supports cache tags, group related models (e.g. posts, post:42, category:7) so a category rename can invalidate all dependent post listings. If it doesn't support tags on your chosen store, you're stuck with per-key invalidation — design your key strategy accordingly, and prefer coarse keys ("the homepage list") over fine-grained ones so there are fewer things to invalidate correctly.
Combination with eager loading. Cache the final hydrated collection, including relationships you need. Caching the parent query and then lazy-loading relations on each request defeats most of the benefit and risks N+1 inside the cached path.
Testing. Point the test environment at the array store, and flush between tests. Assert both halves of the contract:
public function test_post_cache_invalidates_on_save()
{
$post = factory(Post::class)->create();
$this->assertSame('A', app(PostRepository::class)->first($post->id)->title);
$post->update(['title' => 'B']);
$this->assertSame('B', app(PostRepository::class)->first($post->id)->title);
}
Treat this as abandoned software. Last release February 2016, 31 stars, zero dependents. Its composer.json will constrain illuminate/* to the Laravel 5.x line, which means it will not install on Laravel 6+ without --ignore-platform-reqs hacks, and it predates PHP 8 type-safety changes. Before you invest, run composer require in a throwaway app and confirm it resolves at all. If it doesn't, stop here.
Infinite TTL is the whole risk model. There is no natural expiry to save you from a missed invalidation. Every code path that mutates a cached model is now load-bearing. Enumerate them before shipping:
Cache store must support tags. Tag-based invalidation requires Redis, Memcached, or the array store. The file and database drivers do not support tags and will either error or silently misbehave. Check the package's expected store against your config/cache.php default before anything else.
Bindings and key derivation. Query-derived cache keys are typically built from the SQL plus bindings. Watch for:
where() ordering may produce different keys,Carbon/DateTime objects serialized into keys,null vs empty string bindings,Serialization of models. Cached entries hold serialized Eloquent instances. If you later rename a column, change a cast, or remove an attribute, deserializing a stale cached model can produce confusing errors or silently missing data. Flush the cache on every deploy that touches a cached model's schema or casts.
Never cache authenticated or per-user scopes under a shared key. Anything downstream of Auth::id(), tenant IDs, or permission checks must either be excluded or have the user/tenant baked into the key. Getting this wrong is a data-leak bug, not a performance bug.
Unbounded growth. "Infinity" also describes the key space if you cache per-ID lookups without eviction. Monitor your Redis memory. If the package offers per-call TTL overrides, use a finite TTL for high-cardinality keys (per-post detail pages) and reserve infinite TTL for low-cardinality, high-value keys (site settings, nav).
Debugging technique. When something looks stale, don't guess — inspect the store directly:
redis-cli --scan --pattern '*posts*' | head -50
Then write to the model and confirm those keys disappear. If they don't, the write path bypassed Eloquent. Also check whether a saved/deleted listener is being suppressed by a transaction that rolls back, or by an observer registered after the package's.
Extension points. The trait is the natural place to override. If you need custom key generation, tenant scoping, or logging, override the relevant method in your model (or better, in a shared base model) rather than patching the vendor directory. Avoid editing vendor/ — it will be lost on the next composer install.
Migration path — plan it now. For a Laravel project today, the same problem is solved by maintained alternatives: genealabs/laravel-model-caching for model-level caching with automatic invalidation, or plain Cache::remember() / Cache::flexible() (Laravel 11+) inside a repository for query-level caching. Given this package's age and adoption, the pragmatic recommendation is: prototype with it only if you're pinned to Laravel 5.x, and otherwise implement the invalidation pattern yourself in ~50 lines inside a repository class. You'll get the same benefit with a supported dependency and full control over the key strategy.
How can I help you explore Laravel packages today?