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/Resources/doc/asset-versioning.rst b/Resources/doc/asset-versioning.rst index 997ce44f..b7331599 100644 --- a/Resources/doc/asset-versioning.rst +++ b/Resources/doc/asset-versioning.rst @@ -17,6 +17,11 @@ setting for ``framework.assets.version``. It strips the version from the file name and appends it to the resulting image URL so that the file is found and cache busting is used. +Since LiipImagineBundle version 2.12, we integrate with the configuration +setting for ``framework.assets.json_manifest_path``. The manifest file is used +to lookup the location of the actual file, and append the versioning string to +the resulting image URL so that cache busting is used. + Cache Busting ~~~~~~~~~~~~~ @@ -25,6 +30,12 @@ versioning to bust the cache of your images. This can help for example after you changed the settings of a filter set. If you use ``framework.assets.version``, change the asset version in that case. +If you use ``framework.assets.json_manifest_path``, then rebuild the manifest +in your asset pipeline. Note that your versioning string might be calculated +using a content hash. Changing a filter setting in these cases will *not* bust +the previous cache. Either rename your filter in that case or use a different +versioning strategy within your asset pipeline that ensures a new hash for each +build. If you do not use Symfony asset versioning, set the ``liip_imagine.twig.assets_version`` parameter. Note that you still need to clear/refresh the cached images to have them updated, the asset version is only 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 55dbcad2..866b8fdc 100644 --- a/composer.json +++ b/composer.json @@ -53,6 +53,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",