Skip to content

Commit da9ee7a

Browse files
authored
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.
1 parent 3aaae61 commit da9ee7a

7 files changed

Lines changed: 124 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# Changelog
2+
## Unreleased
3+
### Add
4+
- Add SsrSearchResultsReceivedEvent to RecordList
25
## [v6.5.1] - 2026.01.27
36
### Change
47
- Upgrade Web Components version to v5.1.8

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,27 @@ Without SSR enabled, web crawlers could not have a chance to scan the element re
150150

151151
**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)
152152

153+
##### SSR Record List Data Event
154+
155+
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.
156+
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.
157+
158+
This event is especially useful for:
159+
160+
- Enriching product data with custom attributes.
161+
- Modifying record lists depending on user session or context.
162+
- Filtering or reordering products before rendering.
163+
- Injecting additional metadata used by frontend components.
164+
165+
Applying business rules that cannot be easily implemented on the frontend.
166+
167+
**SsrSearchResultsReceivedEvent** is dispatched right before the SSR HTML output is generated.
168+
It provides full access to:
169+
- The product record list data.
170+
- The current Shopware context.
171+
- The current HTTP request and user session.
172+
173+
This allows deep customization of rendered results. (Check example implementation)[]
153174

154175
## Features Settings
155176

@@ -669,6 +690,41 @@ class EnrichProxyDataEventSubscriber implements EventSubscriberInterface
669690

