Skip to content

Commit 5984d00

Browse files
Copiloteminos
andauthored
Add optional Statamic static-cache invalidation integration for targeted Cloudflare purges (#6)
* Initial plan * feat: add configurable statamic static cache invalidation support Co-authored-by: eminos <1682784+eminos@users.noreply.github.com> * docs: clarify static cache mode and add debug logging coverage Co-authored-by: eminos <1682784+eminos@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eminos <1682784+eminos@users.noreply.github.com>
1 parent 68c6baf commit 5984d00

5 files changed

Lines changed: 222 additions & 1 deletion

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Automatically purge your Cloudflare cache when content changes in Statamic.
77
- Automatically purges Cloudflare cache when Statamic content changes.
88
- **Multi-zone support** for Statamic multisite installations with different domains.
99
- Configurable events that trigger cache purging.
10+
- Optional support for Statamic `static_caching` invalidation rules via `UrlInvalidated`/`StaticCacheCleared` events.
1011
- Optional queuing of purge jobs for background processing.
1112
- CLI command for manual cache purging.
1213
- Simple configuration with backward compatibility.
@@ -79,6 +80,11 @@ Once configured, the addon will automatically purge the Cloudflare cache when co
7980

8081
You can configure which events trigger cache purging in the config file.
8182

83+
#### Event handling modes (either/or)
84+
85+
- **Default (`use_statamic_static_cache_invalidation = false`)**: uses this addon’s own content events (`entry_*`, `term_*`, `asset_*`, etc.).
86+
- **Statamic static cache mode (`use_statamic_static_cache_invalidation = true`)**: uses Statamic Static Cache events (`url_invalidated`, `static_cache_cleared`) and skips the addon’s legacy content events.
87+
8288
#### Queued Purging
8389

8490
If you prefer to handle cache purging in the background to avoid potential delays during web requests, you can enable queued purging. Set the `CLOUDFLARE_CACHE_QUEUE_PURGE` environment variable to `true` or set `'queue_purge' => true` in the configuration file.
@@ -136,8 +142,14 @@ return [
136142
'nav_tree_saved' => true,
137143
'global_set_saved' => true,
138144
'global_set_deleted' => true,
145+
'url_invalidated' => true, // Statamic Static Cache: UrlInvalidated event
146+
'static_cache_cleared' => true, // Statamic Static Cache: StaticCacheCleared event
139147
],
140148

149+
// When enabled, only Statamic Static Cache events are handled (UrlInvalidated/StaticCacheCleared).
150+
// Legacy addon content events are skipped in this mode.
151+
'use_statamic_static_cache_invalidation' => env('CLOUDFLARE_CACHE_USE_STATAMIC_STATIC_CACHE_INVALIDATION', false),
152+
141153
// Dispatch purge jobs to the queue instead of running synchronously
142154
'queue_purge' => env('CLOUDFLARE_CACHE_QUEUE_PURGE', false),
143155

config/cloudflare-cache.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,14 @@
4747
'nav_tree_saved' => true,
4848
'global_set_saved' => true,
4949
'global_set_deleted' => true,
50+
'url_invalidated' => true, // Statamic Static Cache: UrlInvalidated event
51+
'static_cache_cleared' => true, // Statamic Static Cache: StaticCacheCleared event
5052
],
5153

54+
// When enabled, only Statamic Static Cache events are handled (UrlInvalidated/StaticCacheCleared).
55+
// Legacy addon content events are skipped in this mode.
56+
'use_statamic_static_cache_invalidation' => env('CLOUDFLARE_CACHE_USE_STATAMIC_STATIC_CACHE_INVALIDATION', false),
57+
5258
'queue_purge' => env('CLOUDFLARE_CACHE_QUEUE_PURGE', false), // Dispatch purge jobs to the queue
5359

