Skip to content

Commit b1176a7

Browse files
committed
FFWEB-3437: Add proxy feature
Add proxy feature with fallback and data enrichment mechanism
1 parent 2cabf0d commit b1176a7

9 files changed

Lines changed: 158 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
## Unreleased
33
### Add
44
- Add cart button for product list
5+
- Implement proxy - more information in README file
56

67
### Change
78
- Support tab navigation for search, suggest and paging components

README.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ For more advanced features please check our official [WebComponnents documentati
2525
- [Test FTP Connection Button](#test-ftp-connection)
2626
- [Update Field Roles Button](#update-field-roles)
2727
- [Advanced Settings](#advanced-settings)
28+
- [Proxy](#proxy)
2829
- [Features Settings](#features-settings)
2930
- [Using FACT-Finder® on category pages](#using-fact-finder-on-category-pages)
3031
- [Feed Settings](#feed-settings)
@@ -39,6 +40,8 @@ For more advanced features please check our official [WebComponnents documentati
3940
- [Click on Product](#click-on-product)
4041
- [Add Product to Cart](#add-product-to-cart)
4142
- [Place an Order](#place-an-order)
43+
- [Modification Examples](#modifications-examples)
44+
- [Enrich data received from FACT-Finder](#enrich-data-received-from-fact-finder)
4245
- [Contribute](#contribute)
4346
- [License](#license)
4447

@@ -121,10 +124,44 @@ This functionality uses form data, so there is no need to save first.
121124

122125
### Advanced Settings
123126
![Advanced Settings](docs/assets/advanced-settings.png "Advanced settings")
124-
* `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.
127+
* `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.
128+
* `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.
129+
**Note:** If you plan to use proxy, consider reading below paragraph as it requires full instruction how to enable it properly.
125130
* `How to count single click on "Add to cart" button?` - select how would you like to count single click on "Add to cart" button
126131
* `Send the SID as userId when user not logged in?`
127132

133+
#### Proxy
134+
Proxy feature adds a oxid controller which serves as a middleware between Web Components and FACT-Finder®.
135+
The data flow with proxy enabled is illustrated by the graph below.
136+
![Communication Overview](docs/assets/communication-overview.png "Communication Overview")
137+
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
138+
details about implementation [here](#enrich-data-received-from-fact-finder-in-proxycontroller).
139+
In addition, if forwarded request does not result with a correct response, you can implement fallback strategy, starting from this point.
140+
141+
```php
142+
//src/Controller/SearchResultController.php:84
143+
protected function fallback(): void
144+
{
145+
//this function could be used to implement fallback logic in case of any communication error.
146+
$this->showJsonAndExit('Error: Unable to process the request.');
147+
}
148+
```
149+
150+
To enable proxy you need to change your HTTP server configuration by adding rewrite rules.
151+
This is necessary because Web Components appends a URL parts to the base URL making it unreadable by the Oxid.
152+
This is because Oxid use query parameters `cl` and `fnc` to instantiate specific controller and execute its function.
153+
There is no routing that use url parts, hence any AJAX requests must target index.php file with the aforementioned parameters.
154+
Without these rules any request will lead to 404.
155+
156+
APACHE
157+
158+
```apache
159+
RewriteRule ^(rest/v[0-9].*)$ index.php [L]
160+
```
161+
162+
**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.
163+
164+
128165
### Features Settings
129166
![Features Settings](docs/assets/features-settings.png "Features settings")
130167

@@ -245,6 +282,35 @@ We offer a `registerAddToCartListener` function which helps to register `click`
245282
### Place an Order
246283
This event is tracked by the `ff-checkout-tracking` element which is implemented on order confirmation page
247284

285+
## Modifications Examples
286+
287+
### Enrich data received from FACT-Finder
288+
289+
```php
290+
291+
use Omikron\FactFinder\Oxid\Event\EnrichProxyDataEvent;
292+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
293+
294+
class EnrichProxyDataEventSubscriber implements EventSubscriberInterface
295+
{
296+
public static function getSubscribedEvents(): array
297+
{
298+
return [EnrichProxyDataEvent::class => 'enrichData'];
299+
}
300+
301+
public function enrichData(EnrichProxyDataEvent $event): void
302+
{
303+
$data = $event->getData();
304+
$data['example_data'] = [
305+
'some_data' => 'data_1',
306+
'next_data' => 'data_2',
307+
];
308+
$event->setData($data);
309+
}
310+
}
311+
312+
```
313+
248314
## Contribute
249315
For more information, click [here](.github/CONTRIBUTING.md)
250316

metadata.php

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,13 @@
8383
'value' => true,
8484
'position' => $settingPosition++,
8585
],
86-
// TODO Refactor proxy
87-
// [
88-
// 'group' => 'ffAdvanced',
89-
// 'name' => 'ffUseProxy',
90-
// 'type' => 'bool',
91-
// 'value' => false,
92-
// 'position' => $settingPosition++,
93-
// ],
86+
[
87+
'group' => 'ffAdvanced',
88+
'name' => 'ffUseProxy',
89+
'type' => 'bool',
90+
'value' => false,
91+
'position' => $settingPosition++,
92+
],
9493
[
9594
'group' => 'ffAdvanced',
9695
'name' => 'ffSidAsUserId',

src/Controller/SearchResultController.php

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66

77
use Omikron\FactFinder\Communication\Client\ClientBuilder;
88
use Omikron\FactFinder\Communication\Version;
9+
use Omikron\FactFinder\Oxid\Event\EnrichProxyDataEvent;
10+
use Omikron\FactFinder\Oxid\Subscriber\EnrichProxyDataEventSubscriber;
911
use OxidEsales\Eshop\Application\Controller\FrontendController;
1012
use OxidEsales\Eshop\Core\Registry;
1113
use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory;
1214
use OxidEsales\EshopCommunity\Internal\Framework\Module\Facade\ModuleSettingServiceInterface;
13-
use Psr\Http\Message\ResponseInterface;
15+
use Symfony\Component\EventDispatcher\EventDispatcher;
1416

1517
class SearchResultController extends FrontendController
1618
{
@@ -41,19 +43,30 @@ public function proxy(): void
4143
switch ($httpMethod) {
4244
case 'GET':
4345
$query = (string) $this->removeOxidParams(parse_url($currentUrl, PHP_URL_QUERY));
44-
$this->showJsonAndExit($this->unwrapResponse($client->request('GET', $endpoint . '?' . $query)));
45-
46+
$response = $client->request('GET', $endpoint . '?' . $query);
4647
break;
4748
case 'POST':
48-
$this->showJsonAndExit($this->unwrapResponse($client->request('POST', $endpoint, [
49-
'body' => $this->getRequest()->getContent(),
49+
$rawBody = file_get_contents('php://input');
50+
$body = json_decode($rawBody, true) ?: [];
51+
52+
if (json_last_error() !== JSON_ERROR_NONE) {
53+
throw new \Exception('Invalid JSON in request body');
54+
}
55+
56+
$response = $client->request('POST', $endpoint, [
57+
'body' => json_encode($body),
5058
'headers' => ['Content-Type' => 'application/json'],
51-
])));
59+
]);
5260

5361
break;
5462
default:
5563
throw new \Exception(sprintf('HTTP Method %s is not supported', $httpMethod));
5664
}
65+
$eventDispatcher = new EventDispatcher();
66+
$eventDispatcher->addSubscriber(new EnrichProxyDataEventSubscriber());
67+
$event = new EnrichProxyDataEvent(json_decode($response->getBody()->getContents(), true) ?? []);
68+
$eventDispatcher->dispatch($event, EnrichProxyDataEvent::class);
69+
$this->showJsonAndExit(json_encode($event->getData()));
5770
} catch (\Exception $e) {
5871
$this->fallback();
5972
echo json_encode(['error' => $e->getMessage()]);
@@ -71,7 +84,7 @@ protected function showJsonAndExit(string $jsonResponse): void
7184
protected function fallback(): void
7285
{
7386
// this function could be used to implement fallback logic in case of any communication error.
74-
$this->showJsonAndExit('');
87+
$this->showJsonAndExit('Error: Unable to process the request.');
7588
}
7689

7790
protected function getConfigParam(string $key): string
@@ -89,13 +102,12 @@ private function getEndpoint(string $currentUrl): string
89102
return $match[1] ?? '';
90103
}
91104

92-
private function removeOxidParams(string $queryString): string
105+
private function removeOxidParams(?string $queryString): string
93106
{
94-
return preg_replace('/(fnc|cl)=[A-Za-z0-9_]*&?/', '', $queryString);
95-
}
107+
if ($queryString === null) {
108+
return '';
109+
}
96110

97-
private function unwrapResponse(ResponseInterface $response): string
98-
{
99-
return $response->getBody()->getContents();
111+
return preg_replace('/(fnc|cl)=[A-Za-z0-9_]*&?/', '', $queryString);
100112
}
101113
}

src/Event/EnrichProxyDataEvent.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Omikron\FactFinder\Oxid\Event;
5+
6+
use Symfony\Contracts\EventDispatcher\Event;
7+
8+
class EnrichProxyDataEvent extends Event
9+
{
10+
private array $data;
11+
12+
public function __construct(array $data)
13+
{
14+
$this->data = $data;
15+
}
16+
17+
public function getData(): array
18+
{
19+
return $this->data;
20+
}
21+
22+
public function setData(array $data): void
23+
{
24+
$this->data = $data;
25+
}
26+
}

src/Model/Config/Communication.php

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,9 @@ protected function getLocale(string $abbr): string
7070

7171
protected function getServerUrl(): string
7272
{
73-
return (string) $this->moduleSettingService->getString('ffServerUrl', 'ffwebcomponents');
74-
75-
// TODO Refactor proxy
76-
// return (string) $this->moduleSettingService->getBoolean('ffUseProxy', 'ffwebcomponents') ?
77-
// 'index.php' :
78-
// (string) $this->moduleSettingService->getString('ffServerUrl', 'ffwebcomponents');
73+
return (string) $this->moduleSettingService->getBoolean('ffUseProxy', 'ffwebcomponents') ?
74+
'' :
75+
(string) $this->moduleSettingService->getString('ffServerUrl', 'ffwebcomponents');
7976
}
8077

8178
protected function getCategoryPath(Category $category): string
@@ -118,8 +115,6 @@ protected function getApiVersion(): string
118115

119116
private function useProxy(): bool
120117
{
121-
return false;
122-
// TODO Refactor proxy
123-
// return (bool) $this->moduleSettingService->getBoolean('ffUseProxy', 'ffwebcomponents');
118+
return (bool) $this->moduleSettingService->getBoolean('ffUseProxy', 'ffwebcomponents');
124119
}
125120
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Omikron\FactFinder\Oxid\Subscriber;
5+
use Omikron\FactFinder\Oxid\Event\EnrichProxyDataEvent;
6+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
7+
8+
class EnrichProxyDataEventSubscriber implements EventSubscriberInterface
9+
{
10+
public static function getSubscribedEvents(): array
11+
{
12+
return [EnrichProxyDataEvent::class => 'enrichData'];
13+
}
14+
15+
public function enrichData(EnrichProxyDataEvent $event): void
16+
{
17+
$data = $event->getData();
18+
$data['example_data'] = [
19+
'some_data' => 'data_1',
20+
'some_data2' => 'data_2',
21+
];
22+
$event->setData($data);
23+
}
24+
}

tests/Unit/Export/Exporter/ExportEntitiesTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ class ExportEntitiesTest extends TestCase
2323
/** @var CsvVariant */
2424
private $stream;
2525

26+
/** @var string[] */
27+
private array $columns;
28+
2629
protected function setUp(): void
2730
{
2831
$this->columns = [

views/twig/extensions/themes/default/layout/base.html.twig

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
document.addEventListener(`ffCoreReady`, ({ factfinder, init, initialSearch }) => {
1818
init({
1919
ff: {
20-
url: `{{oViewConf.getFFStringConfigParam('ffServerUrl')}}`,
20+
url: `{{communicationParams['url']}}`,
2121
channel: `{{communicationParams['channel']}}`,
2222
apiKey: `{{oViewConf.getFFStringConfigParam('ffApiKey')}}`,
2323
},
@@ -137,25 +137,6 @@
137137
}
138138
}
139139
});
140-
141-
{# TODO Implement proxy #}
142-
{# {% if oViewConf.getFFBoolConfigParam('ffUseProxy') %}#}
143-
{# factfinder.__experimental.sandboxMode.enable = true;#}
144-
145-
{# factfinder.eventAggregator.addBeforeDispatchingCallback(function (event) {#}
146-
{# event.cl = 'search_result';#}
147-
{# event.fnc = 'proxy';#}
148-
{# });#}
149-
150-
{# document.addEventListener('ffUrlWrite', function (event, historyState) {#}
151-
{# let url = event.url.replace(/([?&]?)fnc=proxy/, '');#}
152-
{# {% if oView.getClassKey() == "alist" %}#}
153-
{# url = url.replace(/[?&]?cl=search_result/, '');#}
154-
{# {% endif %}#}
155-
{# history.replaceState(historyState, "", url);#}
156-
{# });#}
157-
{# {% endif %}#}
158-
159140
</script>
160141
161142
{% if oView.getClassKey() == "details" %}

0 commit comments

Comments
 (0)