Skip to content
Open
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
48 changes: 36 additions & 12 deletions src/Console/Commands/StaticWarm.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class StaticWarm extends Command

private $uris;

private $paginated = [];

public function handle()
{
if (! config('statamic.static_caching.strategy')) {
Expand Down Expand Up @@ -114,20 +116,22 @@ private function warm(): void
$promise = $pool->promise();

$promise->wait();

$this->warmPaginatedPages();
}
}

private function warmPaginatedPages(string $url, int $currentPage, int $totalPages, string $pageName): void
/**
* Paginated pages are only known about once their first page has responded. They're
* warmed together afterwards, so the pool of other URLs is never left waiting on them.
*/
private function warmPaginatedPages(): void
{
$urls = collect(range($currentPage, $totalPages))->map(function ($page) use ($url, $pageName) {
$url = "{$url}?{$pageName}={$page}";

if (config('statamic.static_caching.background_recache', false)) {
$url = RecacheToken::addToUrl($url);
}
$urls = $this->paginatedUrls();

return $url;
});
if ($urls->isEmpty()) {
return;
}

$headers = $this->parseHeaders($this->option('header'));

Expand Down Expand Up @@ -175,12 +179,32 @@ public function outputSuccessLine(Response $response, $index): void
$this->components->twoColumnDetail($this->getRelativeUri($this->uris()->get($index)), '<info>✓ Cached</info>');

if ($response->hasHeader('X-Statamic-Pagination')) {
[$currentPage, $totalPages, $pageName] = $this->paginationHeader($response);

$this->warmPaginatedPages($this->uris()->get($index), $currentPage, $totalPages, $pageName);
$this->paginated[] = [$this->uris()->get($index), ...$this->paginationHeader($response)];
}
}

private function paginatedUrls(): Collection
{
$cacher = app(StaticCacher::class);

return collect($this->paginated)->flatMap(function ($paginated) use ($cacher) {
[$url, $currentPage, $totalPages, $pageName] = $paginated;

$cachedAs = $cacher->getUrl(HttpRequest::create($url));

return collect(range($currentPage, $totalPages))
->map(fn ($page) => $url.(str_contains($url, '?') ? '&' : '?')."{$pageName}={$page}")
// When query strings are ignored, a page is cached under the same URL
// as the one that has just been warmed, so there's nothing left to warm.
->reject(fn ($pageUrl) => $cacher->getUrl(HttpRequest::create($pageUrl)) === $cachedAs)
->map(function ($pageUrl) {
return config('statamic.static_caching.background_recache', false)
? RecacheToken::addToUrl($pageUrl)
: $pageUrl;
});
})->values();
}

