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
:
md5($media->getId())
– the first six digits are used and split every 2 character for directories like ab/e3/ed- $media->getUploadedAt()->getTimestamp()
- $media->getFilename()
- (in case of a thumbnail) _<width>x<height>
- .<extension>
Media example
/00/0e/94/1630072217/my_file.jpg
Thumbnail example
/00/0e/94/1630072217/my_file_400x400.jpg
While digging down the code using your article, I found a “blacklist” and wondered what that’s for: https://stackoverflow.com/questions/69855801/why-is-there-a-this-blacklist-in-the-path-name-generation-of-shopware-6/69855802#69855802
I agree with your stackoverflow comment: I think ad in the url is a bad thing and therefore they filter it. No better explanation from my side.