Skip to content

Commit

Permalink
Implement preload URL generation for caching strategies
Browse files Browse the repository at this point in the history
Revised the caching strategy to generate preload URLs dynamically. A new class, PreloadUrlsGeneratorManager, was added to manage all instances of URL generators. URL generation is now supported in the CacheStrategies service to generate additional or specific URLs for caches. An attribute 'PreloadUrl' was also implemented to attach directly to controllers or methods. Removed depreciated DummyController test.
  • Loading branch information
Spomky committed Apr 7, 2024
1 parent 27b1587 commit a2549f2
Show file tree
Hide file tree
Showing 21 changed files with 493 additions and 83 deletions.
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@
"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",
"phpstan/phpstan-phpunit": "^1.0",
"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",
Expand Down
27 changes: 21 additions & 6 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ parameters:
count: 2
path: src/CachingStrategy/FontCache.php

-
message: "#^Foreach overwrites \\$generator with its value variable\\.$#"
count: 1
path: src/CachingStrategy/PreloadUrlsGeneratorManager.php

-
message: "#^Only iterables can be unpacked, mixed given in argument \\#1\\.$#"
count: 1
Expand Down Expand Up @@ -231,12 +236,7 @@ parameters:
path: src/Dto/Manifest.php

-
message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\PageCache has an uninitialized property \\$matchCallback\\. Give it default value or assign it in the constructor\\.$#"
count: 1
path: src/Dto/PageCache.php

-
message: "#^PHPDoc tag @var for property SpomkyLabs\\\\PwaBundle\\\\Dto\\\\PageCache\\:\\:\\$cacheableResponseHeaders with type array\\<string, string\\>\\|null is not subtype of native type array\\.$#"
message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\PageCache extends @final class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\ResourceCache\\.$#"
count: 1
path: src/Dto/PageCache.php

Expand All @@ -260,6 +260,16 @@ parameters:
count: 1
path: src/Dto/RelatedApplication.php

-
message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\ResourceCache has an uninitialized property \\$matchCallback\\. Give it default value or assign it in the constructor\\.$#"
count: 1
path: src/Dto/ResourceCache.php

-
message: "#^PHPDoc tag @var for property SpomkyLabs\\\\PwaBundle\\\\Dto\\\\ResourceCache\\:\\:\\$cacheableResponseHeaders with type array\\<string, string\\>\\|null is not subtype of native type array\\.$#"
count: 1
path: src/Dto/ResourceCache.php

-
message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\ScopeExtension has an uninitialized property \\$origin\\. Give it default value or assign it in the constructor\\.$#"
count: 1
Expand Down Expand Up @@ -340,6 +350,11 @@ parameters:
count: 1
path: src/Dto/Url.php

-
message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Dto\\\\Url\\:\\:create\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
count: 1
path: src/Dto/Url.php

-
message: "#^Class SpomkyLabs\\\\PwaBundle\\\\Dto\\\\Widget has an uninitialized property \\$adaptativeCardTemplate\\. Give it default value or assign it in the constructor\\.$#"
count: 1
Expand Down
5 changes: 4 additions & 1 deletion rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions src/Attribute/PreloadUrl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Attribute;

use Attribute;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final readonly class PreloadUrl
{
/**
* @param array<string, mixed> $params
*/
public function __construct(
public string $alias,
public array $params = [],
public int $pathTypeReference = UrlGeneratorInterface::ABSOLUTE_PATH,
) {
}
}
131 changes: 131 additions & 0 deletions src/Attribute/PreloadUrlCompilerPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\Attribute;

use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
use RuntimeException;
use SpomkyLabs\PwaBundle\CachingStrategy\PreloadUrlsTagGenerator;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Routing\Attribute\Route;
use Throwable;
use function array_key_exists;
use function is_string;

final class PreloadUrlCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
foreach ($this->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<string, array{route: string, alias: string, params?: array<string, mixed>, 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<object> $reflectionClass
* @return iterable<array{alias: string, route: string, params: array<string, mixed>, 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<ReflectionMethod> $methods
* @return iterable<array{alias: string, route: string, params: array<string, mixed>, pathTypeReference: int}>
*/
private function findAllRoutesToPreload(PreloadUrl $preloadAttribute, array $methods): iterable
{
foreach ($methods as $method) {
yield from $this->findAllRoutesForMethod($preloadAttribute, $method);
}
}

/**
* @return iterable<array{alias: string, route: string, params: array<string, mixed>, 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;
}
}
}
}
17 changes: 17 additions & 0 deletions src/CachingStrategy/PreloadUrlsGeneratorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\CachingStrategy;

use SpomkyLabs\PwaBundle\Dto\Url;

interface PreloadUrlsGeneratorInterface
{
public function getAlias(): string;

/**
* @return iterable<Url|string>
*/
public function generateUrls(): iterable;
}
45 changes: 45 additions & 0 deletions src/CachingStrategy/PreloadUrlsGeneratorManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\CachingStrategy;

use InvalidArgumentException;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use function array_key_exists;

final class PreloadUrlsGeneratorManager
{
/**
* @var array<string, PreloadUrlsGeneratorInterface>
*/
private array $generators = [];

/**
* @param PreloadUrlsGeneratorInterface[] $generators
*/
public function __construct(
#[TaggedIterator('spomky_labs_pwa.preload_urls_generator')]
iterable $generators
) {
foreach ($generators as $generator) {
$this->add($generator);
}
}

public function add(PreloadUrlsGeneratorInterface $generator, PreloadUrlsGeneratorInterface ...$generators): void
{
$this->generators[$generator->getAlias()] = $generator;
foreach ($generators as $generator) {
$this->generators[$generator->getAlias()] = $generator;
}
}

public function get(string $alias): PreloadUrlsGeneratorInterface
{
if (! array_key_exists($alias, $this->generators)) {
throw new InvalidArgumentException(sprintf('The generator with alias "%s" does not exist.', $alias));
}
return $this->generators[$alias];
}
}
41 changes: 41 additions & 0 deletions src/CachingStrategy/PreloadUrlsTagGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace SpomkyLabs\PwaBundle\CachingStrategy;

use SpomkyLabs\PwaBundle\Dto\Url;

final readonly class PreloadUrlsTagGenerator implements PreloadUrlsGeneratorInterface
{
/**
* @var array<Url>
*/
private array $urls;

/**
* @param array{route: string, params: array<string, mixed>, pathTypeReference: int}[] $urls
*/
public function __construct(
private string $alias,
array $urls
) {
$this->urls = array_map(
static fn (array $url): Url => Url::create($url['route'], $url['params'], $url['pathTypeReference']),
$urls
);
}

public function getAlias(): string
{
return $this->alias;
}

/**
* @return iterable<Url>
*/
public function generateUrls(): iterable
{
return $this->urls;
}
}
Loading

0 comments on commit a2549f2

Please sign in to comment.