PHPackages                             kai-init/laravel-normcache - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. kai-init/laravel-normcache

ActiveLibrary[Database &amp; ORM](/categories/database)

kai-init/laravel-normcache
==========================

Normalized caching for Laravel Eloquent. Self-invalidating, Redis-backed. Caches query IDs and model entities separately with versioned invalidation.

v2.0.0(1w ago)246↑30.4%MITPHPPHP ^8.2CI failing

Since May 8Pushed 1w agoCompare

[ Source](https://github.com/kai-init/laravel-normcache)[ Packagist](https://packagist.org/packages/kai-init/laravel-normcache)[ RSS](/packages/kai-init-laravel-normcache/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (5)Dependencies (6)Versions (7)Used By (0)

Laravel Normcache
=================

[](#laravel-normcache)

**Normalized caching for Laravel Eloquent. Self-invalidating, Redis-backed.**

[![Tests](https://github.com/kai-init/laravel-normcache/actions/workflows/tests.yml/badge.svg)](https://github.com/kai-init/laravel-normcache/actions/workflows/tests.yml)[![PHPStan](https://camo.githubusercontent.com/0729e562e10fac943b16dbb271b4af26488f779a33fc82cc3eef1e37a432c0b4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230352d627269676874677265656e2e737667)](phpstan.neon)[![Latest Version on Packagist](https://camo.githubusercontent.com/ede8d924b2494840f74a00146640f36ea7cde6a4f4a73efa8edee9f53546f3f2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6b61692d696e69742f6c61726176656c2d6e6f726d63616368652e737667)](https://packagist.org/packages/kai-init/laravel-normcache)[![License](https://camo.githubusercontent.com/2305da5b933c8c591d490f40100740e5f68084867f62541aa0c7e70805a8179a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6b61692d696e69742f6c61726176656c2d6e6f726d63616368652e737667)](LICENSE)

Most caching packages store each query result as one serialized collection. Normcache takes a different approach: a query cache only stores the matching IDs, while each model's attributes live in their own key. The same model can appear in many cached queries but is only stored once, so a single version bump invalidates everything that returned it, in O(1).

```
query:{posts}:v3:...  →  [4, 7, 12]
model:{posts}:4       →  { id:4, title:..., body:... }
model:{posts}:7       →  { id:7, title:..., body:... }
model:{posts}:12      →  { id:12, title:..., body:... }

```

**Requirements:** PHP 8.2+, Laravel 11/12/13, Redis 4.0+

Table of Contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Usage](#usage)
- [Cache Bypasses](#cache-bypasses)
- [Limitations](#limitations)
- [Configuration](#configuration)
- [Observability](#observability)
- [Redis Clustering](#redis-clustering)
- [Octane &amp; Horizon](#octane--horizon)
- [Performance](#performance)
- [License](#license)

---

Installation
------------

[](#installation)

```
composer require kai-init/laravel-normcache
```

Add the `Cacheable` trait to any model you want cached:

```
use NormCache\Traits\Cacheable;

class Post extends Model
{
    use Cacheable;
}
```

---

Usage
-----

[](#usage)

### Basic Queries

[](#basic-queries)

```
Post::all();
Post::where('active', true)->get();
Post::find(1);
Post::paginate(20);
```

### Bypassing the Cache

[](#bypassing-the-cache)

```
Post::withoutCache()->get();
```

### Cross-Table Queries

[](#cross-table-queries)

Queries that span multiple tables are not cached by default — Normcache can't infer which model writes should invalidate them. `dependsOn()` lets you declare the dependency explicitly:

```
Author::whereHas('posts', fn($q) => $q->where('published', true))
    ->dependsOn([Post::class])
    ->get();

// Works for any query shape — JOIN, GROUP BY, DISTINCT, subquery WHERE, raw ORDER BY:
Author::join('posts', 'posts.author_id', '=', 'authors.id')->dependsOn([Post::class])->get();
Post::select('author_id', DB::raw('SUM(views) as total'))
    ->groupBy('author_id')->dependsOn([Post::class])->get();
```

All `dependsOn` queries are cached as versioned raw rows. When any declared model class is written, the versioned key becomes unreachable and the next read re-populates from the database. Pessimistic locks always bypass the cache.

**List every table the query reads.** An under-declared dependency means silent staleness until TTL. Use `tag()` / `flushTag()` when you need manual invalidation for events the model version system cannot see.

### Per-Query TTL

[](#per-query-ttl)

```
Post::query()->remember(600)->get();
```

### Aggregates

[](#aggregates)

`withCount`, `withSum`, `withAvg`, `withMin`, `withMax`, and `withExists` are cached automatically. The result set is cached as a single versioned blob and invalidated when any related model version changes.

```
Post::withCount('comments')->get();
Post::withoutAggregateCache()->withCount('comments')->get(); // skip aggregate cache
```

### Relationship Caching

[](#relationship-caching)

`BelongsTo`, `BelongsToMany`, `MorphTo`, `MorphToMany`, `MorphedByMany`, `HasManyThrough`, and `HasOneThrough` are cached for eager loads — on a warm hit no SQL is executed. `HasOne`, `HasMany`, `MorphOne`, and `MorphMany` are cached via the query cache when the related model uses `Cacheable`.

`attach`, `detach`, `sync`, and `updateExistingPivot` automatically invalidate the relevant pivot cache.

### Manual Flush

[](#manual-flush)

```
php artisan normcache:flush --model="App\Models\Post"
php artisan normcache:flush
```

```
NormCache::flushModel(Post::class);
NormCache::flushAll();
```

If you mutate cacheable tables outside Eloquent, flush manually after the write:

```
DB::table('posts')->update(['published' => true]);
NormCache::flushModel(Post::class);
```

### Tag-Based Flush

[](#tag-based-flush)

Tag any query to group cache entries for manual flushing — useful for invalidation events the version system can't see (deploys, config changes, nightly rebuilds). Tags must not contain `: { } *` or whitespace.

```
Author::whereHas('posts')->dependsOn([Post::class])->tag('homepage')->get();

NormCache::flushTag(Author::class, 'homepage');   // single model — single-slot scan
NormCache::flushTagAcrossModels('homepage');       // all models — cluster-wide scan
```

---

Cache Bypasses
--------------

[](#cache-bypasses)

Query featureWorkaroundPessimistic locking (`lockForUpdate` / `sharedLock`)None — must hit DBInside a database transactionNone — must hit DBRaw SQL / `DB::table(...)`None — flush manuallyEverything else — `JOIN`, `GROUP BY`, `DISTINCT`, subquery `WHERE`, raw `ORDER BY`, calculated columns — is cacheable with `dependsOn()`.

---

Limitations
-----------

[](#limitations)

- Normcache only hooks Eloquent models that use the `Cacheable` trait. Query builder calls such as `DB::table(...)`, `DB::select()`, and `DB::statement()` are never cached.
- Writes outside Eloquent are invisible to the model version system. Flush the affected model or tag manually after imports, raw updates, maintenance jobs, or external syncs.
- Dynamic connection switching (`Post::on('replica')`) is not supported. Use separate model classes with fixed `$connection` values when the same table is read through multiple connections.
- `dependsOn()` is explicit by design. If a query reads another table, include that model class or manually flush a tag that covers the query.
- Models are expected to use standard single-column primary keys.
- Packages that replace Eloquent builders, relation classes, or hydration behavior may bypass parts of Normcache.

---

Configuration
-------------

[](#configuration)

```
// config/normcache.php
return [
    'connection'        => env('NORMCACHE_CONNECTION', 'cache'),
    'enabled'           => env('NORMCACHE_ENABLED', true),
    'ttl'               => env('NORMCACHE_TTL', 604800),
    'query_ttl'         => env('NORMCACHE_QUERY_TTL', 3600),
    'key_prefix'        => env('NORMCACHE_PREFIX', ''),
    'slotting'          => env('NORMCACHE_SLOTTING', false),
    'cooldown'          => env('NORMCACHE_COOLDOWN', 0),
    'building_lock_ttl' => env('NORMCACHE_BUILDING_LOCK_TTL', 5),
    'stampede_wait_ms'  => env('NORMCACHE_STAMPEDE_WAIT_MS', 200),
    'stale_version_depth' => env('NORMCACHE_STALE_VERSION_DEPTH', 3),
    'cluster'           => env('NORMCACHE_CLUSTER', false),
    'events'            => env('NORMCACHE_EVENTS', true),
    'fallback'          => env('NORMCACHE_FALLBACK', false),
    'fire_retrieved'    => env('NORMCACHE_FIRE_RETRIEVED', false),
    'debugbar'          => env('NORMCACHE_DEBUGBAR', false),
];
```

- **`ttl`** — Lifetime of individual model attribute keys. Default: 7 days.
- **`query_ttl`** — Lifetime of query, raw, pivot, and through cache keys. Default: 1 hour.
- **`slotting`** — When `false` (default), all NormCache keys are placed on one Redis Cluster slot using the `{nc}` slot prefix.
- **`cooldown`** — Useful for write-heavy models. Version bump debounce in seconds. Consecutive writes within the window bump the version only once. Manual calls to `NormCache::flushModel()` always invalidate immediately regardless of this setting.
- **`building_lock_ttl`** — How long a cache-build lock is held before it expires and another request can take over.
- **`stampede_wait_ms`** — How long a waiter blocks on a wake channel before falling back to the database. Requires Redis 6.0+ for sub-second precision.
- **`stale_version_depth`** — How many old query-cache versions to serve as stale data while a rebuild is in progress. Set to `0` to disable stale serving. (`NORMCACHE_STALE_TTL_DEPTH` is accepted as a deprecated fallback.)
- **`fallback`** — When `true`, Redis exceptions disable the cache for the request and queries fall back to the database silently.
- **`events`** — Set to `false` to skip hit/miss event dispatches on hot paths.
- **`fire_retrieved`** — When `true`, models hydrated from Redis fire Eloquent's `retrieved` event.

---

Observability
-------------

[](#observability)

### Laravel Debugbar

[](#laravel-debugbar)

When [`fruitcake/laravel-debugbar`](https://github.com/fruitcake/laravel-debugbar) is installed, enable the Normcache collector:

```
'debugbar' => env('NORMCACHE_DEBUGBAR', false),
```

This adds a **Normcache** timeline tab showing every query hit, miss, bypass, and model fetch — with key, kind, and duration — for the current request.

### Events

[](#events)

EventFired whenProperties`QueryCacheHit`Cached query result served from Redis`modelClass`, `key``QueryCacheMiss`Query not cached — DB queried`modelClass`, `key``ModelCacheHit`Model attributes served from Redis`modelClass`, `ids[]``ModelCacheMiss`Model attributes not cached — DB queried`modelClass`, `ids[]`---

Redis Clustering
----------------

[](#redis-clustering)

By default, Redis Cluster support uses single-slot mode. With `cluster` enabled and `slotting` disabled, every NormCache key is prefixed with `{nc}:`, so cross-model operations can keep version checks, reads, and build-lock acquisition in one single-slot Lua command.

```
'cluster' => true,
'slotting' => false, // default
```

Set `slotting` to `true` only when you want Redis Cluster slot sharding across model groups. In sharded mode, single-model operations keep keys on one slot via per-model hash tags (`{posts}`, `{analytics:posts}`). Cross-model operations (`dependsOn`, pivot, through, `withCount`) resolve each model's version key with separate single-slot Lua calls, then read or write on the primary model's slot.

**Consistency note:** sharded cross-model version resolution is not atomic. A writer that bumps a dependency version between version reads may cause stale response before the next request uses the new version. This is the same eventually-consistent trade-off accepted by most distributed caches.

`flushAll()` is supported.

---

Octane &amp; Horizon
--------------------

[](#octane--horizon)

Works out of the box. State is reset between Octane requests and queue jobs — including re-enabling the cache if a Redis error disabled it mid-job.

---

Performance
-----------

[](#performance)

- **Single round trip on cache hit** — version check + ID fetch + model `MGET` in one Lua `EVAL`.
- **`MGET` for bulk reads** — all model attributes for a result set in one Redis call.
- **No scanning on invalidation** — version bump makes stale keys unreachable; TTL handles eviction.
- **Stampede protection** — waiters `BRPOP` a wake channel (200ms) instead of storming the DB. Requires Redis 6.0+ for sub-second precision; both PhpRedis and Predis support this.
- **igbinary support** — smaller payloads and faster serialization when the extension is installed.

---

License
-------

[](#license)

MIT

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance98

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~5 days

Total

5

Last Release

11d ago

Major Versions

v1.1.0 → v2.0.02026-05-29

### Community

Maintainers

![](https://www.gravatar.com/avatar/aa58f0ad5402722c001aba32eb45060090a0e4076eb2230dfe3338b123efc2a3?d=identicon)[kai-init](/maintainers/kai-init)

---

Top Contributors

[![kai-init](https://avatars.githubusercontent.com/u/32325424?v=4)](https://github.com/kai-init "kai-init (129 commits)")

---

Tags

cacheeloquenteloquent-cachelaravelmodel-cachingoctanephp-packagequery-cachingredisredis-clusterlaraveleloquentrediscachemodel-cacheQuery Cachenormcachenormalized-cache

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/kai-init-laravel-normcache/health.svg)

```
[![Health](https://phpackages.com/badges/kai-init-laravel-normcache/health.svg)](https://phpackages.com/packages/kai-init-laravel-normcache)
```

###  Alternatives

[anourvalar/eloquent-serialize

Laravel Query Builder (Eloquent) serialization

11122.5M32](/packages/anourvalar-eloquent-serialize)[mostafaznv/laracache

LaraCache is a customizable cache trait to cache queries on model's events

27249.4k2](/packages/mostafaznv-laracache)[spiritix/lada-cache

A Redis based, automated and scalable database caching layer for Laravel

592452.8k2](/packages/spiritix-lada-cache)[ymigval/laravel-model-cache

Laravel package for caching Eloquent model queries

7955.4k4](/packages/ymigval-laravel-model-cache)[mostafaznv/nova-laracache

LaraCache Tool for Laravel Nova

113.9k](/packages/mostafaznv-nova-laracache)[authentik/eloquent-cache

Easily cache your Laravel's Eloquent models

573.9k](/packages/authentik-eloquent-cache)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
