diff --git a/README.md b/README.md index 8e3bfed..211cf6a 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Automatically purge your Cloudflare cache when content changes in Statamic. - Automatically purges Cloudflare cache when Statamic content changes. - **Multi-zone support** for Statamic multisite installations with different domains. - Configurable events that trigger cache purging. +- Optional support for Statamic `static_caching` invalidation rules via `UrlInvalidated`/`StaticCacheCleared` events. - Optional queuing of purge jobs for background processing. - CLI command for manual cache purging. - Simple configuration with backward compatibility. @@ -79,6 +80,11 @@ Once configured, the addon will automatically purge the Cloudflare cache when co You can configure which events trigger cache purging in the config file. +#### Event handling modes (either/or) + +- **Default (`use_statamic_static_cache_invalidation = false`)**: uses this addon’s own content events (`entry_*`, `term_*`, `asset_*`, etc.). +- **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. + #### Queued Purging 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 [ 'nav_tree_saved' => true, 'global_set_saved' => true, 'global_set_deleted' => true, + 'url_invalidated' => true, // Statamic Static Cache: UrlInvalidated event + 'static_cache_cleared' => true, // Statamic Static Cache: StaticCacheCleared event ], + // When enabled, only Statamic Static Cache events are handled (UrlInvalidated/StaticCacheCleared). + // Legacy addon content events are skipped in this mode. + 'use_statamic_static_cache_invalidation' => env('CLOUDFLARE_CACHE_USE_STATAMIC_STATIC_CACHE_INVALIDATION', false), + // Dispatch purge jobs to the queue instead of running synchronously 'queue_purge' => env('CLOUDFLARE_CACHE_QUEUE_PURGE', false), diff --git a/config/cloudflare-cache.php b/config/cloudflare-cache.php index 3e58d8a..12e8b59 100644 --- a/config/cloudflare-cache.php +++ b/config/cloudflare-cache.php @@ -47,8 +47,14 @@ 'nav_tree_saved' => true, 'global_set_saved' => true, 'global_set_deleted' => true, + 'url_invalidated' => true, // Statamic Static Cache: UrlInvalidated event + 'static_cache_cleared' => true, // Statamic Static Cache: StaticCacheCleared event ], + // When enabled, only Statamic Static Cache events are handled (UrlInvalidated/StaticCacheCleared). + // Legacy addon content events are skipped in this mode. + 'use_statamic_static_cache_invalidation' => env('CLOUDFLARE_CACHE_USE_STATAMIC_STATIC_CACHE_INVALIDATION', false), + 'queue_purge' => env('CLOUDFLARE_CACHE_QUEUE_PURGE', false), // Dispatch purge jobs to the queue /* diff --git a/src/CloudflareCacheServiceProvider.php b/src/CloudflareCacheServiceProvider.php index 3764d23..b58c765 100644 --- a/src/CloudflareCacheServiceProvider.php +++ b/src/CloudflareCacheServiceProvider.php @@ -15,6 +15,8 @@ use Statamic\Events\NavTreeSaved; use Statamic\Events\GlobalSetSaved; use Statamic\Events\GlobalSetDeleted; +use Statamic\Events\UrlInvalidated; +use Statamic\Events\StaticCacheCleared; class CloudflareCacheServiceProvider extends AddonServiceProvider { @@ -53,6 +55,12 @@ class CloudflareCacheServiceProvider extends AddonServiceProvider GlobalSetDeleted::class => [ PurgeCloudflareCache::class, ], + UrlInvalidated::class => [ + PurgeCloudflareCache::class, + ], + StaticCacheCleared::class => [ + PurgeCloudflareCache::class, + ], ]; /** diff --git a/src/Listeners/PurgeCloudflareCache.php b/src/Listeners/PurgeCloudflareCache.php index c3f1a8d..c080faa 100644 --- a/src/Listeners/PurgeCloudflareCache.php +++ b/src/Listeners/PurgeCloudflareCache.php @@ -16,6 +16,8 @@ use Statamic\Events\NavTreeSaved; use Statamic\Events\GlobalSetSaved; use Statamic\Events\GlobalSetDeleted; +use Statamic\Events\UrlInvalidated; +use Statamic\Events\StaticCacheCleared; use Statamic\Facades\URL; use Illuminate\Support\Facades\Log; // Already present, but good to confirm @@ -31,6 +33,11 @@ public function __construct(Client $client) public function handle(Event $event): void { if (!config('cloudflare-cache.enabled')) { + if (config('cloudflare-cache.debug')) { + Log::debug('[Cloudflare Cache] Skipping purge because addon is disabled.', [ + 'event' => get_class($event), + ]); + } return; } @@ -108,7 +115,28 @@ protected function purgeSynchronously(array $urls): void */ protected function shouldHandleEvent(Event $event): bool { + $useStatamicInvalidation = config('cloudflare-cache.use_statamic_static_cache_invalidation', false); + $isStaticCacheInvalidationEvent = $event instanceof UrlInvalidated || $event instanceof StaticCacheCleared; $eventClass = get_class($event); + + if ($useStatamicInvalidation && !$isStaticCacheInvalidationEvent) { + if (config('cloudflare-cache.debug')) { + Log::debug('[Cloudflare Cache] Skipping event because Statamic static cache invalidation mode is enabled and this is a legacy addon event.', [ + 'event' => $eventClass, + ]); + } + return false; + } + + if (!$useStatamicInvalidation && $isStaticCacheInvalidationEvent) { + if (config('cloudflare-cache.debug')) { + Log::debug('[Cloudflare Cache] Skipping event because Statamic static cache invalidation mode is disabled.', [ + 'event' => $eventClass, + ]); + } + return false; + } + $eventMap = [ 'Statamic\Events\EntrySaved' => 'entry_saved', 'Statamic\Events\EntryDeleted' => 'entry_deleted', @@ -120,11 +148,31 @@ protected function shouldHandleEvent(Event $event): bool 'Statamic\Events\NavTreeSaved' => 'nav_tree_saved', 'Statamic\Events\GlobalSetSaved' => 'global_set_saved', 'Statamic\Events\GlobalSetDeleted' => 'global_set_deleted', + 'Statamic\Events\UrlInvalidated' => 'url_invalidated', + 'Statamic\Events\StaticCacheCleared' => 'static_cache_cleared', ]; $configKey = $eventMap[$eventClass] ?? null; - return $configKey && config("cloudflare-cache.purge_on.{$configKey}"); + if (!$configKey) { + if (config('cloudflare-cache.debug')) { + Log::debug('[Cloudflare Cache] Skipping event because no purge_on mapping exists.', [ + 'event' => $eventClass, + ]); + } + return false; + } + + $shouldHandle = (bool) config("cloudflare-cache.purge_on.{$configKey}"); + + if (!$shouldHandle && config('cloudflare-cache.debug')) { + Log::debug('[Cloudflare Cache] Skipping event because purge_on setting is disabled.', [ + 'event' => $eventClass, + 'config_key' => "cloudflare-cache.purge_on.{$configKey}", + ]); + } + + return $shouldHandle; } /** @@ -186,6 +234,10 @@ protected function getUrlsToPurge(Event $event): array // existing purge_everything_fallback logic (sync or queued). } + if ($event instanceof UrlInvalidated && $event->url) { + $urls[] = URL::makeAbsolute($event->url); + } + $urls = array_filter($urls); return array_unique($urls); diff --git a/tests/Feature/PurgeCacheListenerTest.php b/tests/Feature/PurgeCacheListenerTest.php index cb8a1ac..e1779be 100644 --- a/tests/Feature/PurgeCacheListenerTest.php +++ b/tests/Feature/PurgeCacheListenerTest.php @@ -8,11 +8,14 @@ use Statamic\Events\EntrySaved; use Statamic\Events\GlobalSetSaved; use Statamic\Events\GlobalSetDeleted; +use Statamic\Events\UrlInvalidated; +use Statamic\Events\StaticCacheCleared; use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\Entries\Collection; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; // Add Queue facade +use Illuminate\Support\Facades\Log; use Eminos\StatamicCloudflareCache\Events\CachePurged; use Eminos\StatamicCloudflareCache\Jobs\PurgeCloudflareCacheJob; use Mockery; @@ -355,4 +358,144 @@ public function it_fires_cache_purged_event_after_synchronous_purge_everything() return $e->purgedEverything && empty($e->urls); }); } + + #[Test] + public function it_ignores_legacy_events_when_statamic_static_cache_invalidation_is_enabled() + { + Event::fake([CachePurged::class]); + config([ + 'cloudflare-cache.use_statamic_static_cache_invalidation' => true, + 'cloudflare-cache.queue_purge' => false, + ]); + + $entry = $this->mockEntry(); + $event = new EntrySaved($entry); + + $clientMock = $this->mock(Client::class); + $clientMock->shouldNotReceive('purgeUrls'); + $clientMock->shouldNotReceive('purgeEverything'); + + $listener = $this->app->make(PurgeCloudflareCache::class); + $listener->handle($event); + + Queue::assertNothingPushed(); + Event::assertNotDispatched(CachePurged::class); + } + + #[Test] + public function it_purges_invalidated_urls_when_statamic_static_cache_invalidation_is_enabled() + { + Event::fake([CachePurged::class]); + config([ + 'cloudflare-cache.use_statamic_static_cache_invalidation' => true, + 'cloudflare-cache.queue_purge' => false, + ]); + + $event = new UrlInvalidated('/test-entry', 'https://example.com'); + + $clientMock = $this->mock(Client::class); + $clientMock->shouldReceive('purgeUrls')->once()->with(['https://example.com/test-entry']); + $clientMock->shouldNotReceive('purgeEverything'); + + $listener = $this->app->make(PurgeCloudflareCache::class); + $listener->handle($event); + + Queue::assertNothingPushed(); + Event::assertDispatched(CachePurged::class, function ($e) { + return !$e->purgedEverything && $e->urls === ['https://example.com/test-entry']; + }); + } + + #[Test] + public function it_purges_everything_when_static_cache_is_cleared_and_statamic_static_cache_invalidation_is_enabled() + { + config([ + 'cloudflare-cache.use_statamic_static_cache_invalidation' => true, + 'cloudflare-cache.queue_purge' => false, + 'cloudflare-cache.purge_everything_fallback' => true, + ]); + + $event = new StaticCacheCleared(); + + $clientMock = $this->mock(Client::class); + $clientMock->shouldReceive('purgeEverything')->once(); + $clientMock->shouldNotReceive('purgeUrls'); + + $listener = $this->app->make(PurgeCloudflareCache::class); + $listener->handle($event); + + Queue::assertNothingPushed(); + } + + #[Test] + public function it_does_not_purge_when_static_cache_is_cleared_and_fallback_is_disabled() + { + config([ + 'cloudflare-cache.use_statamic_static_cache_invalidation' => true, + 'cloudflare-cache.queue_purge' => false, + 'cloudflare-cache.purge_everything_fallback' => false, + ]); + + $event = new StaticCacheCleared(); + + $clientMock = $this->mock(Client::class); + $clientMock->shouldNotReceive('purgeEverything'); + $clientMock->shouldNotReceive('purgeUrls'); + + $listener = $this->app->make(PurgeCloudflareCache::class); + $listener->handle($event); + + Queue::assertNothingPushed(); + } + + #[Test] + public function it_logs_when_legacy_event_is_skipped_in_statamic_static_cache_invalidation_mode() + { + config([ + 'cloudflare-cache.use_statamic_static_cache_invalidation' => true, + 'cloudflare-cache.debug' => true, + ]); + + Log::shouldReceive('debug')->once()->withArgs(function ($message, $context = []) { + return str_contains($message, 'Statamic static cache invalidation mode is enabled') + && ($context['event'] ?? null) === EntrySaved::class; + }); + + $entry = $this->mockEntry(); + $event = new EntrySaved($entry); + + $clientMock = $this->mock(Client::class); + $clientMock->shouldNotReceive('purgeUrls'); + $clientMock->shouldNotReceive('purgeEverything'); + + $listener = $this->app->make(PurgeCloudflareCache::class); + $listener->handle($event); + + Queue::assertNothingPushed(); + } + + #[Test] + public function it_logs_when_static_cache_event_is_skipped_while_mode_is_disabled() + { + config([ + 'cloudflare-cache.use_statamic_static_cache_invalidation' => false, + 'cloudflare-cache.debug' => true, + ]); + + Log::shouldReceive('debug')->once()->withArgs(function ($message, $context = []) { + return str_contains($message, 'Statamic static cache invalidation mode is disabled') + && ($context['event'] ?? null) === UrlInvalidated::class; + }); + + $event = new UrlInvalidated('/test-entry', 'https://example.com'); + + $clientMock = $this->mock(Client::class); + $clientMock->shouldNotReceive('purgeUrls'); + $clientMock->shouldNotReceive('purgeEverything'); + + $listener = $this->app->make(PurgeCloudflareCache::class); + $listener->handle($event); + + Queue::assertNothingPushed(); + } }