symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony

Home Page:https://ux.symfony.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[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 ? :)