diff --git a/src/Console/Commands/StaticWarm.php b/src/Console/Commands/StaticWarm.php index 7bb26f59b6..03e33da0b5 100644 --- a/src/Console/Commands/StaticWarm.php +++ b/src/Console/Commands/StaticWarm.php @@ -38,6 +38,9 @@ class StaticWarm extends Command {--p|password= : HTTP authentication password} {--insecure : Skip SSL verification} {--uncached : Only warm uncached URLs} + {--max-depth= : Maximum depth of URLs to warm} + {--include= : Only warm specific URLs} + {--exclude= : Exclude specific URLs} '; protected $description = 'Warms the static cache by visiting all URLs'; @@ -179,6 +182,9 @@ private function uris(): Collection ->merge($this->customRouteUris()) ->merge($this->additionalUris()) ->unique() + ->filter(fn ($uri) => $this->shouldInclude($uri)) + ->reject(fn ($uri) => $this->shouldExclude($uri)) + ->reject(fn ($uri) => $this->exceedsMaxDepth($uri)) ->reject(function ($uri) use ($cacher) { if ($this->option('uncached') && $cacher->hasCachedPage(HttpRequest::create($uri))) { return true; @@ -192,6 +198,54 @@ private function uris(): Collection ->values(); } + private function shouldInclude($uri): bool + { + if (! $inclusions = $this->option('include')) { + return true; + } + + $inclusions = explode(',', $inclusions); + + return collect($inclusions)->contains(fn ($included) => $this->uriMatches($uri, $included)); + } + + private function shouldExclude($uri): bool + { + if (! $exclusions = $this->option('exclude')) { + return false; + } + + $exclusions = explode(',', $exclusions); + + return collect($exclusions)->contains(fn ($excluded) => $this->uriMatches($uri, $excluded)); + } + + private function uriMatches($uri, $pattern): bool + { + $uri = URL::makeRelative($uri); + + if (Str::endsWith($pattern, '*')) { + $prefix = Str::removeRight($pattern, '*'); + + if (Str::startsWith($uri, $prefix) && ! (Str::endsWith($prefix, '/') && $uri === $prefix)) { + return true; + } + } elseif (Str::removeRight($uri, '/') === Str::removeRight($pattern, '/')) { + return true; + } + + return false; + } + + private function exceedsMaxDepth($uri): bool + { + if (! $max = $this->option('max-depth')) { + return false; + } + + return count(explode('/', trim(URL::makeRelative($uri), '/'))) > $max; + } + private function shouldVerifySsl(): bool { if ($this->option('insecure')) { diff --git a/tests/Console/Commands/StaticWarmTest.php b/tests/Console/Commands/StaticWarmTest.php index daa5eb03d3..bcb7f5fdd8 100644 --- a/tests/Console/Commands/StaticWarmTest.php +++ b/tests/Console/Commands/StaticWarmTest.php @@ -44,7 +44,7 @@ public function it_warms_the_static_cache() } #[Test] - public function it_only_visits_uncached_urls_when_the_eco_option_is_used() + public function it_only_visits_uncached_urls_when_the_uncached_option_is_used() { $mock = Mockery::mock(Cacher::class); $mock->shouldReceive('hasCachedPage')->times(2)->andReturn(true, false); @@ -58,6 +58,89 @@ public function it_only_visits_uncached_urls_when_the_eco_option_is_used() ->assertExitCode(0); } + #[Test] + public function it_only_visits_included_urls() + { + config(['statamic.static_caching.strategy' => 'half']); + + $this->createPage('blog'); + $this->createPage('news'); + + Collection::make('blog') + ->routes('/blog/{slug}') + ->template('default') + ->save(); + + Collection::make('news') + ->routes('/news/{slug}') + ->template('default') + ->save(); + + EntryFactory::slug('post-1')->collection('blog')->id('blog-post-1')->create(); + EntryFactory::slug('post-2')->collection('blog')->id('blog-post-2')->create(); + EntryFactory::slug('article-1')->collection('news')->id('news-article-1')->create(); + EntryFactory::slug('article-2')->collection('news')->id('news-article-2')->create(); + EntryFactory::slug('article-3')->collection('news')->id('news-article-3')->create(); + + $this->artisan('statamic:static:warm', ['--include' => '/blog/post-1,/news/*']) + ->expectsOutput('Visiting 4 URLs...') + ->assertExitCode(0); + } + + #[Test] + public function it_doesnt_visit_excluded_urls() + { + config(['statamic.static_caching.strategy' => 'half']); + + $this->createPage('blog'); + $this->createPage('news'); + + Collection::make('blog') + ->routes('/blog/{slug}') + ->template('default') + ->save(); + + Collection::make('news') + ->routes('/news/{slug}') + ->template('default') + ->save(); + + EntryFactory::slug('post-1')->collection('blog')->id('blog-post-1')->create(); + EntryFactory::slug('post-2')->collection('blog')->id('blog-post-2')->create(); + EntryFactory::slug('article-1')->collection('news')->id('news-article-1')->create(); + EntryFactory::slug('article-2')->collection('news')->id('news-article-2')->create(); + EntryFactory::slug('article-3')->collection('news')->id('news-article-3')->create(); + + $this->artisan('statamic:static:warm', ['--exclude' => '/about,/contact,/blog/*,/news/article-2']) + ->expectsOutput('Visiting 4 URLs...') + ->assertExitCode(0); + } + + #[Test] + public function it_respects_max_depth() + { + config(['statamic.static_caching.strategy' => 'half']); + + Collection::make('blog') + ->routes('/awesome/blog/{slug}') + ->template('default') + ->save(); + + Collection::make('news') + ->routes('/news/{slug}') + ->template('default') + ->save(); + + EntryFactory::slug('post-1')->collection('blog')->id('blog-post-1')->create(); + EntryFactory::slug('post-2')->collection('blog')->id('blog-post-2')->create(); + EntryFactory::slug('post-3')->collection('blog')->id('blog-post-3')->create(); + EntryFactory::slug('article-1')->collection('news')->id('news-article-1')->create(); + + $this->artisan('statamic:static:warm', ['--max-depth' => 2]) + ->expectsOutput('Visiting 3 URLs...') + ->assertExitCode(0); + } + #[Test] public function it_doesnt_queue_the_requests_when_connection_is_set_to_sync() {