670691
```
671692

693+
### Enrich SSR record list data by adding a custom flag to each product.
694+
695+
```php
696+
<?php
697+
698+
declare(strict_types=1);
699+
700+
namespace Omikron\FactFinder\Shopware6\Subscriber;
701+
702+
use Omikron\FactFinder\Shopware6\Events\SsrSearchResultsReceivedEvent;
703+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
704+
705+
class EnrichSsrRecordListEventSubscriber implements EventSubscriberInterface
706+
{
707+
public static function getSubscribedEvents(): array
708+
{
709+
return [SsrSearchResultsReceivedEvent::class => 'enrichData'];
710+
}
711+
712+
public function enrichData(SsrSearchResultsReceivedEvent $event): void
713+
{
714+
$data = $event->getData();
715+
$records = $data['records'] ?? [];
716+
717+
foreach ($records as &$record) {
718+
if (isset($record['record']) && is_array($record['record'])) {
719+
$record['record']['custom_badge'] = 'recommended';
720+
}
721+
}
722+
723+
$data['records'] = $records;
724+
$event->setData($data);
725+
}
726+
}
727+
```
672728

673729
## Contribute
674730

spec/Storefront/Controller/ResultControllerSpec.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Shopware\Storefront\Framework\Routing\RequestTransformer;
2323
use Shopware\Storefront\Page\GenericPageLoader;
2424
use Shopware\Storefront\Page\Page;
25+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2526
use Symfony\Component\HttpFoundation\ParameterBag;
2627
use Symfony\Component\HttpFoundation\Request;
2728
use Symfony\Component\HttpFoundation\RequestStack;
@@ -78,15 +79,15 @@ public function let(
7879
$this->container->get(SeoUrlPlaceholderHandlerInterface::class)->willReturn($seoUrlPlaceholderHandler);
7980
$this->container->get(MediaUrlPlaceholderHandlerInterface::class)->willReturn($mediaUrlPlaceholderHandler);
8081
$this->container->get('twig')->willReturn($twig);
81-
$this->setTwig($twig);
8282
$nestedEventDispatcher->dispatch(Argument::any())->willReturn(Argument::any());
8383
$this->setContainer($container);
8484
}
8585

8686
public function it_should_return_original_response_content_when_ssr_is_not_active(
8787
SearchAdapter $searchAdapter,
8888
Page $page,
89-
Engine $handlebars
89+
Engine $handlebars,
90+
EventDispatcherInterface $eventDispatcher
9091
): void {
9192
$content = 'original content';
9293
$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
9899
$this->request,
99100
$this->salesChannelContext,
100101
$searchAdapter,
101-
$handlebars
102+
$handlebars,
103+
$eventDispatcher
102104
);
103105

104106
$response->shouldBeAnInstanceOf(Response::class);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Omikron\FactFinder\Shopware6\Events;
6+
7+
use Symfony\Contracts\EventDispatcher\Event;
8+
9+
class SsrSearchResultsReceivedEvent extends Event
10+
{
11+
public const NAME = 'factfinder.ssr.search_results.received';
12+
13+
private array $data;
14+
15+
public function __construct(array $data)
16+
{
17+
$this->data = $data;
18+
}
19+
20+
public function getData(): array
21+
{
22+
return $this->data;
23+
}
24+
25+
public function setData(array $data): void
26+
{
27+
$this->data = $data;
28+
}
29+
}

src/Storefront/Controller/ResultController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@
1414
use Shopware\Core\System\SalesChannel\SalesChannelContext;
1515
use Shopware\Storefront\Controller\StorefrontController;
1616
use Shopware\Storefront\Page\GenericPageLoader;
17+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1718
use Symfony\Component\HttpFoundation\RedirectResponse;
1819
use Symfony\Component\HttpFoundation\Request;
1920
use Symfony\Component\HttpFoundation\Response;
2021
use Symfony\Component\Routing\Annotation\Route;
2122

23+
/**
24+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
25+
*/
2226
#[Route(defaults: ['_routeScope' => ['storefront']])]
2327
class ResultController extends StorefrontController
2428
{
@@ -35,6 +39,7 @@ public function result(
3539
SalesChannelContext $context,
3640
SearchAdapter $searchAdapter,
3741
Engine $handlebars,
42+
EventDispatcherInterface $eventDispatcher,
3843
): Response {
3944
$page = $this->pageLoader->load($request, $context);
4045
$response = $this->renderStorefront('@Parent/storefront/page/factfinder/result.html.twig', ['page' => $page]);
@@ -48,6 +53,7 @@ public function result(
4853
$handlebars,
4954
$searchAdapter,
5055
$this->config,
56+
$eventDispatcher,
5157
$context->getSalesChannelId(),
5258
$response->getContent(),
5359
);

src/Subscriber/CategoryPageResponseSubscriber.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
1515
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
1616
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
17+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1718
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1819
use Symfony\Component\HttpFoundation\RedirectResponse;
1920
use Symfony\Component\HttpFoundation\Request;
@@ -31,6 +32,7 @@ class CategoryPageResponseSubscriber implements EventSubscriberInterface
3132
private SearchAdapter $searchAdapter;
3233
private Engine $handlebars;
3334
private CategoryPath $categoryPath;
35+
private EventDispatcherInterface $eventDispatcher;
3436

3537
public function __construct(
3638
bool $httpCacheEnabled,
@@ -39,13 +41,15 @@ public function __construct(
3941
SearchAdapter $searchAdapter,
4042
Engine $handlebars,
4143
CategoryPath $categoryPath,
44+
EventDispatcherInterface $eventDispatcher,
4245
) {
43-
$this->httpCacheEnabled = $httpCacheEnabled;
44-
$this->categoryRepository = $categoryRepository;
45-
$this->config = $config;
46-
$this->searchAdapter = $searchAdapter;
47-
$this->handlebars = $handlebars;
48-
$this->categoryPath = $categoryPath;
46+
$this->httpCacheEnabled = $httpCacheEnabled;
47+
$this->categoryRepository = $categoryRepository;
48+
$this->config = $config;
49+
$this->searchAdapter = $searchAdapter;
50+
$this->handlebars = $handlebars;
51+
$this->categoryPath = $categoryPath;
52+
$this->eventDispatcher = $eventDispatcher;
4953
}
5054

5155
public static function getSubscribedEvents()
@@ -79,6 +83,7 @@ public function onPageRendered(ResponseEvent $event): void
7983
$this->handlebars,
8084
$this->searchAdapter,
8185
$this->config,
86+
$this->eventDispatcher,
8287
$request->attributes->get('sw-sales-channel-id'),
8388
$response->getContent(),
8489
);

src/Utilites/Ssr/Template/RecordList.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
namespace Omikron\FactFinder\Shopware6\Utilites\Ssr\Template;
66

77
use Omikron\FactFinder\Shopware6\Config\Communication;
8+
use Omikron\FactFinder\Shopware6\Events\SsrSearchResultsReceivedEvent;
89
use Omikron\FactFinder\Shopware6\Utilites\Ssr\Exception\DetectRedirectException;
910
use Omikron\FactFinder\Shopware6\Utilites\Ssr\SearchAdapter;
11+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1012
use Symfony\Component\HttpFoundation\Request;
1113

1214
class RecordList
@@ -21,21 +23,24 @@ class RecordList
2123
private string $content;
2224
private string $template;
2325
private Communication $pluginConfig;
26+
private EventDispatcherInterface $eventDispatcher;
2427

2528
public function __construct(
2629
Request $request,
2730
Engine $handlebars,
2831
SearchAdapter $searchAdapter,
2932
Communication $pluginConfig,
33+
EventDispatcherInterface $eventDispatcher,
3034
string $salesChannelId,
3135
string $content,
3236
) {
33-
$this->request = $request;
34-
$this->handlebars = $handlebars;
35-
$this->searchAdapter = $searchAdapter;
36-
$this->salesChannelId = $salesChannelId;
37-
$this->content = $content;
38-
$this->pluginConfig = $pluginConfig;
37+
$this->request = $request;
38+
$this->handlebars = $handlebars;
39+
$this->searchAdapter = $searchAdapter;
40+
$this->salesChannelId = $salesChannelId;
41+
$this->content = $content;
42+
$this->pluginConfig = $pluginConfig;
43+
$this->eventDispatcher = $eventDispatcher;
3944
$this->setTemplateString();
4045
}
4146

@@ -49,6 +54,9 @@ public function getContent(
4954
bool $isNavigationRequest = false,
5055
): string {
5156
$results = $this->searchResults($paramString, $isNavigationRequest);
57+
$event = new SsrSearchResultsReceivedEvent($results);
58+
$this->eventDispatcher->dispatch($event);
59+
$results = $event->getData();
5260

5361
// Support redirect campaigns for SSR
5462
if ($this->getRedirectCampaign($results)) {

0 commit comments

Comments
 (0)