diff --git a/composer.json b/composer.json index b92a7f8..60322b3 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,7 @@ "dbrekelmans/bdi": "^1.1", "infection/infection": "^0.28", "phpstan/extension-installer": "^1.1", + "phpstan/phpdoc-parser": "^1.28", "phpstan/phpstan": "^1.0", "phpstan/phpstan-beberlei-assert": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0", @@ -54,6 +55,7 @@ "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^10.1", "rector/rector": "^1.0", + "staabm/phpstan-todo-by": "^0.1.25", "symfony/filesystem": "^6.4|^7.0", "symfony/framework-bundle": "^6.4|^7.0", "symfony/mime": "^6.4|^7.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7c77dde..82eed52 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -41,12 +41,17 @@ parameters: path: src/CachingStrategy/PreloadUrlsGeneratorManager.php - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\CachingStrategy\\\\ResourceCaches\\:\\:prepareMatchCallback\\(\\) is unused\\.$#" + message: "#^Only iterables can be unpacked, mixed given in argument \\#1\\.$#" count: 1 path: src/CachingStrategy/ResourceCaches.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" + message: "#^Parameter \\#1 \\$preloadUrl of method SpomkyLabs\\\\PwaBundle\\\\CachingStrategy\\\\WorkboxCacheStrategy\\:\\:withPreloadUrl\\(\\) expects string, mixed given\\.$#" + count: 1 + path: src/CachingStrategy/ResourceCaches.php + + - + message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, mixed given\\.$#" count: 1 path: src/CachingStrategy/ResourceCaches.php diff --git a/src/Attribute/PreloadUrl.php b/src/Attribute/PreloadUrl.php index 7402ba4..9fb68d0 100644 --- a/src/Attribute/PreloadUrl.php +++ b/src/Attribute/PreloadUrl.php @@ -5,12 +5,18 @@ namespace SpomkyLabs\PwaBundle\Attribute; use Attribute; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] final readonly class PreloadUrl { + /** + * @param array $params + */ public function __construct( public string $alias, + public array $params = [], + public int $pathTypeReference = UrlGeneratorInterface::ABSOLUTE_PATH, ) { } } diff --git a/src/Attribute/PreloadUrlCompilerPass.php b/src/Attribute/PreloadUrlCompilerPass.php new file mode 100644 index 0000000..327cb53 --- /dev/null +++ b/src/Attribute/PreloadUrlCompilerPass.php @@ -0,0 +1,131 @@ +findAllTaggedRoutes($container) as $alias => $urls) { + $definitionId = sprintf('spomky_labs_pwa.preload_urls_tag_generator.%s', $alias); + $definition = new ChildDefinition(PreloadUrlsTagGenerator::class); + $definition + ->setArguments([ + '$alias' => $alias, + '$urls' => $urls, + ]) + ->addTag('spomky_labs_pwa.preload_urls_generator') + ; + $container->setDefinition($definitionId, $definition); + } + } + + /** + * @return array, pathTypeReference: int}[]> + */ + private function findAllTaggedRoutes(ContainerBuilder $container): array + { + $routes = []; + $controllers = $container->findTaggedServiceIds('controller.service_arguments'); + foreach (array_keys($controllers) as $controller) { + if (! is_string($controller) || ! class_exists($controller)) { + continue; + } + $reflectionClass = new ReflectionClass($controller); + $result = $this->findAllPreloadAttributesForClass($reflectionClass); + foreach ($result as $route) { + if (! array_key_exists($route['alias'], $routes)) { + $routes[$route['alias']] = []; + } + $routes[$route['alias']][] = $route; + } + } + + return $routes; + } + + /** + * @param ReflectionClass $reflectionClass + * @return iterable, pathTypeReference: int}> + */ + private function findAllPreloadAttributesForClass(ReflectionClass $reflectionClass): iterable + { + foreach ($reflectionClass->getAttributes(PreloadUrl::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + try { + /** @var PreloadUrl $preloadAttribute */ + $preloadAttribute = $attribute->newInstance(); + yield from $this->findAllRoutesToPreload( + $preloadAttribute, + $reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) + ); + } catch (Throwable $e) { + throw new RuntimeException(sprintf('Unable to create attribute instance: %s', $e->getMessage()), 0, $e); + } + } + foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + foreach ($method->getAttributes(PreloadUrl::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + try { + /** @var PreloadUrl $preloadAttribute */ + $preloadAttribute = $attribute->newInstance(); + yield from $this->findAllRoutesForMethod($preloadAttribute, $method); + } catch (Throwable $e) { + throw new RuntimeException(sprintf( + 'Unable to create attribute instance: %s', + $e->getMessage() + ), 0, $e); + } + } + } + } + + /** + * @param array $methods + * @return iterable, pathTypeReference: int}> + */ + private function findAllRoutesToPreload(PreloadUrl $preloadAttribute, array $methods): iterable + { + foreach ($methods as $method) { + yield from $this->findAllRoutesForMethod($preloadAttribute, $method); + } + } + + /** + * @return iterable, pathTypeReference: int}> + */ + private function findAllRoutesForMethod(PreloadUrl $preloadAttribute, ReflectionMethod $method): iterable + { + foreach ($method->getAttributes(Route::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + try { + /** @var Route $routeAttribute */ + $routeAttribute = $attribute->newInstance(); + $routeName = $routeAttribute->getName(); + if ($routeName === null) { + continue; + } + yield [ + 'alias' => $preloadAttribute->alias, + 'route' => $routeName, + 'params' => $preloadAttribute->params, + 'pathTypeReference' => $preloadAttribute->pathTypeReference, + ]; + } catch (Throwable) { + continue; + } + } + } +} diff --git a/src/CachingStrategy/PreloadUrlsTagGenerator.php b/src/CachingStrategy/PreloadUrlsTagGenerator.php index 99b9ef4..9181ff8 100644 --- a/src/CachingStrategy/PreloadUrlsTagGenerator.php +++ b/src/CachingStrategy/PreloadUrlsTagGenerator.php @@ -9,17 +9,21 @@ final readonly class PreloadUrlsTagGenerator implements PreloadUrlsGeneratorInterface { /** - * @param iterable $urls + * @var array + */ + private array $urls; + + /** + * @param array{route: string, params: array, pathTypeReference: int}[] $urls */ public function __construct( private string $alias, - private array $urls + array $urls ) { - } - - public static function create(string $alias, iterable $urls): self - { - return new self($alias, $urls); + $this->urls = array_map( + static fn (array $url): Url => Url::create($url['route'], $url['params'], $url['pathTypeReference']), + $urls + ); } public function getAlias(): string @@ -32,6 +36,6 @@ public function getAlias(): string */ public function generateUrls(): iterable { - yield from $this->urls; + return $this->urls; } } diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index 88b3f56..4c0b81d 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -5,6 +5,7 @@ use Facebook\WebDriver\WebDriverDimension; use SpomkyLabs\PwaBundle\CachingStrategy\HasCacheStrategiesInterface; use SpomkyLabs\PwaBundle\CachingStrategy\PreloadUrlsGeneratorManager; +use SpomkyLabs\PwaBundle\CachingStrategy\PreloadUrlsTagGenerator; use SpomkyLabs\PwaBundle\Command\CreateIconsCommand; use SpomkyLabs\PwaBundle\Command\CreateScreenshotCommand; use SpomkyLabs\PwaBundle\Command\ListCacheStrategiesCommand; @@ -27,6 +28,7 @@ use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Panther\Client; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg; use function Symfony\Component\DependencyInjection\Loader\Configurator\param; use function Symfony\Component\DependencyInjection\Loader\Configurator\service; @@ -131,4 +133,11 @@ $container->instanceof(UrlGeneratorInterface::class) ->tag('spomky_labs_pwa.preload_urls_generator') ; + $container->set(PreloadUrlsTagGenerator::class) + ->abstract() + ->args([ + '$alias' => abstract_arg('alias'), + '$urls' => abstract_arg('urls'), + ]) + ; }; diff --git a/src/SpomkyLabsPwaBundle.php b/src/SpomkyLabsPwaBundle.php index d922beb..77e87f4 100644 --- a/src/SpomkyLabsPwaBundle.php +++ b/src/SpomkyLabsPwaBundle.php @@ -4,24 +4,14 @@ 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\Attribute\PreloadUrlCompilerPass; 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 { @@ -32,6 +22,11 @@ public function configure(DefinitionConfigurator $definition): void $definition->import('Resources/config/definition/*.php'); } + public function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new PreloadUrlCompilerPass()); + } + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { $container->import('Resources/config/services.php'); @@ -61,18 +56,6 @@ 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 @@ -90,91 +73,4 @@ 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/config.php b/tests/config.php index 2b498cc..38c09c6 100644 --- a/tests/config.php +++ b/tests/config.php @@ -9,18 +9,16 @@ use SpomkyLabs\PwaBundle\Tests\TestFilesystem; return static function (ContainerConfigurator $container) { - $container->services() - ->set(DummyImageProcessor::class); - $container->services() - ->set('asset_mapper.local_public_assets_filesystem', TestFilesystem::class) + $services = $container + ->services() + ->defaults() + ->autowire() ->autoconfigure() - ->autowire(); - - $container->services() - ->load('SpomkyLabs\\PwaBundle\\Tests\\Controller\\', __DIR__ . '/Controller/') -// ->tag('controller.service_arguments') ; - $container->services() + $services->set(DummyImageProcessor::class); + $services->set('asset_mapper.local_public_assets_filesystem', TestFilesystem::class); + $services->load('SpomkyLabs\\PwaBundle\\Tests\\Controller\\', __DIR__ . '/Controller/'); + $services ->set(DummyUrlsGenerator::class) ->tag('spomky_labs_pwa.preload_urls_generator') ; @@ -237,7 +235,7 @@ 'strategy' => 'StaleWhileRevalidate', 'cache_name' => 'page-cache', 'broadcast' => true, - 'preload_urls' => ['privacy_policy', 'terms_of_service'/*, '@static-pages', '@widgets'*/], + 'preload_urls' => ['privacy_policy', 'terms_of_service', '@static-pages', '@widgets'], ], ], 'offline_fallback' => [