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] Warm paginated pages with static:warm command #9493

Open
wants to merge 14 commits into
base: 5.x
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions config/static_caching.php
Expand Up @@ -102,6 +102,10 @@

'ignore_query_strings' => false,

'allowed_query_parameters' => [
'page',
],

/*
|--------------------------------------------------------------------------
| Replacers
Expand Down
54 changes: 42 additions & 12 deletions src/Console/Commands/StaticWarm.php
Expand Up @@ -71,13 +71,6 @@ public function handle()

private function warm(): void
{
$client = new Client([
'verify' => $this->shouldVerifySsl(),
'auth' => $this->option('user') && $this->option('password')
? [$this->option('user'), $this->option('password')]
: null,
]);

$this->output->newLine();
$this->line('Compiling URLs...');

Expand All @@ -95,7 +88,7 @@ private function warm(): void
} else {
$this->line('Visiting '.count($requests).' URLs...');

$pool = new Pool($client, $requests, [
$pool = new Pool($this->client(), $requests, [
'concurrency' => $this->concurrency(),
'fulfilled' => [$this, 'outputSuccessLine'],
'rejected' => [$this, 'outputFailureLine'],
Expand All @@ -107,6 +100,37 @@ private function warm(): void
}
}

private function warmPaginatedPages(string $url, int $currentPage, int $totalPages, string $pageName): void
{
$urls = collect(range($currentPage, $totalPages))->map(function ($page) use ($url, $pageName) {
return "{$url}?{$pageName}={$page}";
});

$requests = $urls->map(fn (string $url) => new Request('GET', $url))->all();

$pool = new Pool($this->client(), $requests, [
'concurrency' => $this->concurrency(),
'fulfilled' => function (Response $response, $index) use ($urls) {
$this->checkLine($this->getRelativeUri($urls->get($index)));
},
'rejected' => [$this, 'outputFailureLine'],
]);

$promise = $pool->promise();

$promise->wait();
}

private function client(): Client
{
return new Client([
'verify' => $this->shouldVerifySsl(),
'auth' => $this->option('user') && $this->option('password')
? [$this->option('user'), $this->option('password')]
: null,
]);
}

private function concurrency(): int
{
$strategy = config('statamic.static_caching.strategy');
Expand All @@ -116,12 +140,18 @@ private function concurrency(): int

public function outputSuccessLine(Response $response, $index): void
{
$this->checkLine($this->getRelativeUri($index));
$this->checkLine($this->getRelativeUri($this->uris()->get($index)));

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

$this->warmPaginatedPages($this->uris()->get($index), $currentPage, $totalPages, $pageName);
}
}

public function outputFailureLine($exception, $index): void
{
$uri = $this->getRelativeUri($index);
$uri = $this->getRelativeUri($this->uris()->get($index));

if ($exception instanceof RequestException && $exception->hasResponse()) {
$response = $exception->getResponse();
Expand All @@ -138,9 +168,9 @@ public function outputFailureLine($exception, $index): void
$this->crossLine("$uri → <fg=cyan>$message</fg=cyan>");
}

private function getRelativeUri(int $index): string
private function getRelativeUri(string $uri): string
{
return Str::start(Str::after($this->uris()->get($index), config('app.url')), '/');
return Str::start(Str::after($uri, config('app.url')), '/');
}

private function requests()
Expand Down
14 changes: 13 additions & 1 deletion src/Console/Commands/StaticWarmJob.php
Expand Up @@ -24,6 +24,18 @@ public function __construct(Request $request)

public function handle(Client $client)
{
$client->send($this->request);
$response = $client->send($this->request);

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

collect(range($currentPage, $totalPages))
->map(function (int $page) use ($pageName) {
return "{$this->request->getUri()}?{$pageName}={$page}";
})
->each(function (string $uri) {
StaticWarmJob::dispatch(new Request('GET', $uri));
});
}
}
}
16 changes: 15 additions & 1 deletion src/StaticCaching/Cachers/AbstractCacher.php
Expand Up @@ -8,6 +8,7 @@
use Statamic\Facades\Site;
use Statamic\StaticCaching\Cacher;
use Statamic\StaticCaching\UrlExcluder;
use Statamic\Support\Arr;
use Statamic\Support\Str;

abstract class AbstractCacher implements Cacher
Expand Down Expand Up @@ -139,7 +140,20 @@ public function getUrl(Request $request)
$url = $request->getUri();

if ($this->config('ignore_query_strings')) {
$url = explode('?', $url)[0];
$originalUrl = $url;

$url = Arr::get(explode('?', $originalUrl), 0);
$queryParams = Arr::get(explode('?', $originalUrl), 1);

$allowedQueryParams = collect($this->config('allowed_query_parameters', []))
->map(fn ($param) => Str::ensureRight($param, '='))
->all();

if ($queryParams && $allowedQueryParams) {
$url .= '?'.collect(explode('&', $queryParams))->filter(function ($param) use ($allowedQueryParams) {
return Str::startsWith($param, $allowedQueryParams);
})->implode('&');
}
}

return $url;
Expand Down
12 changes: 11 additions & 1 deletion src/StaticCaching/Cachers/FileCacher.php
Expand Up @@ -165,7 +165,17 @@ public function getFilePath($url, $site = null)
$urlParts = parse_url($url);
$pathParts = pathinfo($urlParts['path']);
$slug = $pathParts['basename'];
$query = $this->config('ignore_query_strings') ? '' : Arr::get($urlParts, 'query', '');
$query = Arr::get($urlParts, 'query', '');

if ($this->config('ignore_query_strings')) {
$allowedQueryParams = collect($this->config('allowed_query_parameters', []))
->map(fn ($param) => Str::ensureRight($param, '='))
->all();

$query = collect(explode('&', $query))->filter(function ($param) use ($allowedQueryParams) {
return Str::startsWith($param, $allowedQueryParams);
})->implode('&');
}

if ($this->isBasenameTooLong($basename = $slug.'_'.$query.'.html')) {
$basename = $slug.'_lqs_'.md5($query).'.html';
Expand Down
11 changes: 11 additions & 0 deletions src/StaticCaching/Middleware/Cache.php
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Statamic\Facades\Blink;
use Statamic\Facades\File;
use Statamic\Statamic;
use Statamic\StaticCaching\Cacher;
Expand Down Expand Up @@ -65,6 +66,16 @@ public function handle($request, Closure $next)
$this->makeReplacementsAndCacheResponse($request, $response);

$this->nocache->write();

if ($paginator = Blink::get('tag-paginator')) {
if ($paginator->hasMorePages()) {
$response->headers->set('X-Statamic-Pagination', [
'current' => $paginator->currentPage(),
'total' => $paginator->lastPage(),
'name' => $paginator->getPageName(),
]);
}
}
} elseif (! $response->isRedirect()) {
$this->makeReplacements($response);
}
Expand Down
1 change: 1 addition & 0 deletions src/StaticCaching/StaticCacheManager.php
Expand Up @@ -57,6 +57,7 @@ protected function getConfig($name)
return array_merge($config, [
'exclude' => $this->app['config']['statamic.static_caching.exclude'] ?? [],
'ignore_query_strings' => $this->app['config']['statamic.static_caching.ignore_query_strings'] ?? false,
'allowed_query_parameters' => $this->app['config']['statamic.static_caching.allowed_query_parameters'] ?? [],
'locale' => Site::current()->handle(),
]);
}
Expand Down
40 changes: 40 additions & 0 deletions tests/Console/Commands/StaticWarmJobTest.php
Expand Up @@ -7,6 +7,7 @@
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Illuminate\Support\Facades\Queue;
use Statamic\Console\Commands\StaticWarmJob;
use Tests\TestCase;

Expand All @@ -29,4 +30,43 @@ public function it_sends_a_get_request()

$this->assertEquals('/about', $mock->getLastRequest()->getUri()->getPath());
}

/** @test */
public function it_sends_a_get_request_and_dispatches_static_warm_job_for_page_with_pagination()
{
Queue::fake();

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

$handlerStack = HandlerStack::create($mock);

$client = new Client(['handler' => $handlerStack]);

$job = new StaticWarmJob(new Request('GET', '/blog'));

$job->handle($client);

$this->assertEquals('/blog', $mock->getLastRequest()->getUri()->getPath());

Queue::assertPushed(StaticWarmJob::class, function (StaticWarmJob $job) {
return $job->request->getUri()->getPath() === '/blog'
&& $job->request->getUri()->getQuery() === 'page=1';
});

Queue::assertPushed(StaticWarmJob::class, function (StaticWarmJob $job) {
return $job->request->getUri()->getPath() === '/blog'
&& $job->request->getUri()->getQuery() === 'page=2';
});

Queue::assertPushed(StaticWarmJob::class, function (StaticWarmJob $job) {
return $job->request->getUri()->getPath() === '/blog'
&& $job->request->getUri()->getQuery() === 'page=3';
});
}
}
13 changes: 13 additions & 0 deletions tests/StaticCaching/CacherTest.php
Expand Up @@ -57,6 +57,19 @@ public function gets_a_url_with_query_strings_disabled()
$this->assertEquals('http://example.com/test', $cacher->getUrl($request));
}

/** @test */
public function gets_a_url_with_query_strings_disabled_and_whitelisted_query_params()
{
$cacher = $this->cacher(['ignore_query_strings' => true, 'allowed_query_parameters' => ['page']]);

$request = Request::create('http://example.com/test', 'GET', [
'page' => 5,
'foo' => 'bar',
]);

$this->assertEquals('http://example.com/test?page=5', $cacher->getUrl($request));
}

/** @test */
public function gets_the_base_url_using_the_deprecated_config_value()
{
Expand Down