From 6c506be320979e5606e35fc696dc34997ee72ed7 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Thu, 18 Jan 2024 15:22:01 +0100 Subject: [PATCH] Smart URLs and Translations --- README.md | 135 ++++++------------ composer.json | 1 + src/DependencyInjection/Configuration.php | 101 ++++++------- src/Dto/FileHandler.php | 10 +- src/Dto/Manifest.php | 33 +++++ src/Dto/ProtocolHandler.php | 10 +- src/Dto/RelatedApplication.php | 2 +- src/Dto/Screenshot.php | 8 ++ src/Dto/ShareTarget.php | 10 +- src/Dto/ShareTargetParameters.php | 14 ++ src/Dto/Shortcut.php | 21 ++- src/Dto/TranslatableTrait.php | 29 ++++ src/Dto/Url.php | 15 ++ src/Dto/Widget.php | 20 +++ src/Normalizer/ProtocolHandlerNormalizer.php | 51 ------- src/Normalizer/ShareTargetNormalizer.php | 59 -------- src/Normalizer/ShortcutNormalizer.php | 16 +-- ...andlerNormalizer.php => UrlNormalizer.php} | 31 ++-- .../Functional/ManifestFileDevServerTest.php | 3 +- tests/config.php | 55 ++++--- tests/translations/messages.en.yaml | 11 ++ 21 files changed, 298 insertions(+), 337 deletions(-) create mode 100644 src/Dto/TranslatableTrait.php create mode 100644 src/Dto/Url.php delete mode 100644 src/Normalizer/ProtocolHandlerNormalizer.php delete mode 100644 src/Normalizer/ShareTargetNormalizer.php rename src/Normalizer/{FileHandlerNormalizer.php => UrlNormalizer.php} (50%) create mode 100644 tests/translations/messages.en.yaml diff --git a/README.md b/README.md index e36f2e7..61251df 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Please have a look at the [Web app manifests](https://developer.mozilla.org/en-U Install the bundle with Composer: ```bash -composer require --dev spomky-labs/phpwa +composer require spomky-labs/phpwa ``` This project follows the [semantic versioning](http://semver.org/) strictly. @@ -69,12 +69,22 @@ pwa: 'application/json': ['.json'] ``` +### Using the Manifest + +The manifest can be used in your HTML pages with the following Twig function in the `` section. +It will automatically add the manifest your HTML pages and any other useful meta tags. + +```html +{{ pwa() }} +``` + ### Manifest Generation -When the configuration is set, you can generate the manifest with the following command: +On `dev` or `test` environment, the manifest will be generated for you. +On `prod` environment, the manifest is compiled during the deployment with Asset Mapper. ```bash -symfony console pwa:build +symfony console asset-map:compile ``` By default, the manifest will be generated in the `public` directory with the name `/site.webmanifest`. @@ -83,26 +93,12 @@ You can change the output file name and the output folder with the following con ```yaml # config/packages/phpwa.yaml pwa: - manifest_filepath: '%kernel.project_dir%/public/sub-folder/pwa.json' + manifest_public_url: '/foo/pwa.json' ``` -### Using the Manifest - -The manifest can be used in your HTML pages with the following code in the `` section. -In you customized the output filename or the public folder, please replace `/site.webmanifest` with the path to your manifest file. - -```html - -``` - -### Manifest Icons - -The bundle is able to generate icons from a source image. -This is applicable to the application icons, shortcut icons or Windows 10 widget icons members of the manifest. -The icons must be square and the source image should be at best quality as possible. -To process the icons, you should set an icon processor. The bundle provides a GD processor and an Imagick processor. -Depending on your system, you may have to install one extension or the other. +### Manifest Icons and Screenshots +The bundle is able to link your assets to the manifest file. Please note that the icons of a size greater than 1024px may be ignored by the browser. ```yaml @@ -110,69 +106,36 @@ Please note that the icons of a size greater than 1024px may be ignored by the b pwa: image_processor: 'pwa.image_processor.gd' # or 'pwa.image_processor.imagick' icons: - - src: "%kernel.project_dir%/assets/images/logo.png" - sizes: [48, 57, 60, 72, 76, 96, 114, 128, 144, 152, 180, 192, 256, 384, 512, 1024] - format: 'webp' - - src: "%kernel.project_dir%/assets/images/mask.png" - sizes: [48, 57, 60, 72, 76, 96, 114, 128, 144, 152, 180, 192, 256, 384, 512, 1024] - purpose: 'maskable' - - src: "%kernel.project_dir%/assets/images/logo.svg" + - src: "images/logo.png" + sizes: [48, 96, 128, 256, 512, 1024] + - src: "images/logo.svg" sizes: [0] # 0 means `any` size and is suitable for vector images + - src: "images/logo.svg" + purpose: 'maskable' + sizes: [0] + screenshots: + - src: "screenshots/android_dashboard.png" + platform: 'android' + label: "View of the dashboard on Android" + - "screenshots/android_feature1.png" + - "screenshots/android_feature2.png" shortcuts: - name: "Shortcut 1" short_name: "shortcut-1" url: "/shortcut1" description: "Shortcut 1 description" icons: - - src: "%kernel.project_dir%/assets/images/shortcut1.png" - sizes: [48, 72, 96, 128, 144, 192, 256, 384, 512] - format: 'webp' + - src: "images/shortcut1.png" + sizes: [48, 96, 128, 256, 512, 1024] - name: "Shortcut 2" short_name: "shortcut-2" url: "/shortcut2" description: "Shortcut 2 description" icons: - - src: "%kernel.project_dir%/assets/images/shortcut2.png" - sizes: [48, 72, 96, 128, 144, 192, 256, 384, 512] - format: 'webp' + - src: "images/shortcut2.png" + sizes: [48, 96, 128, 256, 512, 1024] ``` -With the configuration above, the bundle will generate -* 16 icons from the `logo.png` image. The icons will be converted from `png` to `webp`. -* 16 icons from the `mask.png` image. The format will be `png` and the purpose will be `maskable`. -* And 1 icon from the `logo.svg` image. The format will be `svg` and the size will be `any`. - -If the `format` member is present, the bundle will convert the image to the specified format. -In the example above, the `logo.png` image will be converted to `webp`, but the `mask.png` image will not be converted. - -### Manifest Screenshots - -The bundle is able to generate screenshots from a source image. -This is applicable to the application screenshots or Windows 10 widget screenshot members of the manifest. -Similar to icons, the source image should be at best quality as possible. - -You can select a folder where the source images are stored. -The bundle will automatically generate screenshots from all the images in the folder. - -```yaml -# config/packages/phpwa.yaml -pwa: - image_processor: 'pwa.image_processor.gd' # or 'pwa.image_processor.imagick' - screenshots: - - src: "%kernel.project_dir%/assets/screenshots/narrow/" - form_factor: "narrow" - - src: "%kernel.project_dir%/assets/screenshots/wide/" - form_factor: "wide" - - src: "%kernel.project_dir%/assets/screenshots/android_dashboard.png" - platform: 'android' - format: 'webp' - label: "View of the dashboard on Android" -``` - -The bundle will automatically generate screenshots from the source images and add additional information in the manifest -such as the `sizes` and the `form_factor` (`wide` or `narrow`). -The `format` parameter is optional. It indicates the format of the generated image. If not set, the format will be the same as the source image. - ### Manifest Shortcuts The `shortcuts` member may contain a list of action shortcuts that point to specific URLs in your application. @@ -194,21 +157,28 @@ pwa: ## Service Worker -The following command will generate a Service Worker in the `public` directory with the name `/sw.js`: +The service worker is a JavaScript file that is executed by the browser. +It can be served by Asset Mapper. -```bash -symfony console pwa:sw -``` +```yaml +# config/packages/phpwa.yaml +pwa: + serviceworker: 'script/service-worker.js' -If you want override the existing Service Worker, you can use the `--force` option: +``` -```bash -symfony console pwa:sw --force +```yaml +#The following configuration is similar +pwa: + serviceworker: + src: 'script/service-worker.js' + dest: '/sw.js' + scope: '/' ``` Next, you have to register the Service Worker in your HTML pages with the following code in the `` section. It can also be done in a JavaScript file such as `app.js`. -In you customized the output filename or the public folder, please replace `sw.js` with the path to your Service Worker file. +In you customized the destination filename, please replace `/sw.js` with the path to your Service Worker file. ```html ``` -The location of the Service Worker is important. It must be at the root of your application. -In addition, the `serviceworker.src` member of the manifest must be set to the same location. The `serviceworker.scope` member may be set to the same location or to a sub-folder. Do not forget to update the `scope` member in the JS configuration. -```yaml -# config/packages/phpwa.yaml -pwa: - serviceworker: - filepath: '%kernel.project_dir%/public/sub-folder/service-worker.js' - src: '/sub-folder/service-worker.js' - scope: '/' -``` - ### Service Worker Configuration The Service Worker uses Workbox and comes with predefined configuration and recipes. diff --git a/composer.json b/composer.json index 016911d..9ff1517 100644 --- a/composer.json +++ b/composer.json @@ -54,6 +54,7 @@ "symfony/mime": "^6.4|^7.0", "symfony/panther": "^2.1", "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/translation": "^7.0", "symfony/yaml": "^6.4|^7.0", "symplify/easy-coding-standard": "^12.0" }, diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 6d179f1..0290fa5 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -63,18 +63,7 @@ private function setupShortcuts(ArrayNodeDefinition $node): void ->info('The description of the shortcut.') ->example('Awesome shortcut') ->end() - ->scalarNode('url') - ->isRequired() - ->info('The URL of the shortcut.') - ->example('https://example.com') - ->end() - ->arrayNode('url_params') - ->treatFalseLike([]) - ->treatTrueLike([]) - ->treatNullLike([]) - ->prototype('variable')->end() - ->info('The parameters of the action. Only used if the action is a route to a controller.') - ->end() + ->append($this->getUrlNode('url', 'The URL of the shortcut.')) ->append($this->getIconsNode('The icons of the shortcut.')) ->end() ->end() @@ -103,18 +92,7 @@ private function setupFileHandlers(ArrayNodeDefinition $node): void ->treatNullLike([]) ->arrayPrototype() ->children() - ->scalarNode('action') - ->isRequired() - ->info('The action to take.') - ->example('/handle-audio-file') - ->end() - ->arrayNode('action_params') - ->treatFalseLike([]) - ->treatTrueLike([]) - ->treatNullLike([]) - ->prototype('variable')->end() - ->info('The parameters of the action. Only used if the action is a route to a controller.') - ->end() + ->append($this->getUrlNode('action', 'The action to take.', ['/handle-audio-file'])) ->arrayNode('accept') ->requiresAtLeastOneElement() ->useAttributeAsKey('name') @@ -140,18 +118,9 @@ private function setupSharedTarget(ArrayNodeDefinition $node): void ->treatNullLike([]) ->info('The share target of the application.') ->children() - ->scalarNode('action') - ->isRequired() - ->info('The action of the share target.') - ->example('/shared-content-receiver/') - ->end() - ->arrayNode('action_params') - ->treatFalseLike([]) - ->treatTrueLike([]) - ->treatNullLike([]) - ->prototype('variable')->end() - ->info('The parameters of the action. Only used if the action is a route to a controller.') - ->end() + ->append( + $this->getUrlNode('action', 'The action of the share target.', ['/shared-content-receiver/']) + ) ->scalarNode('method') ->info('The method of the share target.') ->example('GET') @@ -211,18 +180,7 @@ private function setupProtocolHandlers(ArrayNodeDefinition $node): void ->info('The protocol of the handler.') ->example('web+jngl') ->end() - ->scalarNode('url') - ->isRequired() - ->info('The URL of the handler.') - ->example('/lookup?type=%s') - ->end() - ->arrayNode('url_params') - ->treatFalseLike([]) - ->treatTrueLike([]) - ->treatNullLike([]) - ->prototype('variable')->end() - ->info('The parameters of the action. Only used if the action is a route to a controller.') - ->end() + ->append($this->getUrlNode('url', 'The URL of the handler.')) ->end() ->end() ->end() @@ -269,11 +227,11 @@ private function setupRelatedApplications(ArrayNodeDefinition $node): void ->info('The platform of the application.') ->example('play') ->end() - ->scalarNode('url') - ->isRequired() - ->info('The URL of the application.') - ->example('https://play.google.com/store/apps/details?id=com.example.app1') - ->end() + ->append( + $this->getUrlNode('url', 'The URL of the application.', [ + 'https://play.google.com/store/apps/details?id=com.example.app1', + ]) + ) ->scalarNode('id') ->info('The ID of the application.') ->example('com.example.app1') @@ -314,7 +272,7 @@ private function setupSimpleOptions(ArrayNodeDefinition $node): void ->end() ->end() ->scalarNode('manifest_public_url') - ->defaultValue('/site.manifest') + ->defaultValue('/site.webmanifest') ->cannotBeEmpty() ->info('The public URL of the manifest file.') ->example('/site.manifest') @@ -639,4 +597,39 @@ private function setupWidgets(ArrayNodeDefinition $node): void ->end() ->end(); } + + /** + * @param array $examples + */ + private function getUrlNode(string $name, string $info, null|array $examples = null): ArrayNodeDefinition + { + $treeBuilder = new TreeBuilder($name); + $node = $treeBuilder->getRootNode(); + assert($node instanceof ArrayNodeDefinition); + $node + ->info($info) + ->beforeNormalization() + ->ifString() + ->then(static fn (string $v): array => [ + 'path' => $v, + ]) + ->end() + ->children() + ->scalarNode('path') + ->isRequired() + ->info('The URL of the shortcut.') + ->example($examples ?? ['https://example.com', 'app_action_route', '/do/action']) + ->end() + ->arrayNode('params') + ->treatFalseLike([]) + ->treatTrueLike([]) + ->treatNullLike([]) + ->prototype('variable')->end() + ->info('The parameters of the action. Only used if the action is a route to a controller.') + ->end() + ->end() + ->end(); + + return $node; + } } diff --git a/src/Dto/FileHandler.php b/src/Dto/FileHandler.php index 77b2240..47bb07a 100644 --- a/src/Dto/FileHandler.php +++ b/src/Dto/FileHandler.php @@ -4,17 +4,9 @@ namespace SpomkyLabs\PwaBundle\Dto; -use Symfony\Component\Serializer\Attribute\SerializedName; - final class FileHandler { - public string $action; - - /** - * @var array - */ - #[SerializedName('action_params')] - public array $actionParameters = []; + public Url $action; /** * @var array diff --git a/src/Dto/Manifest.php b/src/Dto/Manifest.php index 49b980e..a49b688 100644 --- a/src/Dto/Manifest.php +++ b/src/Dto/Manifest.php @@ -5,9 +5,12 @@ namespace SpomkyLabs\PwaBundle\Dto; use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Contracts\Translation\TranslatableInterface; final class Manifest { + use TranslatableTrait; + #[SerializedName('background_color')] public null|string $backgroundColor = null; @@ -108,4 +111,34 @@ final class Manifest #[SerializedName('serviceworker')] public null|ServiceWorker $serviceWorker = null; + + /** + * @return array + */ + public function getCategories(): array + { + return $this->provideTranslation($this->categories); + } + + public function getDescription(): string|TranslatableInterface + { + return $this->provideTranslation($this->description); + } + + public function getName(): string|TranslatableInterface + { + return $this->provideTranslation($this->name); + } + + #[SerializedName('short_name')] + public function getShortName(): string|TranslatableInterface + { + return $this->provideTranslation($this->shortName); + } + + #[SerializedName('start_url')] + public function getStartUrl(): string|TranslatableInterface + { + return $this->provideTranslation($this->startUrl); + } } diff --git a/src/Dto/ProtocolHandler.php b/src/Dto/ProtocolHandler.php index 1d9ac7d..0e07856 100644 --- a/src/Dto/ProtocolHandler.php +++ b/src/Dto/ProtocolHandler.php @@ -4,17 +4,9 @@ namespace SpomkyLabs\PwaBundle\Dto; -use Symfony\Component\Serializer\Attribute\SerializedName; - final class ProtocolHandler { public string $protocol; - /** - * @var array - */ - #[SerializedName('url_params')] - public array $urlParameters = []; - - public string $url; + public Url $url; } diff --git a/src/Dto/RelatedApplication.php b/src/Dto/RelatedApplication.php index ba5ade4..37c9939 100644 --- a/src/Dto/RelatedApplication.php +++ b/src/Dto/RelatedApplication.php @@ -8,7 +8,7 @@ final class RelatedApplication { public string $platform; - public string $url; + public Url $url; public null|string $id = null; } diff --git a/src/Dto/Screenshot.php b/src/Dto/Screenshot.php index 93415a5..b8b7a58 100644 --- a/src/Dto/Screenshot.php +++ b/src/Dto/Screenshot.php @@ -5,9 +5,12 @@ namespace SpomkyLabs\PwaBundle\Dto; use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Contracts\Translation\TranslatableInterface; final class Screenshot { + use TranslatableTrait; + public null|string $src = null; public null|int $height = null; @@ -22,4 +25,9 @@ final class Screenshot public null|string $platform = null; public null|string $format = null; + + public function getLabel(): string|TranslatableInterface + { + return $this->provideTranslation($this->label); + } } diff --git a/src/Dto/ShareTarget.php b/src/Dto/ShareTarget.php index a669587..a85fc95 100644 --- a/src/Dto/ShareTarget.php +++ b/src/Dto/ShareTarget.php @@ -4,17 +4,9 @@ namespace SpomkyLabs\PwaBundle\Dto; -use Symfony\Component\Serializer\Attribute\SerializedName; - final class ShareTarget { - public string $action; - - /** - * @var array - */ - #[SerializedName('action_params')] - public array $actionParameters = []; + public Url $action; public null|string $method = null; diff --git a/src/Dto/ShareTargetParameters.php b/src/Dto/ShareTargetParameters.php index 3d117d7..f43d21e 100644 --- a/src/Dto/ShareTargetParameters.php +++ b/src/Dto/ShareTargetParameters.php @@ -4,8 +4,12 @@ namespace SpomkyLabs\PwaBundle\Dto; +use Symfony\Contracts\Translation\TranslatableInterface; + final class ShareTargetParameters { + use TranslatableTrait; + public null|string $title = null; public null|string $text = null; @@ -16,4 +20,14 @@ final class ShareTargetParameters * @var array */ public array $files = []; + + public function getTitle(): string|TranslatableInterface + { + return $this->provideTranslation($this->title); + } + + public function getText(): string|TranslatableInterface + { + return $this->provideTranslation($this->text); + } } diff --git a/src/Dto/Shortcut.php b/src/Dto/Shortcut.php index cd9eaad..553c9ed 100644 --- a/src/Dto/Shortcut.php +++ b/src/Dto/Shortcut.php @@ -5,9 +5,12 @@ namespace SpomkyLabs\PwaBundle\Dto; use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Contracts\Translation\TranslatableInterface; final class Shortcut { + use TranslatableTrait; + public string $name; #[SerializedName('short_name')] @@ -15,16 +18,20 @@ final class Shortcut public null|string $description = null; - public string $url; - - /** - * @var array - */ - #[SerializedName('url_params')] - public array $urlParameters = []; + public Url $url; /** * @var array */ public array $icons = []; + + public function getName(): string|TranslatableInterface + { + $this->provideTranslation($this->name); + } + + public function getDescription(): string|TranslatableInterface + { + return $this->provideTranslation($this->description); + } } diff --git a/src/Dto/TranslatableTrait.php b/src/Dto/TranslatableTrait.php new file mode 100644 index 0000000..a4abf34 --- /dev/null +++ b/src/Dto/TranslatableTrait.php @@ -0,0 +1,29 @@ + $data + * + * @return null|string|TranslatableInterface|array + */ + public function provideTranslation(null|string|array $data): null|string|TranslatableInterface|array + { + if (! interface_exists(TranslatableInterface::class) || $data === null) { + return $data; + } + if (is_array($data)) { + return array_map(fn (string $value): TranslatableInterface => new TranslatableMessage($value), $data); + } + + return new TranslatableMessage($data); + } +} diff --git a/src/Dto/Url.php b/src/Dto/Url.php new file mode 100644 index 0000000..341cdf5 --- /dev/null +++ b/src/Dto/Url.php @@ -0,0 +1,15 @@ + + */ + public array $params = []; +} diff --git a/src/Dto/Widget.php b/src/Dto/Widget.php index 7ed39f9..84c1b82 100644 --- a/src/Dto/Widget.php +++ b/src/Dto/Widget.php @@ -5,6 +5,8 @@ namespace SpomkyLabs\PwaBundle\Dto; use Symfony\Component\Serializer\Attribute\SerializedName; +use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatableInterface; final class Widget { @@ -41,4 +43,22 @@ final class Widget public null|int $update = null; public bool $multiple = true; + + public function getName(): string|TranslatableInterface + { + if (! interface_exists(TranslatableInterface::class)) { + return $this->name; + } + + return new TranslatableMessage($this->name); + } + + public function getDescription(): string|TranslatableInterface + { + if (! interface_exists(TranslatableInterface::class) || $this->description === null) { + return $this->description; + } + + return new TranslatableMessage($this->description); + } } diff --git a/src/Normalizer/ProtocolHandlerNormalizer.php b/src/Normalizer/ProtocolHandlerNormalizer.php deleted file mode 100644 index 6db5c83..0000000 --- a/src/Normalizer/ProtocolHandlerNormalizer.php +++ /dev/null @@ -1,51 +0,0 @@ -url; - if (! str_starts_with($url, '/') && ! filter_var($url, FILTER_VALIDATE_URL)) { - $url = $this->router->generate($url, $object->urlParameters, $this->referenceType); - } - - return [ - 'url' => $url, - 'protocol' => $object->protocol, - ]; - } - - public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool - { - return $data instanceof ProtocolHandler; - } - - /** - * @return array - */ - public function getSupportedTypes(?string $format): array - { - return [ - ProtocolHandler::class => true, - ]; - } -} diff --git a/src/Normalizer/ShareTargetNormalizer.php b/src/Normalizer/ShareTargetNormalizer.php deleted file mode 100644 index b593529..0000000 --- a/src/Normalizer/ShareTargetNormalizer.php +++ /dev/null @@ -1,59 +0,0 @@ -action; - if (! str_starts_with($url, '/') && ! filter_var($url, FILTER_VALIDATE_URL)) { - $url = $this->router->generate($url, $object->actionParameters, $this->referenceType); - } - - $result = [ - 'action' => $url, - 'method' => $object->method, - 'enctype' => $object->enctype, - 'params' => $object->params, - ]; - - $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 ShareTarget; - } - - /** - * @return array - */ - public function getSupportedTypes(?string $format): array - { - return [ - ShareTarget::class => true, - ]; - } -} diff --git a/src/Normalizer/ShortcutNormalizer.php b/src/Normalizer/ShortcutNormalizer.php index 89b6913..400d5d7 100644 --- a/src/Normalizer/ShortcutNormalizer.php +++ b/src/Normalizer/ShortcutNormalizer.php @@ -5,38 +5,24 @@ namespace SpomkyLabs\PwaBundle\Normalizer; use SpomkyLabs\PwaBundle\Dto\Shortcut; -use Symfony\Component\DependencyInjection\Attribute\Autowire; -use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use function assert; -use const FILTER_VALIDATE_URL; final class ShortcutNormalizer implements NormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; - public function __construct( - private readonly RouterInterface $router, - #[Autowire('%spomky_labs_pwa.routes.reference_type%')] - private readonly int $referenceType, - ) { - } - public function normalize(mixed $object, string $format = null, array $context = []): array { assert($object instanceof Shortcut); - $url = $object->url; - if (! str_starts_with($url, '/') && ! filter_var($url, FILTER_VALIDATE_URL)) { - $url = $this->router->generate($url, $object->urlParameters, $this->referenceType); - } $result = [ 'name' => $object->name, 'short_name' => $object->shortName, 'description' => $object->description, - 'url' => $url, + 'url' => $this->normalizer->normalize($object->url, $format, $context), 'icons' => $this->normalizer->normalize($object->icons, $format, $context), ]; diff --git a/src/Normalizer/FileHandlerNormalizer.php b/src/Normalizer/UrlNormalizer.php similarity index 50% rename from src/Normalizer/FileHandlerNormalizer.php rename to src/Normalizer/UrlNormalizer.php index c4ec167..0e563f9 100644 --- a/src/Normalizer/FileHandlerNormalizer.php +++ b/src/Normalizer/UrlNormalizer.php @@ -4,39 +4,40 @@ namespace SpomkyLabs\PwaBundle\Normalizer; -use SpomkyLabs\PwaBundle\Dto\FileHandler; +use SpomkyLabs\PwaBundle\Dto\Url; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use function assert; use const FILTER_VALIDATE_URL; -final readonly class FileHandlerNormalizer implements NormalizerInterface +final class UrlNormalizer implements NormalizerInterface, NormalizerAwareInterface { + use NormalizerAwareTrait; + public function __construct( - private RouterInterface $router, + private readonly RouterInterface $router, #[Autowire('%spomky_labs_pwa.routes.reference_type%')] - private int $referenceType, + private readonly int $referenceType, ) { } - public function normalize(mixed $object, string $format = null, array $context = []): array + public function normalize(mixed $object, string $format = null, array $context = []): ?string { - assert($object instanceof FileHandler); - $url = $object->action; - if (! str_starts_with($url, '/') && ! filter_var($url, FILTER_VALIDATE_URL)) { - $url = $this->router->generate($url, $object->actionParameters, $this->referenceType); + assert($object instanceof Url); + + if (! str_starts_with($object->path, '/') && ! filter_var($object->path, FILTER_VALIDATE_URL)) { + return $this->router->generate($object->path, $object->params, $this->referenceType); } - return [ - 'action' => $url, - 'accept' => $object->accept, - ]; + return $object->path; } public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool { - return $data instanceof FileHandler; + return $data instanceof Url; } /** @@ -45,7 +46,7 @@ public function supportsNormalization(mixed $data, string $format = null, array public function getSupportedTypes(?string $format): array { return [ - FileHandler::class => true, + Url::class => true, ]; } } diff --git a/tests/Functional/ManifestFileDevServerTest.php b/tests/Functional/ManifestFileDevServerTest.php index 2479ebc..bdf492b 100644 --- a/tests/Functional/ManifestFileDevServerTest.php +++ b/tests/Functional/ManifestFileDevServerTest.php @@ -19,10 +19,11 @@ public static function aScreenshotIsCorrectlyTake(): void $client = static::createClient(); // When - $client->request('GET', '/site.manifest'); + $client->request('GET', '/site.webmanifest'); // Then static::assertResponseIsSuccessful(); static::assertResponseHeaderSame('Content-Type', 'application/manifest+json'); + dump($client->getResponse()->getContent()); } } diff --git a/tests/config.php b/tests/config.php index befd97e..3bd2f61 100644 --- a/tests/config.php +++ b/tests/config.php @@ -31,19 +31,27 @@ 'utf8' => true, 'resource' => '%kernel.project_dir%/tests/routes.php', ], + 'default_locale' => 'en', + 'translator' => [ + 'enabled' => true, + 'default_path' => '%kernel.project_dir%/tests/translations', + 'fallbacks' => ['en'], + ], ]); $container->extension('pwa', [ 'image_processor' => DummyImageProcessor::class, 'background_color' => 'red', - 'categories' => ['books', 'education', 'medical'], - 'description' => 'Awesome application that will help you achieve your dreams.', + 'categories' => ['pwa.categories.0', 'pwa.categories.1', 'pwa.categories.2'], + 'description' => 'pwa.description', 'display' => 'standalone', 'display_override' => ['fullscreen', 'minimal-ui'], 'file_handlers' => [ [ - 'action' => 'audio_file_handler', - 'action_params' => [ - 'param1' => 'audio', + 'action' => [ + 'path' => 'audio_file_handler', + 'params' => [ + 'param1' => 'audio', + ], ], 'accept' => [ 'audio/wav' => ['.wav'], @@ -76,7 +84,7 @@ 'sizes' => [0], ], ], - 'id' => '?homescreen=1', + 'id' => '/?homescreen=1', 'launch_handler' => [ 'client_mode' => ['focus-existing', 'auto'], ], @@ -84,8 +92,8 @@ 'prefer_related_applications' => true, 'dir' => 'rtl', 'lang' => 'ar', - 'name' => 'تطبيق رائع', - 'short_name' => 'رائع', + 'name' => 'pwa.name', + 'short_name' => 'pwa.short_name', 'protocol_handlers' => [ [ 'protocol' => 'web+jngl', @@ -111,15 +119,22 @@ 'url' => 'https://apps.microsoft.com/store/detail/example-app1/id123456789', ], ], - 'scope' => '/app/', - 'start_url' => 'https://example.com', + 'scope' => '/', + 'start_url' => 'pwa.start_url', 'theme_color' => 'red', - 'screenshots' => ['pwa/screenshots/360x800.svg'], + 'screenshots' => [ + [ + 'src' => 'pwa/screenshots/360x800.svg', + 'label' => 'pwa.screenshots.0', + ], + ], 'share_target' => [ - 'action' => 'shared_content_receiver', - 'action_params' => [ - 'param1' => 'value1', - 'param2' => 'value2', + 'action' => [ + 'path' => 'shared_content_receiver', + 'params' => [ + 'param1' => 'value1', + 'param2' => 'value2', + ], ], 'method' => 'GET', 'params' => [ @@ -131,9 +146,11 @@ 'shortcuts' => [ [ 'name' => "Today's agenda", - 'url' => 'agenda', - 'url_params' => [ - 'date' => 'today', + 'url' => [ + 'path' => 'agenda', + 'params' => [ + 'date' => 'today', + ], ], 'description' => 'List of events planned for today', ], @@ -174,7 +191,7 @@ 'description' => 'widget to control the PWAmp music player', 'tag' => 'pwamp', 'template' => 'pwamp-template', - 'ms_ac_template' => 'widgets/mini-player-template.json', + 'ms_ac_template' => '/widgets/mini-player-template.json', 'data' => 'widgets/mini-player-data.json', 'type' => 'application/json', 'screenshots' => [ diff --git a/tests/translations/messages.en.yaml b/tests/translations/messages.en.yaml new file mode 100644 index 0000000..3eda119 --- /dev/null +++ b/tests/translations/messages.en.yaml @@ -0,0 +1,11 @@ +pwa: + name: 'Weather App' + short_name: 'weather-app' + start_url: './' + description: 'A simple weather app built as a PWA with Symfony' + categories: + - 'utility' + - 'productivity' + - 'weather' + screenshots: + - 'This is a screenshot of the app' \ No newline at end of file