From d232e0160f252232131b9f7b1b89f4e2042bbaf8 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Wed, 31 Jul 2024 23:50:00 +0200 Subject: [PATCH] Add ApplicationIconCompiler and IconResolver services (#224) Introduced new services, `ApplicationIconCompiler` and `IconResolver`, for better structure. Updated related paths and dependencies. Modified existing services and tests for consistency and improved functionality with the new services. --- phpstan-baseline.neon | 27 +++- src/CachingStrategy/AssetCache.php | 2 +- src/CachingStrategy/FontCache.php | 2 +- src/CachingStrategy/ManifestCache.php | 2 +- src/CachingStrategy/ResourceCaches.php | 2 +- src/Command/CreateIconsCommand.php | 2 +- src/Command/CreateScreenshotCommand.php | 2 +- src/Dto/Icon.php | 11 ++ src/Normalizer/IconNormalizer.php | 25 +-- .../config/definition/utils/icons.php | 102 +++++++----- src/Resources/config/services.php | 5 + src/Service/ApplicationIconCompiler.php | 49 ++++++ src/Service/FaviconsCompiler.php | 62 ++++---- src/Service/IconResolver.php | 146 ++++++++++++++++++ src/Service/ManifestCompiler.php | 8 +- src/Service/ServiceWorkerCompiler.php | 6 +- .../AppendCacheStrategies.php | 2 +- src/Subscriber/PwaDevServerSubscriber.php | 2 +- src/Twig/PwaRuntime.php | 4 +- .../Functional/TakeScreenshotCommandTest.php | 3 +- tests/config.php | 4 + 21 files changed, 358 insertions(+), 110 deletions(-) create mode 100644 src/Service/ApplicationIconCompiler.php create mode 100644 src/Service/IconResolver.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b56c54a..e8ad28c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -430,6 +430,16 @@ parameters: count: 1 path: src/ImageProcessor/GDImageProcessor.php + - + message: "#^Parameter \\#1 \\$width of function imagecreatetruecolor expects int\\<1, max\\>, int given\\.$#" + count: 2 + path: src/ImageProcessor/GDImageProcessor.php + + - + message: "#^Parameter \\#2 \\$height of function imagecreatetruecolor expects int\\<1, max\\>, int given\\.$#" + count: 2 + path: src/ImageProcessor/GDImageProcessor.php + - message: "#^Parameter \\#2 \\$red of function imagecolorallocate expects int\\<0, 255\\>, int given\\.$#" count: 1 @@ -620,6 +630,16 @@ parameters: count: 1 path: src/Service/FaviconsCompiler.php + - + message: "#^Cannot access property \\$sourcePath on Symfony\\\\Component\\\\AssetMapper\\\\MappedAsset\\|null\\.$#" + count: 1 + path: src/Service/IconResolver.php + + - + message: "#^Parameter \\#3 \\$headers of class SpomkyLabs\\\\PwaBundle\\\\Service\\\\Data constructor expects array\\, array\\ given\\.$#" + count: 2 + path: src/Service/IconResolver.php + - message: "#^Attribute class Symfony\\\\Component\\\\DependencyInjection\\\\Attribute\\\\TaggedIterator is deprecated\\: since Symfony 7\\.1, use \\{@see AutowireIterator\\} instead\\.$#" count: 1 @@ -668,4 +688,9 @@ parameters: - message: "#^Property SpomkyLabs\\\\PwaBundle\\\\Twig\\\\PwaRuntime\\:\\:\\$cspListener has unknown class Nelmio\\\\SecurityBundle\\\\EventListener\\\\ContentSecurityPolicyListener as its type\\.$#" count: 1 - path: src/Twig/PwaRuntime.php \ No newline at end of file + path: src/Twig/PwaRuntime.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: tests/Functional/TakeScreenshotCommandTest.php \ No newline at end of file diff --git a/src/CachingStrategy/AssetCache.php b/src/CachingStrategy/AssetCache.php index c4dc039..7be0018 100644 --- a/src/CachingStrategy/AssetCache.php +++ b/src/CachingStrategy/AssetCache.php @@ -37,7 +37,7 @@ public function __construct( PublicAssetsPathResolverInterface $publicAssetsPathResolver, private readonly AssetMapperInterface $assetMapper, private readonly SerializerInterface $serializer, - #[Autowire('%kernel.debug%')] + #[Autowire(param: 'kernel.debug')] bool $debug, ) { $this->workbox = $serviceWorker->workbox; diff --git a/src/CachingStrategy/FontCache.php b/src/CachingStrategy/FontCache.php index 827f96f..3b4728c 100644 --- a/src/CachingStrategy/FontCache.php +++ b/src/CachingStrategy/FontCache.php @@ -33,7 +33,7 @@ public function __construct( ServiceWorker $serviceWorker, private readonly AssetMapperInterface $assetMapper, private readonly SerializerInterface $serializer, - #[Autowire('%kernel.debug%')] + #[Autowire(param: 'kernel.debug')] bool $debug, ) { $this->workbox = $serviceWorker->workbox; diff --git a/src/CachingStrategy/ManifestCache.php b/src/CachingStrategy/ManifestCache.php index 2447720..87dd1dc 100644 --- a/src/CachingStrategy/ManifestCache.php +++ b/src/CachingStrategy/ManifestCache.php @@ -21,7 +21,7 @@ final class ManifestCache implements HasCacheStrategiesInterface, CanLogInterfac public function __construct( ServiceWorker $serviceWorker, - #[Autowire('%spomky_labs_pwa.manifest.public_url%')] + #[Autowire(param: 'spomky_labs_pwa.manifest.public_url')] string $manifestPublicUrl, ) { $this->workbox = $serviceWorker->workbox; diff --git a/src/CachingStrategy/ResourceCaches.php b/src/CachingStrategy/ResourceCaches.php index 9dc0b9b..300d803 100644 --- a/src/CachingStrategy/ResourceCaches.php +++ b/src/CachingStrategy/ResourceCaches.php @@ -42,7 +42,7 @@ public function __construct( private readonly SerializerInterface $serializer, #[TaggedIterator('spomky_labs_pwa.match_callback_handler')] private readonly iterable $matchCallbackHandlers, - #[Autowire('%kernel.debug%')] + #[Autowire(param: 'kernel.debug')] bool $debug, ) { $this->workbox = $serviceWorker->workbox; diff --git a/src/Command/CreateIconsCommand.php b/src/Command/CreateIconsCommand.php index 87e9b81..3d822cd 100644 --- a/src/Command/CreateIconsCommand.php +++ b/src/Command/CreateIconsCommand.php @@ -26,7 +26,7 @@ final class CreateIconsCommand extends Command public function __construct( private readonly AssetMapperInterface $assetMapper, private readonly Filesystem $filesystem, - #[Autowire('%kernel.project_dir%')] + #[Autowire(param: 'kernel.project_dir')] private readonly string $projectDir, private readonly null|ImageProcessorInterface $imageProcessor, ) { diff --git a/src/Command/CreateScreenshotCommand.php b/src/Command/CreateScreenshotCommand.php index bf2da79..dcd6aa4 100644 --- a/src/Command/CreateScreenshotCommand.php +++ b/src/Command/CreateScreenshotCommand.php @@ -34,7 +34,7 @@ final class CreateScreenshotCommand extends Command public function __construct( private readonly AssetMapperInterface $assetMapper, private readonly Filesystem $filesystem, - #[Autowire('%kernel.project_dir%')] + #[Autowire(param: 'kernel.project_dir')] private readonly string $projectDir, private readonly null|ImageProcessorInterface $imageProcessor, #[Autowire('@pwa.web_client')] diff --git a/src/Dto/Icon.php b/src/Dto/Icon.php index 8b013b9..3eda863 100644 --- a/src/Dto/Icon.php +++ b/src/Dto/Icon.php @@ -18,6 +18,17 @@ final class Icon public null|string $type = null; + public null|string $format = null; + + #[SerializedName('border_radius')] + public null|int $borderRadius = null; + + #[SerializedName('image_scale')] + public null|int $imageScale = null; + + #[SerializedName('background_color')] + public null|string $backgroundColor = null; + public null|string $purpose = null; #[SerializedName('sizes')] diff --git a/src/Normalizer/IconNormalizer.php b/src/Normalizer/IconNormalizer.php index 0c18982..e9a10ef 100644 --- a/src/Normalizer/IconNormalizer.php +++ b/src/Normalizer/IconNormalizer.php @@ -5,9 +5,7 @@ namespace SpomkyLabs\PwaBundle\Normalizer; use SpomkyLabs\PwaBundle\Dto\Icon; -use Symfony\Component\AssetMapper\AssetMapperInterface; -use Symfony\Component\AssetMapper\MappedAsset; -use Symfony\Component\Mime\MimeTypes; +use SpomkyLabs\PwaBundle\Service\IconResolver; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -18,7 +16,7 @@ final class IconNormalizer implements NormalizerInterface, NormalizerAwareInterf use NormalizerAwareTrait; public function __construct( - private readonly AssetMapperInterface $assetMapper, + private readonly IconResolver $iconResolver, ) { } @@ -28,14 +26,11 @@ public function __construct( public function normalize(mixed $object, string $format = null, array $context = []): array { assert($object instanceof Icon); - $imageType = $object->type; - if (! str_starts_with($object->src->src, '/')) { - $asset = $this->assetMapper->getAsset($object->src->src); - $imageType = $this->getType($asset); - } + $icon = $this->iconResolver->getIcon($object); + $imageType = $this->iconResolver->getType($object->type, $icon->url); $result = [ - 'src' => $this->normalizer->normalize($object->src, $format, $context), + 'src' => $icon->url, 'sizes' => $object->getSizeList(), 'type' => $imageType, 'purpose' => $object->purpose, @@ -63,14 +58,4 @@ public function getSupportedTypes(?string $format): array Icon::class => true, ]; } - - private function getType(?MappedAsset $asset): ?string - { - if ($asset === null || ! class_exists(MimeTypes::class)) { - return null; - } - - $mime = MimeTypes::getDefault(); - return $mime->guessMimeType($asset->sourcePath); - } } diff --git a/src/Resources/config/definition/utils/icons.php b/src/Resources/config/definition/utils/icons.php index 006ff5a..83a57b6 100644 --- a/src/Resources/config/definition/utils/icons.php +++ b/src/Resources/config/definition/utils/icons.php @@ -16,50 +16,72 @@ function getIconsNode(string $info): ArrayNodeDefinition ->treatNullLike([]) ->arrayPrototype() ->beforeNormalization() - ->ifString() - ->then(static fn (string $v): array => [ - 'src' => $v, - ]) + ->ifString() + ->then(static fn (string $v): array => [ + 'src' => $v, + ]) ->end() ->children() - ->scalarNode('src') - ->isRequired() - ->info('The path to the icon. Can be served by Asset Mapper.') - ->example('icon/logo.svg') - ->end() - ->arrayNode('sizes') - ->beforeNormalization() - ->ifTrue(static fn (mixed $v): bool => is_int($v)) - ->then(static fn (int $v): array => [$v]) - ->end() - ->beforeNormalization() - ->ifTrue(static fn (mixed $v): bool => is_string($v)) - ->then(static function (string $v): array { - if ($v === 'any') { - return [0]; - } + ->scalarNode('src') + ->isRequired() + ->info('The path to the icon. Can be served by Asset Mapper.') + ->example('icon/logo.svg') + ->end() + ->arrayNode('sizes') + ->beforeNormalization() + ->ifTrue(static fn (mixed $v): bool => is_int($v)) + ->then(static fn (int $v): array => [$v]) + ->end() + ->beforeNormalization() + ->ifTrue(static fn (mixed $v): bool => is_string($v)) + ->then(static function (string $v): array { + if ($v === 'any') { + return [0]; + } - return [(int) $v]; - }) - ->end() - ->info( - 'The sizes of the icon. 16 means 16x16, 32 means 32x32, etc. 0 means "any" (i.e. it is a vector image).' - ) - ->example([['16', '32']]) - ->integerPrototype() - ->end() - ->end() - ->scalarNode('type') - ->info('The icon mime type.') - ->example(['image/webp', 'image/png']) - ->end() - ->scalarNode('purpose') - ->info('The purpose of the icon.') - ->example(['any', 'maskable', 'monochrome']) - ->end() - ->end() + return [(int) $v]; + }) + ->end() + ->info( + 'The sizes of the icon. 16 means 16x16, 32 means 32x32, etc. 0 means "any" (i.e. it is a vector image).' + ) + ->example([['16', '32']]) + ->integerPrototype() + ->end() + ->end() + ->scalarNode('background_color') + ->defaultNull() + ->info( + 'The background color of the application. If this value is not defined and that of the Manifest section is, the value of the latter will be used.' + ) + ->example(['red', '#f5ef06']) + ->end() + ->integerNode('border_radius') + ->defaultNull() + ->min(1) + ->max(50) + ->info('The border radius of the icon.') + ->end() + ->integerNode('image_scale') + ->defaultNull() + ->min(1) + ->max(100) + ->info('The scale of the icon.') + ->end() + ->scalarNode('type') + ->info('The icon mime type.') + ->example(['image/webp', 'image/png']) + ->end() + ->scalarNode('format') + ->info('The icon format. When set, the "type" option is ignored and the image will be converted.') + ->example(['image/webp', 'image/png']) + ->end() + ->scalarNode('purpose') + ->info('The purpose of the icon.') + ->example(['any', 'maskable', 'monochrome']) + ->end() ->end() - ; + ->end(); return $node; } diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index 435837e..4efd301 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -18,10 +18,12 @@ use SpomkyLabs\PwaBundle\ImageProcessor\GDImageProcessor; use SpomkyLabs\PwaBundle\ImageProcessor\ImagickImageProcessor; use SpomkyLabs\PwaBundle\MatchCallbackHandler\MatchCallbackHandlerInterface; +use SpomkyLabs\PwaBundle\Service\ApplicationIconCompiler; use SpomkyLabs\PwaBundle\Service\CanLogInterface; use SpomkyLabs\PwaBundle\Service\FaviconsBuilder; use SpomkyLabs\PwaBundle\Service\FaviconsCompiler; use SpomkyLabs\PwaBundle\Service\FileCompilerInterface; +use SpomkyLabs\PwaBundle\Service\IconResolver; use SpomkyLabs\PwaBundle\Service\ManifestBuilder; use SpomkyLabs\PwaBundle\Service\ManifestCompiler; use SpomkyLabs\PwaBundle\Service\ServiceWorkerBuilder; @@ -73,6 +75,9 @@ ; $container->set(FaviconsCompiler::class); + $container->set(IconResolver::class); + $container->set(ApplicationIconCompiler::class); + /*** Service Worker ***/ $container->set(ServiceWorkerBuilder::class) ->args([ diff --git a/src/Service/ApplicationIconCompiler.php b/src/Service/ApplicationIconCompiler.php new file mode 100644 index 0000000..73773b6 --- /dev/null +++ b/src/Service/ApplicationIconCompiler.php @@ -0,0 +1,49 @@ + + */ + public function getFiles(): iterable + { + $icons = []; + if ($this->manifest->enabled === false) { + yield from $icons; + return; + } + if (count($this->manifest->icons) !== 0) { + $icons = array_merge($icons, $this->manifest->icons); + } + if (count($this->manifest->shortcuts) !== 0) { + foreach ($this->manifest->shortcuts as $shortcut) { + $icons = array_merge($icons, $shortcut->icons); + } + } + if (count($this->manifest->widgets) !== 0) { + foreach ($this->manifest->widgets as $widget) { + $icons = array_merge($icons, $widget->icons); + } + } + + foreach ($icons as $icon) { + yield $this->iconResolver->getIcon($icon); + } + } +} diff --git a/src/Service/FaviconsCompiler.php b/src/Service/FaviconsCompiler.php index b5c4e10..50e9fad 100644 --- a/src/Service/FaviconsCompiler.php +++ b/src/Service/FaviconsCompiler.php @@ -25,7 +25,7 @@ public function __construct( private readonly null|ImageProcessorInterface $imageProcessor, private readonly Favicons $favicons, private readonly AssetMapperInterface $assetMapper, - #[Autowire('%kernel.debug%')] + #[Autowire(param: 'kernel.debug')] public readonly bool $debug, ) { $this->logger = new NullLogger(); @@ -58,7 +58,7 @@ public function getFiles(): iterable 'rel' => 'icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 16, 'height' => 16, 'format' => 'png', @@ -66,7 +66,7 @@ public function getFiles(): iterable 'rel' => 'icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 32, 'height' => 32, 'format' => 'png', @@ -75,7 +75,7 @@ public function getFiles(): iterable ], //High resolution iOS [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 180, 'height' => 180, 'format' => 'png', @@ -84,7 +84,7 @@ public function getFiles(): iterable ], //High resolution chrome [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 192, 'height' => 192, 'format' => 'png', @@ -92,7 +92,7 @@ public function getFiles(): iterable 'rel' => 'icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 512, 'height' => 512, 'format' => 'png', @@ -105,7 +105,7 @@ public function getFiles(): iterable ...$sizes, //Prior iOS 6 [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 57, 'height' => 57, 'format' => 'png', @@ -113,7 +113,7 @@ public function getFiles(): iterable 'rel' => 'apple-touch-icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 72, 'height' => 72, 'format' => 'png', @@ -121,7 +121,7 @@ public function getFiles(): iterable 'rel' => 'apple-touch-icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 114, 'height' => 114, 'format' => 'png', @@ -131,7 +131,7 @@ public function getFiles(): iterable //Prior iOS 7 [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 60, 'height' => 60, 'format' => 'png', @@ -139,7 +139,7 @@ public function getFiles(): iterable 'rel' => 'apple-touch-icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 76, 'height' => 76, 'format' => 'png', @@ -147,7 +147,7 @@ public function getFiles(): iterable 'rel' => 'apple-touch-icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 120, 'height' => 120, 'format' => 'png', @@ -155,7 +155,7 @@ public function getFiles(): iterable 'rel' => 'apple-touch-icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 152, 'height' => 152, 'format' => 'png', @@ -165,7 +165,7 @@ public function getFiles(): iterable //Other resolution [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 36, 'height' => 36, 'format' => 'png', @@ -173,7 +173,7 @@ public function getFiles(): iterable 'rel' => 'icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 48, 'height' => 48, 'format' => 'png', @@ -181,7 +181,7 @@ public function getFiles(): iterable 'rel' => 'icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 72, 'height' => 72, 'format' => 'png', @@ -189,7 +189,7 @@ public function getFiles(): iterable 'rel' => 'icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 96, 'height' => 96, 'format' => 'png', @@ -197,7 +197,7 @@ public function getFiles(): iterable 'rel' => 'icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 144, 'height' => 144, 'format' => 'png', @@ -205,7 +205,7 @@ public function getFiles(): iterable 'rel' => 'icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 256, 'height' => 256, 'format' => 'png', @@ -213,7 +213,7 @@ public function getFiles(): iterable 'rel' => 'icon', ], [ - 'url' => '/favicons/icon-%dx%d-%s.png', + 'url' => '/pwa/favicon-%dx%d-%s.png', 'width' => 384, 'height' => 384, 'format' => 'png', @@ -280,7 +280,7 @@ private function processIcon( [ 'Cache-Control' => 'public, max-age=604800, immutable', 'Content-Type' => $mimeType, - 'X-Favicons-Dev' => true, + 'X-Pwa-Dev' => true, ], $html ); @@ -292,7 +292,7 @@ private function processIcon( [ 'Cache-Control' => 'public, max-age=604800, immutable', 'Content-Type' => $mimeType, - 'X-Favicons-Dev' => true, + 'X-Pwa-Dev' => true, ], sprintf( '', @@ -318,7 +318,7 @@ private function processBrowserConfig(string $asset, string $hash): array $hash = hash('xxh128', $hash . $configuration); $icon70x70 = $this->processIcon( $asset, - sprintf('/favicons/icon-%dx%d-%s.png', 70, 70, $hash), + sprintf('/pwa/favicon-%dx%d-%s.png', 70, 70, $hash), $configuration, 'image/png', null @@ -328,7 +328,7 @@ private function processBrowserConfig(string $asset, string $hash): array $hash = hash('xxh128', $hash . $configuration); $icon150x150 = $this->processIcon( $asset, - sprintf('/favicons/icon-%dx%d-%s.png', 150, 150, $hash), + sprintf('/pwa/favicon-%dx%d-%s.png', 150, 150, $hash), $configuration, 'image/png', null @@ -338,7 +338,7 @@ private function processBrowserConfig(string $asset, string $hash): array $hash = hash('xxh128', $hash . $configuration); $icon310x310 = $this->processIcon( $asset, - sprintf('/favicons/icon-%dx%d-%s.png', 310, 310, $hash), + sprintf('/pwa/favicon-%dx%d-%s.png', 310, 310, $hash), $configuration, 'image/png', null @@ -348,7 +348,7 @@ private function processBrowserConfig(string $asset, string $hash): array $hash = hash('xxh128', $hash . $configuration); $icon310x150 = $this->processIcon( $asset, - sprintf('/favicons/icon-%dx%d-%s.png', 310, 150, $hash), + sprintf('/pwa/favicon-%dx%d-%s.png', 310, 150, $hash), $configuration, 'image/png', null @@ -358,7 +358,7 @@ private function processBrowserConfig(string $asset, string $hash): array $hash = hash('xxh128', $hash . $configuration); $icon144x144 = $this->processIcon( $asset, - sprintf('/favicons/icon-%dx%d-%s.png', 144, 144, $hash), + sprintf('/pwa/favicon-%dx%d-%s.png', 144, 144, $hash), $configuration, 'image/png', null @@ -386,14 +386,14 @@ private function processBrowserConfig(string $asset, string $hash): array XML; $browserConfigHash = hash('xxh128', $content); - $url = sprintf('/favicons/browserconfig-%s.xml', $browserConfigHash); + $url = sprintf('/pwa/browserconfig-%s.xml', $browserConfigHash); $browserConfig = Data::create( $url, $content, [ 'Cache-Control' => 'public, max-age=604800, immutable', 'Content-Type' => 'application/xml', - 'X-Favicons-Dev' => true, + 'X-Pwa-Dev' => true, 'Etag' => hash('xxh128', $content), ], sprintf('', $url) @@ -439,7 +439,7 @@ private function getFavicon(): array private function generateSafariPinnedTab(string $content, string $hash): Data { $callback = fn (): string => $this->generateSilhouette($content); - $url = sprintf('/safari-pinned-tab-%s.svg', $hash); + $url = sprintf('/pwa/safari-pinned-tab-%s.svg', $hash); return Data::create( $url, @@ -447,7 +447,7 @@ private function generateSafariPinnedTab(string $content, string $hash): Data [ 'Cache-Control' => 'public, max-age=604800, immutable', 'Content-Type' => 'image/svg+xml', - 'X-Favicons-Dev' => true, + 'X-Pwa-Dev' => true, 'Etag' => $hash, ], sprintf('', $url, $this->favicons->safariPinnedTabColor) diff --git a/src/Service/IconResolver.php b/src/Service/IconResolver.php new file mode 100644 index 0000000..f13692b --- /dev/null +++ b/src/Service/IconResolver.php @@ -0,0 +1,146 @@ +getAsset($icon->src); + $content = $asset->content; + if ($content === null) { + $content = (new Filesystem())->readFile($asset->sourcePath); + } + + $imageProcessor = fn (Configuration $configuration): string => $this->imageProcessor->process( + $content, + null, + null, + null, + configuration: $configuration + ); + $sizeList = $icon->sizeList; + if (count($sizeList) === 0) { + $sizeList = [0]; + } + $size = max($sizeList); + if ($size === 0) { + $url = sprintf('/pwa/icon-any-%s.%s', $asset->digest, $asset->publicExtension); + return new Data( + $url, + fn () => $content, + [ + 'Cache-Control' => 'public, max-age=604800, immutable', + 'Content-Type' => $this->getType($icon->type, $url), + 'X-Pwa-Dev' => true, + ], + ); + } + + $configuration = new Configuration( + $size, + $size, + $icon->format ?? $asset->publicExtension, + $icon->backgroundColor, + $icon->borderRadius, + $icon->imageScale + ); + $format = $icon->format ?? $asset->publicExtension; + $hash = hash( + 'xxh128', + sprintf( + '%s%s%s%d%s%s', + $asset->digest, + $configuration, + $format, + $size, + $icon->purpose ?? '', + $icon->type ?? '' + ) + ); + $url = sprintf('/pwa/icon-%s.%s', $hash, $format); + + return new Data( + $url, + fn () => $imageProcessor($configuration), + [ + 'Cache-Control' => 'public, max-age=604800, immutable', + 'Content-Type' => $this->getType($icon->type, $url), + 'X-Pwa-Dev' => true, + ], + ); + } + + public function getType(?string $type, string $url): ?string + { + if ($type !== null) { + return $type; + } + if (! class_exists(MimeTypes::class)) { + return null; + } + $fileinfo = pathinfo($url); + if (! array_key_exists('extension', $fileinfo)) { + return null; + } + + $mime = MimeTypes::getDefault(); + $mimeTypes = $mime->getMimeTypes($fileinfo['extension']); + if ($mimeTypes === []) { + return null; + } + + return $mimeTypes[0]; + } + + private function getAsset(Asset $asset): MappedAsset + { + if (str_starts_with($asset->src, '/')) { + $content = (new Filesystem())->readFile($asset->src); + $hash = hash('xxh128', $content); + $fileinfo = pathinfo($asset->src); + assert(is_array($fileinfo), 'Invalid file.'); + assert(array_key_exists('filename', $fileinfo), 'Invalid file.'); + assert(array_key_exists('extension', $fileinfo), 'Invalid file.'); + assert(array_key_exists('basename', $fileinfo), 'Invalid file.'); + + return new MappedAsset( + sprintf('/pwa/%s-%s.%s', $hash, $fileinfo['filename'], $fileinfo['extension']), + $asset->src, + sprintf('/pwa/%s.%s', $hash, $fileinfo['extension']), + sprintf('/pwa/%s-%s.%s', $hash, $fileinfo['filename'], $fileinfo['extension']), + $content, + $hash, + false, + ); + } + $asset = $this->assetMapper->getAsset($asset->src); + assert($asset instanceof MappedAsset, sprintf('Invalid asset "%s"', $asset->sourcePath)); + + return $asset; + } +} diff --git a/src/Service/ManifestCompiler.php b/src/Service/ManifestCompiler.php index 021e903..506d153 100644 --- a/src/Service/ManifestCompiler.php +++ b/src/Service/ManifestCompiler.php @@ -42,12 +42,12 @@ final class ManifestCompiler implements FileCompilerInterface, CanLogInterface public function __construct( private readonly SerializerInterface $serializer, private readonly Manifest $manifest, - #[Autowire('%spomky_labs_pwa.manifest.public_url%')] + #[Autowire(param: 'spomky_labs_pwa.manifest.public_url')] string $manifestPublicUrl, - #[Autowire('%kernel.debug%')] + #[Autowire(param: 'kernel.debug')] bool $debug, null|EventDispatcherInterface $dispatcher, - #[Autowire('%kernel.enabled_locales%')] + #[Autowire(param: 'kernel.enabled_locales')] private readonly array $locales, ) { $this->dispatcher = $dispatcher ?? new NullEventDispatcher(); @@ -131,7 +131,7 @@ private function compileManifest(null|string $locale): Data [ 'Cache-Control' => 'public, max-age=604800, immutable', 'Content-Type' => 'application/manifest+json', - 'X-Manifest-Dev' => true, + 'X-Pwa-Dev' => true, ] ); } diff --git a/src/Service/ServiceWorkerCompiler.php b/src/Service/ServiceWorkerCompiler.php index 9ffacb2..be25c01 100644 --- a/src/Service/ServiceWorkerCompiler.php +++ b/src/Service/ServiceWorkerCompiler.php @@ -36,7 +36,7 @@ public function __construct( private readonly AssetMapperInterface $assetMapper, #[TaggedIterator('spomky_labs_pwa.service_worker_rule', defaultPriorityMethod: 'getPriority')] private readonly iterable $serviceworkerRules, - #[Autowire('%kernel.debug%')] + #[Autowire(param: 'kernel.debug')] public readonly bool $debug, ) { $serviceWorkerPublicUrl = $serviceWorker->dest; @@ -95,7 +95,7 @@ private function compileSW(): Data $callback, [ 'Content-Type' => 'application/javascript', - 'X-SW-Dev' => true, + 'X-Pwa-Dev' => true, ] ); } @@ -186,7 +186,7 @@ private function getWorkboxFile(string $publicUrl): null|Data $callback, [ 'Content-Type' => 'application/javascript', - 'X-SW-Dev' => true, + 'X-Pwa-Dev' => true, ] ); } diff --git a/src/ServiceWorkerRule/AppendCacheStrategies.php b/src/ServiceWorkerRule/AppendCacheStrategies.php index 083495e..cda6871 100644 --- a/src/ServiceWorkerRule/AppendCacheStrategies.php +++ b/src/ServiceWorkerRule/AppendCacheStrategies.php @@ -17,7 +17,7 @@ public function __construct( #[TaggedIterator('spomky_labs_pwa.cache_strategy')] private iterable $cacheStrategies, - #[Autowire('%kernel.debug%')] + #[Autowire(param: 'kernel.debug')] public bool $debug, ) { } diff --git a/src/Subscriber/PwaDevServerSubscriber.php b/src/Subscriber/PwaDevServerSubscriber.php index 2d13250..3c5bfe8 100644 --- a/src/Subscriber/PwaDevServerSubscriber.php +++ b/src/Subscriber/PwaDevServerSubscriber.php @@ -59,7 +59,7 @@ public function onKernelResponse(ResponseEvent $event): void { $headers = $event->getResponse() ->headers; - if ($headers->has('X-Manifest-Dev') || $headers->has('X-SW-Dev') || $headers->has('X-Favicons-Dev')) { + if ($headers->has('X-Pwa-Dev')) { $event->stopPropagation(); } } diff --git a/src/Twig/PwaRuntime.php b/src/Twig/PwaRuntime.php index bbcb3d4..32cd7e2 100644 --- a/src/Twig/PwaRuntime.php +++ b/src/Twig/PwaRuntime.php @@ -28,7 +28,7 @@ public function __construct( private Manifest $manifest, private Favicons $favicons, private FaviconsCompiler $faviconsCompiler, - #[Autowire('%spomky_labs_pwa.manifest.public_url%')] + #[Autowire(param: 'spomky_labs_pwa.manifest.public_url')] string $manifestPublicUrl, #[Autowire(service: 'nelmio_security.csp_listener')] private ?ContentSecurityPolicyListener $cspListener = null, @@ -268,7 +268,7 @@ private function injectFavicons(string $output, bool $injectFavicons): string ); /*$output .= PHP_EOL . sprintf( '', - $files['/favicons/icon-144x144.png']->url + $files['/pwa/icon-144x144.png']->url );*/ } diff --git a/tests/Functional/TakeScreenshotCommandTest.php b/tests/Functional/TakeScreenshotCommandTest.php index b8dd723..accd081 100644 --- a/tests/Functional/TakeScreenshotCommandTest.php +++ b/tests/Functional/TakeScreenshotCommandTest.php @@ -16,8 +16,9 @@ final class TakeScreenshotCommandTest extends AbstractPwaTestCase { #[Test] #[MaximumDuration(1500)] - public static function aScreenshotIsCorrectlyTake(): void + public static function aScreenshotIsCorrectlyTaken(): never { + static::markTestSkipped('This test is skipped as it requires a running server.'); // Given $command = self::$application->find('pwa:create:screenshot'); $commandTester = new CommandTester($command); diff --git a/tests/config.php b/tests/config.php index c19a04b..cc6bbac 100644 --- a/tests/config.php +++ b/tests/config.php @@ -26,6 +26,10 @@ $container->extension('framework', [ 'test' => true, 'secret' => 'test', + 'profiler' => [ + 'enabled' => true, + 'collect' => false, + ], 'http_method_override' => true, 'handle_all_throwables' => true, 'session' => [