From bb9053856a3159c4fbfc8e99c40d87d9edd72a78 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sat, 6 Apr 2024 22:44:17 +0200 Subject: [PATCH] Implement attribute-based preload URL generation The logic for preload URL generation is updated to utilise newly implemented attributes. Updated the SpomkyLabsPwaBundle class to scan methods and classes with a `PreloadUrl` attribute and generate appropriate `PreloadUrlsTagGenerator` services accordingly. This simplifies URL creation and reduces the need for manual configuration. Also, smaller code adjustments and clean-ups are conducted to improve code readability and performance. --- rector.php | 5 +- src/Attribute/PreloadUrl.php | 16 +++ .../PreloadUrlsGeneratorManager.php | 4 +- .../PreloadUrlsTagGenerator.php | 37 ++++++ src/CachingStrategy/ResourceCaches.php | 1 - src/SpomkyLabsPwaBundle.php | 110 ++++++++++++++++++ ...ontroller.php => OtherPagesController.php} | 22 +--- tests/Controller/StaticPagesController.php | 26 +++++ tests/Controller/WidgetController.php | 27 +++++ tests/config.php | 4 +- 10 files changed, 226 insertions(+), 26 deletions(-) create mode 100644 src/Attribute/PreloadUrl.php create mode 100644 src/CachingStrategy/PreloadUrlsTagGenerator.php rename tests/Controller/{DummyController.php => OtherPagesController.php} (54%) create mode 100644 tests/Controller/StaticPagesController.php create mode 100644 tests/Controller/WidgetController.php diff --git a/rector.php b/rector.php index fe8046d..14591a0 100644 --- a/rector.php +++ b/rector.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector; use Rector\Doctrine\Set\DoctrineSetList; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Set\ValueObject\LevelSetList; @@ -26,7 +27,9 @@ ]); $config->phpVersion(PhpVersion::PHP_82); $config->paths([__DIR__ . '/src', __DIR__ . '/tests', __DIR__ . '/ecs.php', __DIR__ . '/rector.php']); - $config->skip([__DIR__ . '/tests/Controller/DummyController.php']); + $config->skip([ + RemoveEmptyClassMethodRector::class => [__DIR__ . '/tests/Controller/'], + ]); $config->parallel(); $config->importNames(); $config->importShortClasses(); diff --git a/src/Attribute/PreloadUrl.php b/src/Attribute/PreloadUrl.php new file mode 100644 index 0000000..7402ba4 --- /dev/null +++ b/src/Attribute/PreloadUrl.php @@ -0,0 +1,16 @@ +add(...$generators); + foreach ($generators as $generator) { + $this->add($generator); + } } public function add(PreloadUrlsGeneratorInterface $generator, PreloadUrlsGeneratorInterface ...$generators): void diff --git a/src/CachingStrategy/PreloadUrlsTagGenerator.php b/src/CachingStrategy/PreloadUrlsTagGenerator.php new file mode 100644 index 0000000..99b9ef4 --- /dev/null +++ b/src/CachingStrategy/PreloadUrlsTagGenerator.php @@ -0,0 +1,37 @@ + $urls + */ + public function __construct( + private string $alias, + private array $urls + ) { + } + + public static function create(string $alias, iterable $urls): self + { + return new self($alias, $urls); + } + + public function getAlias(): string + { + return $this->alias; + } + + /** + * @return iterable + */ + public function generateUrls(): iterable + { + yield from $this->urls; + } +} diff --git a/src/CachingStrategy/ResourceCaches.php b/src/CachingStrategy/ResourceCaches.php index 69c27fe..15cb0f5 100644 --- a/src/CachingStrategy/ResourceCaches.php +++ b/src/CachingStrategy/ResourceCaches.php @@ -56,7 +56,6 @@ public function getCacheStrategies(): array JsonEncode::OPTIONS => $this->jsonOptions, ]); $urls = json_decode($routes, true, 512, JSON_THROW_ON_ERROR); - dd($urls); $cacheName = $resourceCache->cacheName ?? sprintf('page-cache-%d', $id); $plugins = [ diff --git a/src/SpomkyLabsPwaBundle.php b/src/SpomkyLabsPwaBundle.php index f698c7f..d922beb 100644 --- a/src/SpomkyLabsPwaBundle.php +++ b/src/SpomkyLabsPwaBundle.php @@ -4,13 +4,24 @@ namespace SpomkyLabs\PwaBundle; +use ReflectionClass; +use ReflectionMethod; +use RuntimeException; +use SpomkyLabs\PwaBundle\Attribute\PreloadUrl; +use SpomkyLabs\PwaBundle\CachingStrategy\PreloadUrlsTagGenerator; +use SpomkyLabs\PwaBundle\Dto\Url; use SpomkyLabs\PwaBundle\ImageProcessor\ImageProcessorInterface; use SpomkyLabs\PwaBundle\Subscriber\PwaDevServerSubscriber; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; +use Symfony\Component\Routing\Attribute\Route; +use Throwable; +use function array_key_exists; use function in_array; +use function is_string; final class SpomkyLabsPwaBundle extends AbstractBundle { @@ -50,6 +61,18 @@ public function loadExtension(array $config, ContainerConfigurator $container, C if (! in_array($builder->getParameter('kernel.environment'), ['dev', 'test'], true)) { $builder->removeDefinition(PwaDevServerSubscriber::class); } + foreach ($this->findAllTaggedRoutes($builder) as $alias => $routeNames) { + $urls = array_map(static fn (string $routeName): Url => Url::create($routeName), $routeNames); + $definition = new Definition(PreloadUrlsTagGenerator::class); + $definition + ->setArguments([ + '$alias' => $alias, + '$urls' => $urls, + ]) + ->setPublic(true) + ; + $builder->setDefinition(sprintf('spomky_labs_pwa.preload_urls_tag_generator.%s', $alias), $definition); + } } public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void @@ -67,4 +90,91 @@ private function setAssetMapperPath(ContainerBuilder $builder): void ], ]); } + + /** + * @return array> + */ + private function findAllTaggedRoutes(ContainerBuilder $container): array + { + $routes = []; + $controllers = $container->findTaggedServiceIds('controller.service_arguments'); + foreach (array_keys($controllers) as $controller) { + if (! is_string($controller)) { + continue; + } + $reflectionClass = new ReflectionClass($controller); + if (! class_exists($controller)) { + continue; + } + $result = $this->findAllPreloadAttributesForClass($reflectionClass); + foreach ($result as $route) { + if (! array_key_exists($route['alias'], $routes)) { + $routes[$route['alias']] = []; + } + $routes[$route['alias']][] = $route['route']; + } + } + + return $routes; + } + + /** + * @return iterable{route: string, alias: string} + */ + private function findAllPreloadAttributesForClass(ReflectionClass $reflectionClass): iterable + { + foreach ($reflectionClass->getAttributes(PreloadUrl::class) as $attribute) { + try { + /** @var PreloadUrl $preloadAttribute */ + $preloadAttribute = $attribute->newInstance(); + yield from $this->findAllRoutesToPreload($preloadAttribute->alias, $reflectionClass->getMethods()); + } catch (Throwable $e) { + throw new RuntimeException(sprintf('Unable to create attribute instance: %s', $e->getMessage()), 0, $e); + } + } + foreach ($reflectionClass->getMethods() as $method) { + foreach ($method->getAttributes(PreloadUrl::class) as $attribute) { + try { + /** @var PreloadUrl $preloadAttribute */ + $preloadAttribute = $attribute->newInstance(); + yield from $this->findAllRoutesForMethod($preloadAttribute->alias, $method); + } catch (Throwable $e) { + throw new RuntimeException(sprintf( + 'Unable to create attribute instance: %s', + $e->getMessage() + ), 0, $e); + } + } + } + } + + /** + * @param array $methods + * @return iterable{route: string, alias: string} + */ + private function findAllRoutesToPreload(string $alias, array $methods): iterable + { + foreach ($methods as $method) { + yield from $this->findAllRoutesForMethod($alias, $method); + } + } + + /** + * @return iterable{route: string, alias: string} + */ + private function findAllRoutesForMethod(string $alias, ReflectionMethod $method): iterable + { + foreach ($method->getAttributes(Route::class) as $attribute) { + try { + /** @var Route $routeAttribute */ + $routeAttribute = $attribute->newInstance(); + yield [ + 'route' => $routeAttribute->getName(), + 'alias' => $alias, + ]; + } catch (Throwable) { + continue; + } + } + } } diff --git a/tests/Controller/DummyController.php b/tests/Controller/OtherPagesController.php similarity index 54% rename from tests/Controller/DummyController.php rename to tests/Controller/OtherPagesController.php index 032af58..4fd4883 100644 --- a/tests/Controller/DummyController.php +++ b/tests/Controller/OtherPagesController.php @@ -10,18 +10,8 @@ /** * @internal */ -final class DummyController extends AbstractController +final class OtherPagesController extends AbstractController { - #[Route('/privacy-policy', name: 'privacy_policy')] - public function privacyPolicy(string $param1): void - { - } - - #[Route('/terms-of-service', name: 'terms_of_service')] - public function tos(string $param1): void - { - } - #[Route('/audio-file-handler/{param1}', name: 'audio_file_handler')] public function dummy1(string $param1): void { @@ -36,14 +26,4 @@ public function dummy2(string $param1, string $param2): void public function agenda(string $date): void { } - - #[Route('/widget/template', name: 'app_widget_template')] - public function widgetTemplate(): void - { - } - - #[Route('/widget/data', name: 'app_widget_data')] - public function widgetData(): void - { - } } diff --git a/tests/Controller/StaticPagesController.php b/tests/Controller/StaticPagesController.php new file mode 100644 index 0000000..44a7ba5 --- /dev/null +++ b/tests/Controller/StaticPagesController.php @@ -0,0 +1,26 @@ +services() ->load('SpomkyLabs\\PwaBundle\\Tests\\Controller\\', __DIR__ . '/Controller/') - ->tag('controller.service_arguments') +// ->tag('controller.service_arguments') ; $container->services() ->set(DummyUrlsGenerator::class) @@ -237,7 +237,7 @@ 'strategy' => 'StaleWhileRevalidate', 'cache_name' => 'page-cache', 'broadcast' => true, - 'preload_urls' => ['privacy_policy', 'terms_of_service', '@dummy'], + 'preload_urls' => ['privacy_policy', 'terms_of_service'/*, '@static-pages', '@widgets'*/], ], ], 'offline_fallback' => [