Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Enable background re-caching of static cache #9396

Open
wants to merge 13 commits into
base: 5.x
Choose a base branch
from
12 changes: 12 additions & 0 deletions config/static_caching.php
Expand Up @@ -125,4 +125,16 @@

'warm_queue' => null,

/*
|--------------------------------------------------------------------------
| Background Re-cache
|--------------------------------------------------------------------------
|
| When this enabled, Statamic will re-cache URLs in the background,
| overwriting the existing cache, without removing it first.
|
*/

'background_recache' => env('STATAMIC_BACKGROUND_RECACHE', false),

];
39 changes: 39 additions & 0 deletions src/Jobs/StaticRecacheJob.php
@@ -0,0 +1,39 @@
<?php

namespace Statamic\Jobs;

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Hash;
use Statamic\StaticCaching\Cacher;

class StaticRecacheJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;

public Request $request;

public $tries = 1;

public function __construct($url, $domain)
{
$domain ??= app(Cacher::class)->getBaseUrl();

$url = $domain.$url;

$param = '__recache='.Hash::make($url);

$url .= (str_contains($url, '?') ? '&' : '?').$param;

$this->request = new Request('GET', $url);
}

public function handle(Client $client)
{
$client->send($this->request);
}
}
16 changes: 16 additions & 0 deletions src/StaticCaching/Cacher.php
Expand Up @@ -51,6 +51,22 @@ public function invalidateUrl($url);
*/
public function invalidateUrls($urls);

/**
* Recache a URL.
*
* @param string $url
* @return void
*/
public function recacheUrl($url);

/**
* Recache multiple URLs.
*
* @param array $urls
* @return void
*/
public function recacheUrls($urls);

/**
* Get all the URLs that have been cached.
*
Expand Down
65 changes: 65 additions & 0 deletions src/StaticCaching/Cachers/AbstractCacher.php
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Statamic\Facades\Site;
use Statamic\Jobs\StaticRecacheJob;
use Statamic\StaticCaching\Cacher;
use Statamic\StaticCaching\UrlExcluder;
use Statamic\Support\Str;
Expand Down Expand Up @@ -138,6 +139,13 @@ public function getUrl(Request $request)
{
$url = $request->getUri();

if ($recache = $request->input('__recache')) {
$url = str_replace('__recache='.$recache, '', $url);
if (substr($url, -1, 1) == '?') {
$url = substr($url, 0, -1);
}
}

if ($this->config('ignore_query_strings')) {
$url = explode('?', $url)[0];
}
Expand Down Expand Up @@ -247,6 +255,63 @@ public function invalidateUrls($urls)
});
}

/**
* Recache multiple URLs.
*
* @param array $urls
* @return void
*/
public function recacheUrls($urls)
{
collect($urls)
->map(fn ($url) => is_array($url) ? $url : [$url, null])
->each(function ($parts) {
[$path, $domain] = $parts;

if (Str::contains($path, '*')) {
$this->recacheWildcardUrl($path, $domain);

return;
}

$this->recacheUrl($path, $domain);
});
}

/**
* Recache an individual URLs.
*
* @param string $path
* @param string|null $domain
* @return void
*/
public function recacheUrl($path, $domain = null)
{
StaticRecacheJob::dispatch($path, $domain);
}

/**
* Recache a wildcard URL.
*
* @param string $wildcard
* @param string|null $domain
*/
protected function recacheWildcardUrl($wildcard, $domain = null)
{
// Remove the asterisk
$wildcard = substr($wildcard, 0, -1);

if (! $domain) {
[$wildcard, $domain] = $this->getPathAndDomain($wildcard);
}

$this->getUrls($domain)->filter(function ($url) use ($wildcard) {
return Str::startsWith($url, $wildcard);
})->each(function ($url) use ($domain) {
$this->recacheUrl($url, $domain);
});
}

