Skip to content

Commit

Permalink
SW/Workbox Rules as services
Browse files Browse the repository at this point in the history
  • Loading branch information
Spomky committed Mar 8, 2024
1 parent f04f6ad commit e7e320a
Show file tree
Hide file tree
Showing 18 changed files with 778 additions and 445 deletions.
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -655,11 +655,6 @@ parameters:
count: 1
path: src/Resources/config/definition/web_client.php

-
message: "#^Property SpomkyLabs\\\\PwaBundle\\\\Service\\\\ServiceWorkerCompiler\\:\\:\\$jsonOptions type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Service/ServiceWorkerCompiler.php

-
message: "#^Method SpomkyLabs\\\\PwaBundle\\\\SpomkyLabsPwaBundle\\:\\:loadExtension\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
count: 1
Expand Down
11 changes: 11 additions & 0 deletions src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use SpomkyLabs\PwaBundle\ImageProcessor\GDImageProcessor;
use SpomkyLabs\PwaBundle\ImageProcessor\ImagickImageProcessor;
use SpomkyLabs\PwaBundle\Service\ManifestBuilder;
use SpomkyLabs\PwaBundle\Service\Rule\ServiceWorkerRule;
use SpomkyLabs\PwaBundle\Service\Rule\WorkboxRule;
use SpomkyLabs\PwaBundle\Service\ServiceWorkerBuilder;
use SpomkyLabs\PwaBundle\Service\ServiceWorkerCompiler;
use SpomkyLabs\PwaBundle\Subscriber\ManifestCompileEventListener;
Expand Down Expand Up @@ -103,4 +105,13 @@
$container->set(PwaRuntime::class)
->tag('twig.runtime')
;

/*** Service Worker Compiler Rules ***/
$container->instanceof(ServiceWorkerRule::class)
->tag('spomky_labs_pwa.service_worker_rule')
;
$container->instanceof(WorkboxRule::class)
->tag('spomky_labs_pwa.workbox_rule')
;
$container->load('SpomkyLabs\\PwaBundle\\Service\\Rule\\', '../../Service/Rule/*');
};
94 changes: 94 additions & 0 deletions src/Service/Rule/AssetCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Service\Rule;

use SpomkyLabs\PwaBundle\Dto\Workbox;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use function count;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
use const PHP_EOL;

final readonly class AssetCache implements WorkboxRule
{
/**
* @var array<string, mixed>
*/
private array $jsonOptions;

private string $assetPublicPrefix;

public function __construct(
#[Autowire(service: 'asset_mapper.public_assets_path_resolver')]
PublicAssetsPathResolverInterface $publicAssetsPathResolver,
private AssetMapperInterface $assetMapper,
private SerializerInterface $serializer,
#[Autowire('%kernel.debug%')]
bool $debug,
) {
$this->assetPublicPrefix = rtrim($publicAssetsPathResolver->resolvePublicPath(''), '/');
$options = [
AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => true,
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
JsonEncode::OPTIONS => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
];
if ($debug === true) {
$options[JsonEncode::OPTIONS] |= JSON_PRETTY_PRINT;
}
$this->jsonOptions = $options;
}

public function process(Workbox $workbox, string $body): string
{
if ($workbox->assetCache->enabled === false) {
return $body;
}
$assets = [];
foreach ($this->assetMapper->allAssets() as $asset) {
if (preg_match($workbox->assetCache->regex, $asset->sourcePath) === 1) {
$assets[] = $asset->publicPath;
}
}
$assetUrls = $this->serializer->serialize($assets, 'json', $this->jsonOptions);
$assetUrlsLength = count($assets) * 2;

$declaration = <<<ASSET_CACHE_RULE_STRATEGY
const assetCacheStrategy = new workbox.strategies.CacheFirst({
cacheName: '{$workbox->assetCache->cacheName}',
plugins: [
new workbox.cacheableResponse.CacheableResponsePlugin({statuses: [0, 200]}),
new workbox.expiration.ExpirationPlugin({
maxEntries: {$assetUrlsLength},
maxAgeSeconds: 365 * 24 * 60 * 60,
}),
],
});
workbox.routing.registerRoute(
({url}) => url.pathname.startsWith('{$this->assetPublicPrefix}'),
assetCacheStrategy
);
self.addEventListener('install', event => {
const done = {$assetUrls}.map(
path =>
assetCacheStrategy.handleAll({
event,
request: new Request(path),
})[1]
);
event.waitUntil(Promise.all(done));
});
ASSET_CACHE_RULE_STRATEGY;

return $body . PHP_EOL . PHP_EOL . trim($declaration);
}
}
53 changes: 53 additions & 0 deletions src/Service/Rule/BackgroundSync.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Service\Rule;

use SpomkyLabs\PwaBundle\Dto\Workbox;
use const PHP_EOL;

