16.09.2021・TechStuff
16.09.2021・TechStuff

How to: Shopware image urls

Fabian Blechschmidt

How is a Shopware image url generated, let’s dive into it and start really early so everyone can follow.

MediaEntity::getUrl into the Template

This is part of vendor/shopware/storefront/Resources/views/storefront/utilities/thumbnail.html.twig

<img {% if load %}src="{{ media|sw_encode_media_url }}" {% else %}data-src="{{ media|sw_encode_media_url }}" {% endif %}
    {% if media.thumbnails|length > 0 %}
        {% if load %}srcset="{{ srcsetValue }}" {% else %}data-srcset="{{ srcsetValue }}" {% endif %}
        {% if sizes['default'] %}
        sizes="{{ sizes['default'] }}"
        {% elseif sizes|length > 0 %}
        sizes="{{ sizesValue }}"
        {% endif %}
    {% endif %}
    {% for key, value in attributes %}{% if value != '' %} {{ key }}="{{ value }}"{% endif %}{% endfor %}
/>

I’ll just dig down the methods and will sum everything up in the end.

\Shopware\Storefront\Framework\Twig\Extension\UrlEncodingTwigFilter::encodeMediaUrl

    public function encodeMediaUrl(?MediaEntity $media): ?string
    {
        if ($media === null || !$media->hasFile()) {
            return null;
        }

        return $this->encodeUrl($media->getUrl());
    }

So far so good. So the big question is, where is \Shopware\Core\Content\Media\MediaEntity::getUrl from?

Database to MediaEntity::setUrl

When we start searching, where the url is set on the MediaEntity we’ll find \Shopware\Core\Content\Media\Subscriber\MediaLoadedSubscriber::addUrls, especially the following line:

$media->setUrl($this->urlGenerator->getAbsoluteMediaUrl($media));

The method is from an interface (\Shopware\Core\Content\Media\Pathname\UrlGeneratorInterface::getAbsoluteMediaUrl) and implemented here: \Shopware\Core\Content\Media\Pathname\UrlGenerator::getAbsoluteMediaUrl

public function getAbsoluteMediaUrl(MediaEntity $media): string
    {
        return $this->getBaseUrl() . '/' . $this->getRelativeMediaUrl($media);
    }
    public function getRelativeThumbnailUrl(MediaEntity $media, MediaThumbnailEntity $thumbnail): string
    {
        $this->validateMedia($media);

        return $this->toPathString([
            'thumbnail',
            $this->pathnameStrategy->generatePathHash($media),
            $this->pathnameStrategy->generatePathCacheBuster($media),
            $this->pathnameStrategy->generatePhysicalFilename($media, $thumbnail),
        ]);
    }

All these methods belong to the interface \Shopware\Core\Content\Media\Pathname\PathnameStrategy\PathnameStrategyInterface and are implemented by default in \Shopware\Core\Content\Media\Pathname\PathnameStrategy\IdPathnameStrategy

Why by default?

In your .env file you set a value for SHOPWARE_CDN_STRATEGY_DEFAULT="id" which defines via Shopware\Core\Content\Media\Pathname\PathnameStrategy\StrategyFactory which implementation of the PathnameStrategyInterface to use. The default is id

PathnameStrategy

The following methods are implementation in \Shopware\Core\Content\Media\Pathname\PathnameStrategy\IdPathnameStrategy (or inherited from ancestor classes.

generatePathHash

I don’t know why it is needed to generate a md5 hash from a GUID, but people which are smarter than me know. Maybe because it is shorter than the GUID or they don’t want to give away the GUIDs of the images? Another idea I have is to avoid having similar ids and therefore all images would end up in the same directory, but as far as I know GUIDs are built to distribute maximally like most hash algorithms as well (like md5) ?

public function generatePathHash(MediaEntity $media, ?MediaThumbnailEntity $thumbnail = null): ?string
    {
        return $this->generateMd5Path($media->getId());
    }

generatePathCacheBuster

To make sure, that the current image is online and not an old one, the timestamp of the upload is used – important here is to note, that it is NOT the timestamp on the file, but the entry on the database in media.uploaded_at.

    public function generatePathCacheBuster(MediaEntity $media, ?MediaThumbnailEntity $thumbnail = null): ?string
    {
        $uploadedAt = $media->getUploadedAt();

        if ($uploadedAt === null) {
            return null;
        }

        return (string) $uploadedAt->getTimestamp();
    }

generatePhysicalFilename

And the filename is pretty straight forward, the filename and the extension. In case of a thumbnail the image size is added as a suffix.

    public function generatePhysicalFilename(MediaEntity $media, ?MediaThumbnailEntity $thumbnail = null): string
    {
        $filenameSuffix = '';
        if ($thumbnail !== null) {
            $filenameSuffix = sprintf('_%dx%d', $thumbnail->getWidth(), $thumbnail->getHeight());
        }

        $extension = $media->getFileExtension() ? '.' . $media->getFileExtension() : '';

        return $media->getFileName() . $filenameSuffix . $extension;
    }

TL;DR

The url consists of the following parts, where $media is a MediaEntity:

Media example

/00/0e/94/1630072217/my_file.jpg

Thumbnail example

/00/0e/94/1630072217/my_file_400x400.jpg