From 990500809ce7f1997ada204854787fb4fe7f8548 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sat, 9 Mar 2024 18:09:06 +0100 Subject: [PATCH] Allow multiple page cache (#123) Allow multiple page cache --- phpstan-baseline.neon | 68 +++---- src/Dto/OfflineFallback.php | 2 - src/Dto/PageCache.php | 6 +- src/Dto/Workbox.php | 7 +- .../config/definition/service_worker.php | 166 ++++++++---------- src/Service/Rule/BackgroundSync.php | 5 +- src/Service/Rule/GoogleFontCache.php | 6 +- src/Service/Rule/OfflineFallback.php | 8 +- .../Rule/{PageCache.php => PageCaches.php} | 98 ++++++----- tests/config.php | 10 +- 10 files changed, 177 insertions(+), 199 deletions(-) rename src/Service/Rule/{PageCache.php => PageCaches.php} (57%) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e89efcb..b0262d2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -175,6 +175,16 @@ parameters: count: 1 path: src/Dto/Manifest.php + - + message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\PageCache has an uninitialized property \\$cacheName\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: src/Dto/PageCache.php + + - + message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\PageCache has an uninitialized property \\$regex\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: src/Dto/PageCache.php + - message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\ProtocolHandler has an uninitialized property \\$protocol\\. Give it default value or assign it in the constructor\\.$#" count: 1 @@ -336,7 +346,7 @@ parameters: path: src/Dto/Workbox.php - - message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\Workbox has an uninitialized property \\$pageCache\\. Give it default value or assign it in the constructor\\.$#" + message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\Workbox has an uninitialized property \\$pageCaches\\. Give it default value or assign it in the constructor\\.$#" count: 1 path: src/Dto/Workbox.php @@ -482,7 +492,7 @@ parameters: - message: "#^Anonymous function should return array but returns mixed\\.$#" - count: 10 + count: 6 path: src/Resources/config/definition/service_worker.php - @@ -510,11 +520,6 @@ parameters: count: 1 path: src/Resources/config/definition/service_worker.php - - - message: "#^Cannot access offset 'font_fallback' on mixed\\.$#" - count: 1 - path: src/Resources/config/definition/service_worker.php - - message: "#^Cannot access offset 'font_regex' on mixed\\.$#" count: 1 @@ -530,11 +535,6 @@ parameters: count: 1 path: src/Resources/config/definition/service_worker.php - - - message: "#^Cannot access offset 'image_fallback' on mixed\\.$#" - count: 1 - path: src/Resources/config/definition/service_worker.php - - message: "#^Cannot access offset 'image_regex' on mixed\\.$#" count: 1 @@ -560,44 +560,14 @@ parameters: count: 1 path: src/Resources/config/definition/service_worker.php - - - message: "#^Cannot access offset 'network_timeout…' on mixed\\.$#" - count: 1 - path: src/Resources/config/definition/service_worker.php - - - - message: "#^Cannot access offset 'offline_fallback' on mixed\\.$#" - count: 2 - path: src/Resources/config/definition/service_worker.php - - - - message: "#^Cannot access offset 'page_cache' on mixed\\.$#" - count: 2 - path: src/Resources/config/definition/service_worker.php - - - - message: "#^Cannot access offset 'page_cache_name' on mixed\\.$#" - count: 1 - path: src/Resources/config/definition/service_worker.php - - - - message: "#^Cannot access offset 'page_fallback' on mixed\\.$#" - count: 1 - path: src/Resources/config/definition/service_worker.php - - message: "#^Cannot access offset 'static_regex' on mixed\\.$#" count: 1 path: src/Resources/config/definition/service_worker.php - - - message: "#^Cannot access offset 'warm_cache_urls' on mixed\\.$#" - count: 1 - path: src/Resources/config/definition/service_worker.php - - message: "#^Strict comparison using \\!\\=\\= between mixed and null will always evaluate to true\\.$#" - count: 4 + count: 3 path: src/Resources/config/definition/service_worker.php - @@ -660,10 +630,20 @@ parameters: count: 1 path: src/Service/Rule/AssetCache.php + - + message: "#^Parameter \\#6 \\$options of static method SpomkyLabs\\\\PwaBundle\\\\Service\\\\CacheStrategy\\:\\:create\\(\\) expects array\\{maxTimeout\\?\\: int, maxAge\\?\\: int, maxEntries\\?\\: int, warmUrls\\?\\: array\\, plugins\\?\\: array\\\\}, array\\{maxAge\\: int\\|null, maxEntries\\: int\\|null\\} given\\.$#" + count: 1 + path: src/Service/Rule/GoogleFontCache.php + + - + message: "#^Strict comparison using \\=\\=\\= between int\\<1, max\\> and 0 will always evaluate to false\\.$#" + count: 1 + path: src/Service/Rule/OfflineFallback.php + - message: "#^Parameter \\#6 \\$options of static method SpomkyLabs\\\\PwaBundle\\\\Service\\\\CacheStrategy\\:\\:create\\(\\) expects array\\{maxTimeout\\?\\: int, maxAge\\?\\: int, maxEntries\\?\\: int, warmUrls\\?\\: array\\, plugins\\?\\: array\\\\}, array\\{maxTimeout\\: int, plugins\\: array\\{0\\: 'CacheableResponsePl…', 1\\?\\: 'BroadcastUpdatePlug…'\\}, warmUrls\\: mixed\\} given\\.$#" count: 1 - path: src/Service/Rule/PageCache.php + path: src/Service/Rule/PageCaches.php - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\SpomkyLabsPwaBundle\\:\\:loadExtension\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#" diff --git a/src/Dto/OfflineFallback.php b/src/Dto/OfflineFallback.php index b39df04..07a9df0 100644 --- a/src/Dto/OfflineFallback.php +++ b/src/Dto/OfflineFallback.php @@ -8,8 +8,6 @@ final class OfflineFallback { - public bool $enabled = true; - #[SerializedName('page')] public null|Url $pageFallback = null; diff --git a/src/Dto/PageCache.php b/src/Dto/PageCache.php index 63b501e..43270d7 100644 --- a/src/Dto/PageCache.php +++ b/src/Dto/PageCache.php @@ -8,13 +8,11 @@ final class PageCache { - public bool $enabled = true; - #[SerializedName('cache_name')] - public string $cacheName = 'pages'; + public string $cacheName; #[SerializedName('regex')] - public string $regex = '/\.(ico|png|jpe?g|gif|svg|webp|bmp)$/'; + public string $regex; #[SerializedName('network_timeout')] public int $networkTimeout = 3; diff --git a/src/Dto/Workbox.php b/src/Dto/Workbox.php index 4b4a132..0e25b10 100644 --- a/src/Dto/Workbox.php +++ b/src/Dto/Workbox.php @@ -27,8 +27,11 @@ final class Workbox #[SerializedName('font_cache')] public FontCache $fontCache; - #[SerializedName('page_cache')] - public PageCache $pageCache; + /** + * @var array + */ + #[SerializedName('page_caches')] + public array $pageCaches; #[SerializedName('asset_cache')] public AssetCache $assetCache; diff --git a/src/Resources/config/definition/service_worker.php b/src/Resources/config/definition/service_worker.php index 92ff563..61e75d8 100644 --- a/src/Resources/config/definition/service_worker.php +++ b/src/Resources/config/definition/service_worker.php @@ -94,38 +94,6 @@ return $v; }) ->end() - ->beforeNormalization() - ->ifTrue(static fn (mixed $v): bool => true) - ->then(static function (mixed $v): array { - if (isset($v['page_cache'])) { - return $v; - } - $v['page_cache'] = array_filter([ - 'enabled' => true, - 'cache_name' => $v['page_cache_name'] ?? 'pages', - 'network_timeout' => $v['network_timeout_seconds'] ?? 3, - 'urls' => $v['warm_cache_urls'] ?? [], - ], static fn (mixed $v): bool => $v !== null); - - return $v; - }) - ->end() - ->beforeNormalization() - ->ifTrue(static fn (mixed $v): bool => true) - ->then(static function (mixed $v): array { - if (isset($v['offline_fallback'])) { - return $v; - } - $v['offline_fallback'] = array_filter([ - 'enabled' => true, - 'page' => $v['page_fallback'] ?? null, - 'image' => $v['image_fallback'] ?? null, - 'font' => $v['font_fallback'] ?? null, - ], static fn (mixed $v): bool => $v !== null); - - return $v; - }) - ->end() ->children() ->booleanNode('use_cdn') ->defaultFalse() @@ -207,7 +175,9 @@ ->info('Whether to clear the cache during the service worker activation.') ->end() ->arrayNode('offline_fallback') - ->canBeDisabled() + ->treatNullLike([]) + ->treatFalseLike([]) + ->treatTrueLike([]) ->children() ->append(getUrlNode('page', 'The URL of the offline page fallback.')) ->append(getUrlNode('image', 'The URL of the offline image fallback.')) @@ -281,64 +251,74 @@ ->end() ->end() ->end() - ->arrayNode('page_cache') - ->canBeDisabled() - ->children() - ->scalarNode('cache_name') - ->defaultValue('pages') - ->info('The name of the page cache.') - ->end() - ->integerNode('network_timeout') - ->defaultValue(3) - ->info( - 'The network timeout in seconds before cache is called (for warm cache URLs only).' - ) - ->example([1, 2, 5]) - ->end() - ->scalarNode('strategy') - ->defaultValue('networkFirst') - ->info( - 'The caching strategy. Only "networkFirst" and "staleWhileRevalidate" are supported.' - ) - ->example(['networkFirst', 'staleWhileRevalidate']) - ->end() - ->booleanNode('broadcast') - ->defaultFalse() - ->info( - 'Whether to broadcast the cache update events. Only supported with "staleWhileRevalidate" strategy.' - ) - ->end() - ->arrayNode('broadcast_headers') - ->treatNullLike(['Content-Length', 'ETag', 'Last-Modified']) - ->treatFalseLike(['Content-Length', 'ETag', 'Last-Modified']) - ->treatTrueLike(['Content-Length', 'ETag', 'Last-Modified']) - ->defaultValue(['Content-Length', 'ETag', 'Last-Modified']) - ->scalarPrototype()->end() - ->end() - ->arrayNode('urls') - ->treatNullLike([]) - ->treatFalseLike([]) - ->treatTrueLike([]) - ->info('The URLs to warm the cache. The URLs shall be served by the application.') - ->arrayPrototype() - ->beforeNormalization() - ->ifString() - ->then(static fn (string $v): array => [ - 'path' => $v, - ]) - ->end() - ->children() - ->scalarNode('path') - ->isRequired() - ->info('The URL of the shortcut.') - ->example('app_homepage') + ->arrayNode('page_caches') + ->treatNullLike([]) + ->treatFalseLike([]) + ->treatTrueLike([]) + ->arrayPrototype() + ->children() + ->scalarNode('regex') + ->isRequired() + ->info('The regex to match the URLs.') + ->end() + ->scalarNode('cache_name') + ->isRequired() + ->info('The name of the page cache.') + ->end() + ->integerNode('network_timeout') + ->defaultValue(3) + ->info( + 'The network timeout in seconds before cache is called (for "networkFirst" strategy only).' + ) + ->example([1, 2, 5]) + ->end() + ->scalarNode('strategy') + ->defaultValue('networkFirst') + ->info( + 'The caching strategy. Only "networkFirst" and "staleWhileRevalidate" are supported.' + ) + ->example(['networkFirst', 'staleWhileRevalidate']) + ->end() + ->booleanNode('broadcast') + ->defaultFalse() + ->info( + 'Whether to broadcast the cache update events (for "staleWhileRevalidate" strategy only).' + ) + ->end() + ->arrayNode('broadcast_headers') + ->treatNullLike(['Content-Length', 'ETag', 'Last-Modified']) + ->treatFalseLike(['Content-Length', 'ETag', 'Last-Modified']) + ->treatTrueLike(['Content-Length', 'ETag', 'Last-Modified']) + ->defaultValue(['Content-Length', 'ETag', 'Last-Modified']) + ->scalarPrototype()->end() + ->end() + ->arrayNode('urls') + ->treatNullLike([]) + ->treatFalseLike([]) + ->treatTrueLike([]) + ->info( + 'The URLs to warm the cache. The URLs shall be served by the application.' + ) + ->arrayPrototype() + ->beforeNormalization() + ->ifString() + ->then(static fn (string $v): array => [ + 'path' => $v, + ]) ->end() - ->arrayNode('params') - ->treatFalseLike([]) - ->treatTrueLike([]) - ->treatNullLike([]) - ->prototype('variable')->end() - ->info('The parameters of the action.') + ->children() + ->scalarNode('path') + ->isRequired() + ->info('The URL of the shortcut.') + ->example('app_homepage') + ->end() + ->arrayNode('params') + ->treatFalseLike([]) + ->treatTrueLike([]) + ->treatNullLike([]) + ->prototype('variable')->end() + ->info('The parameters of the action.') + ->end() ->end() ->end() ->end() @@ -409,7 +389,7 @@ ->setDeprecated( 'spomky-labs/phpwa', '1.1.0', - 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_cache.cache_name" instead.' + 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_caches[].cache_name" instead.' ) ->end() ->scalarNode('asset_cache_name') @@ -501,7 +481,7 @@ ->setDeprecated( 'spomky-labs/phpwa', '1.1.0', - 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_cache.network_timeout" instead.' + 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_caches[].network_timeout" instead.' ) ->end() ->arrayNode('warm_cache_urls') @@ -512,7 +492,7 @@ ->setDeprecated( 'spomky-labs/phpwa', '1.1.0', - 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_cache.urls" instead.' + 'The "%node%" option is deprecated and will be removed in 2.0.0. Please use "pwa.serviceworker.workbox.page_caches[].urls" instead.' ) ->arrayPrototype() ->beforeNormalization() diff --git a/src/Service/Rule/BackgroundSync.php b/src/Service/Rule/BackgroundSync.php index bee1ac4..2abaf1b 100644 --- a/src/Service/Rule/BackgroundSync.php +++ b/src/Service/Rule/BackgroundSync.php @@ -69,15 +69,12 @@ public function getCacheStrategies(): array $strategies = []; foreach ($this->workbox->backgroundSync as $sync) { $strategies[] = CacheStrategy::create( - '---', + 'BackgroundSync API', CacheStrategy::STRATEGY_NETWORK_ONLY, $sync->regex, $this->workbox->enabled, true, [ - 'maxTimeout' => 0, - 'maxAge' => 0, - 'maxEntries' => 0, 'plugins' => [ sprintf('backgroundSync: "%s"', $sync->queueName), sprintf('broadcastChannel: "%s"', $sync->broadcastChannel ?? '---'), diff --git a/src/Service/Rule/GoogleFontCache.php b/src/Service/Rule/GoogleFontCache.php index 12a0ed8..a5ccca1 100644 --- a/src/Service/Rule/GoogleFontCache.php +++ b/src/Service/Rule/GoogleFontCache.php @@ -77,7 +77,11 @@ public function getCacheStrategies(): array 'workbox.recipes.googleFontsCache', 'Google Fonts Cache', $this->workbox->enabled && $this->workbox->googleFontCache->enabled, - $this->workbox->googleFontCache->enabled + $this->workbox->googleFontCache->enabled, + [ + 'maxAge' => $this->workbox->googleFontCache->maxAge, + 'maxEntries' => $this->workbox->googleFontCache->maxEntries, + ] ), ]; } diff --git a/src/Service/Rule/OfflineFallback.php b/src/Service/Rule/OfflineFallback.php index 3feef82..07b58b9 100644 --- a/src/Service/Rule/OfflineFallback.php +++ b/src/Service/Rule/OfflineFallback.php @@ -46,10 +46,7 @@ public function __construct( public function process(string $body): string { - if ($this->workbox->enabled === false) { - return $body; - } - if ($this->workbox->offlineFallback->enabled === false) { + if ($this->workbox->enabled === false || ! isset($this->workbox->offlineFallback)) { return $body; } $options = [ @@ -58,6 +55,9 @@ public function process(string $body): string 'fontFallback' => $this->workbox->offlineFallback->fontFallback, ]; $options = array_filter($options, static fn (mixed $v): bool => $v !== null); + if (count($options) === 0) { + return $body; + } $options = count($options) === 0 ? '' : $this->serializer->serialize($options, 'json', $this->jsonOptions); $declaration = << @@ -50,42 +51,81 @@ public function process(string $body): string if ($this->workbox->enabled === false) { return $body; } - if ($this->workbox->pageCache->enabled === false) { - return $body; + + foreach (array_values($this->workbox->pageCaches) as $id => $pageCache) { + $body = $this->processPageCache($id, $pageCache, $body); + } + + return $body; + } + + public function getCacheStrategies(): array + { + $strategies = []; + foreach ($this->workbox->pageCaches as $pageCache) { + $strategy = match ($pageCache->strategy) { + 'staleWhileRevalidate' => CacheStrategy::STRATEGY_STALE_WHILE_REVALIDATE, + default => CacheStrategy::STRATEGY_NETWORK_FIRST, + }; + $plugins = ['CacheableResponsePlugin']; + if ($pageCache->broadcast === true && $strategy === CacheStrategy::STRATEGY_STALE_WHILE_REVALIDATE) { + $plugins[] = 'BroadcastUpdatePlugin'; + } + $routes = $this->serializer->serialize($pageCache->urls, 'json', $this->jsonOptions); + $url = json_decode($routes, true, 512, JSON_THROW_ON_ERROR); + $strategies[] = + CacheStrategy::create( + $pageCache->cacheName, + $strategy, + $pageCache->regex, + $this->workbox->enabled, + true, + [ + 'maxTimeout' => $pageCache->networkTimeout, + 'plugins' => $plugins, + 'warmUrls' => $url, + ] + ); } - $routes = $this->serializer->serialize($this->workbox->pageCache->urls, 'json', $this->jsonOptions); - $strategy = match ($this->workbox->pageCache->strategy) { + + return $strategies; + } + + private function processPageCache(int $id, PageCache $pageCache, string $body): string + { + $routes = $this->serializer->serialize($pageCache->urls, 'json', $this->jsonOptions); + $strategy = match ($pageCache->strategy) { 'staleWhileRevalidate' => 'StaleWhileRevalidate', default => 'NetworkFirst', }; $broadcastHeaders = json_encode( - $this->workbox->pageCache->broadcastHeaders === [] ? [ + $pageCache->broadcastHeaders === [] ? [ 'Content-Type', 'ETag', 'Last-Modified', - ] : $this->workbox->pageCache->broadcastHeaders, + ] : $pageCache->broadcastHeaders, JSON_THROW_ON_ERROR, 512 ); - $broadcastUpdate = ($strategy === 'StaleWhileRevalidate' && $this->workbox->pageCache->broadcast === true) ? sprintf( + $broadcastUpdate = ($strategy === 'StaleWhileRevalidate' && $pageCache->broadcast === true) ? sprintf( ',new workbox.broadcastUpdate.BroadcastUpdatePlugin({headersToCheck: %s})', $broadcastHeaders ) : ''; $declaration = <<workbox->pageCache->networkTimeout}, - cacheName: '{$this->workbox->pageCache->cacheName}', +const pageCache{$id}Strategy = new workbox.strategies.{$strategy}({ + networkTimeoutSeconds: {$pageCache->networkTimeout}, + cacheName: '{$pageCache->cacheName}', plugins: [new workbox.cacheableResponse.CacheableResponsePlugin({statuses: [0, 200]}){$broadcastUpdate}], }); workbox.routing.registerRoute( - ({request}) => request.mode === 'navigate', - pageCacheStrategy + new RegExp('{$pageCache->regex}'), + pageCache{$id}Strategy ); self.addEventListener('install', event => { const done = {$routes}.map( path => - pageCacheStrategy.handleAll({ + pageCache{$id}Strategy.handleAll({ event, request: new Request(path), })[1] @@ -100,7 +140,7 @@ public function process(string $body): string const urls = event.data.payload.urls || []; const done = urls.map( path => - pageCacheStrategy.handleAll({ + pageCache{$id}Strategy.handleAll({ event, request: new Request(path), })[1] @@ -112,32 +152,4 @@ public function process(string $body): string return $body . PHP_EOL . PHP_EOL . trim($declaration); } - - public function getCacheStrategies(): array - { - $strategy = match ($this->workbox->pageCache->strategy) { - 'staleWhileRevalidate' => CacheStrategy::STRATEGY_STALE_WHILE_REVALIDATE, - default => CacheStrategy::STRATEGY_NETWORK_FIRST, - }; - $plugins = ['CacheableResponsePlugin']; - if ($this->workbox->pageCache->broadcast === true && $strategy === CacheStrategy::STRATEGY_STALE_WHILE_REVALIDATE) { - $plugins[] = 'BroadcastUpdatePlugin'; - } - $routes = $this->serializer->serialize($this->workbox->pageCache->urls, 'json', $this->jsonOptions); - $url = json_decode($routes, true, 512, JSON_THROW_ON_ERROR); - return [ - CacheStrategy::create( - $this->workbox->pageCache->cacheName, - $strategy, - "({request}) => request.mode === 'navigate'", - $this->workbox->enabled && $this->workbox->pageCache->enabled, - true, - [ - 'maxTimeout' => $this->workbox->pageCache->networkTimeout, - 'plugins' => $plugins, - 'warmUrls' => $url, - ] - ), - ]; - } } diff --git a/tests/config.php b/tests/config.php index 8987d1f..071237f 100644 --- a/tests/config.php +++ b/tests/config.php @@ -226,8 +226,14 @@ 'scope' => '/', 'use_cache' => true, 'workbox' => [ - 'page_cache' => [ - 'urls' => ['privacy_policy', 'terms_of_service'], + 'page_caches' => [ + [ + 'regex' => '.*', + 'strategy' => 'staleWhileRevalidate', + 'cache_name' => 'page-cache', + 'broadcast' => true, + 'urls' => ['privacy_policy', 'terms_of_service'], + ], ], 'offline_fallback' => [ 'page' => '/offline.html',