Cache Invalidation
Who is this for? Developers adding custom models that should invalidate frontend caches when changed (e.g., custom news articles, product data, or settings).
TL;DR: Register your custom model with
CacheInvalidationRegistry::registerDependency()to declare “when this model changes, flush these cache keys.” Resolve the registry from the container; it is not a static utility.
When to use this
Section titled “When to use this”Cache invalidation via CacheInvalidationRegistry is for registering relationships between custom models and cache keys. Capell ships with built-in registrations for Site, Language, Page, Navigation, and SiteDomain. If you:
- Add a custom model (e.g.,
BlogPost,TeamMember) - Want frontend caches to bust automatically when that model is saved/deleted
- Don’t want to manually call
Cache::forget()in observers
…then CacheInvalidationRegistry is your tool.
This is not a replacement for fragment caching. Fragment caching uses surrogate keys inside Blade templates; this registry uses model lifecycle events to decide which cache keys to flush. They compose: a model observer calls the registry to flush keys, then flushes fragments tagged with surrogate keys (if any).
How It’s Wired
Section titled “How It’s Wired”The registry is a container-managed service. Register dependencies during service-provider boot, then call invalidateForModel() from observers or listeners that handle model changes. Observers in packages/core/src/Observers/ (e.g., SiteObserver, PageObserver, LanguageObserver) trigger events (PageSaved, PageDeleted) that are consumed by listeners in packages/frontend/src/Listeners/.
Current flow (built-in models):
- Model observer (e.g.,
PageObserver::saved()) callsCapellCoreHelper::flushCache()with enum-based keys, or fires an event likePageSaved - Frontend listeners like
PurgeCdnCacheOnPageChangeListenerhandle the event and call custom logic (e.g.,PurgeCdnCacheByPageAction) - For custom models, wire the observer to call
resolve(CacheInvalidationRegistry::class)->invalidateForModel(ModelClass::class)
File locations:
- Registry:
packages/frontend/src/Support/Cache/CacheInvalidationRegistry.php - Built-in registrations: inside the registry’s
$modelDependenciesproperty
Public API
Section titled “Public API”| Method | Signature | Purpose |
|---|---|---|
registerDependency | registerDependency(string $modelClass, string|array $cachePatterns): void | Register one or more cache patterns for a model class. Patterns are merged with any existing registrations for that class. Pass a string or array of strings. |
invalidateForModel | invalidateForModel(string $modelClass): void | Look up patterns for a model class and flush the appropriate cache keys. Called from model observers. |
What “patterns” means
Section titled “What “patterns” means”Patterns are cache keys or tags, passed to Laravel’s cache driver:
-
Exact-key patterns (no
*wildcard): forwarded toCache::forget($pattern), matching exact cache keys- Example:
'sites','languages','page-error-404'
- Example:
-
Wildcard patterns (contain
*): trigger a full tag flush instead- Example:
'page-*','site-related-*' - When any wildcard pattern is detected, the registry calls
Cache::tags(['capell-frontend'])->flush()and returns early - This is a safety mechanism: rather than implementing wildcard matching across cache backends, the registry opts for full flush
- Example:
Why the conservative approach? Different cache drivers (Redis, Memcached, file) handle wildcards differently. A full tag flush is predictable and avoids stale data.
Example — registering a custom model
Section titled “Example — registering a custom model”Create a custom model observer for your BlogPost model:
<?php
declare(strict_types=1);
namespace App\Observers;
use App\Models\BlogPost;use Capell\Frontend\Support\Cache\CacheInvalidationRegistry;
final class BlogPostObserver{ public function saved(BlogPost $blogPost): void { resolve(CacheInvalidationRegistry::class)->invalidateForModel(BlogPost::class); }
public function deleted(BlogPost $blogPost): void { resolve(CacheInvalidationRegistry::class)->invalidateForModel(BlogPost::class); }}Register the dependency in your app’s service provider:
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Models\BlogPost;use Capell\Frontend\Support\Cache\CacheInvalidationRegistry;use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider{ public function boot(CacheInvalidationRegistry $cacheInvalidation): void { $cacheInvalidation->registerDependency( modelClass: BlogPost::class, cachePatterns: ['blog-posts', 'blog-posts:*', 'homepage-featured'], ); }}Now, when a BlogPost is saved or deleted, the observer calls invalidateForModel(), which flushes 'blog-posts' and 'homepage-featured' keys exactly, and (because 'blog-posts:*' contains *) triggers a full Cache::tags('capell-frontend')->flush().
Companion APIs
Section titled “Companion APIs”Fragment caching vs. invalidation registry:
-
FragmentCache::invalidateBySurrogateKey(string $surrogateKey)— Used inside Blade templates to tag fragments. Called manually from event listeners when you want surgical invalidation of specific fragments.- Example:
@cache('featured-posts', 3600, ['featured-posts:list']); later,FragmentCache::invalidateBySurrogateKey('featured-posts:list')clears just that fragment.
- Example:
-
CacheInvalidationRegistry::registerDependency()+invalidateForModel()— Used for model-driven invalidation. Declares “when MODEL X changes, flush THESE cache keys.” Typically called from model observers, not directly in templates.
The registry is declaration-time (in service providers); fragment invalidation is event-time (in observers/listeners). Use both:
// Service provider: declare the registry$registry = resolve(CacheInvalidationRegistry::class);
$registry->registerDependency( BlogPost::class, ['blog-posts', 'blog-featured-*'],);
// Observer: invalidate both registry keys and fragment surrogatespublic function saved(BlogPost $blogPost): void { resolve(CacheInvalidationRegistry::class)->invalidateForModel(BlogPost::class); FragmentCache::invalidateBySurrogateKey('blog-featured');}Gotchas
Section titled “Gotchas”-
Register in
boot(), notregister()— service providers should register dependencies inboot()rather thanregister(), ensuring other packages have already initialized. Many CMS extensions register their own cache patterns and depend on a predictable boot order. -
Model class must be fully qualified — pass the full namespace:
BlogPost::classor'App\Models\BlogPost', not just'BlogPost'. -
Multiple registrations merge — calling
registerDependency()twice for the same model class merges the pattern arrays:$registry = resolve(CacheInvalidationRegistry::class);$registry->registerDependency(Post::class, ['posts']);$registry->registerDependency(Post::class, ['featured']); // Now Post maps to ['posts', 'featured'] -
Wildcard triggers full flush — if even one pattern contains
*, the entirecapell-frontendtag is flushed. Wildcard patterns are an all-or-nothing signal. Use sparingly if your app has many cache keys. -
Observer must be wired — the registry only works if your model observer is registered. Register it in a service provider:
BlogPost::observe(BlogPostObserver::class); -
No automatic wiring — unlike Capell’s built-in models (registered in core observers), custom models are not automatically observed. You must wire the observer yourself.
Related
Section titled “Related”- Fragment caching — cache expensive Blade partials with surrogate key invalidation.
- ETag and conditional responses — validate client caches without recomputing.
- Critical asset optimization — defer non-critical CSS and JS for faster first paint.
- Performance index