public function outputFailureLine($exception, $index): void
{
$this->outputFailureLineFor($this->uris()->get($index), $exception);
Expand Down
8 changes: 8 additions & 0 deletions src/Console/Commands/StaticWarmJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Request as HttpRequest;
use Illuminate\Queue\InteractsWithQueue;
use Psr\Http\Message\ResponseInterface;
use Statamic\Console\Commands\Concerns\NormalizesPaginationHeader;
use Statamic\StaticCaching\Cacher;

class StaticWarmJob implements ShouldBeUnique, ShouldQueue
{
Expand All @@ -31,6 +33,9 @@ public function handle()
if ($this->shouldWarmPaginatedPages($response)) {
[$currentPage, $totalPages, $pageName] = $this->paginationHeader($response);

$cacher = app(Cacher::class);
$cachedAs = $cacher->getUrl(HttpRequest::create((string) $this->request->getUri()));

collect(range($currentPage, $totalPages))
->map(function (int $page) use ($pageName): string {
$url = $this->request->getUri();
Expand All @@ -41,6 +46,9 @@ public function handle()
"{$pageName}={$page}",
]);
})
// When query strings are ignored, a page is cached under the same URL
// as the one that has just been warmed, so there's nothing left to warm.
->reject(fn (string $uri) => $cacher->getUrl(HttpRequest::create($uri)) === $cachedAs)
->each(fn (string $uri) => static::dispatch(
new Request('GET', $uri, $this->request->getHeaders()),
$this->clientConfig
Expand Down
25 changes: 25 additions & 0 deletions tests/Console/Commands/StaticWarmJobTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ public function it_dispatches_paginated_pages_with_the_same_job_class()
Queue::assertPushed(CustomPaginatedStaticWarmJob::class, 2);
}

#[Test]
public function it_doesnt_dispatch_paginated_pages_that_are_cached_under_the_same_url()
{
config([
'statamic.static_caching.strategy' => 'half',
'statamic.static_caching.ignore_query_strings' => true,
]);

Queue::fake();

$mock = new MockHandler([
(new Response(200))->withHeader('X-Statamic-Pagination', [
'current' => 1,
'total' => 3,
'name' => 'page',
]),
]);

$job = new StaticWarmJob(new Request('GET', 'http://localhost/blog'), ['handler' => HandlerStack::create($mock)]);

$job->handle();

Queue::assertNothingPushed();
}

#[Test]
public function it_sends_a_get_request_and_dispatches_static_warm_job_for_page_with_pagination()
{
Expand Down
122 changes: 122 additions & 0 deletions tests/Console/Commands/StaticWarmTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Console\Commands\StaticWarm;
use Statamic\Console\Commands\StaticWarmJob;
use Statamic\Console\Commands\StaticWarmUncachedJob;
use Statamic\Facades\Collection;
Expand Down Expand Up @@ -370,6 +371,127 @@ public function it_sets_custom_headers_on_paginated_requests()
}
}

#[Test]
public function it_warms_paginated_pages_after_the_other_urls()
{
config([
'statamic.static_caching.strategy' => 'half',
'statamic.static_caching.strategies.half.warm_concurrency' => 1,
]);

$history = [];
$this->mockClient([
(new Response(200))->withHeader('X-Statamic-Pagination', [
'current' => 1,
'total' => 3,
'name' => 'page',
]),
new Response(200),
new Response(200),
new Response(200),
new Response(200),
], $history);

$this->artisan('statamic:static:warm')->assertExitCode(0);

$visited = collect($history)->map(function ($transaction) {
$uri = $transaction['request']->getUri();

return $uri->getPath().($uri->getQuery() ? '?'.$uri->getQuery() : '');
})->all();

$this->assertEquals(['/about', '/contact', '/about?page=1', '/about?page=2', '/about?page=3'], $visited);
}

#[Test]
public function it_adds_the_page_to_a_url_that_already_has_a_query_string()
{
config([
'statamic.static_caching.strategy' => 'half',
'statamic.static_caching.strategies.half.warm_concurrency' => 1,
]);

StaticWarm::hook('additional', fn () => collect(['/zebra?filter=stripes']));

$history = [];
$this->mockClient([
new Response(200),
new Response(200),
(new Response(200))->withHeader('X-Statamic-Pagination', [
'current' => 1,
'total' => 2,
'name' => 'page',
]),
new Response(200),
new Response(200),
], $history);

$this->artisan('statamic:static:warm')->assertExitCode(0);

$queries = collect($history)->map(fn ($transaction) => $transaction['request']->getUri()->getQuery())->filter()->values()->all();

$this->assertEquals(['filter=stripes', 'filter=stripes&page=1', 'filter=stripes&page=2'], $queries);
}

#[Test]
public function it_doesnt_warm_paginated_pages_that_are_cached_under_the_same_url()
{
config([
'statamic.static_caching.strategy' => 'half',
'statamic.static_caching.ignore_query_strings' => true,
]);

$history = [];
$this->mockClient([
(new Response(200))->withHeader('X-Statamic-Pagination', [
'current' => 1,
'total' => 3,
'name' => 'page',
]),
new Response(200),
new Response(200),
new Response(200),
new Response(200),
], $history);

$this->artisan('statamic:static:warm')->assertExitCode(0);

$this->assertCount(2, $history);
}

#[Test]
public function it_only_warms_paginated_pages_whose_query_string_is_part_of_the_cached_url()
{
config([
'statamic.static_caching.strategy' => 'half',
'statamic.static_caching.allowed_query_strings' => ['page'],
]);

$history = [];
$this->mockClient([
(new Response(200))->withHeader('X-Statamic-Pagination', [
'current' => 1,
'total' => 2,
'name' => 'page',
]),
(new Response(200))->withHeader('X-Statamic-Pagination', [
'current' => 1,
'total' => 2,
'name' => 'p',
]),
new Response(200),
new Response(200),
new Response(200),
new Response(200),
], $history);

$this->artisan('statamic:static:warm')->assertExitCode(0);

$queries = collect($history)->map(fn ($transaction) => $transaction['request']->getUri()->getQuery())->filter()->values()->all();

$this->assertEquals(['page=1', 'page=2'], $queries);
}

private function mockClient(array $responses, array &$history)
{
$handlerStack = HandlerStack::create(new MockHandler($responses));
Expand Down
Loading