From b1176a7fc0290bc4b61584944b6432dccc41e4f2 Mon Sep 17 00:00:00 2001 From: robertsaternus Date: Tue, 12 Aug 2025 12:00:02 +0200 Subject: [PATCH 1/3] FFWEB-3437: Add proxy feature Add proxy feature with fallback and data enrichment mechanism --- CHANGELOG.md | 1 + README.md | 68 ++++++++++++++++++- metadata.php | 15 ++-- src/Controller/SearchResultController.php | 38 +++++++---- src/Event/EnrichProxyDataEvent.php | 26 +++++++ src/Model/Config/Communication.php | 13 ++-- .../EnrichProxyDataEventSubscriber.php | 24 +++++++ .../Export/Exporter/ExportEntitiesTest.php | 3 + .../themes/default/layout/base.html.twig | 21 +----- 9 files changed, 158 insertions(+), 51 deletions(-) create mode 100644 src/Event/EnrichProxyDataEvent.php create mode 100644 src/Subscriber/EnrichProxyDataEventSubscriber.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f0298d42..2d8aa1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Add - Add cart button for product list +- Implement proxy - more information in README file ### Change - Support tab navigation for search, suggest and paging components diff --git a/README.md b/README.md index c9d3a468..ebead9a6 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ For more advanced features please check our official [WebComponnents documentati - [Test FTP Connection Button](#test-ftp-connection) - [Update Field Roles Button](#update-field-roles) - [Advanced Settings](#advanced-settings) + - [Proxy](#proxy) - [Features Settings](#features-settings) - [Using FACT-FinderĀ® on category pages](#using-fact-finder-on-category-pages) - [Feed Settings](#feed-settings) @@ -39,6 +40,8 @@ For more advanced features please check our official [WebComponnents documentati - [Click on Product](#click-on-product) - [Add Product to Cart](#add-product-to-cart) - [Place an Order](#place-an-order) +- [Modification Examples](#modifications-examples) + - [Enrich data received from FACT-Finder](#enrich-data-received-from-fact-finder) - [Contribute](#contribute) - [License](#license) @@ -121,10 +124,44 @@ This functionality uses form data, so there is no need to save first. ### Advanced Settings ![Advanced Settings](docs/assets/advanced-settings.png "Advanced settings") -* `Anonymize User ID?` - check this option if you want to send user id with tracking requests in anonymized form. By default the regular id field from user table is sent. +* `Anonymize User ID?` - check this option if you want to send user id with tracking requests in anonymized form. By default, the regular id field from user table is sent. +* `Use Proxy` - check this option if you want each request sends by Web Components first reach the dedicated module controller which forwards it to the FACT-Finder. + **Note:** If you plan to use proxy, consider reading below paragraph as it requires full instruction how to enable it properly. * `How to count single click on "Add to cart" button?` - select how would you like to count single click on "Add to cart" button * `Send the SID as userId when user not logged in?` +#### Proxy +Proxy feature adds a oxid controller which serves as a middleware between Web Components and FACT-FinderĀ®. +The data flow with proxy enabled is illustrated by the graph below. +![Communication Overview](docs/assets/communication-overview.png "Communication Overview") +Having a middleware controller brings many possibilities to customize the request and the response. You can use `EnrichProxyDataEvent` to enrich data received from FACT-Finder. You can find more +details about implementation [here](#enrich-data-received-from-fact-finder-in-proxycontroller). +In addition, if forwarded request does not result with a correct response, you can implement fallback strategy, starting from this point. + +```php + //src/Controller/SearchResultController.php:84 + protected function fallback(): void + { + //this function could be used to implement fallback logic in case of any communication error. + $this->showJsonAndExit('Error: Unable to process the request.'); + } +``` + +To enable proxy you need to change your HTTP server configuration by adding rewrite rules. +This is necessary because Web Components appends a URL parts to the base URL making it unreadable by the Oxid. +This is because Oxid use query parameters `cl` and `fnc` to instantiate specific controller and execute its function. +There is no routing that use url parts, hence any AJAX requests must target index.php file with the aforementioned parameters. +Without these rules any request will lead to 404. + +APACHE + +```apache + RewriteRule ^(rest/v[0-9].*)$ index.php [L] +``` + +**Note:** Sending each request to FACT-Finder instance trough Shopware, you lose on performance as each request need to be handled first by HTTP server and then, by Shopware itself. This additional traffic could be easily avoided by not activating this feature if there's no clear reason to use it. + + ### Features Settings ![Features Settings](docs/assets/features-settings.png "Features settings") @@ -245,6 +282,35 @@ We offer a `registerAddToCartListener` function which helps to register `click` ### Place an Order This event is tracked by the `ff-checkout-tracking` element which is implemented on order confirmation page +## Modifications Examples + +### Enrich data received from FACT-Finder + +```php + +use Omikron\FactFinder\Oxid\Event\EnrichProxyDataEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class EnrichProxyDataEventSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [EnrichProxyDataEvent::class => 'enrichData']; + } + + public function enrichData(EnrichProxyDataEvent $event): void + { + $data = $event->getData(); + $data['example_data'] = [ + 'some_data' => 'data_1', + 'next_data' => 'data_2', + ]; + $event->setData($data); + } +} + +``` + ## Contribute For more information, click [here](.github/CONTRIBUTING.md) diff --git a/metadata.php b/metadata.php index 9d72ea20..37f2c0f2 100755 --- a/metadata.php +++ b/metadata.php @@ -83,14 +83,13 @@ 'value' => true, 'position' => $settingPosition++, ], -// TODO Refactor proxy -// [ -// 'group' => 'ffAdvanced', -// 'name' => 'ffUseProxy', -// 'type' => 'bool', -// 'value' => false, -// 'position' => $settingPosition++, -// ], + [ + 'group' => 'ffAdvanced', + 'name' => 'ffUseProxy', + 'type' => 'bool', + 'value' => false, + 'position' => $settingPosition++, + ], [ 'group' => 'ffAdvanced', 'name' => 'ffSidAsUserId', diff --git a/src/Controller/SearchResultController.php b/src/Controller/SearchResultController.php index 14771a7c..841eb22c 100644 --- a/src/Controller/SearchResultController.php +++ b/src/Controller/SearchResultController.php @@ -6,11 +6,13 @@ use Omikron\FactFinder\Communication\Client\ClientBuilder; use Omikron\FactFinder\Communication\Version; +use Omikron\FactFinder\Oxid\Event\EnrichProxyDataEvent; +use Omikron\FactFinder\Oxid\Subscriber\EnrichProxyDataEventSubscriber; use OxidEsales\Eshop\Application\Controller\FrontendController; use OxidEsales\Eshop\Core\Registry; use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidEsales\EshopCommunity\Internal\Framework\Module\Facade\ModuleSettingServiceInterface; -use Psr\Http\Message\ResponseInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; class SearchResultController extends FrontendController { @@ -41,19 +43,30 @@ public function proxy(): void switch ($httpMethod) { case 'GET': $query = (string) $this->removeOxidParams(parse_url($currentUrl, PHP_URL_QUERY)); - $this->showJsonAndExit($this->unwrapResponse($client->request('GET', $endpoint . '?' . $query))); - + $response = $client->request('GET', $endpoint . '?' . $query); break; case 'POST': - $this->showJsonAndExit($this->unwrapResponse($client->request('POST', $endpoint, [ - 'body' => $this->getRequest()->getContent(), + $rawBody = file_get_contents('php://input'); + $body = json_decode($rawBody, true) ?: []; + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception('Invalid JSON in request body'); + } + + $response = $client->request('POST', $endpoint, [ + 'body' => json_encode($body), 'headers' => ['Content-Type' => 'application/json'], - ]))); + ]); break; default: throw new \Exception(sprintf('HTTP Method %s is not supported', $httpMethod)); } + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addSubscriber(new EnrichProxyDataEventSubscriber()); + $event = new EnrichProxyDataEvent(json_decode($response->getBody()->getContents(), true) ?? []); + $eventDispatcher->dispatch($event, EnrichProxyDataEvent::class); + $this->showJsonAndExit(json_encode($event->getData())); } catch (\Exception $e) { $this->fallback(); echo json_encode(['error' => $e->getMessage()]); @@ -71,7 +84,7 @@ protected function showJsonAndExit(string $jsonResponse): void protected function fallback(): void { // this function could be used to implement fallback logic in case of any communication error. - $this->showJsonAndExit(''); + $this->showJsonAndExit('Error: Unable to process the request.'); } protected function getConfigParam(string $key): string @@ -89,13 +102,12 @@ private function getEndpoint(string $currentUrl): string return $match[1] ?? ''; } - private function removeOxidParams(string $queryString): string + private function removeOxidParams(?string $queryString): string { - return preg_replace('/(fnc|cl)=[A-Za-z0-9_]*&?/', '', $queryString); - } + if ($queryString === null) { + return ''; + } - private function unwrapResponse(ResponseInterface $response): string - { - return $response->getBody()->getContents(); + return preg_replace('/(fnc|cl)=[A-Za-z0-9_]*&?/', '', $queryString); } } diff --git a/src/Event/EnrichProxyDataEvent.php b/src/Event/EnrichProxyDataEvent.php new file mode 100644 index 00000000..5b3ff9e2 --- /dev/null +++ b/src/Event/EnrichProxyDataEvent.php @@ -0,0 +1,26 @@ +data = $data; + } + + public function getData(): array + { + return $this->data; + } + + public function setData(array $data): void + { + $this->data = $data; + } +} diff --git a/src/Model/Config/Communication.php b/src/Model/Config/Communication.php index 3acc3821..0e279ed2 100755 --- a/src/Model/Config/Communication.php +++ b/src/Model/Config/Communication.php @@ -70,12 +70,9 @@ protected function getLocale(string $abbr): string protected function getServerUrl(): string { - return (string) $this->moduleSettingService->getString('ffServerUrl', 'ffwebcomponents'); - - // TODO Refactor proxy - // return (string) $this->moduleSettingService->getBoolean('ffUseProxy', 'ffwebcomponents') ? - // 'index.php' : - // (string) $this->moduleSettingService->getString('ffServerUrl', 'ffwebcomponents'); + return (string) $this->moduleSettingService->getBoolean('ffUseProxy', 'ffwebcomponents') ? + '' : + (string) $this->moduleSettingService->getString('ffServerUrl', 'ffwebcomponents'); } protected function getCategoryPath(Category $category): string @@ -118,8 +115,6 @@ protected function getApiVersion(): string private function useProxy(): bool { - return false; - // TODO Refactor proxy - // return (bool) $this->moduleSettingService->getBoolean('ffUseProxy', 'ffwebcomponents'); + return (bool) $this->moduleSettingService->getBoolean('ffUseProxy', 'ffwebcomponents'); } } diff --git a/src/Subscriber/EnrichProxyDataEventSubscriber.php b/src/Subscriber/EnrichProxyDataEventSubscriber.php new file mode 100644 index 00000000..002bde2a --- /dev/null +++ b/src/Subscriber/EnrichProxyDataEventSubscriber.php @@ -0,0 +1,24 @@ + 'enrichData']; + } + + public function enrichData(EnrichProxyDataEvent $event): void + { + $data = $event->getData(); + $data['example_data'] = [ + 'some_data' => 'data_1', + 'some_data2' => 'data_2', + ]; + $event->setData($data); + } +} diff --git a/tests/Unit/Export/Exporter/ExportEntitiesTest.php b/tests/Unit/Export/Exporter/ExportEntitiesTest.php index ac02bbfd..255a1479 100644 --- a/tests/Unit/Export/Exporter/ExportEntitiesTest.php +++ b/tests/Unit/Export/Exporter/ExportEntitiesTest.php @@ -23,6 +23,9 @@ class ExportEntitiesTest extends TestCase /** @var CsvVariant */ private $stream; + /** @var string[] */ + private array $columns; + protected function setUp(): void { $this->columns = [ diff --git a/views/twig/extensions/themes/default/layout/base.html.twig b/views/twig/extensions/themes/default/layout/base.html.twig index 415ebfe9..ff6f191f 100644 --- a/views/twig/extensions/themes/default/layout/base.html.twig +++ b/views/twig/extensions/themes/default/layout/base.html.twig @@ -17,7 +17,7 @@ document.addEventListener(`ffCoreReady`, ({ factfinder, init, initialSearch }) => { init({ ff: { - url: `{{oViewConf.getFFStringConfigParam('ffServerUrl')}}`, + url: `{{communicationParams['url']}}`, channel: `{{communicationParams['channel']}}`, apiKey: `{{oViewConf.getFFStringConfigParam('ffApiKey')}}`, }, @@ -137,25 +137,6 @@ } } }); - -{# TODO Implement proxy #} -{# {% if oViewConf.getFFBoolConfigParam('ffUseProxy') %}#} -{# factfinder.__experimental.sandboxMode.enable = true;#} - -{# factfinder.eventAggregator.addBeforeDispatchingCallback(function (event) {#} -{# event.cl = 'search_result';#} -{# event.fnc = 'proxy';#} -{# });#} - -{# document.addEventListener('ffUrlWrite', function (event, historyState) {#} -{# let url = event.url.replace(/([?&]?)fnc=proxy/, '');#} -{# {% if oView.getClassKey() == "alist" %}#} -{# url = url.replace(/[?&]?cl=search_result/, '');#} -{# {% endif %}#} -{# history.replaceState(historyState, "", url);#} -{# });#} -{# {% endif %}#} - {% if oView.getClassKey() == "details" %} From 97fd60a3188b4b505221d09a9634c9105414fec0 Mon Sep 17 00:00:00 2001 From: robertsaternus Date: Tue, 12 Aug 2025 13:10:00 +0200 Subject: [PATCH 2/3] Fix CI --- src/Controller/SearchResultController.php | 8 +++++--- src/Event/EnrichProxyDataEvent.php | 1 + src/Subscriber/EnrichProxyDataEventSubscriber.php | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Controller/SearchResultController.php b/src/Controller/SearchResultController.php index 841eb22c..458c0938 100644 --- a/src/Controller/SearchResultController.php +++ b/src/Controller/SearchResultController.php @@ -42,19 +42,20 @@ public function proxy(): void switch ($httpMethod) { case 'GET': - $query = (string) $this->removeOxidParams(parse_url($currentUrl, PHP_URL_QUERY)); + $query = (string) $this->removeOxidParams(parse_url($currentUrl, PHP_URL_QUERY)); $response = $client->request('GET', $endpoint . '?' . $query); + break; case 'POST': $rawBody = file_get_contents('php://input'); - $body = json_decode($rawBody, true) ?: []; + $body = json_decode($rawBody, true) ?: []; if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception('Invalid JSON in request body'); } $response = $client->request('POST', $endpoint, [ - 'body' => json_encode($body), + 'body' => json_encode($body), 'headers' => ['Content-Type' => 'application/json'], ]); @@ -99,6 +100,7 @@ protected function getConfigParam(string $key): string private function getEndpoint(string $currentUrl): string { preg_match('#/([A-Za-z]+\.ff|rest/v[^?]*)#', $currentUrl, $match); + return $match[1] ?? ''; } diff --git a/src/Event/EnrichProxyDataEvent.php b/src/Event/EnrichProxyDataEvent.php index 5b3ff9e2..c1207db4 100644 --- a/src/Event/EnrichProxyDataEvent.php +++ b/src/Event/EnrichProxyDataEvent.php @@ -1,4 +1,5 @@ Date: Tue, 12 Aug 2025 13:16:51 +0200 Subject: [PATCH 3/3] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebead9a6..898a2971 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ Without these rules any request will lead to 404. APACHE ```apache - RewriteRule ^(rest/v[0-9].*)$ index.php [L] + RewriteRule ^rest/v5/(.*)$ index.php?cl=search_result&fnc=proxy&$1 [L] ``` **Note:** Sending each request to FACT-Finder instance trough Shopware, you lose on performance as each request need to be handled first by HTTP server and then, by Shopware itself. This additional traffic could be easily avoided by not activating this feature if there's no clear reason to use it.