5460
/*

src/CloudflareCacheServiceProvider.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Statamic\Events\NavTreeSaved;
1616
use Statamic\Events\GlobalSetSaved;
1717
use Statamic\Events\GlobalSetDeleted;
18+
use Statamic\Events\UrlInvalidated;
19+
use Statamic\Events\StaticCacheCleared;
1820

1921
class CloudflareCacheServiceProvider extends AddonServiceProvider
2022
{
@@ -53,6 +55,12 @@ class CloudflareCacheServiceProvider extends AddonServiceProvider
5355
GlobalSetDeleted::class => [
5456
PurgeCloudflareCache::class,
5557
],
58+
UrlInvalidated::class => [
59+
PurgeCloudflareCache::class,
60+
],
61+
StaticCacheCleared::class => [
62+
PurgeCloudflareCache::class,
63+
],
5664
];
5765

5866
/**

src/Listeners/PurgeCloudflareCache.php

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Statamic\Events\NavTreeSaved;
1717
use Statamic\Events\GlobalSetSaved;
1818
use Statamic\Events\GlobalSetDeleted;
19+
use Statamic\Events\UrlInvalidated;
20+
use Statamic\Events\StaticCacheCleared;
1921
use Statamic\Facades\URL;
2022
use Illuminate\Support\Facades\Log; // Already present, but good to confirm
2123

@@ -31,6 +33,11 @@ public function __construct(Client $client)
3133
public function handle(Event $event): void
3234
{
3335
if (!config('cloudflare-cache.enabled')) {
36+
if (config('cloudflare-cache.debug')) {
37+
Log::debug('[Cloudflare Cache] Skipping purge because addon is disabled.', [
38+
'event' => get_class($event),
39+
]);
40+
}
3441
return;
3542
}
3643

@@ -108,7 +115,28 @@ protected function purgeSynchronously(array $urls): void
108115
*/
109116
protected function shouldHandleEvent(Event $event): bool
110117
{
118+
$useStatamicInvalidation = config('cloudflare-cache.use_statamic_static_cache_invalidation', false);
119+
$isStaticCacheInvalidationEvent = $event instanceof UrlInvalidated || $event instanceof StaticCacheCleared;
111120
$eventClass = get_class($event);
121+
122+
if ($useStatamicInvalidation && !$isStaticCacheInvalidationEvent) {
123+
if (config('cloudflare-cache.debug')) {
124+
Log::debug('[Cloudflare Cache] Skipping event because Statamic static cache invalidation mode is enabled and this is a legacy addon event.', [
125+
'event' => $eventClass,
126+
]);
127+
}
128+
return false;
129+
}
130+
131+
if (!$useStatamicInvalidation && $isStaticCacheInvalidationEvent) {
132+
if (config('cloudflare-cache.debug')) {
133+
Log::debug('[Cloudflare Cache] Skipping event because Statamic static cache invalidation mode is disabled.', [
134+
'event' => $eventClass,
135+
]);
136+
}
137+
return false;
138+
}
139+
112140
$eventMap = [
113141
'Statamic\Events\EntrySaved' => 'entry_saved',
114142
'Statamic\Events\EntryDeleted' => 'entry_deleted',
@@ -120,11 +148,31 @@ protected function shouldHandleEvent(Event $event): bool
120148
'Statamic\Events\NavTreeSaved' => 'nav_tree_saved',
121149
'Statamic\Events\GlobalSetSaved' => 'global_set_saved',
122150
'Statamic\Events\GlobalSetDeleted' => 'global_set_deleted',
151+
'Statamic\Events\UrlInvalidated' => 'url_invalidated',
152+
'Statamic\Events\StaticCacheCleared' => 'static_cache_cleared',
123153
];
124154

125155
$configKey = $eventMap[$eventClass] ?? null;
126156

127-
return $configKey && config("cloudflare-cache.purge_on.{$configKey}");
157+
if (!$configKey) {
158+
if (config('cloudflare-cache.debug')) {
159+
Log::debug('[Cloudflare Cache] Skipping event because no purge_on mapping exists.', [
160+
'event' => $eventClass,
161+
]);
162+
}
163+
return false;
164+
}
165+
166+
$shouldHandle = (bool) config("cloudflare-cache.purge_on.{$configKey}");
167+
168+
if (!$shouldHandle && config('cloudflare-cache.debug')) {
169+
Log::debug('[Cloudflare Cache] Skipping event because purge_on setting is disabled.', [
170+
'event' => $eventClass,
171+
'config_key' => "cloudflare-cache.purge_on.{$configKey}",
172+
]);
173+
}
174+
175+
return $shouldHandle;
128176
}
129177

