From e479f10889c8f2cf4d1b6460980f9e77775b53ad Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Tue, 6 Feb 2024 23:04:08 +0100 Subject: [PATCH] Font cache support (#58) --- phpstan-baseline.neon | 20 -------- src/DependencyInjection/Configuration.php | 20 ++++++++ src/Dto/Workbox.php | 9 ++++ src/Normalizer/AssetNormalizer.php | 1 - src/Normalizer/IconNormalizer.php | 16 ++++--- src/Normalizer/ScreenshotNormalizer.php | 17 ++++--- src/Normalizer/ServiceWorkerNormalizer.php | 15 +----- src/Normalizer/ShortcutNormalizer.php | 53 ---------------------- src/Service/ServiceWorkerCompiler.php | 24 ++++++++-- 9 files changed, 71 insertions(+), 104 deletions(-) delete mode 100644 src/Normalizer/ShortcutNormalizer.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index aeb4319..68c118d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -455,26 +455,6 @@ parameters: count: 1 path: src/Normalizer/ServiceWorkerNormalizer.php - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\ShortcutNormalizer\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/ShortcutNormalizer.php - - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\ShortcutNormalizer\\:\\:normalize\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/ShortcutNormalizer.php - - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\ShortcutNormalizer\\:\\:normalize\\(\\) should return array\\{description\\?\\: string, icons\\?\\: array, name\\: string, short_name\\?\\: string, url\\: string\\} but returns array\\\\.$#" - count: 1 - path: src/Normalizer/ShortcutNormalizer.php - - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\ShortcutNormalizer\\:\\:supportsNormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/ShortcutNormalizer.php - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\UrlNormalizer\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index f40a59a..248b331 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -150,11 +150,31 @@ private function setupServiceWorker(ArrayNodeDefinition $node): void ->info('The regex to match the images.') ->example('/\.(ico|png|jpe?g|gif|svg|webp|bmp)$/') ->end() + ->scalarNode('static_regex') + ->defaultValue('/\.(css|js|json|xml|txt|woff2|ttf|eot|otf|map|webmanifest)$/') + ->info('The regex to match the static files.') + ->example('/\.(css|js|json|xml|txt|woff2|ttf|eot|otf|map|webmanifest)$/') + ->end() ->integerNode('max_image_cache_entries') ->defaultValue(60) ->info('The maximum number of entries in the image cache.') ->example([50, 100, 200]) ->end() + ->integerNode('max_image_age') + ->defaultValue(60 * 60 * 24 * 365) + ->info('The maximum number of seconds before the image cache is invalidated.') + ->example([60 * 60 * 24 * 365, 60 * 60 * 24 * 30, 60 * 60 * 24 * 7]) + ->end() + ->integerNode('max_font_cache_entries') + ->defaultValue(30) + ->info('The maximum number of entries in the font cache.') + ->example([30, 50, 100]) + ->end() + ->integerNode('max_font_age') + ->defaultValue(60 * 60 * 24 * 365) + ->info('The maximum number of seconds before the font cache is invalidated.') + ->example([60 * 60 * 24 * 365, 60 * 60 * 24 * 30, 60 * 60 * 24 * 7]) + ->end() ->integerNode('network_timeout_seconds') ->defaultValue(3) ->info( diff --git a/src/Dto/Workbox.php b/src/Dto/Workbox.php index a908b43..9d70230 100644 --- a/src/Dto/Workbox.php +++ b/src/Dto/Workbox.php @@ -48,6 +48,15 @@ final class Workbox #[SerializedName('network_timeout_seconds')] public int $networkTimeoutSeconds = 3; + #[SerializedName('max_font_age')] + public int $maxFontAge = 60 * 60 * 24 * 365; + + #[SerializedName('max_font_cache_entries')] + public int $maxFontCacheEntries = 60; + + #[SerializedName('max_image_age')] + public int $maxImageAge = 60 * 60 * 24 * 365; + #[SerializedName('max_image_cache_entries')] public int $maxImageCacheEntries = 60; diff --git a/src/Normalizer/AssetNormalizer.php b/src/Normalizer/AssetNormalizer.php index e73d55a..5521de7 100644 --- a/src/Normalizer/AssetNormalizer.php +++ b/src/Normalizer/AssetNormalizer.php @@ -25,7 +25,6 @@ public function normalize(mixed $object, string $format = null, array $context = { assert($object instanceof Asset); $url = null; - dump($object); if (! str_starts_with($object->src, '/')) { $asset = $this->assetMapper->getAsset($object->src); $url = $asset?->publicPath; diff --git a/src/Normalizer/IconNormalizer.php b/src/Normalizer/IconNormalizer.php index 19beba0..2d42f28 100644 --- a/src/Normalizer/IconNormalizer.php +++ b/src/Normalizer/IconNormalizer.php @@ -8,13 +8,17 @@ use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\MappedAsset; use Symfony\Component\Mime\MimeTypes; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use function assert; -final readonly class IconNormalizer implements NormalizerInterface +final class IconNormalizer implements NormalizerInterface, NormalizerAwareInterface { + use NormalizerAwareTrait; + public function __construct( - private AssetMapperInterface $assetMapper, + private readonly AssetMapperInterface $assetMapper, ) { } @@ -24,16 +28,16 @@ public function __construct( public function normalize(mixed $object, string $format = null, array $context = []): array { assert($object instanceof Icon); - $format = null; + $imageFormat = null; if (! str_starts_with($object->src->src, '/')) { $asset = $this->assetMapper->getAsset($object->src->src); - $format = $this->getFormat($object, $asset); + $imageFormat = $this->getFormat($object, $asset); } $result = [ - 'src' => $object->src, + 'src' => $this->normalizer->normalize($object->src, $format, $context), 'sizes' => $object->getSizeList(), - 'type' => $format, + 'type' => $imageFormat, 'purpose' => $object->purpose, ]; diff --git a/src/Normalizer/ScreenshotNormalizer.php b/src/Normalizer/ScreenshotNormalizer.php index 3e042b2..494b35e 100644 --- a/src/Normalizer/ScreenshotNormalizer.php +++ b/src/Normalizer/ScreenshotNormalizer.php @@ -9,14 +9,18 @@ use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\MappedAsset; use Symfony\Component\Mime\MimeTypes; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use function assert; -final readonly class ScreenshotNormalizer implements NormalizerInterface +final class ScreenshotNormalizer implements NormalizerInterface, NormalizerAwareInterface { + use NormalizerAwareTrait; + public function __construct( - private AssetMapperInterface $assetMapper, - private null|ImageProcessor $imageProcessor, + private readonly AssetMapperInterface $assetMapper, + private readonly null|ImageProcessor $imageProcessor, ) { } @@ -28,10 +32,11 @@ public function normalize(mixed $object, string $format = null, array $context = assert($object instanceof Screenshot); $url = null; $asset = null; + $imageFormat = null; if (! str_starts_with($object->src->src, '/')) { $asset = $this->assetMapper->getAsset($object->src->src); $url = $asset?->publicPath; - $format = $this->getFormat($object, $asset); + $imageFormat = $this->getFormat($object, $asset); } if ($url === null) { $url = $object->src->src; @@ -39,12 +44,12 @@ public function normalize(mixed $object, string $format = null, array $context = ['sizes' => $sizes, 'formFactor' => $formFactor] = $this->getSizes($object, $asset); $result = [ - 'src' => $url, + 'src' => $this->normalizer->normalize($object->src, $format, $context), 'sizes' => $sizes, 'form_factor' => $formFactor, 'label' => $object->label, 'platform' => $object->platform, - 'format' => $format, + 'format' => $imageFormat, ]; $cleanup = static fn (array $data): array => array_filter( diff --git a/src/Normalizer/ServiceWorkerNormalizer.php b/src/Normalizer/ServiceWorkerNormalizer.php index d7c78da..c9d11a8 100644 --- a/src/Normalizer/ServiceWorkerNormalizer.php +++ b/src/Normalizer/ServiceWorkerNormalizer.php @@ -5,33 +5,20 @@ namespace SpomkyLabs\PwaBundle\Normalizer; use SpomkyLabs\PwaBundle\Dto\ServiceWorker; -use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use function assert; final readonly class ServiceWorkerNormalizer implements NormalizerInterface { - public function __construct( - private AssetMapperInterface $assetMapper - ) { - } - /** * @return array{scope?: string, src: string, use_cache?: bool} */ public function normalize(mixed $object, string $format = null, array $context = []): array { assert($object instanceof ServiceWorker); - $url = null; - if (! str_starts_with($object->dest, '/')) { - $url = $this->assetMapper->getAsset($object->dest)?->publicPath; - } - if ($url === null) { - $url = $object->dest; - } $result = [ - 'src' => $url, + 'src' => '/' . trim($object->dest, '/'), 'scope' => $object->scope, 'use_cache' => $object->useCache, ]; diff --git a/src/Normalizer/ShortcutNormalizer.php b/src/Normalizer/ShortcutNormalizer.php deleted file mode 100644 index d219d75..0000000 --- a/src/Normalizer/ShortcutNormalizer.php +++ /dev/null @@ -1,53 +0,0 @@ - $object->name, - 'short_name' => $object->shortName, - 'description' => $object->description, - 'url' => $this->normalizer->normalize($object->url, $format, $context), - 'icons' => $this->normalizer->normalize($object->icons, $format, $context), - ]; - - $cleanup = static fn (array $data): array => array_filter( - $data, - static fn ($value) => ($value !== null && $value !== []) - ); - return $cleanup($result); - } - - public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool - { - return $data instanceof Shortcut; - } - - /** - * @return array - */ - public function getSupportedTypes(?string $format): array - { - return [ - Shortcut::class => true, - ]; - } -} diff --git a/src/Service/ServiceWorkerCompiler.php b/src/Service/ServiceWorkerCompiler.php index 6ffa35b..b395c3e 100644 --- a/src/Service/ServiceWorkerCompiler.php +++ b/src/Service/ServiceWorkerCompiler.php @@ -70,19 +70,19 @@ private function processStandardRules(Workbox $workbox, string $body): string } $images = []; - $static = []; + $statics = []; foreach ($this->assetMapper->allAssets() as $asset) { if (preg_match($workbox->imageRegex, $asset->sourcePath) === 1) { $images[] = $asset->publicPath; } elseif (preg_match($workbox->staticRegex, $asset->sourcePath) === 1) { - $static[] = $asset->publicPath; + $statics[] = $asset->publicPath; } } $jsonOptions = [ JsonEncode::OPTIONS => JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR, ]; $imageUrls = $this->serializer->serialize($images, 'json', $jsonOptions); - $staticUrls = $this->serializer->serialize($static, 'json', $jsonOptions); + $staticUrls = $this->serializer->serialize($statics, 'json', $jsonOptions); $routes = $this->serializer->serialize($workbox->warmCacheUrls, 'json', $jsonOptions); $declaration = <<maxImageCacheEntries}, + maxImageAge: {$workbox->maxImageAge}, warmCache: {$imageUrls} }); workbox.recipes.staticResourceCache({ cacheName: 'assets', warmCache: {$staticUrls} }); +workbox.routing.registerRoute( + ({request}) => request.destination === 'font', + new workbox.strategies.CacheFirst({ + cacheName: 'fonts', + plugins: [ + new workbox.cacheableResponse.CacheableResponsePlugin({ + statuses: [0, 200], + }), + new workbox.expiration.ExpirationPlugin({ + maxAgeSeconds: {$workbox->maxFontAge}, + maxEntries: {$workbox->maxFontCacheEntries}, + }), + ], + }) +); STANDARD_RULE_STRATEGY; return str_replace($workbox->standardRulesPlaceholder, trim($declaration), $body); @@ -138,7 +154,7 @@ private function processOfflineFallback(Workbox $workbox, string $body): string workbox.recipes.offlineFallback({ pageFallback: {$pageFallback}, imageFallback: {$imageFallback}, - pageFallback: {$fontFallback} + fontFallback: {$fontFallback} }); OFFLINE_FALLBACK_STRATEGY;