From c8964693930bd89c3306941e597fb2356e2b35cd Mon Sep 17 00:00:00 2001 From: robertsaternus Date: Fri, 27 Feb 2026 12:38:15 +0100 Subject: [PATCH 1/3] INT-234: Add SsrSearchResultsReceivedEvent to RecordList When Server Side Rendering (SSR) is enabled, the plugin renders the ff-record-list content on the server before sending the HTML response to the client. To allow flexible customization of the rendered data, the plugin dispatches a dedicated Symfony event that enables developers to modify, enrich or filter the SSR result set. This event is especially useful for: - Enriching product data with custom attributes. - Modifying record lists depending on user session or context. - Filtering or reordering products before rendering. - Injecting additional metadata used by frontend components. --- CHANGELOG.md | 3 + README.md | 56 +++++++++++++++++++ src/Events/SsrSearchResultsReceivedEvent.php | 29 ++++++++++ .../Controller/ResultController.php | 3 + .../CategoryPageResponseSubscriber.php | 17 ++++-- src/Utilites/Ssr/Template/RecordList.php | 20 +++++-- 6 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 src/Events/SsrSearchResultsReceivedEvent.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 843fd6ca..b78e6ab7 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog +## Unreleased +### Add +- Add SsrSearchResultsReceivedEvent to RecordList ## [v6.5.1] - 2026.01.27 ### Change - Upgrade Web Components version to v5.1.8 diff --git a/README.md b/README.md index b14b5379..24bf0445 100755 --- a/README.md +++ b/README.md @@ -150,6 +150,27 @@ Without SSR enabled, web crawlers could not have a chance to scan the element re **Note:** If you have a problem with displaying product images or prices correctly, you probably have Field Roles set incorrectly. You can easily fix this by setting [custom fields roles](#set-custom-field-roles) +##### SSR Record List Data Event + +When Server Side Rendering (SSR) is enabled, the plugin renders the ff-record-list content on the server before sending the HTML response to the client. +To allow flexible customization of the rendered data, the plugin dispatches a dedicated Symfony event that enables developers to modify, enrich or filter the SSR result set. + +This event is especially useful for: + +- Enriching product data with custom attributes. +- Modifying record lists depending on user session or context. +- Filtering or reordering products before rendering. +- Injecting additional metadata used by frontend components. + +Applying business rules that cannot be easily implemented on the frontend. + +**SsrSearchResultsReceivedEvent** is dispatched right before the SSR HTML output is generated. +It provides full access to: +- The product record list data. +- The current Shopware context. +- The current HTTP request and user session. + +This allows deep customization of rendered results. (Check example implementation)[] ## Features Settings @@ -669,6 +690,41 @@ class EnrichProxyDataEventSubscriber implements EventSubscriberInterface ``` +### Enrich SSR record list data by adding a custom flag to each product. + +```php + 'enrichData']; + } + + public function enrichData(SsrSearchResultsReceivedEvent $event): void + { + $data = $event->getData(); + $records = $data['records'] ?? []; + + foreach ($records as &$record) { + if (isset($record['record']) && is_array($record['record'])) { + $record['record']['custom_badge'] = 'recommended'; + } + } + + $data['records'] = $records; + $event->setData($data); + } +} +``` ## Contribute diff --git a/src/Events/SsrSearchResultsReceivedEvent.php b/src/Events/SsrSearchResultsReceivedEvent.php new file mode 100644 index 00000000..e62e4efe --- /dev/null +++ b/src/Events/SsrSearchResultsReceivedEvent.php @@ -0,0 +1,29 @@ +data = $data; + } + + public function getData(): array + { + return $this->data; + } + + public function setData(array $data): void + { + $this->data = $data; + } +} diff --git a/src/Storefront/Controller/ResultController.php b/src/Storefront/Controller/ResultController.php index 348b4af3..e4d0b87e 100644 --- a/src/Storefront/Controller/ResultController.php +++ b/src/Storefront/Controller/ResultController.php @@ -14,6 +14,7 @@ use Shopware\Core\System\SalesChannel\SalesChannelContext; use Shopware\Storefront\Controller\StorefrontController; use Shopware\Storefront\Page\GenericPageLoader; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -35,6 +36,7 @@ public function result( SalesChannelContext $context, SearchAdapter $searchAdapter, Engine $handlebars, + EventDispatcherInterface $eventDispatcher, ): Response { $page = $this->pageLoader->load($request, $context); $response = $this->renderStorefront('@Parent/storefront/page/factfinder/result.html.twig', ['page' => $page]); @@ -48,6 +50,7 @@ public function result( $handlebars, $searchAdapter, $this->config, + $eventDispatcher, $context->getSalesChannelId(), $response->getContent(), ); diff --git a/src/Subscriber/CategoryPageResponseSubscriber.php b/src/Subscriber/CategoryPageResponseSubscriber.php index 54a8c47c..f6cdab2a 100644 --- a/src/Subscriber/CategoryPageResponseSubscriber.php +++ b/src/Subscriber/CategoryPageResponseSubscriber.php @@ -14,6 +14,7 @@ use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -31,6 +32,7 @@ class CategoryPageResponseSubscriber implements EventSubscriberInterface private SearchAdapter $searchAdapter; private Engine $handlebars; private CategoryPath $categoryPath; + private EventDispatcherInterface $eventDispatcher; public function __construct( bool $httpCacheEnabled, @@ -39,13 +41,15 @@ public function __construct( SearchAdapter $searchAdapter, Engine $handlebars, CategoryPath $categoryPath, + EventDispatcherInterface $eventDispatcher, ) { - $this->httpCacheEnabled = $httpCacheEnabled; - $this->categoryRepository = $categoryRepository; - $this->config = $config; - $this->searchAdapter = $searchAdapter; - $this->handlebars = $handlebars; - $this->categoryPath = $categoryPath; + $this->httpCacheEnabled = $httpCacheEnabled; + $this->categoryRepository = $categoryRepository; + $this->config = $config; + $this->searchAdapter = $searchAdapter; + $this->handlebars = $handlebars; + $this->categoryPath = $categoryPath; + $this->eventDispatcher = $eventDispatcher; } public static function getSubscribedEvents() @@ -79,6 +83,7 @@ public function onPageRendered(ResponseEvent $event): void $this->handlebars, $this->searchAdapter, $this->config, + $this->eventDispatcher, $request->attributes->get('sw-sales-channel-id'), $response->getContent(), ); diff --git a/src/Utilites/Ssr/Template/RecordList.php b/src/Utilites/Ssr/Template/RecordList.php index ea4a06c8..0bb63974 100644 --- a/src/Utilites/Ssr/Template/RecordList.php +++ b/src/Utilites/Ssr/Template/RecordList.php @@ -5,8 +5,10 @@ namespace Omikron\FactFinder\Shopware6\Utilites\Ssr\Template; use Omikron\FactFinder\Shopware6\Config\Communication; +use Omikron\FactFinder\Shopware6\Events\SsrSearchResultsReceivedEvent; use Omikron\FactFinder\Shopware6\Utilites\Ssr\Exception\DetectRedirectException; use Omikron\FactFinder\Shopware6\Utilites\Ssr\SearchAdapter; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; class RecordList @@ -21,21 +23,24 @@ class RecordList private string $content; private string $template; private Communication $pluginConfig; + private EventDispatcherInterface $eventDispatcher; public function __construct( Request $request, Engine $handlebars, SearchAdapter $searchAdapter, Communication $pluginConfig, + EventDispatcherInterface $eventDispatcher, string $salesChannelId, string $content, ) { - $this->request = $request; - $this->handlebars = $handlebars; - $this->searchAdapter = $searchAdapter; - $this->salesChannelId = $salesChannelId; - $this->content = $content; - $this->pluginConfig = $pluginConfig; + $this->request = $request; + $this->handlebars = $handlebars; + $this->searchAdapter = $searchAdapter; + $this->salesChannelId = $salesChannelId; + $this->content = $content; + $this->pluginConfig = $pluginConfig; + $this->eventDispatcher = $eventDispatcher; $this->setTemplateString(); } @@ -49,6 +54,9 @@ public function getContent( bool $isNavigationRequest = false, ): string { $results = $this->searchResults($paramString, $isNavigationRequest); + $event = new SsrSearchResultsReceivedEvent($results); + $this->eventDispatcher->dispatch($event); + $results = $event->getData(); // Support redirect campaigns for SSR if ($this->getRedirectCampaign($results)) { From 809b5f75b523845bc7aa629cd1c722a63daf16b6 Mon Sep 17 00:00:00 2001 From: robertsaternus Date: Fri, 27 Feb 2026 12:44:45 +0100 Subject: [PATCH 2/3] Fix tests --- spec/Storefront/Controller/ResultControllerSpec.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/Storefront/Controller/ResultControllerSpec.php b/spec/Storefront/Controller/ResultControllerSpec.php index a68fa854..deeca5f6 100644 --- a/spec/Storefront/Controller/ResultControllerSpec.php +++ b/spec/Storefront/Controller/ResultControllerSpec.php @@ -22,6 +22,7 @@ use Shopware\Storefront\Framework\Routing\RequestTransformer; use Shopware\Storefront\Page\GenericPageLoader; use Shopware\Storefront\Page\Page; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -78,7 +79,6 @@ public function let( $this->container->get(SeoUrlPlaceholderHandlerInterface::class)->willReturn($seoUrlPlaceholderHandler); $this->container->get(MediaUrlPlaceholderHandlerInterface::class)->willReturn($mediaUrlPlaceholderHandler); $this->container->get('twig')->willReturn($twig); - $this->setTwig($twig); $nestedEventDispatcher->dispatch(Argument::any())->willReturn(Argument::any()); $this->setContainer($container); } @@ -86,7 +86,8 @@ public function let( public function it_should_return_original_response_content_when_ssr_is_not_active( SearchAdapter $searchAdapter, Page $page, - Engine $handlebars + Engine $handlebars, + EventDispatcherInterface $eventDispatcher ): void { $content = 'original content'; $this->pageLoader->load($this->request, $this->salesChannelContext)->willReturn($page); @@ -98,7 +99,8 @@ public function it_should_return_original_response_content_when_ssr_is_not_activ $this->request, $this->salesChannelContext, $searchAdapter, - $handlebars + $handlebars, + $eventDispatcher ); $response->shouldBeAnInstanceOf(Response::class); From 0960488d9a1cace2cc86460029db6b0848751dad Mon Sep 17 00:00:00 2001 From: robertsaternus Date: Fri, 27 Feb 2026 13:02:03 +0100 Subject: [PATCH 3/3] Fix tests --- src/Storefront/Controller/ResultController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Storefront/Controller/ResultController.php b/src/Storefront/Controller/ResultController.php index e4d0b87e..05d5a891 100644 --- a/src/Storefront/Controller/ResultController.php +++ b/src/Storefront/Controller/ResultController.php @@ -20,6 +20,9 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ #[Route(defaults: ['_routeScope' => ['storefront']])] class ResultController extends StorefrontController {