Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions src/Http/Middleware/CacheTracker.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
use Statamic\Contracts\Assets\Asset;
use Statamic\Contracts\Entries\Entry;
use Statamic\Contracts\Globals\Variables;
use Statamic\Facades\StaticCache;
use Statamic\Facades\URL;
use Statamic\Forms;
use Statamic\StaticCaching\Cacher;
use Statamic\StaticCaching\RecacheToken;
use Statamic\Structures\Nav;
use Statamic\Structures\Page;
use Statamic\Support\Str;
Expand Down Expand Up @@ -42,19 +43,16 @@ public function handle($request, Closure $next)
return $next($request);
}

$cacher = app(Cacher::class);

if ($cacher && $cacher->hasCachedPage($request)) {
return $next($request);
}

$url = $this->url();

if (Str::endsWith($url, '/')) {
$url = substr($url, 0, -1);
}

if (Tracker::has($url)) {
// A background-recache request re-renders a page that is still cached, so
// let it through even when the URL is already tracked - this is how the
// stored tags get refreshed when a page's content dependencies change.
if (! $this->isRecacheRequest($request) && Tracker::has($url)) {
return $next($request);
}

Expand Down Expand Up @@ -102,6 +100,15 @@ private function isEnabled($request)
return $request->method() === 'GET' && ! Str::startsWith($request->path(), [config('statamic.routes.action', '!').'/', config('statamic.assets.image_manipulation.route')]);
}

private function isRecacheRequest($request): bool
{
if (! $token = $request->input(StaticCache::recacheTokenParameter())) {
return false;
}

return StaticCache::checkRecacheToken($token);
}

private function setupAdditionalTracking()
{
$pipelines = Tracker::getAdditionalTrackers();
Expand Down Expand Up @@ -206,6 +213,10 @@ private function setupTagHooks()

private function url()
{
return URL::makeAbsolute(class_exists(Livewire::class) ? Livewire::originalUrl() : URL::getCurrent());
$url = URL::makeAbsolute(class_exists(Livewire::class) ? Livewire::originalUrl() : URL::getCurrent());

// Strip the background-recache token so a recache request keys the same
// entry as the original page rather than creating an orphaned one.
return RecacheToken::removeFromUrl($url);
}
}
20 changes: 15 additions & 5 deletions src/Tracker/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,30 @@ public function invalidate(array $tags = [])
{
$storeData = $this->all();

// With background recaching the page is refreshed in place rather than
// deleted, and the recache request re-tracks the URL with fresh tags. We
// keep the existing entry so that if the recache job fails or is dropped
// the URL stays tracked and future saves will retry it, instead of being
// orphaned in the static cache with no tags.
$keepEntries = config('statamic.static_caching.background_recache', false);

$urls = [];
foreach ($storeData as $key => $data) {
$storeTags = $data['tags'];
$url = $data['url'];
if (! $this->tagsMatch($tags, $data['tags'])) {
continue;
}

if ($this->tagsMatch($tags, $storeTags)) {
$urls[] = $url;
$urls[] = $data['url'];

if (! $keepEntries) {
unset($storeData[$key]);
}
}

if (! empty($urls)) {
$this->cacheStore()->forever($this->cacheKey, $storeData);
if (! $keepEntries) {
$this->cacheStore()->forever($this->cacheKey, $storeData);
}

$this->invalidateUrls($urls);
}
Expand Down
41 changes: 40 additions & 1 deletion tests/Unit/TrackerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPUnit\Framework\Attributes\Test;
use Statamic\Console\Commands\StaticWarmJob;
use Statamic\Events\UrlInvalidated;
use Statamic\Facades\StaticCache;
use Thoughtco\StatamicCacheTracker\Events\ContentTracked;
use Thoughtco\StatamicCacheTracker\Facades\Tracker;
use Thoughtco\StatamicCacheTracker\Tests\TestCase;
Expand Down Expand Up @@ -244,6 +245,44 @@ public function it_queues_a_recache_job_instead_of_invalidating_when_background_

Queue::assertPushed(StaticWarmJob::class);
Event::assertNotDispatched(UrlInvalidated::class);
$this->assertCount(0, Tracker::all());

// The entry is retained so that if the recache job fails or is dropped
// the URL stays tracked and future saves will retry it, instead of being
// orphaned in the static cache with no tags.
$this->assertCount(1, Tracker::all());
$this->assertSame(['pages:home', 'collection:pages'], collect(Tracker::all())->first()['tags']);
}

#[Test]
public function it_refreshes_tags_for_an_already_tracked_url_on_a_recache_request()
{
$this->get('/');

$this->assertSame(['pages:home', 'collection:pages'], collect(Tracker::all())->first()['tags']);

Tracker::addAdditionalTracker(function ($tracker, $next) {
$tracker->addContentTag('fresh::tag');
});

$token = StaticCache::recacheTokenParameter().'='.StaticCache::recacheToken();

$this->get('/?'.$token);

$this->assertCount(1, Tracker::all());
$this->assertSame(['fresh::tag', 'pages:home', 'collection:pages'], collect(Tracker::all())->first()['tags']);
}

#[Test]
public function it_does_not_retrack_an_already_tracked_url_on_a_normal_request()
{
$this->get('/');

Tracker::addAdditionalTracker(function ($tracker, $next) {
$tracker->addContentTag('fresh::tag');
});

$this->get('/');

$this->assertSame(['pages:home', 'collection:pages'], collect(Tracker::all())->first()['tags']);
}
}
3 changes: 3 additions & 0 deletions tests/__fixtures__/resources/views/nav-default.antlers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{ nav }}
{{ title }}
{{ /nav }}
3 changes: 3 additions & 0 deletions tests/__fixtures__/resources/views/nav-test.antlers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{ nav :handle="my_nav" }}
{{ title }}
{{ /nav }}