[LazyImage] Support images from Symfony Asset
Kocal opened this issue · comments
Hi,
I've started to implementing LazyImage on our application, and I quickly run into an issue with images coming from asset()
.
A very simplified example:
{% set illustration = article.hasMainPicture
? { url: asset('build/images/article/default-illustration.jpg', 'local_assets') }
: { url: 'https://...' } %}
<img
src="{{ data_uri_thumbnail(illustration.url, 20, 10) }}"
data-controller="symfony--ux-lazy-image--lazy-image"
data-symfony--ux-lazy-image--lazy-image-src-value="{{ illustration.url }}"
// other attributes ...
/>
If my article has no picture, the output of asset('build/images/article/default-illustration.jpg', 'local_assets')
will be passed to file_get_contents()
, resulting into the following error:
An exception has been thrown during the rendering of a template ("Warning: file_get_contents(/build/images/article/default-illustration.jpg?v=3007fee4): Failed to open stream: No such file or directory").
Which makes sense... but it's totally not user friendly! For the moment I dont use BlurHash when the URL starts with /build
, but this is not ideal IMO.
In my case, we can resolve this issue by implementing the opposite logic of framework.assets.json_manifest_path
and find which file corresponds to /build/images/article/default-illustration.jpg?v=3007fee4
:
{
"//": "...",
"build/images/article/default-illustration.jpg": "/build/images/article/default-illustration.jpg?v=3007fee4",
"//": "..."
}
In a more generic way, maybe we can implement the opposite method of Symfony\Component\Asset\Package::getUrl
, something like getReversedPath
, but this may be too much specific to the JsonManifestVersionStrategy
...
... WDYT?
Is it not possible for you to pass the "path" instead of an URL (and fallback on the URL if needed) ?
To illustrate what i mean, in the documentation, the example uses the local filesystem assets path
<img
src="{{ data_uri_thumbnail('public/image/large.png', 100, 75) }}"
{{ stimulus_controller('symfony/ux-lazy-image/lazy-image', {
src: asset('image/large.png')
}) }}
{# Using BlurHash, the size is required #}
width="200"
height="150"
/>
So for you (even if i'm pretty sure your real case is a bit more complex 😅 than that) ... that could give something like that:
{% set illustration = article.hasMainPicture
? {
url: asset('public/images/article/default-illustration.jpg', 'local_assets'),
path: 'assets/images/article/default-illustration.jpg' ,
}
: { url: 'https://...' } %}
<img
src="{{ data_uri_thumbnail(illustration.path ?? illustration.url, 20, 10) }}"
data-controller="symfony--ux-lazy-image--lazy-image"
data-symfony--ux-lazy-image--lazy-image-src-value="{{ illustration.url }}"
// other attributes ...
/>
Ah... Developers love complexity and I fall for it, I've totally forgot about the examples from documentation and demo 🥲
Yeah I'm gonna try this today, I'm not 100% a big fan, but that's a lot better than reversing the manifest.json
entries.
Thanks :)
EDIT:
So for you (even if i'm pretty sure your real case is a bit more complex 😅 than that) ... that could give something like that:
Yes totally, the <img>
rendering is done in a Twig component, that could accept an external URL or a local path:
<twig:Image src="https://..."/>
<twig:Image :src="asset('build/images/article/default-illustration.jpg', 'local_assets') />
I don't want to introduce a new property, but I guess I don't have the choice
Nevermind, I really don't want to pollute my component with another prop, so I went with a quick and dirty solution to get the reversed asset:
- in my
Image
component, PHP:
// ...
public function isLocalAsset(): bool
{
return str_starts_with($this->src, '/build/');
}
public function getAssetLocalPath(): string
{
if (!$this->isLocalAsset()) {
throw new \LogicException('The image is not a local asset.');
}
// Because our pattern in Webpack Encore is "'images/[path][name].[ext]?v=[contenthash:8]'"
return s($this->src)->before('?v=')->toString();
}
// ...
- in my
Image
component, Twig:
<img {{ attributes.defaults({
...(this.useBlurHash() ? {
src: data_uri_thumbnail(
preload(this.isLocalAsset() ? this.getAssetLocalPath() : src , { as: 'image', fetchpriority: 'high' }),
20,
10
),
'data-controller': 'symfony--ux-lazy-image--lazy-image',
'data-symfony--ux-lazy-image--lazy-image-src-value': src,
} : { src }),
{# ... #}
}) />
- in my custom
FetchImageContent
:
final class FetchImageContent
{
public function __construct(
private readonly HttpClientInterface $httpClient,
#[Autowire(param: 'kernel.public_dir')]
private readonly string $publicDir
) {
}
public function __invoke(string $filename): string
{
// Remote files
if (str_starts_with($filename, 'http')) {
$response = $this->httpClient->request('GET', $filename);
return $response->getContent();
}
// We assume that the file is in the public directory, it should be built by Webpack Encore
if (str_starts_with($filename, '/build')) {
return file_get_contents($this->publicDir .$filename);
}
// Fallback to the initial implementation
if (file_exists($filename)) {
return file_get_contents($filename);
}
throw new \RuntimeException(sprintf('Unable to fetch image "%s".', $filename));
}
}
With 2/3 integrations tests, that an acceptable solution for me, especially for the DX it gaves me.
We can close here ? :)
Yup!