Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update versioning documentation #1530

31 changes: 26 additions & 5 deletions DependencyInjection/Compiler/AssetsVersionCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,24 @@

namespace Liip\ImagineBundle\DependencyInjection\Compiler;

use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy;
use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* 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
{
Expand All @@ -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;
Expand All @@ -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);
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions Resources/config/imagine_twig_mode_lazy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<tag name="twig.runtime" />
<argument type="service" id="liip_imagine.cache.manager" />
<argument>null</argument>
<argument>null</argument>
</service>

</services>
Expand Down
11 changes: 11 additions & 0 deletions Resources/doc/asset-versioning.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~

Expand All @@ -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
Expand Down
59 changes: 45 additions & 14 deletions Templating/LazyFilterRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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;
}
}
37 changes: 37 additions & 0 deletions Tests/Templating/LazyFilterRuntimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down