diff --git a/src/StaticCaching/DefaultInvalidator.php b/src/StaticCaching/DefaultInvalidator.php index 3e0ed222c2a..9f548145b40 100644 --- a/src/StaticCaching/DefaultInvalidator.php +++ b/src/StaticCaching/DefaultInvalidator.php @@ -8,6 +8,7 @@ use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\Forms\Form; use Statamic\Contracts\Globals\Variables; +use Statamic\Contracts\Routing\UrlBuilder; use Statamic\Contracts\Structures\Nav; use Statamic\Contracts\Structures\NavTree; use Statamic\Facades\Antlers; @@ -37,7 +38,10 @@ public function invalidate($item) return; } - $urls = $this->getItemUrls($item); + $urls = [ + ...$this->getItemUrls($item), + ...$this->getItemOldUrls($item), + ]; $this->cacher->invalidateUrls($urls); } @@ -50,6 +54,11 @@ public function refresh($item) return; } + // Old URLs no longer resolve so they cannot be recached, only invalidated. + if ($oldUrls = $this->getItemOldUrls($item)) { + $this->cacher->invalidateUrls($oldUrls); + } + if ($this->rules === 'all') { $this->cacher->refreshUrls($this->cacher->getUrls()->all()); @@ -88,6 +97,32 @@ protected function getItemUrls($item) return $urls; } + protected function getItemOldUrls($item) + { + return $item instanceof Entry ? $this->getOldEntryUrls($item) : []; + } + + protected function getOldEntryUrls($entry) + { + if ( + is_null($originalSlug = $entry->getOriginal('slug')) + || $originalSlug === $entry->slug() + || ! ($route = $entry->route()) + ) { + return []; + } + + $uri = app(UrlBuilder::class)->content($entry)->merge([ + 'parent_uri' => $entry->parent()?->uri(), + 'slug' => $originalSlug, + ])->build($route); + + $oldUrl = URL::tidy($entry->site()->url().'/'.$uri); + + // Anything cached under the old URL (descendants, mounted collections) is stale too. + return [$oldUrl, Str::finish($oldUrl, '/').'*']; + } + protected function getFormUrls($form) { $rules = collect(Arr::get($this->rules, "forms.{$form->handle()}.urls")); diff --git a/tests/StaticCaching/DefaultInvalidatorTest.php b/tests/StaticCaching/DefaultInvalidatorTest.php index d56fb552f61..a70f24f8028 100644 --- a/tests/StaticCaching/DefaultInvalidatorTest.php +++ b/tests/StaticCaching/DefaultInvalidatorTest.php @@ -314,6 +314,7 @@ public function collection_urls_can_be_invalidated_by_an_entry() $m->shouldReceive('descendants')->andReturn(collect()); $m->shouldReceive('site')->andReturn(Site::default()); $m->shouldReceive('parent')->andReturnNull(); + $m->shouldReceive('getOriginal')->andReturnNull(); $m->shouldReceive('toAugmentedCollection') ->andReturnSelf() ->shouldReceive('merge') @@ -368,6 +369,7 @@ public function collection_urls_can_be_invalidated_by_an_entry_in_a_multisite() $m->shouldReceive('descendants')->andReturn(collect()); $m->shouldReceive('site')->andReturn(Site::get('fr')); $m->shouldReceive('parent')->andReturnNull(); + $m->shouldReceive('getOriginal')->andReturnNull(); $m->shouldReceive('toAugmentedCollection') ->andReturnSelf() ->shouldReceive('merge') @@ -417,6 +419,7 @@ public function invalidation_urls_respect_trailing_slash_enforcement() $m->shouldReceive('descendants')->andReturn(collect()); $m->shouldReceive('site')->andReturn(Site::default()); $m->shouldReceive('parent')->andReturnNull(); + $m->shouldReceive('getOriginal')->andReturnNull(); $m->shouldReceive('toAugmentedCollection') ->andReturnSelf() ->shouldReceive('merge') @@ -456,6 +459,7 @@ public function entry_urls_are_not_invalidated_by_an_entry_with_a_redirect() $m->shouldReceive('collectionHandle')->andReturn('blog'); $m->shouldReceive('descendants')->andReturn(collect()); $m->shouldReceive('parent')->andReturnNull(); + $m->shouldReceive('getOriginal')->andReturnNull(); $m->shouldReceive('site')->andReturn(Site::default()); $m->shouldReceive('toAugmentedCollection') ->andReturnSelf() @@ -479,6 +483,68 @@ public function entry_urls_are_not_invalidated_by_an_entry_with_a_redirect() $this->assertNull($invalidator->invalidate($entry)); } + #[Test] + public function old_entry_url_is_invalidated_when_the_slug_changes() + { + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/blog/new-slug', + 'http://localhost/blog/old-slug', + 'http://localhost/blog/old-slug/*', + ])->once(); + }); + + $entry = tap(Mockery::mock(Entry::class), function ($m) { + $m->shouldReceive('isRedirect')->andReturn(false); + $m->shouldReceive('absoluteUrl')->andReturn('http://localhost/blog/new-slug'); + $m->shouldReceive('collectionHandle')->andReturn('blog'); + $m->shouldReceive('descendants')->andReturn(collect()); + $m->shouldReceive('site')->andReturn(Site::default()); + $m->shouldReceive('parent')->andReturnNull(); + $m->shouldReceive('getOriginal')->with('slug')->andReturn('old-slug'); + $m->shouldReceive('slug')->andReturn('new-slug'); + $m->shouldReceive('route')->andReturn('/blog/{slug}'); + $m->shouldReceive('routeData')->andReturn(['slug' => 'new-slug']); + $m->shouldReceive('toAugmentedCollection') + ->andReturnSelf() + ->shouldReceive('merge') + ->andReturn(collect(['parent_uri' => null])); + }); + + $invalidator = new Invalidator($cacher, []); + + $this->assertNull($invalidator->invalidate($entry)); + } + + #[Test] + public function old_entry_url_is_not_invalidated_when_the_slug_is_unchanged() + { + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->with([ + 'http://localhost/blog/my-slug', + ])->once(); + }); + + $entry = tap(Mockery::mock(Entry::class), function ($m) { + $m->shouldReceive('isRedirect')->andReturn(false); + $m->shouldReceive('absoluteUrl')->andReturn('http://localhost/blog/my-slug'); + $m->shouldReceive('collectionHandle')->andReturn('blog'); + $m->shouldReceive('descendants')->andReturn(collect()); + $m->shouldReceive('site')->andReturn(Site::default()); + $m->shouldReceive('parent')->andReturnNull(); + $m->shouldReceive('getOriginal')->with('slug')->andReturn('my-slug'); + $m->shouldReceive('slug')->andReturn('my-slug'); + $m->shouldReceive('toAugmentedCollection') + ->andReturnSelf() + ->shouldReceive('merge') + ->andReturn(collect(['parent_uri' => null])); + }); + + $invalidator = new Invalidator($cacher, []); + + $this->assertNull($invalidator->invalidate($entry)); + } + #[Test] public function taxonomy_urls_can_be_invalidated() { @@ -961,6 +1027,7 @@ public function it_doesnt_recache_when_background_recache_token_is_disabled() $m->shouldReceive('descendants')->andReturn(collect()); $m->shouldReceive('site')->andReturn(Site::default()); $m->shouldReceive('parent')->andReturnNull(); + $m->shouldReceive('getOriginal')->andReturnNull(); $m->shouldReceive('toAugmentedCollection') ->andReturnSelf() ->shouldReceive('merge') @@ -1001,6 +1068,7 @@ public function it_recaches_when_background_recache_token_is_enabled() $m->shouldReceive('descendants')->andReturn(collect()); $m->shouldReceive('site')->andReturn(Site::default()); $m->shouldReceive('parent')->andReturnNull(); + $m->shouldReceive('getOriginal')->andReturnNull(); $m->shouldReceive('toAugmentedCollection') ->andReturnSelf() ->shouldReceive('merge') @@ -1020,4 +1088,41 @@ public function it_recaches_when_background_recache_token_is_enabled() $this->assertNull($invalidator->refresh($entry)); } + + #[Test] + public function old_entry_url_is_invalidated_rather_than_recached_when_background_recache_is_enabled() + { + config()->set('statamic.static_caching.background_recache', true); + + $cacher = tap(Mockery::mock(Cacher::class), function ($cacher) { + $cacher->shouldReceive('invalidateUrls')->once()->with([ + 'http://localhost/blog/old-slug', + 'http://localhost/blog/old-slug/*', + ]); + $cacher->shouldReceive('refreshUrls')->once()->with([ + 'http://localhost/blog/new-slug', + ]); + }); + + $entry = tap(Mockery::mock(Entry::class), function ($m) { + $m->shouldReceive('isRedirect')->andReturn(false); + $m->shouldReceive('absoluteUrl')->andReturn('http://localhost/blog/new-slug'); + $m->shouldReceive('collectionHandle')->andReturn('blog'); + $m->shouldReceive('descendants')->andReturn(collect()); + $m->shouldReceive('site')->andReturn(Site::default()); + $m->shouldReceive('parent')->andReturnNull(); + $m->shouldReceive('getOriginal')->with('slug')->andReturn('old-slug'); + $m->shouldReceive('slug')->andReturn('new-slug'); + $m->shouldReceive('route')->andReturn('/blog/{slug}'); + $m->shouldReceive('routeData')->andReturn(['slug' => 'new-slug']); + $m->shouldReceive('toAugmentedCollection') + ->andReturnSelf() + ->shouldReceive('merge') + ->andReturn(collect(['parent_uri' => null])); + }); + + $invalidator = new Invalidator($cacher, []); + + $this->assertNull($invalidator->refresh($entry)); + } }