diff --git a/CHANGELOG.md b/CHANGELOG.md index a98d3ffd..a3eea964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ This file contains a complete enumeration of all [pull requests](https://github. for a given releases. Unreleased, upcoming changes will be updated here periodically; reference the next release on our [milestones](https://github.com/liip/LiipImagineBundle/milestones) page for the latest changes. +## [2.12.0](https://github.com/liip/LiipImagineBundle/tree/2.12.0) + +- Support JsonManifestVersionStrategy that was added in Symfony 6. + ## [2.11.0](https://github.com/liip/LiipImagineBundle/tree/2.11.0) - Compatibility with Symfony 6.3 (We do not expect users to extend a compiler passes or the DI extension of this bundle. If you did, you might need to adjust return types) ([mbabker](https://github.com/liip/LiipImagineBundle/pull/1514)) diff --git a/DependencyInjection/Compiler/AssetsVersionCompilerPass.php b/DependencyInjection/Compiler/AssetsVersionCompilerPass.php index fba063ec..afd34dc3 100644 --- a/DependencyInjection/Compiler/AssetsVersionCompilerPass.php +++ b/DependencyInjection/Compiler/AssetsVersionCompilerPass.php @@ -11,6 +11,7 @@ namespace Liip\ImagineBundle\DependencyInjection\Compiler; +use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -18,14 +19,16 @@ * Inject the Symfony framework assets version parameter to the * LiipImagineBundle twig extension if possible. * - * We extract the version parameter from the StaticVersionStrategy service - * definition. If anything is not as expected, we log a warning and do nothing. + * We extract either: + * - the version parameter from the StaticVersionStrategy service + * - the json manifest from the JsonManifestVersionStrategy service + * If anything is not as expected, we log a warning and do nothing. * * The expectation is for the user to configure the assets version in liip * imagine for custom setups. * - * Anything other than StaticVersionStrategy needs to be implemented by the - * user in CacheResolveEvent event listeners. + * Anything other than StaticVersionStrategy or JsonManifestVersionStrategy needs + * to be implemented by the user in CacheResolveEvent event listeners. */ class AssetsVersionCompilerPass extends AbstractCompilerPass { @@ -46,7 +49,7 @@ public function process(ContainerBuilder $container): void } $versionStrategyDefinition = $container->findDefinition('assets._version__default'); - if (!is_a($versionStrategyDefinition->getClass(), StaticVersionStrategy::class, true)) { + if (!is_a($versionStrategyDefinition->getClass(), StaticVersionStrategy::class, true) && !is_a($versionStrategyDefinition->getClass(), JsonManifestVersionStrategy::class, true)) { $this->log($container, 'Symfony assets versioning strategy "'.$versionStrategyDefinition->getClass().'" not automatically supported. Configure liip_imagine.twig.assets_version if you have problems with assets versioning'); return; @@ -61,6 +64,24 @@ public function process(ContainerBuilder $container): void } $runtimeDefinition->setArgument(1, $version); + + if (is_a($versionStrategyDefinition->getClass(), JsonManifestVersionStrategy::class, true)) { + $jsonManifestString = file_get_contents($version); + + if (!\is_string($jsonManifestString)) { + $this->log($container, 'Can not handle assets versioning with "'.$versionStrategyDefinition->getClass().'". The manifest file at "'.$version.' " could not be read'); + + return; + } + $jsonManifest = json_decode($jsonManifestString, true); + if (!\is_array($jsonManifest)) { + $this->log($container, 'Can not handle assets versioning with "'.$versionStrategyDefinition->getClass().'". The manifest file at "'.$version.' " does not contain valid JSON'); + + return; + } + $runtimeDefinition->setArgument(1, null); + $runtimeDefinition->setArgument(2, $jsonManifest); + } } /** diff --git a/Resources/config/imagine_twig_mode_lazy.xml b/Resources/config/imagine_twig_mode_lazy.xml index 9e4a9f1b..ac9ec6f1 100644 --- a/Resources/config/imagine_twig_mode_lazy.xml +++ b/Resources/config/imagine_twig_mode_lazy.xml @@ -14,6 +14,7 @@ null + null diff --git a/Templating/LazyFilterRuntime.php b/Templating/LazyFilterRuntime.php index 59a300e5..cc41e414 100644 --- a/Templating/LazyFilterRuntime.php +++ b/Templating/LazyFilterRuntime.php @@ -29,10 +29,22 @@ final class LazyFilterRuntime implements RuntimeExtensionInterface */ private $assetVersion; - public function __construct(CacheManager $cache, string $assetVersion = null) + /** + * @var array|null + */ + private $jsonManifest; + + /** + * @var array|null + */ + private $jsonManifestLookup; + + public function __construct(CacheManager $cache, string $assetVersion = null, array $jsonManifest = null) { $this->cache = $cache; $this->assetVersion = $assetVersion; + $this->jsonManifest = $jsonManifest; + $this->jsonManifestLookup = $jsonManifest ? array_flip($jsonManifest) : null; } /** @@ -41,9 +53,9 @@ public function __construct(CacheManager $cache, string $assetVersion = null) public function filter(string $path, string $filter, array $config = [], string $resolver = null, int $referenceType = UrlGeneratorInterface::ABSOLUTE_URL): string { $path = $this->cleanPath($path); - $path = $this->cache->getBrowserPath($path, $filter, $config, $resolver, $referenceType); + $resolvedPath = $this->cache->getBrowserPath($path, $filter, $config, $resolver, $referenceType); - return $this->appendAssetVersion($path); + return $this->appendAssetVersion($resolvedPath, $path); } /** @@ -57,33 +69,52 @@ public function filterCache(string $path, string $filter, array $config = [], st if (\count($config)) { $path = $this->cache->getRuntimePath($path, $config); } - $path = $this->cache->resolve($path, $filter, $resolver); + $resolvedPath = $this->cache->resolve($path, $filter, $resolver); - return $this->appendAssetVersion($path); + return $this->appendAssetVersion($resolvedPath, $path); } private function cleanPath(string $path): string { - if (!$this->assetVersion) { + if (!$this->assetVersion && !$this->jsonManifest) { return $path; } - $start = mb_strrpos($path, $this->assetVersion); - if (mb_strlen($path) - mb_strlen($this->assetVersion) === $start) { - return rtrim(mb_substr($path, 0, $start), '?'); + if ($this->assetVersion) { + $start = mb_strrpos($path, $this->assetVersion); + if (mb_strlen($path) - mb_strlen($this->assetVersion) === $start) { + return rtrim(mb_substr($path, 0, $start), '?'); + } + } + + if ($this->jsonManifest) { + if (\array_key_exists($path, $this->jsonManifestLookup)) { + return $this->jsonManifestLookup[$path]; + } } return $path; } - private function appendAssetVersion(string $path): string + private function appendAssetVersion(string $resolvedPath, string $path): string { - if (!$this->assetVersion) { - return $path; + if (!$this->assetVersion && !$this->jsonManifest) { + return $resolvedPath; } - $separator = false !== mb_strpos($path, '?') ? '&' : '?'; + if ($this->assetVersion) { + $separator = false !== mb_strpos($resolvedPath, '?') ? '&' : '?'; + + return $resolvedPath.$separator.$this->assetVersion; + } + + if (\array_key_exists($path, $this->jsonManifest)) { + $prefixedSlash = '/' !== mb_substr($path, 0, 1) && '/' === mb_substr($this->jsonManifest[$path], 0, 1); + $versionedPath = $prefixedSlash ? mb_substr($this->jsonManifest[$path], 1) : $this->jsonManifest[$path]; + + $resolvedPath = str_replace($path, $versionedPath, $resolvedPath); + } - return $path.$separator.$this->assetVersion; + return $resolvedPath; } } diff --git a/Tests/Templating/LazyFilterRuntimeTest.php b/Tests/Templating/LazyFilterRuntimeTest.php index 0167feb4..6e55b9ef 100644 --- a/Tests/Templating/LazyFilterRuntimeTest.php +++ b/Tests/Templating/LazyFilterRuntimeTest.php @@ -23,6 +23,12 @@ class LazyFilterRuntimeTest extends AbstractTest { private const FILTER = 'thumbnail'; private const VERSION = 'v2'; + private const JSON_MANIFEST = [ + 'image/cats.png' => '/image/cats.png?v=bc321bd12a', + 'image/dogs.png' => '/image/dogs.ac38d2a1bc.png', + '/image/cows.png' => '/image/cows.png?v=a5de32a2c4', + '/image/sheep.png' => '/image/sheep.7ca26b36af.png', + ]; /** * @var LazyFilterRuntime @@ -101,6 +107,37 @@ public function testDifferentVersion(): void $this->assertSame($cachePath.'?'.self::VERSION, $actualPath); } + /** + * @dataProvider provideJsonManifest + */ + public function testJsonManifestVersionHandling(string $sourcePath, string $versionedPath): void + { + $this->runtime = new LazyFilterRuntime($this->manager, null, self::JSON_MANIFEST); + + $cachePath = 'image/cache/'.self::FILTER.'/'.('/' === (mb_substr($sourcePath, 0, 1)) ? mb_substr($sourcePath, 1) : $sourcePath); + $expectedPath = 'image/cache/'.self::FILTER.'/'.('/' === (mb_substr($versionedPath, 0, 1)) ? mb_substr($versionedPath, 1) : $versionedPath); + + $this->manager + ->expects($this->once()) + ->method('getBrowserPath') + ->with($sourcePath, self::FILTER) + ->willReturn($cachePath); + + $actualPath = $this->runtime->filter($versionedPath, self::FILTER); + + $this->assertSame($expectedPath, $actualPath); + } + + public function provideJsonManifest(): array + { + return [ + 'query parameter, no slash' => [array_keys(self::JSON_MANIFEST)[0], array_values(self::JSON_MANIFEST)[0]], + 'in filename, no slash' => [array_keys(self::JSON_MANIFEST)[1], array_values(self::JSON_MANIFEST)[1]], + 'query parameter, slash' => [array_keys(self::JSON_MANIFEST)[2], array_values(self::JSON_MANIFEST)[2]], + 'in filename, slash' => [array_keys(self::JSON_MANIFEST)[3], array_values(self::JSON_MANIFEST)[3]], + ]; + } + public function testInvokeFilterCacheMethod(): void { $expectedInputPath = 'thePathToTheImage'; diff --git a/composer.json b/composer.json index e66af07e..91923f8e 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,7 @@ "phpstan/phpstan": "^1.10.0", "psr/cache": "^1.0|^2.0|^3.0", "psr/log": "^1.0", + "symfony/asset": "^3.4|^4.4|^5.3|^6.0", "symfony/browser-kit": "^3.4|^4.4|^5.3|^6.0", "symfony/cache": "^3.4|^4.4|^5.3|^6.0", "symfony/console": "^3.4|^4.4|^5.3|^6.0", @@ -53,6 +54,7 @@ }, "suggest": { "ext-exif": "required to read EXIF metadata from images", + "ext-json": "required to read JSON manifest versioning", "ext-gd": "required to use gd driver", "ext-gmagick": "required to use gmagick driver", "ext-imagick": "required to use imagick driver", @@ -65,6 +67,7 @@ "league/flysystem": "required to use FlySystem data loader or cache resolver", "monolog/monolog": "A psr/log compatible logger is required to enable logging", "rokka/imagine-vips": "required to use 'vips' driver", + "symfony/asset": "If you want to use asset versioning", "symfony/messenger": "If you like to process images in background", "symfony/templating": "required to use deprecated Templating component instead of Twig" },