osamaelnagar / oomycache
Transparent, correctness-first Eloquent query caching for Laravel: version-token invalidation, any cache store, optional Filament bridge.
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/cache: ^12.0||^13.0
- illuminate/console: ^12.0||^13.0
- illuminate/contracts: ^12.0||^13.0
- illuminate/database: ^12.0||^13.0
- illuminate/support: ^12.0||^13.0
Requires (Dev)
- larastan/larastan: ^3.9
- laravel/agent-detector: ^2.0
- laravel/chisel: ^0.1
- laravel/pao: ^1.0
- laravel/pint: ^1.29
- laravel/prompts: ^0.3
- orchestra/testbench: ^10.0||^11.0
- pestphp/pest: ^4.6||^5.0
- pestphp/pest-plugin-laravel: ^4.1||^5.0
- pestphp/pest-plugin-type-coverage: ^4.0||^5.0
- phpstan/extension-installer: ^1.4
Suggests
- filament/filament: Required for the optional Filament cache management page/widgets (plan/04).
- livewire/livewire: Required for the optional Filament bridge.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-11 15:40:24 UTC
README
OoMyCache
Transparent, correctness-first Eloquent query caching for Laravel.
Any app · any cache store · version-token invalidation · Filament bridge optional.
Status: stable
v0.1.0. Core engine, tenancy, payload hardening and the optional Filament bridge are implemented, tested and documented; seedocs/implementation.
OoMyCache makes repeated Eloquent reads disappear — transparently — with a correctness-first invalidation model that works in any Laravel application: APIs, queues, dashboards, back offices, and Filament panels (via the optional bridge).
use Oomycache\Oomycache\Concerns\HasQueryCache; final class Product extends Model { use HasQueryCache; }
That's it. Every plain Eloquent query on Product now goes through the caching builder. No resource
changes, no base-class changes, no fork. Filament resources, tables, widgets and relation managers get
the same benefit when you install the optional bridge — the core package itself has zero Filament
dependency.
Why another model cache?
| Feature | OoMyCache |
|---|---|
| Transparent Eloquent caching | ✅ newEloquentBuilder() + newBaseQueryBuilder() subclasses; no framework forks |
| Filament paginator COUNT | ✅ cached via the base query builder — any Laravel paginator benefits; the Filament bridge is optional |
| Invalidation | ✅ version-token namespaces keyed by connection + table; O(1) on Redis/Memcached/database/file/array |
| Relationships | ✅ eager loads, withCount/withSum/withExists, whereHas, join/subquery tables recorded as dependencies |
| Multi-tenant / multi-panel safe | ✅ tenant + panel segments, fail-closed |
| Transactions | ✅ reads bypass cache inside transactions; invalidation deferred to afterCommit |
| Laravel 13 serialisation defaults | ✅ arrays-JSON payload by default (cache.serializable_classes=false safe); models opt-in with allow-list |
| Payload hardening | ✅ optional gzip/deflate/zstd compression envelope, transparent chunking above chunk_bytes, per-request decoded-payload memo, negative caching with short TTL |
| Maintenance | ✅ oomycache:prune for database cache stores: keyset-ordered, expiry-guarded, never touches cache_locks |
| Filament-aware | ✅ zero Filament dependency in core; first-party companion bridge (studio page, tenants, invalidation) |
Installation
composer require osamaelnagar/oomycache php artisan vendor:publish --tag=oomycache-config
Add the trait to the models you want cached. That is opt-in by design: un-audited models stay
uncached. Recommended defaults are production-safe out of the box; see config/oomycache.php.
No Redis? Dedicated file store
The package can use a different store from the rest of the application. On a single host with a writable
storage/ directory, keep Laravel's app cache on database but put OoMyCache on file:
CACHE_STORE=database OOMYCACHE_STORE=file
This removes the cache-table SELECTs that a database-backed query cache adds to every warm read. If
stats are enabled, set OOMYCACHE_STATS_STORE=file too (it defaults to OOMYCACHE_STORE in the
current release). File cache is per-server and not shared across load-balanced nodes; benchmark it on
the deployment host before enabling model traits, using the app_queries/store_queries split from
oomycache:bench. Because versioned invalidation leaves old payload files behind, schedule a periodic
php artisan cache:clear file when the file store is dedicated to OoMyCache. Redis/Memcached remain
the right choice for multi-node deployments; array is for tests only. See
docs/implementation/store-selection.md for the full
trade-off table, including Hostinger-style shared hosting.
Usage
// all reads on opted-in models are cached automatically $products = Product::query()->with('category')->where('is_active', true)->paginate(25); // per-query control Product::query()->cacheFor(120)->get(); Product::query()->cacheForForever()->where('is_system', true)->get(); Product::query()->dontCache()->get(); Product::query()->cacheKey('catalog-v2')->get(); Product::query()->cacheIf(fn ($result) => $result->isNotEmpty())->get(); // namespace operations (O(1) version bumps, never touches other cache keys) Product::flushQueryCache(); Oomycache::flush([Product::class, Category::class]); Oomycache::clear();
Many-to-many pivots are covered automatically for cacheable models: the package wraps
belongsToMany, morphToMany and the morphedByMany inverse, and flushes both table namespaces
after attach, detach, sync, toggle and pivot updates (disable with
OOMYCACHE_PIVOT_INVALIDATION=false). Raw pivot-table writes still need an explicit flush.
Caching raw report/dashboard queries
DB::table() reports cannot use the Eloquent builder. Use rememberQuery() with an explicit key and
table dependency list:
use Oomycache\Oomycache\Facades\Oomycache; use Illuminate\Support\Facades\DB; $rows = Oomycache::rememberQuery( tables: ['journal_lines', 'journal_entries'], key: 'owner-dashboard:'.$year, ttl: 120, callback: fn (): array => DB::table('journal_lines') ->where('fiscal_year_id', $year) ->orderBy('date') ->get() ->map(fn (object $row): array => (array) $row) ->all(), ); // the caller owns the key: include every filter that changes the result // invalidate explicitly when those tables are written outside cacheable models Oomycache::flushTables(['journal_lines', 'journal_entries']);
The key includes tenant segments and the version tokens of all listed tables. If the listed tables are
written through cacheable Eloquent models, those writes invalidate the entry automatically; otherwise
call flushTables() after the write path. Prefer returning arrays/scalars; raw result objects are
normalized to arrays.
Per-model overrides:
public function queryCacheOptions(): array { return [ 'ttl' => 600, 'cache_aggregates' => true, 'dependencies' => [Supplier::class], // extra invalidation namespaces 'skip_query_patterns' => ['/\bstock_qty\b/i'], ]; }
Artisan commands
| Command | Purpose |
|---|---|
oomycache:status |
configuration, store capabilities, cacheable models, version tokens, optional stats counters |
oomycache:clear [--model=…] |
invalidate one/many models (or the whole namespace) |
oomycache:doctor |
diagnose store support, payload mode, common traps |
oomycache:bench {model} |
cold vs warm app-query counts, separate cache-store statement counts, and timings for a cacheable model (--limit, --samples, --json); isolated benchmark keys, memo reset between samples, no global cache clear unless --clear |
oomycache:prune |
delete expired database-cache rows (keyset-ordered, expiry-guarded; --dry-run, --chunk, --max-seconds; schedule hourly) |
Production adoption
Before enabling more than a pilot, read:
docs/implementation/production-runbook.md— store choice, deploy order, config-cache/test isolation, worker restarts, file-store maintenance, rollback.docs/implementation/adoption-checklist.md— model/write-path audit, correctness tests, measurements and rollout gates.docs/implementation/store-selection.md— database/file/Redis/ Memcached trade-offs, including no-Redis hosts.
Benchmark the deployment host store with:
php artisan config:clear pwsh vendor/osamaelnagar/oomycache/scripts/benchmark-stores.ps1 "App\Models\Product"
php artisan config:clear
vendor/osamaelnagar/oomycache/scripts/benchmark-stores.sh "App\\Models\\Product"
Filament compatibility
Core has no Filament or Livewire dependency. The package works with Filament 5 / Livewire 4 today
through the two builder subclasses, and the compatibility contract (tenants, pagination COUNT,
invalidation matrix, bridge UX) is documented in docs/plan/04-filament-compatibility.md.
The Phase-2 bridge is a standalone companion package, osamaelnagar/oomycache-filament (packages/OoMyCacheFilament), which adds:
- a Filament-aware tenant resolver: the current panel is part of every cache key, and tenancy active without a resolved tenant fails closed (query runs uncached);
- a
{panel-path}/query-cacheQuery Cache studio page (e.g./admin/query-cache, or/query-cacheon a panel with path''): store status, cacheable models + version tokens, live stats counters, selective model invalidation and a global purge action; - an
ActionCalledinvalidator for Filament actions that bypass Eloquent events (attach/detach/associate/dissociate/reorder/import and the bulk mass branches).
composer require osamaelnagar/oomycache osamaelnagar/oomycache-filament
// app/Providers/Filament/AdminPanelProvider.php use Oomycache\Filament\OomycachePlugin; return $panel // ... ->plugin(OomycachePlugin::make());
Verified in the dogfood app
- Filament 5 resources/tables/widgets/custom page warm renders: 0 ERP-table queries (includes paginator totals).
- Filament relation-manager
AttachAction/DetachActionon a belongsToMany relation invalidate cached aggregates (real E2E tests). - Plain Livewire 4 front-office pages warm renders: 0 ERP-table queries.
- Database, file, real Redis and real Memcached cache stores validated; Laravel 13
serializable_classes=falsepayload safety proven. oomycache:benchon the seeded app: cold 41.4 ms / 1 app query vs warm 20.3 ms / 0 app queries (database store).
See docs/implementation/phase-2-progress.md.
Safety model
- Opt-in per model. No trait, no caching.
- Versioned invalidation. A write bumps the table's version token; all cache keys embedding it
become unreachable. No tags required on any driver. Cacheable models'
belongsToMany,morphToManyandmorphedByManypivot operations flush both sides automatically. - One notification per logical write chain. Builder flushes and Eloquent lifecycle events (update, soft delete, restore, force delete) bump the token as needed, but audit/stats receive one invalidation notification for the chain.
- Explicit raw-query invalidation.
rememberQuery()entries are versioned by the tables you list;flushTables()covers write paths that do not go through cacheable models. - Fail-open reads, loud invalidation. Cache outages fall back to the DB; failed invalidations are logged/thrown instead of silently serving stale data.
- Transaction-aware. Reads inside a transaction bypass the cache; writes invalidate after commit.
- No global flush.
oomycache:clearbumps a version token; it never callsCache::flush()(which on Redis isflushdb()and ignores prefixes). - Arrays payload by default. Strings (not PHP objects) go into the cache store, so Laravel 13's
default
cache.serializable_classes=falsepolicy cannot break payloads.
Testing
composer test # pint + phpstan + 100% type coverage + Pest # local real-driver lanes (services must be running) OOMYCACHE_TEST_DRIVER=redis vendor/bin/pest tests/Feature/RealCacheDriverTest.php OOMYCACHE_TEST_DRIVER=memcached vendor/bin/pest tests/Feature/RealCacheDriverTest.php OOMYCACHE_TEST_DB=mysql DB_HOST=127.0.0.1 DB_DATABASE=oomycache_smoke DB_USERNAME=root \ vendor/bin/pest tests/Feature/DatabaseDriverTest.php
In an application with config:cache enabled, phpunit.xml environment overrides are ignored; run
php artisan config:clear before the test suite, then re-cache after it. Keep the suite cache-inert
with OOMYCACHE_ENABLED=false / OOMYCACHE_STORE=array unless it is explicitly testing the cache.
After changing OOMYCACHE_* values and rebuilding the config cache, restart the application's
PHP-FPM/PHP-CGI workers; long-lived web workers can otherwise keep serving the previous config.
Long-running processes that keep one Laravel application instance alive (queue workers, Octane,
custom daemon loops) must call app(\Oomycache\Oomycache\Cache\QueryCacheEngine::class)->resetRuntimeState()
at each request/job boundary. The package resets automatically on queued jobs and Octane request
events; a custom while loop must reset it itself when it expects to observe writes from other
processes.
The package was dogfooded in a real Filament 5 ERP app (4 resources, relation managers, select-heavy
forms, custom page/actions, widgets, plain-Livewire front-office pages) with 3,000 products /
2,500 purchase orders / 5,000 invoices; warm Filament list pages execute zero ERP-table queries.
See docs/implementation/phase-1-dogfood.md.
For AI agents (Laravel Boost)
The package ships a Laravel Boost skill at
resources/boost/skills/oomycache-development/SKILL.md.
When the package is installed in a Laravel app, Boost-aware agents discover it automatically and get
installation, configuration, per-query API, invalidation, command, tenancy and anti-pattern guidance —
including the Filament bridge being optional. The package-level AGENTS.md documents the
maintenance conventions for agents working on OoMyCache itself.
Documentation
docs/implementation/production-runbook.md— production rolloutdocs/implementation/adoption-checklist.md— model adoption gatesdocs/implementation/store-selection.md— store trade-offsdocs/implementation/security-review.md— residual risks and controlsdocs/implementation/config-reference.md— every config keydocs/README.md— research & design dossier (competitor analysis + blueprint)docs/plan/03-architecture.md— key composition, invalidation algebra, ADRsdocs/plan/05-qa-checklist.md— release gates
Security
See SECURITY.md for supported versions and private vulnerability reporting. Do not open
public issues for security reports.
Contributing
See CONTRIBUTING.md for setup, tests, conventions and release rules.
License
MIT. See LICENSE.md.