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.4.0(4w ago)277↓50%MITPHPPHP ^8.2CI passing

Since May 8Pushed 1mo 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 3w ago

READMEChangelog (10)Dependencies (12)Versions (16)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 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)

---

What's New in v2
----------------

[](#whats-new-in-v2)

Version 2 extends Normcache beyond normalized caching into a full read-path cache layer:

- **`dependsOn([Model::class])`** and **`dependsOnTables(['table'])`** — cache cross-table queries by declaring what should invalidate them. Simple cases stay normalized; complex shapes use a versioned result cache.
- **Scalar and aggregate caching** — `count`, `sum`, `avg`, `withCount`, `withSum`, and friends are cached automatically under versioned keys.
- **Stampede protection** — waiters serve stale data or block on a wake channel instead of storming the database during a rebuild.
- **Redis Cluster support** — single-slot mode by default; opt into per-model slot sharding with `slotting`.
- **Tag-based flushing** — group query entries under a tag and flush them together on deploy or config change.
- **Debugbar integration** — hits, misses, and bypasses appear on the request timeline when Debugbar is installed.

---

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)

Simple `whereHas` / `whereDoesntHave` on a non-nested `Cacheable` relation and plain string `join()` calls with an explicit root-table projection are inferred automatically — no `dependsOn()` needed. Join inference is conservative: it bypasses joins with implicit aliases (`join('posts p', ...)`), expression join targets, raw or subquery join predicates, and unsupported join-clause conditions — those require `dependsOn()`/`dependsOnTables()` like everything else below.

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

Author::join('posts', 'posts.author_id', '=', 'authors.id')
    ->select('authors.*')
    ->get(); // also works with count(), sum(), exists(), paginate()
```

Everything else requires `dependsOn()`: manual `whereExists`, raw predicates, nested relations, `GROUP BY`, `DISTINCT`, and calculated columns. Use `dependsOnTables()` when the joined table has no `Cacheable` model:

```
Author::join('legacy_stats', 'legacy_stats.author_id', '=', 'authors.id')
    ->select('authors.*')
    ->dependsOnTables(['legacy_stats'])
    ->get();
```

> **Note:** `dependsOnTables()` declares a read dependency only. Call `NormCache::invalidateTableVersion('mysql', 'legacy_stats')` after any external write to that table.

Normcache chooses the best caching strategy automatically:

- **Normalized Cache**: Used for simple queries on the primary table. If you add `dependsOn()`, it stays normalized but becomes versioned against the extra models too.
- **Result Cache**: Used for complex queries with `dependsOn()`. The entire result set is cached as a versioned blob.

Pessimistic locks always bypass the cache.

### Per-Query TTL

[](#per-query-ttl)

Use `ttl()` to set a custom cache duration:

```
Post::where('active', true)->ttl(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)

To prevent data corruption, NormCache will automatically bypass caching and fall back to the database for:

Query featureWorkaroundPessimistic locking (`lockForUpdate` / `sharedLock`)None — must hit DBInside a database transactionNone — must hit DBRaw SQL / `DB::table(...)`None — flush manuallyRaw `WHERE` or `ORDER BY` clausesUse `dependsOn()`Cross-table aggregate and scalar queriesUse `dependsOn()``chunk()`, `each()`, `lazy()`None — always hits DB`sole()`None — always hits DBEverything else — `JOIN`, `GROUP BY`, `DISTINCT`, subquery `WHERE`, and calculated columns — is also 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.
- Normcache caches each model's connection name and table in static properties for performance. Call `CacheKeyBuilder::reset()` after switching tenants to clear the metadata cache.
- `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', false),
    '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. 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.
- **`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.

---

Correctness Guarantees
----------------------

[](#correctness-guarantees)

NormCache is designed to be as transparent as possible to native Eloquent, but it operates under specific guarantees and intentional limitations:

### Safe &amp; Transparent Caching

[](#safe--transparent-caching)

NormCache matches native Eloquent hydration for supported query shapes, with the following intentional limitations:

- **Universal Query Patterns:** Standard model lookups, primary key fast-paths, and complex result sets across both Normalized and Result modes.
- **Full Relationship Support:** Eager-loaded relations including nested chains, pivot table attributes, and through-relations.
- **Native Model Lifecycle:** Full support for standard Eloquent behavior including global scopes and soft deletes. `retrieved` events fire only when `fire_retrieved` is enabled (see Configuration).
- **Eloquent Extensibility:** Custom casts (JSON/Enum) and custom collection classes. Cached hydration reconstructs models from raw attributes directly and does not invoke custom `newFromBuilder()` overrides.

### Requires `dependsOn()`

[](#requires-dependson)

Simple `whereHas` and plain `join()` with an explicit root-table projection are inferred automatically. Everything else — manual `whereExists`, raw predicates, expression joins, nested relations, `GROUP BY`, `DISTINCT`, and calculated columns — requires `dependsOn()` or `dependsOnTables()`.

- **Debug Warning:** If `app.debug` is true, NormCache will log a warning if it detects a query touching a table not declared in `dependsOn()`.

### Cluster Mode &amp; Consistency

[](#cluster-mode--consistency)

- **Single Node / Hash Tagging:** Provides strong multi-key atomicity.
- **Slotting Mode:** Offers better distribution but weaker cross-key atomicity. Multi-dependency queries are automatically routed to the result cache in slotting mode to prevent cross-slot consistency errors.

### Stale Serving

[](#stale-serving)

To prevent cache stampedes, NormCache may serve slightly stale data (up to the configured stale depth) while a background writer rebuilds the cache. This guarantees high availability under extreme load at the cost of immediate read-your-writes consistency.

---

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. (Manual operations like `flushAll()` and tag flushing do use `SCAN`).
- **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

Maintenance93

Actively maintained with recent releases

Popularity15

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity54

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 ~4 days

Total

14

Last Release

25d ago

Major Versions

v1.1.0 → v2.0.02026-06-03

v2.4.0 → v3.x-dev2026-06-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 (223 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

11223.5M33](/packages/anourvalar-eloquent-serialize)[mostafaznv/laracache

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

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

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

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

Laravel package for caching Eloquent model queries

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

LaraCache Tool for Laravel Nova

114.0k](/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)
