Skip to content

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.


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).

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):

  1. Model observer (e.g., PageObserver::saved()) calls CapellCoreHelper::flushCache() with enum-based keys, or fires an event like PageSaved
  2. Frontend listeners like PurgeCdnCacheOnPageChangeListener handle the event and call custom logic (e.g., PurgeCdnCacheByPageAction)
  3. 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 $modelDependencies property
MethodSignaturePurpose
registerDependencyregisterDependency(string $modelClass, string|array $cachePatterns): voidRegister 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.
invalidateForModelinvalidateForModel(string $modelClass): voidLook up patterns for a model class and flush the appropriate cache keys. Called from model observers.

Patterns are cache keys or tags, passed to Laravel’s cache driver:

  • Exact-key patterns (no * wildcard): forwarded to Cache::forget($pattern), matching exact cache keys

    • Example: 'sites', 'languages', 'page-error-404'
  • 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

Why the conservative approach? Different cache drivers (Redis, Memcached, file) handle wildcards differently. A full tag flush is predictable and avoids stale data.

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().

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.
  • 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 surrogates
public function saved(BlogPost $blogPost): void {
resolve(CacheInvalidationRegistry::class)->invalidateForModel(BlogPost::class);
FragmentCache::invalidateBySurrogateKey('blog-featured');
}
  • Register in boot(), not register() — service providers should register dependencies in boot() rather than register(), 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::class or '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 entire capell-frontend tag 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.