Repository: shopware/shopware
Pull request #8478: feat: Add extension to enable overwriting of the thumbnail url
Author: login: TheBreaken
Labels: name: component/media
Created: 2025-04-10T13:43:18Z | Merged: 2025-04-15T06:42:46Z

### 1. Why is this change necessary?

I want to override thumbnail URLs in cases where generating a URL is more complex and a pattern alone isn't sufficient. For example, when I need additional information for an image proxy

### 2. What does this change do, exactly?

It implements an event wich can be used to manipulate the thumbnail url before it is returned.

### 3. Describe each step to reproduce the issue or behaviour.

There is no issue.

### 4. Please link to the relevant issues (if any).

### 5. Checklist

- [x] I have written tests and verified that they fail without my change
- [x] I have created a [changelog file](https://github.com/shopware/shopware/blob/trunk/adr/2020-08-03-implement-new-changelog.md) with all necessary information about my changes
- [ ] I have written or adjusted the documentation according to my changes
- [x] This change has comments for package types, values, functions, and non-obvious lines of code
- [x] I have read the contribution requirements and fulfill them.



Diff:
diff --git a/changelog/_unreleased/2025-04-10-add-remote-thumbnail-url-extension.md b/changelog/_unreleased/2025-04-10-add-remote-thumbnail-url-extension.md
new file mode 100644
index 00000000000..0136d9a11b3
--- /dev/null
+++ b/changelog/_unreleased/2025-04-10-add-remote-thumbnail-url-extension.md
@@ -0,0 +1,8 @@
+---
+title: Add remote thumbnail url extension
+author: Vladislav Sultanov
+author_email: vladislav.sultanov@netlogix.de
+author_github: @TheBreaken
+---
+# Core
+* Added extension to `Shopware\Core\Content\Media\Core\Application\RemoteThumbnailLoader::getUrl` to manipulate the url before it is used in the thumbnail
diff --git a/src/Core/Content/DependencyInjection/media_path.xml b/src/Core/Content/DependencyInjection/media_path.xml
index c9e3d8f7943..20849515b9c 100644
--- a/src/Core/Content/DependencyInjection/media_path.xml
+++ b/src/Core/Content/DependencyInjection/media_path.xml
@@ -18,6 +18,7 @@
             <argument type="service" id="Shopware\Core\Content\Media\Core\Application\AbstractMediaUrlGenerator"/>
             <argument type="service" id="Doctrine\DBAL\Connection"/>
             <argument type="service" id="shopware.filesystem.public"/>
+            <argument type="service" id="Shopware\Core\Framework\Extensions\ExtensionDispatcher"/>
             <argument>%shopware.media.remote_thumbnails.pattern%</argument>
 
             <tag name="kernel.reset" method="reset"/>
diff --git a/src/Core/Content/Media/Core/Application/RemoteThumbnailLoader.php b/src/Core/Content/Media/Core/Application/RemoteThumbnailLoader.php
index 652bdd7659a..ac417afc5bb 100644
--- a/src/Core/Content/Media/Core/Application/RemoteThumbnailLoader.php
+++ b/src/Core/Content/Media/Core/Application/RemoteThumbnailLoader.php
@@ -7,7 +7,9 @@
 use Shopware\Core\Content\Media\Aggregate\MediaThumbnail\MediaThumbnailCollection;
 use Shopware\Core\Content\Media\Aggregate\MediaThumbnail\MediaThumbnailEntity;
 use Shopware\Core\Content\Media\Core\Params\UrlParams;
+use Shopware\Core\Content\Media\Extension\ResolveRemoteThumbnailUrlExtension;
 use Shopware\Core\Framework\DataAbstractionLayer\Entity;
+use Shopware\Core\Framework\Extensions\ExtensionDispatcher;
 use Shopware\Core\Framework\Log\Package;
 use Shopware\Core\Framework\Uuid\Uuid;
 use Symfony\Contracts\Service\ResetInterface;
@@ -32,6 +34,7 @@ public function __construct(
         private readonly AbstractMediaUrlGenerator $generator,
         private readonly Connection $connection,
         private readonly FilesystemOperator $filesystem,
+        private readonly ExtensionDispatcher $extensions,
         private readonly string $pattern = ''
     ) {
     }
@@ -167,20 +170,33 @@ private function getBaseUrl(): string
 
     private function getUrl(string $mediaUrl, string $mediaPath, string $width, string $height, ?\DateTimeInterface $mediaUpdatedAt): string
     {
-        $replacements = [
-            str_starts_with($mediaPath, 'http') ? '' : $mediaUrl,
-            $mediaPath,
-            $width,
-            $height,
-            (string) $mediaUpdatedAt?->getTimestamp() ?: '',
-        ];
-
-        $url = str_replace(
-            ['{mediaUrl}', '{mediaPath}', '{width}', '{height}', '{mediaUpdatedAt}'],
-            $replacements,
-            $this->pattern
+        return $this->extensions->publish(
+            name: ResolveRemoteThumbnailUrlExtension::NAME,
+            extension: new ResolveRemoteThumbnailUrlExtension(
+                $mediaUrl,
+                $mediaPath,
+                $width,
+                $height,
+                $this->pattern,
+                $mediaUpdatedAt
+            ),
+            function: function (string $mediaUrl, string $mediaPath, string $width, string $height, string $pattern, ?\DateTimeInterface $mediaUpdatedAt) {
+                $replacements = [
+                    str_starts_with($mediaPath, 'http') ? '' : $mediaUrl,
+                    $mediaPath,
+                    $width,
+                    $height,
+                    (string) $mediaUpdatedAt?->getTimestamp() ?: '',
+                ];
+
+                $url = str_replace(
+                    ['{mediaUrl}', '{mediaPath}', '{width}', '{height}', '{mediaUpdatedAt}'],
+                    $replacements,
+                    $pattern
+                );
+
+                return str_starts_with($mediaPath, 'http') ? ltrim($url, '/') : $url;
+            }
         );
-
-        return str_starts_with($mediaPath, 'http') ? ltrim($url, '/') : $url;
     }
 }
diff --git a/src/Core/Content/Media/Extension/ResolveRemoteThumbnailUrlExtension.php b/src/Core/Content/Media/Extension/ResolveRemoteThumbnailUrlExtension.php
new file mode 100644
index 00000000000..e0e30bba613
--- /dev/null
+++ b/src/Core/Content/Media/Extension/ResolveRemoteThumbnailUrlExtension.php
@@ -0,0 +1,31 @@
+<?php
+declare(strict_types=1);
+
+namespace Shopware\Core\Content\Media\Extension;
+
+use Shopware\Core\Framework\Extensions\Extension;
+use Shopware\Core\Framework\Log\Package;
+
+/**
+ * @extends Extension<string>
+ *
+ * @codeCoverageIgnore
+ */
+#[Package('discovery')]
+final class ResolveRemoteThumbnailUrlExtension extends Extension
+{
+    public const NAME = 'remote_thumbnail_url.resolve';
+
+    /**
+     * @internal shopware owns the __constructor, but the properties are public API
+     */
+    public function __construct(
+        public string $mediaUrl,
+        public string $mediaPath,
+        public string $width,
+        public string $height,
+        public string $pattern,
+        public ?\DateTimeInterface $mediaUpdatedAt
+    ) {
+    }
+}
diff --git a/tests/unit/Core/Content/Media/Core/Application/RemoteThumbnailLoaderTest.php b/tests/unit/Core/Content/Media/Core/Application/RemoteThumbnailLoaderTest.php
index 8f19c370017..d6be258c64f 100644
--- a/tests/unit/Core/Content/Media/Core/Application/RemoteThumbnailLoaderTest.php
+++ b/tests/unit/Core/Content/Media/Core/Application/RemoteThumbnailLoaderTest.php
@@ -13,8 +13,10 @@
 use Shopware\Core\Content\Media\Infrastructure\Path\MediaUrlGenerator;
 use Shopware\Core\Framework\Adapter\Filesystem\PrefixFilesystem;
 use Shopware\Core\Framework\DataAbstractionLayer\PartialEntity;
+use Shopware\Core\Framework\Extensions\ExtensionDispatcher;
 use Shopware\Core\Framework\Test\TestCaseHelper\ReflectionHelper;
 use Shopware\Core\Test\Stub\Framework\IdsCollection;
+use Symfony\Component\EventDispatcher\EventDispatcher;
 
 /**
  * @internal
@@ -37,10 +39,14 @@ public function testLoad(IdsCollection $ids, PartialEntity $entity, array $thumb
         $connection = $this->createMock(Connection::class);
         $connection->method('fetchAllAssociative')->willReturn($thumbnailSizes);
 
+        $dispatcher = new EventDispatcher();
+        $extensionDispatcher = new ExtensionDispatcher($dispatcher);
+
         $loader = new RemoteThumbnailLoader(
             new MediaUrlGenerator($filesystem),
             $connection,
             $prefixFilesystem,
+            $extensionDispatcher,
             '{mediaUrl}/{mediaPath}?width={width}&ts={mediaUpdatedAt}'
         );
 
@@ -171,10 +177,14 @@ public function testReset(): void
             'private' => false,
         ]);
 
+        $dispatcher = new EventDispatcher();
+        $extensionDispatcher = new ExtensionDispatcher($dispatcher);
+
         $loader = new RemoteThumbnailLoader(
             new MediaUrlGenerator($filesystem),
             $connection,
             $this->createMock(PrefixFilesystem::class),
+            $extensionDispatcher,
             '{mediaUrl}/{mediaPath}?width={width}&ts={mediaUpdatedAt}'
         );
 


Reviews:
user: login: shyim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T06:28:47Z
user: login: shyim
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T06:28:57Z
user: login: TheBreaken
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:33:59Z
user: login: TheBreaken
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:34:05Z
user: login: shyim
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-11T07:02:07Z
user: login: MartinKrzykawski
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T06:27:43Z

Review comments:
diff_hunk: @@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+/*
+ * Created by netlogix GmbH & Co. KG
+ *
+ * @copyright netlogix GmbH & Co. KG
+ */
+
+namespace Shopware\Core\Content\Media\Extension;
+
+use Shopware\Core\Framework\Extensions\Extension;
+use Shopware\Core\Framework\Log\Package;
+
+/**
+ * @extends Extension<string>
path: src/Core/Content/Media/Extension/ResolveRemoteThumbnailUrlExtension.php
user: login: shyim
body: can you add codecoverageignore here as phpdoc
created_at: 2025-04-11T06:28:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+/*
+ * Created by netlogix GmbH & Co. KG
+ *
+ * @copyright netlogix GmbH & Co. KG
+ */
+
path: src/Core/Content/Media/Extension/ResolveRemoteThumbnailUrlExtension.php
user: login: shyim
body: please remove the file header
created_at: 2025-04-11T06:28:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+/*
+ * Created by netlogix GmbH & Co. KG
+ *
+ * @copyright netlogix GmbH & Co. KG
+ */
+
path: src/Core/Content/Media/Extension/ResolveRemoteThumbnailUrlExtension.php
user: login: TheBreaken
body: Done
created_at: 2025-04-11T06:33:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+/*
+ * Created by netlogix GmbH & Co. KG
+ *
+ * @copyright netlogix GmbH & Co. KG
+ */
+
+namespace Shopware\Core\Content\Media\Extension;
+
+use Shopware\Core\Framework\Extensions\Extension;
+use Shopware\Core\Framework\Log\Package;
+
+/**
+ * @extends Extension<string>
path: src/Core/Content/Media/Extension/ResolveRemoteThumbnailUrlExtension.php
user: login: TheBreaken
body: Done
created_at: 2025-04-11T06:34:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-10T13:48:58Z
body: ## [Codecov](https://app.codecov.io/gh/shopware/shopware/pull/8478?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 53.16%. Comparing base [(`0915eb2`)](https://app.codecov.io/gh/shopware/shopware/commit/0915eb278d589931957b55d3e2220086703a8853?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware) to head [(`15e0a37`)](https://app.codecov.io/gh/shopware/shopware/commit/15e0a3760a72407c7c5de7bf843c796ea619e9d2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware).
> Report is 13 commits behind head on trunk.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##            trunk    #8478   +/-   ##
=======================================
  Coverage   53.16%   53.16%           
=======================================
  Files        4709     4709           
  Lines      159001   159019   +18     
  Branches     8736     8736           
=======================================
+ Hits        84527    84545   +18     
  Misses      71950    71950           
  Partials     2524     2524           
```

| [Flag](https://app.codecov.io/gh/shopware/shopware/pull/8478/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware) | Coverage Δ | |
|---|---|---|
| [jest-admin](https://app.codecov.io/gh/shopware/shopware/pull/8478/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware) | `60.41% <ø> (+<0.01%)` | :arrow_up: |
| [jest-storefront](https://app.codecov.io/gh/shopware/shopware/pull/8478/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware) | `70.83% <ø> (ø)` | |
| [phpunit-migration](https://app.codecov.io/gh/shopware/shopware/pull/8478/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware) | `43.32% <ø> (ø)` | |
| [phpunit-unit](https://app.codecov.io/gh/shopware/shopware/pull/8478/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware) | `45.47% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/shopware/shopware/pull/8478?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shopware).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: mitelg
created_at: 2025-04-15T06:53:28Z
body: thanks for your contribution @TheBreaken 👍🎉💙
author_association: MEMBER