/**
* Determine if a given URL should be excluded from caching.
*
Expand Down
2 changes: 1 addition & 1 deletion src/StaticCaching/Cachers/FileCacher.php
Expand Up @@ -60,7 +60,7 @@ public function cachePage(Request $request, $content)

$content = $this->normalizeContent($content);

$path = $this->getFilePath($request->getUri());
$path = $this->getFilePath($url);

if (! $this->writer->write($path, $content, $this->config('lock_hold_length'))) {
return;
Expand Down
10 changes: 10 additions & 0 deletions src/StaticCaching/Cachers/NullCacher.php
Expand Up @@ -37,6 +37,16 @@ public function invalidateUrl($url)
//
}

public function recacheUrls($urls)
{
//
}

public function recacheUrl($url)
{
//
}

public function getUrls($domain = null)
{
return collect();
Expand Down
118 changes: 75 additions & 43 deletions src/StaticCaching/DefaultInvalidator.php
Expand Up @@ -28,90 +28,122 @@ public function invalidate($item)
return $this->cacher->flush();
}

$urls = collect();

if ($item instanceof Entry) {
$this->invalidateEntryUrls($item);
$urls = $this->getEntryUrls($item);
} elseif ($item instanceof Term) {
$this->invalidateTermUrls($item);
$urls = $this->getTermUrls($item);
} elseif ($item instanceof Nav) {
$this->invalidateNavUrls($item);
$urls = $this->getNavUrls($item);
} elseif ($item instanceof GlobalSet) {
$this->invalidateGlobalUrls($item);
$urls = $this->getGlobalUrls($item);
} elseif ($item instanceof Collection) {
$this->invalidateCollectionUrls($item);
$urls = $this->getCollectionUrls($item);
} elseif ($item instanceof Asset) {
$this->invalidateAssetUrls($item);
$urls = $this->getAssetUrls($item);
} elseif ($item instanceof Form) {
$this->invalidateFormUrls($item);
$urls = $this->getFormUrls($item);
}

collect($urls)
->filter(fn ($url) => is_array($url))
->each(fn ($url) => $this->cacher->invalidateUrl(...$url));

$urls = collect($urls)->filter(fn ($url) => ! is_array($url));
if ($urls->isNotEmpty()) {
$this->cacher->invalidateUrls($urls->values()->all());
}
}

public function invalidateAndRecache($item)
{
if (! config('statamic.static_caching.background_recache', false)) {
return $this->invalidate($item);
}

$urls = [];

if ($this->rules === 'all') {
$this->recacheUrls($this->cacher->getUrls());

return;
}

if ($item instanceof Entry) {
$urls = $this->getEntryUrls($item);
} elseif ($item instanceof Term) {
$urls = $this->getTermUrls($item);
} elseif ($item instanceof Nav) {
$urls = $this->getNavUrls($item);
} elseif ($item instanceof GlobalSet) {
$urls = $this->getGlobalUrls($item);
} elseif ($item instanceof Collection) {
$urls = $this->getCollectionUrls($item);
} elseif ($item instanceof Asset) {
$urls = $this->getAssetUrls($item);
} elseif ($item instanceof Form) {
$urls = $this->getFormUrls($item);
}

$this->cacher->recacheUrls($urls);
}

protected function invalidateFormUrls($form)
private function getFormUrls($form)
{
$this->cacher->invalidateUrls(
Arr::get($this->rules, "forms.{$form->handle()}.urls")
);
return Arr::get($this->rules, "forms.{$form->handle()}.urls");
}

protected function invalidateAssetUrls($asset)
protected function getAssetUrls($asset)
{
$this->cacher->invalidateUrls(
Arr::get($this->rules, "assets.{$asset->container()->handle()}.urls")
);
return Arr::get($this->rules, "assets.{$asset->container()->handle()}.urls");
}

protected function invalidateEntryUrls($entry)
protected function getEntryUrls($entry)
{
$entry->descendants()->merge([$entry])->each(function ($entry) {
$urls = $entry->descendants()->merge([$entry])->map(function ($entry) {
if (! $entry->isRedirect() && $url = $entry->absoluteUrl()) {
$this->cacher->invalidateUrl(...$this->splitUrlAndDomain($url));
return $this->splitUrlAndDomain($url);
}
});
})->filter();

$this->cacher->invalidateUrls(
Arr::get($this->rules, "collections.{$entry->collectionHandle()}.urls")
);
return $urls->merge(Arr::get($this->rules, "collections.{$entry->collectionHandle()}.urls"))->all();
}

protected function invalidateTermUrls($term)
protected function getTermUrls($term)
{
$urls = collect();
if ($url = $term->absoluteUrl()) {
$this->cacher->invalidateUrl(...$this->splitUrlAndDomain($url));
$urls = $urls->push($this->splitUrlAndDomain($url));

$term->taxonomy()->collections()->each(function ($collection) use ($term) {
$urls = $urls->merge($term->taxonomy()->collections()->map(function ($collection) use ($term) {
if ($url = $term->collection($collection)->absoluteUrl()) {
$this->cacher->invalidateUrl(...$this->splitUrlAndDomain($url));
return $this->splitUrlAndDomain($url);
}
});
}))->filter();
}

$this->cacher->invalidateUrls(
Arr::get($this->rules, "taxonomies.{$term->taxonomyHandle()}.urls")
);
return $urls->merge(Arr::get($this->rules, "taxonomies.{$term->taxonomyHandle()}.urls"))->all();
}

protected function invalidateNavUrls($nav)
protected function getNavUrls($nav)
{
$this->cacher->invalidateUrls(
Arr::get($this->rules, "navigation.{$nav->handle()}.urls")
);
return Arr::get($this->rules, "navigation.{$nav->handle()}.urls");
}

protected function invalidateGlobalUrls($set)
protected function getGlobalUrls($set)
{
$this->cacher->invalidateUrls(
Arr::get($this->rules, "globals.{$set->handle()}.urls")
);
return Arr::get($this->rules, "globals.{$set->handle()}.urls");
}

protected function invalidateCollectionUrls($collection)
protected function getCollectionUrls($collection)
{
$urls = [];
if ($url = $collection->absoluteUrl()) {
$this->cacher->invalidateUrl(...$this->splitUrlAndDomain($url));
$urls[] = $this->splitUrlAndDomain($url);
}

$this->cacher->invalidateUrls(
Arr::get($this->rules, "collections.{$collection->handle()}.urls")
);
return array_merge($urls, Arr::get($this->rules, "collections.{$collection->handle()}.urls"));
}

private function splitUrlAndDomain(string $url)
Expand Down