final readonly class BackgroundSync implements WorkboxRule
{
public function process(Workbox $workbox, string $body): string
{
if ($workbox->backgroundSync === []) {
return $body;
}

$declaration = '';
foreach ($workbox->backgroundSync as $sync) {
$forceSyncFallback = $sync->forceSyncFallback === true ? 'true' : 'false';
$broadcastChannel = '';
if ($sync->broadcastChannel !== null) {
$broadcastChannel = <<<BROADCAST_CHANNEL
,
"onSync": async ({queue}) => {
try {
await queue.replayRequests();
} catch (error) {
// Failed to replay one or more requests
} finally {
remainingRequests = await queue.getAll();
const bc = new BroadcastChannel('{$sync->broadcastChannel}');
bc.postMessage({name: '{$sync->queueName}', remaining: remainingRequests.length});
bc.close();
}
}
BROADCAST_CHANNEL;
}
$declaration .= <<<BACKGROUND_SYNC_RULE_STRATEGY
workbox.routing.registerRoute(
new RegExp('{$sync->regex}'),
new workbox.strategies.NetworkOnly({plugins: [new workbox.backgroundSync.BackgroundSyncPlugin('{$sync->queueName}',{
"maxRetentionTime": {$sync->maxRetentionTime},
"forceSyncFallback": {$forceSyncFallback}{$broadcastChannel}
})] }),
'{$sync->method}'
);
BACKGROUND_SYNC_RULE_STRATEGY;
}

return $body . PHP_EOL . PHP_EOL . trim($declaration);
}
}
33 changes: 33 additions & 0 deletions src/Service/Rule/ClearCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Service\Rule;

use SpomkyLabs\PwaBundle\Dto\Workbox;
use const PHP_EOL;

final readonly class ClearCache implements WorkboxRule
{
public function process(Workbox $workbox, string $body): string
{
if ($workbox->clearCache === false) {
return $body;
}

$declaration = <<<CLEAR_CACHE
self.addEventListener("install", function (event) {
event.waitUntil(caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
return caches.delete(cacheName);
})
);
})
);
});
CLEAR_CACHE;

return $body . PHP_EOL . PHP_EOL . trim($declaration);
}
}
88 changes: 88 additions & 0 deletions src/Service/Rule/FontCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Service\Rule;

use SpomkyLabs\PwaBundle\Dto\Workbox;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
use const PHP_EOL;

final readonly class FontCache implements WorkboxRule
{
/**
* @var array<string, mixed>
*/
private array $jsonOptions;

public function __construct(
private AssetMapperInterface $assetMapper,
private SerializerInterface $serializer,
#[Autowire('%kernel.debug%')]
bool $debug,
) {
$options = [
AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => true,
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
JsonEncode::OPTIONS => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
];
if ($debug === true) {
$options[JsonEncode::OPTIONS] |= JSON_PRETTY_PRINT;
}
$this->jsonOptions = $options;
}

public function process(Workbox $workbox, string $body): string
{
if ($workbox->fontCache->enabled === false) {
return $body;
}
$fonts = [];
foreach ($this->assetMapper->allAssets() as $asset) {
if (preg_match($workbox->fontCache->regex, $asset->sourcePath) === 1) {
$fonts[] = $asset->publicPath;
}
}
$fontUrls = $this->serializer->serialize($fonts, 'json', $this->jsonOptions);

$declaration = <<<FONT_CACHE_RULE_STRATEGY
const fontCacheStrategy = new workbox.strategies.CacheFirst({
cacheName: '{$workbox->fontCache->cacheName}',
plugins: [
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [0, 200],
}),
new workbox.expiration.ExpirationPlugin({
maxAgeSeconds: {$workbox->fontCache->maxAge},
maxEntries: {$workbox->fontCache->maxEntries},
}),
],
});
workbox.routing.registerRoute(
({request}) => request.destination === 'font',
fontCacheStrategy
);
self.addEventListener('install', event => {
const done = {$fontUrls}.map(
path =>
fontCacheStrategy.handleAll({
event,
request: new Request(path),
})[1]
);
event.waitUntil(Promise.all(done));
});
FONT_CACHE_RULE_STRATEGY;

return $body . PHP_EOL . PHP_EOL . trim($declaration);
}
}
61 changes: 61 additions & 0 deletions src/Service/Rule/GoogleFontCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Service\Rule;

use SpomkyLabs\PwaBundle\Dto\Workbox;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use function count;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
use const PHP_EOL;

final readonly class GoogleFontCache implements WorkboxRule
{
/**
* @var array<string, mixed>
*/
private array $jsonOptions;

public function __construct(
private SerializerInterface $serializer,
#[Autowire('%kernel.debug%')]
bool $debug,
) {
$options = [
AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => true,
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
JsonEncode::OPTIONS => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
];
if ($debug === true) {
$options[JsonEncode::OPTIONS] |= JSON_PRETTY_PRINT;
}
$this->jsonOptions = $options;
}

public function process(Workbox $workbox, string $body): string
{
if ($workbox->googleFontCache->enabled === false) {
return $body;
}
$options = [
'cachePrefix' => $workbox->googleFontCache->cachePrefix,
'maxAge' => $workbox->googleFontCache->maxAge,
'maxEntries' => $workbox->googleFontCache->maxEntries,
];
$options = array_filter($options, static fn (mixed $v): bool => ($v !== null && $v !== ''));
$options = count($options) === 0 ? '' : $this->serializer->serialize($options, 'json', $this->jsonOptions);

$declaration = <<<IMAGE_CACHE_RULE_STRATEGY
workbox.recipes.googleFontsCache({$options});
IMAGE_CACHE_RULE_STRATEGY;

return $body . PHP_EOL . PHP_EOL . trim($declaration);
}
}
Loading

0 comments on commit e7e320a

Please sign in to comment.