Layer 2: Eloquent Model Lifecycle (AutoClearsCache)
Add AutoClearsCache to any Eloquent model to automate cache invalidations.
2.1 Automatic Invalidation on DB::afterCommit
Hooks into Eloquent model events: created, updated, deleted, and restored (for SoftDeletes).
When a record changes:
- Resolves item tag:
{table}:{id}(e.g.products:42). - Resolves collection tag:
{table}(e.g.products). - Resolves parent relation tags (if
$cacheParentsis declared). - Resolves custom related tags (if
$relatedCacheTagsis declared). - Defers clearing until
$connection->afterCommit(). If a database transaction rolls back, your cache remains completely untouched!
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use KaziOmar\AtomicCache\Traits\AutoClearsCache;
class Product extends Model
{
use AutoClearsCache;
// Optional: Invalidate additional related tags when any product changes:
protected array $relatedCacheTags = ['landing_page_stats'];
}2.2 Declarative Parent Invalidation ($cacheParents)
Similar in spirit to Laravel's native $touches (operating on a single-level BelongsTo relation), you can declare parent relationships to automatically invalidate parent-scoped cache tags whenever a child record changes:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use KaziOmar\AtomicCache\Traits\AutoClearsCache;
class Order extends Model
{
use AutoClearsCache;
// Automatically invalidates Store caches when an Order changes:
protected array $cacheParents = ['store'];
public function store(): BelongsTo
{
return $this->belongsTo(Store::class);
}
}When an order for Store #5 is created or updated:
- Flushes child item tag:
orders:{id} - Flushes child collection tag:
orders - Flushes parent scoped tag:
stores:5:orders - Flushes parent umbrella tag:
stores:5 - Dirty Foreign Key Handling: If an order is moved from Store 1 to Store 2 (
$order->store_id = 2), Atomic Cache invalidates tags for both Store 1 and Store 2.
Parent Umbrella Tag Scope
By design, $cacheParents flushes both the scoped collection tag (stores:5:orders) and the parent umbrella tag (stores:5). If your application caches the parent model's own record under stores:5, child writes will also refresh the parent's general cache.
2.3 Model-Level Manual Cache Flushing (flushCache & clearCache)
AutoClearsCache provides two convenient helper methods directly on the model:
// 1. Instance helper: manually clear this record and its parent tags:
$product = Product::find(42);
$product->clearCache(); // Expressive alias for clearModelCacheOnCommit()
// 2. Static helper: flush all collection-level caches for this model's table:
Product::where('category_id', 5)->update(['discount' => 10]);
Product::flushCache(); // Wipes all "products" table collection caches in one line!Scope & Transaction Safety
Product::flushCache() invalidates the model's table collection tag (products). It does not invalidate individual item tags (products:42) or parent-scoped relation tags (stores:5:orders). If your model uses parent-scoped caching ($cacheParents), invalidate those tags explicitly via AtomicCache::clearTags(['stores:5:orders', 'stores:5']).
Unlike model lifecycle events which wait for afterCommit, Product::flushCache() executes immediately. If calling it inside an active DB::transaction(), wrap it in DB::afterCommit(fn() => Product::flushCache()) to prevent concurrent readers from caching pre-committed data.
2.4 High-Frequency Update Columns ($cacheIgnoreColumns)
When updating counters or analytics (e.g. view_count, last_login_at, login_count), invalidating the entire table collection cache (products) or parent cache (stores:1:products) on every single page view causes severe cache churn.
Declare $cacheIgnoreColumns on your model to protect collections:
class Product extends Model
{
use AutoClearsCache;
protected array $cacheParents = ['store'];
// Only single-item cache ('products:42') is cleared when these change.
// Heavy table collections ('products') and parent tags ('stores:1:products') remain cached:
protected array $cacheIgnoreColumns = ['view_count', 'meta'];
}Automatic Timestamp Exclusion
Laravel automatically dirties the updated_at column on save. Atomic Cache automatically ignores updated_at when evaluating $cacheIgnoreColumns. If you call $model->touch(), Atomic Cache verifies that no declared ignored column changed and correctly flushes the collection cache.
$cacheIgnoreColumns only applies to updated events. Model created, deleted, and restored events always invalidate collection tags regardless of this setting.
Stale Display & Sorting Warning
Columns added to $cacheIgnoreColumns will remain stale in cached query results and listings until their TTL expires.
For example, if you ignore view_count, any cached query that selects, filters (where), or orders by (orderBy('view_count')) will display the old view count or stale ordering until refreshed.
Rule of Thumb: Only ignore columns that are not displayed in cached queries or critical sorting operations (e.g. internal telemetry, session metadata, or live counters rendered via dedicated un-cached endpoints).
2.5 Strict Mode for Parent Relations (strict_mode)
Catch typos and invalid relationship declarations early during local development:
// config/atomic-cache.php
'strict_mode' => env('ATOMIC_CACHE_STRICT_MODE', null), // null = active in local environment (app()->isLocal() === true)When strict mode is enabled (default in local development, or by setting ATOMIC_CACHE_STRICT_MODE=true in phpunit.xml), invalid declarations throw an immediate \InvalidArgumentException with clear guidance:
- Typo in method name (
$cacheParents = ['strore']): throws missing method error. - Class name passed instead of relation method (
$cacheParents = [Store::class]): detects class name and asks: "Did you pass a class name instead of the BelongsTo relation method name?". - Invalid type passed (
$cacheParents = [123]): throws invalid type error. - Method requiring parameters (
public function store($type)): throws required parameter error. - Non-
BelongsTorelations (hasMany,morphTo): throws relation type error.
In production environments, strict mode safely logs a warning and gracefully continues without interrupting customer transactions.
2.6 Transaction-Safe Bulk Invalidation (AtomicCache::invalidating)
For bulk updates, mass deletes, or service workflows using direct DB queries, wrap execution with AtomicCache::invalidating():
use KaziOmar\AtomicCache\Facades\AtomicCache;
// Outside transaction: flushes immediately
// Inside transaction: automatically defers invalidation to DB::afterCommit!
AtomicCache::invalidating([Order::class, Product::class], function () {
DB::table('orders')->where('status', 'draft')->delete();
DB::table('products')->where('stock', 0)->update(['status' => 'out_of_stock']);
});Uses a try/finally guarantee: outside a transaction, invalidation is guaranteed in a finally block even if the closure throws an exception, preventing stale reads after partial database mutations. Inside an active database transaction, invalidation is safely registered via DB::afterCommit(). If the transaction rolls back, cache is untouched.
2.7 Critical Rules & What Breaks Model Auto-Invalidation
- Mass queries bypass model events: Direct query builder mutations (
Product::where(...)->update(...),delete(), orDB::table('products')->update(...)) do not fire Eloquent events. AutoClearsCache cannot detect them.- Solution: Run
Product::flushCache();right after your bulk query.
- Solution: Run
- Quiet operations bypass events: Calling
$model->saveQuietly(),updateQuietly(),deleteQuietly(), or wrapping code inModel::withoutEvents()intentionally suppresses model events.- Solution: Manually call
$model->clearCache()if you need invalidation after quiet saves.
- Solution: Manually call
- Pivot table changes: Many-to-many
attach(),detach(), orsync()operations on relationships do not fire parent model events.- Solution: Call
AtomicCache::clearTags([Product::class, Category::class])explicitly after syncing.
- Solution: Call
$cacheParentsonly supportsBelongsTo: DeclaringhasMany,hasOne,belongsToMany, ormorphToin$cacheParentswill throw anInvalidArgumentExceptionin strict mode (local/testing) or log a warning in production.- Custom owner keys are skipped: If a relationship uses a custom non-primary owner key (e.g.
belongsTo(Store::class, 'store_slug', 'slug')), it is skipped because the child model holds a string slug rather than the parent's primary ID, which would create mismatched umbrella tags (stores:nikevsstores:5). - Grandparent relations do not auto-cascade (
$touchesdoes not work): Multi-level hierarchies (e.g.OrderItem -> Order -> Store) do not automatically cascade from item to store. Crucially, Laravel's nativeprotected $touches = ['order']property executes a raw database update (rawUpdate) and does NOT fire Eloquent model events. Thus, declaring$toucheswill not triggerAutoClearsCacheon the parent!- Solution 1 (Recommended - Explicit Method): Define
getRelatedCacheTags()on the child model:phppublic function getRelatedCacheTags(): array { return [ "stores:{$this->order->store_id}:orders", "stores:{$this->order->store_id}", ]; } - Solution 2 (Manual Touch): Call
$orderItem->order->touch()directly on the model instance (which callssave()and fires Eloquent'supdatedevent).
- Solution 1 (Recommended - Explicit Method): Define
- Runtime property mutation is ignored: Because configured parent relations are cached in-memory per class for performance, dynamically modifying
$model->cacheParents = [...]on an active model instance at runtime will have no effect. - Octane / Long-Running Workers: If you dynamically manipulate
$cacheParentsduring testing or worker loops, clear the in-memory cache viaModel::flushConfiguredParentRelationsCache(). - Rolled-back transactions do not clear cache: If changes occur inside a
DB::transaction()that throws an exception and rolls back,afterCommit()will not fire. The cache remains untouched.