130178
/**
@@ -186,6 +234,10 @@ protected function getUrlsToPurge(Event $event): array
186234
// existing purge_everything_fallback logic (sync or queued).
187235
}
188236

237+
if ($event instanceof UrlInvalidated && $event->url) {
238+
$urls[] = URL::makeAbsolute($event->url);
239+
}
240+
189241
$urls = array_filter($urls);
190242

191243
return array_unique($urls);

tests/Feature/PurgeCacheListenerTest.php

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
use Statamic\Events\EntrySaved;
99
use Statamic\Events\GlobalSetSaved;
1010
use Statamic\Events\GlobalSetDeleted;
11+
use Statamic\Events\UrlInvalidated;
12+
use Statamic\Events\StaticCacheCleared;
1113
use Statamic\Contracts\Entries\Entry;
1214
use Statamic\Contracts\Entries\Collection;
1315
use Illuminate\Support\Facades\Event;
1416
use Illuminate\Support\Facades\Http;
1517
use Illuminate\Support\Facades\Queue; // Add Queue facade
18+
use Illuminate\Support\Facades\Log;
1619
use Eminos\StatamicCloudflareCache\Events\CachePurged;
1720
use Eminos\StatamicCloudflareCache\Jobs\PurgeCloudflareCacheJob;
1821
use Mockery;
@@ -355,4 +358,144 @@ public function it_fires_cache_purged_event_after_synchronous_purge_everything()
355358
return $e->purgedEverything && empty($e->urls);
356359
});
357360
}
361+
362+
#[Test]
363+
public function it_ignores_legacy_events_when_statamic_static_cache_invalidation_is_enabled()
364+
{
365+
Event::fake([CachePurged::class]);
366+
config([
367+
'cloudflare-cache.use_statamic_static_cache_invalidation' => true,
368+
'cloudflare-cache.queue_purge' => false,
369+
]);
370+
371+
$entry = $this->mockEntry();
372+
$event = new EntrySaved($entry);
373+
374+
$clientMock = $this->mock(Client::class);
375+
$clientMock->shouldNotReceive('purgeUrls');
376+
$clientMock->shouldNotReceive('purgeEverything');
377+
378+
$listener = $this->app->make(PurgeCloudflareCache::class);
379+
$listener->handle($event);
380+
381+
Queue::assertNothingPushed();
382+
Event::assertNotDispatched(CachePurged::class);
383+
}
384+
385+
#[Test]
386+
public function it_purges_invalidated_urls_when_statamic_static_cache_invalidation_is_enabled()
387+
{
388+
Event::fake([CachePurged::class]);
389+
config([
390+
'cloudflare-cache.use_statamic_static_cache_invalidation' => true,
391+
'cloudflare-cache.queue_purge' => false,
392+
]);
393+
394+
$event = new UrlInvalidated('/test-entry', 'https://example.com');
395+
396+
$clientMock = $this->mock(Client::class);
397+
$clientMock->shouldReceive('purgeUrls')->once()->with(['https://example.com/test-entry']);
398+
$clientMock->shouldNotReceive('purgeEverything');
399+
400+
$listener = $this->app->make(PurgeCloudflareCache::class);
401+
$listener->handle($event);
402+
403+
Queue::assertNothingPushed();
404+
Event::assertDispatched(CachePurged::class, function ($e) {
405+
return !$e->purgedEverything && $e->urls === ['https://example.com/test-entry'];
406+
});
407+
}
408+
409+
#[Test]
410+
public function it_purges_everything_when_static_cache_is_cleared_and_statamic_static_cache_invalidation_is_enabled()
411+
{
412+
config([
413+
'cloudflare-cache.use_statamic_static_cache_invalidation' => true,
414+
'cloudflare-cache.queue_purge' => false,
415+
'cloudflare-cache.purge_everything_fallback' => true,
416+
]);
417+
418+
$event = new StaticCacheCleared();
419+
420+
$clientMock = $this->mock(Client::class);
421+
$clientMock->shouldReceive('purgeEverything')->once();
422+
$clientMock->shouldNotReceive('purgeUrls');
423+
424+
$listener = $this->app->make(PurgeCloudflareCache::class);
425+
$listener->handle($event);
426+
427+
Queue::assertNothingPushed();
428+
}
429+
430+
#[Test]
431+
public function it_does_not_purge_when_static_cache_is_cleared_and_fallback_is_disabled()
432+
{
433+
config([
434+
'cloudflare-cache.use_statamic_static_cache_invalidation' => true,
435+
'cloudflare-cache.queue_purge' => false,
436+
'cloudflare-cache.purge_everything_fallback' => false,
437+
]);
438+
439+
$event = new StaticCacheCleared();
440+
441+
$clientMock = $this->mock(Client::class);
442+
$clientMock->shouldNotReceive('purgeEverything');
443+
$clientMock->shouldNotReceive('purgeUrls');
444+
445+
$listener = $this->app->make(PurgeCloudflareCache::class);
446+
$listener->handle($event);
447+
448+
Queue::assertNothingPushed();
449+
}
450+
451+
#[Test]
452+
public function it_logs_when_legacy_event_is_skipped_in_statamic_static_cache_invalidation_mode()
453+
{
454+
config([
455+
'cloudflare-cache.use_statamic_static_cache_invalidation' => true,
456+
'cloudflare-cache.debug' => true,
457+
]);
458+
459+
Log::shouldReceive('debug')->once()->withArgs(function ($message, $context = []) {
460+
return str_contains($message, 'Statamic static cache invalidation mode is enabled')
461+
&& ($context['event'] ?? null) === EntrySaved::class;
462+
});
463+
464+
$entry = $this->mockEntry();
465+
$event = new EntrySaved($entry);
466+
467+
$clientMock = $this->mock(Client::class);
468+
$clientMock->shouldNotReceive('purgeUrls');
469+
$clientMock->shouldNotReceive('purgeEverything');
470+
471+
$listener = $this->app->make(PurgeCloudflareCache::class);
472+
$listener->handle($event);
473+
474+
Queue::assertNothingPushed();
475+
}
476+
477+
#[Test]
478+
public function it_logs_when_static_cache_event_is_skipped_while_mode_is_disabled()
479+
{
480+
config([
481+
'cloudflare-cache.use_statamic_static_cache_invalidation' => false,
482+
'cloudflare-cache.debug' => true,
483+
]);
484+
485+
Log::shouldReceive('debug')->once()->withArgs(function ($message, $context = []) {
486+
return str_contains($message, 'Statamic static cache invalidation mode is disabled')
487+
&& ($context['event'] ?? null) === UrlInvalidated::class;
488+
});
489+
490+
$event = new UrlInvalidated('/test-entry', 'https://example.com');
491+
492+
$clientMock = $this->mock(Client::class);
493+
$clientMock->shouldNotReceive('purgeUrls');
494+
$clientMock->shouldNotReceive('purgeEverything');
495+
496+
$listener = $this->app->make(PurgeCloudflareCache::class);
497+
$listener->handle($event);
498+
499+
Queue::assertNothingPushed();
500+
}
358501
}

0 commit comments

Comments
 (0)