diff --git a/src/bundle/Resources/config/services/factory.yaml b/src/bundle/Resources/config/services/factory.yaml index 87651350..b2720b4f 100644 --- a/src/bundle/Resources/config/services/factory.yaml +++ b/src/bundle/Resources/config/services/factory.yaml @@ -4,12 +4,22 @@ services: autoconfigure: true public: false - EzSystems\EzRecommendationClient\Factory\: - resource: '../../../../src/lib/Factory/*' + EzSystems\EzRecommendationClient\Factory\EzRecommendationClientAPIFactory: ~ - EzSystems\EzRecommendationClient\Factory\ExportParametersFactory: ~ + EzSystems\EzRecommendationClient\Factory\FakeRequestFactory: ~ - EzSystems\EzRecommendationClient\Factory\ConfigurableExportParametersFactory: + EzSystems\EzRecommendationClient\Factory\RequestFactoryInterface: + '@EzSystems\EzRecommendationClient\Factory\FakeRequestFactory' + + EzSystems\EzRecommendationClient\Factory\TokenFactory: ~ + + EzSystems\EzRecommendationClient\Factory\TokenFactoryInterface: + '@EzSystems\EzRecommendationClient\Factory\TokenFactory' + + Ibexa\Personalization\Factory\Export\ParametersFactory: arguments: - $innerService: '@EzSystems\EzRecommendationClient\Factory\ExportParametersFactory' - $credentialsResolver: '@EzSystems\EzRecommendationClient\Config\ExportCredentialsResolver' + $siteAccessService: '@ezpublish.siteaccess_service' + $credentialsResolver: '@EzSystems\EzRecommendationClient\Config\EzRecommendationClientCredentialsResolver' + + Ibexa\Personalization\Factory\Export\ParametersFactoryInterface: + '@Ibexa\Personalization\Factory\Export\ParametersFactory' diff --git a/src/bundle/Resources/config/services/values.yaml b/src/bundle/Resources/config/services/values.yaml index f4b7eb52..af6aedaf 100644 --- a/src/bundle/Resources/config/services/values.yaml +++ b/src/bundle/Resources/config/services/values.yaml @@ -4,9 +4,6 @@ services: autoconfigure: true public: false - EzSystems\EzRecommendationClient\Value\: - resource: '../../../../src/lib/Value/*' - EzSystems\EzRecommendationClient\Value\ContentDataVisitor: tags: - { name: ezpublish_rest.output.value_object_visitor, type: EzSystems\EzRecommendationClient\Value\ContentData } diff --git a/src/lib/Exception/CredentialsNotFoundException.php b/src/lib/Exception/CredentialsNotFoundException.php index e9e87eba..8dc1d3fe 100644 --- a/src/lib/Exception/CredentialsNotFoundException.php +++ b/src/lib/Exception/CredentialsNotFoundException.php @@ -8,10 +8,22 @@ namespace EzSystems\EzRecommendationClient\Exception; -class CredentialsNotFoundException extends NotFoundException +use Throwable; + +final class CredentialsNotFoundException extends NotFoundException { - public function __construct(int $code = 0, ?Throwable $previous = null) + public function __construct(?string $siteAccess = null, int $code = 0, Throwable $previous = null) { - parent::__construct('Credentials for recommendation client are not set', $code, $previous); + $message = 'Credentials for recommendation client are not set'; + + if (null !== $siteAccess) { + $message .= ' for siteAccess: ' . $siteAccess; + } + + parent::__construct( + $message, + $code, + $previous + ); } } diff --git a/src/lib/Exception/ExportCredentialsNotFoundException.php b/src/lib/Exception/ExportCredentialsNotFoundException.php deleted file mode 100644 index 986b9ccf..00000000 --- a/src/lib/Exception/ExportCredentialsNotFoundException.php +++ /dev/null @@ -1,13 +0,0 @@ - $missingParameters + */ + public function __construct(array $missingParameters, string $type, int $code = 0, Throwable $previous = null) + { + $parameters = []; + + if ($type === ParametersFactoryInterface::COMMAND_TYPE) { + foreach ($missingParameters as $parameter) { + $parameters[] = str_replace('_', '-', $parameter); + } + } else { + $parameters = $missingParameters; + } + + parent::__construct( + sprintf( + 'Required parameters: %s are missing', + implode(', ', $parameters) + ), + $code, + $previous + ); + } } diff --git a/src/lib/Factory/ConfigurableExportParametersFactory.php b/src/lib/Factory/ConfigurableExportParametersFactory.php deleted file mode 100644 index f1444417..00000000 --- a/src/lib/Factory/ConfigurableExportParametersFactory.php +++ /dev/null @@ -1,130 +0,0 @@ -credentialsResolver = $credentialsResolver; - $this->configResolver = $configResolver; - $this->siteAccessHelper = $siteAccessHelper; - - parent::__construct($innerService); - } - - /** - * {@inheritdoc} - * - * @throws \EzSystems\EzRecommendationClient\Exception\ExportCredentialsNotFoundException - * @throws \EzSystems\EzRecommendationClient\Exception\MissingExportParameterException - * @throws \eZ\Publish\Core\Base\Exceptions\NotFoundException - */ - public function create(array $properties = []): ExportParameters - { - if (!empty($this->getMissingRequiredOptions($properties))) { - throw new MissingExportParameterException(sprintf( - 'Required parameters %s are missing', - implode(', ', $this->getMissingRequiredOptions($properties)) - )); - } - - $properties['siteaccess'] = $properties['siteaccess'] ?? $this->getSiteAccess(); - - if (!isset($properties['customerId']) && !isset($properties['licenseKey'])) { - /** @var \EzSystems\EzRecommendationClient\Value\Config\ExportCredentials $credentials */ - $credentials = $this->credentialsResolver->getCredentials($properties['siteaccess']); - - if (!$this->credentialsResolver->hasCredentials()) { - throw new ExportCredentialsNotFoundException(sprintf( - 'Recommendation client export credentials are not set for siteAccess: %s', - $properties['siteaccess'] - )); - } - - $properties['customerId'] = $credentials->getLogin(); - $properties['licenseKey'] = $credentials->getPassword(); - } - - $properties['host'] = $properties['host'] ?? $this->getHostUri($properties['siteaccess']); - $properties['webHook'] = $properties['webHook'] ?? $this->getWebHook( - (int)$properties['customerId'], - $properties['siteaccess'] - ); - - return $this->innerService->create($properties); - } - - private function getSiteAccess(): string - { - return current($this->siteAccessHelper->getSiteAccesses()); - } - - private function getHostUri(string $siteAccess): string - { - return $this->configResolver->getParameter( - 'host_uri', - Parameters::NAMESPACE, - $siteAccess - ); - } - - private function getWebHook(int $customerId, string $siteAccess): string - { - return $this->configResolver->getParameter( - 'api.notifier.endpoint', - Parameters::NAMESPACE, - $siteAccess - ) . sprintf(Notifier::ENDPOINT_PATH, $customerId); - } - - private function getMissingRequiredOptions(array $options): array - { - $missingOptions = []; - - if (isset($options['customerId']) || isset($options['licenseKey'])) { - if (!isset($options['customerId'])) { - $missingOptions[] = 'customerId'; - } - - if (!isset($options['licenseKey'])) { - $missingOptions[] = 'licenseKey'; - } - - if (!isset($options['siteaccess'])) { - $missingOptions[] = 'siteaccess'; - } - } - - return $missingOptions; - } -} diff --git a/src/lib/Factory/Export/ParametersFactory.php b/src/lib/Factory/Export/ParametersFactory.php new file mode 100644 index 00000000..0bfdde7b --- /dev/null +++ b/src/lib/Factory/Export/ParametersFactory.php @@ -0,0 +1,341 @@ +credentialsResolver = $credentialsResolver; + $this->configResolver = $configResolver; + $this->siteAccessService = $siteAccessService; + } + + /** + * @throws \EzSystems\EzRecommendationClient\Exception\MissingExportParameterException + * @throws \EzSystems\EzRecommendationClient\Exception\InvalidArgumentException + */ + public function create(array $options, string $type): Parameters + { + $this->exportParametersType = $type; + + return Parameters::fromArray( + $this->getExportParameters($options) + ); + } + + /** + * @phpstan-param array{ + * item_type_identifier_list: string, + * languages: string, + * page_size: string, + * customer_id: ?string, + * license_key: ?string, + * siteaccess: ?string, + * web_hook: ?string, + * host: ?string, + * } $options + * + * @phpstan-return array{ + * item_type_identifier_list: string, + * languages: string, + * page_size: string, + * customer_id: string, + * license_key: string, + * siteaccess: string, + * web_hook: string, + * host: string, + * } + * + * @throws \EzSystems\EzRecommendationClient\Exception\MissingExportParameterException + * @throws \EzSystems\EzRecommendationClient\Exception\InvalidArgumentException + */ + private function getExportParameters(array $options): array + { + if (isset($options['siteaccess']) && !$this->hasConfiguredSiteAccess($options['siteaccess'])) { + throw new InvalidArgumentException(sprintf( + 'SiteAccess %s doesn\'t exists', + $options['siteaccess'] + )); + } + + $configuration = $this->countConfiguredCustomerSettings() === 1 + ? $this->getCredentialsAndSiteAccessForSingleConfiguration($options) + : $this->getCredentialsAndSiteAccessForMultiCustomerConfiguration($options); + + $siteAccess = $configuration['siteaccess']; + $customerId = $configuration['customer_id']; + + return [ + 'customer_id' => $customerId, + 'license_key' => $configuration['license_key'], + 'siteaccess' => $configuration['siteaccess'], + 'item_type_identifier_list' => $options['item_type_identifier_list'], + 'languages' => $options['languages'], + 'host' => $options['host'] ?? $this->getHostUri($siteAccess), + 'web_hook' => $options['web_hook'] ?? $this->getWebHook( + (int)$customerId, + $siteAccess + ), + 'page_size' => $options['page_size'], + ]; + } + + private function hasConfiguredSiteAccess(string $siteAccessName): bool + { + foreach ($this->siteAccessService->getAll() as $siteAccess) { + if ($siteAccessName === $siteAccess->name) { + return true; + } + } + + return false; + } + + private function countConfiguredCustomerSettings(): int + { + $configuredCounter = 0; + + foreach ($this->siteAccessService->getAll() as $siteAccess) { + if ($this->credentialsResolver->hasCredentials($siteAccess->name)) { + ++$configuredCounter; + } + } + + return $configuredCounter; + } + + /** + * @phpstan-param array{ + * item_type_identifier_list: string, + * languages: string, + * page_size: string, + * customer_id: ?string, + * license_key: ?string, + * siteaccess: ?string, + * web_hook: ?string, + * host: ?string, + * } $options + * + * @phpstan-return array{ + * customer_id: string, + * license_key: string, + * siteaccess: string, + * } + * + * @throws \EzSystems\EzRecommendationClient\Exception\MissingExportParameterException + */ + private function getCredentialsAndSiteAccessForSingleConfiguration(array $options): array + { + if ( + (isset($options['customer_id']) || isset($options['license_key'])) + && $this->hasMissingRequiredOptions($options) + ) { + throw new MissingExportParameterException( + $this->getMissingRequiredOptions($options), + $this->exportParametersType + ); + } + + if (isset($options['customer_id'], $options['license_key'], $options['siteaccess'])) { + return [ + 'customer_id' => $options['customer_id'], + 'license_key' => $options['license_key'], + 'siteaccess' => $options['siteaccess'], + ]; + } + + return $this->getCredentialsAndSiteAccess(); + } + + /** + * @phpstan-return array{ + * customer_id: string, + * license_key: string, + * siteaccess: string, + * } + */ + private function getCredentialsAndSiteAccess(): array + { + $siteAccess = $this->getSingleConfiguredSiteAccess(); + $configuredCredentials = $this->getCredentialsForScope($siteAccess); + + return [ + 'customer_id' => $configuredCredentials['customer_id'], + 'license_key' => $configuredCredentials['license_key'], + 'siteaccess' => $siteAccess, + ]; + } + + private function getSingleConfiguredSiteAccess(): string + { + foreach ($this->siteAccessService->getAll() as $siteAccess) { + if ($this->credentialsResolver->hasCredentials($siteAccess->name)) { + return $siteAccess->name; + } + } + + throw new CredentialsNotFoundException(); + } + + /** + * @phpstan-return array{ + * customer_id: string, + * license_key: string, + * } + */ + private function getCredentialsForScope(string $siteAccess): array + { + if (!$this->credentialsResolver->hasCredentials($siteAccess)) { + throw new CredentialsNotFoundException($siteAccess); + } + + /** @var \EzSystems\EzRecommendationClient\Value\Config\EzRecommendationClientCredentials $credentials */ + $credentials = $this->credentialsResolver->getCredentials($siteAccess); + /** @var int $customerId */ + $customerId = $credentials->getCustomerId(); + /** @var string $licenseKey */ + $licenseKey = $credentials->getLicenseKey(); + + return [ + 'customer_id' => (string)$customerId, + 'license_key' => $licenseKey, + ]; + } + + /** + * @phpstan-param array{ + * item_type_identifier_list: string, + * languages: string, + * page_size: string, + * customer_id: ?string, + * license_key: ?string, + * siteaccess: ?string, + * web_hook: ?string, + * host: ?string, + * } $options + * + * @phpstan-return array{ + * customer_id: string, + * license_key: string, + * siteaccess: string, + * } + * + * @throws \EzSystems\EzRecommendationClient\Exception\MissingExportParameterException + */ + private function getCredentialsAndSiteAccessForMultiCustomerConfiguration(array $options): array + { + if ($this->hasMissingRequiredOptions($options)) { + throw new MissingExportParameterException( + $this->getMissingRequiredOptions($options), + $this->exportParametersType + ); + } + + /** @var string $customerId */ + $customerId = $options['customer_id']; + /** @var string $licenseKey */ + $licenseKey = $options['license_key']; + /** @var string $siteAccess */ + $siteAccess = $options['siteaccess']; + + return [ + 'customer_id' => $customerId, + 'license_key' => $licenseKey, + 'siteaccess' => $siteAccess, + ]; + } + + private function getHostUri(string $siteAccess): string + { + return $this->configResolver->getParameter( + self::HOST_URI_PARAMETER_NAME, + self::PARAMETERS_NAMESPACE, + $siteAccess + ); + } + + private function getWebHook(int $customerId, string $siteAccess): string + { + return $this->configResolver->getParameter( + self::NOTIFIER_ENDPOINT_PARAMETER_NAME, + self::PARAMETERS_NAMESPACE, + $siteAccess + ) . sprintf(Notifier::ENDPOINT_PATH, $customerId); + } + + /** + * @phpstan-param array{ + * item_type_identifier_list: string, + * languages: string, + * page_size: string, + * customer_id: ?string, + * license_key: ?string, + * siteaccess: ?string, + * web_hook: ?string, + * host: ?string, + * } $options + */ + private function hasMissingRequiredOptions(array $options): bool + { + return !empty($this->getMissingRequiredOptions($options)); + } + + /** + * Checks if one of the required option is missing. + * For single configuration user doesn't need to provide any of these options, + * but if one of them is provided then rest of it are needed. + * + * @phpstan-param array{ + * item_type_identifier_list: string, + * languages: string, + * page_size: string, + * customer_id: ?string, + * license_key: ?string, + * siteaccess: ?string, + * web_hook: ?string, + * host: ?string, + * } $options + * + * @return array + */ + private function getMissingRequiredOptions(array $options): array + { + return array_diff(self::REQUIRED_OPTIONS, array_keys($options)); + } +} diff --git a/src/lib/Factory/Export/ParametersFactoryInterface.php b/src/lib/Factory/Export/ParametersFactoryInterface.php new file mode 100644 index 00000000..1280f9b4 --- /dev/null +++ b/src/lib/Factory/Export/ParametersFactoryInterface.php @@ -0,0 +1,32 @@ +siteAccessHelper = $siteAccessHelper; - } - - public function create(array $properties = []): ExportParameters - { - $properties['contentTypeIdList'] = ParamsConverterHelper::getIdListFromString( - $properties['contentTypeIdList'] - ); - $properties['languages'] = $this->siteAccessHelper->getLanguages( - (int)$properties['customerId'], - $properties['siteaccess'] - ); - - isset($properties['fields']) ? ParamsConverterHelper::getArrayFromString($properties['fields']) : null; - - return new ExportParameters($properties); - } -} diff --git a/src/lib/Factory/ExportParametersFactoryInterface.php b/src/lib/Factory/ExportParametersFactoryInterface.php deleted file mode 100644 index 677c4d2b..00000000 --- a/src/lib/Factory/ExportParametersFactoryInterface.php +++ /dev/null @@ -1,21 +0,0 @@ -innerService = $innerService; - } - - /** - * {@inheritdoc} - */ - public function create(array $properties = []): ExportParameters - { - return $this->innerService->create($properties); - } -} diff --git a/src/lib/Value/Export/Parameters.php b/src/lib/Value/Export/Parameters.php new file mode 100644 index 00000000..9f712f1b --- /dev/null +++ b/src/lib/Value/Export/Parameters.php @@ -0,0 +1,128 @@ + */ + public array $itemTypeIdentifierList; + + /** @var array */ + public array $languages; + + public string $siteaccess; + + public string $webHook; + + public string $host; + + public int $pageSize; + + /** + * @param array $itemTypeIdentifierList + * @param array $languages + */ + private function __construct( + string $customerId, + string $licenseKey, + array $itemTypeIdentifierList, + array $languages, + string $siteaccess, + string $webHook, + string $host, + int $pageSize + ) { + $this->customerId = $customerId; + $this->licenseKey = $licenseKey; + $this->itemTypeIdentifierList = $itemTypeIdentifierList; + $this->languages = $languages; + $this->siteaccess = $siteaccess; + $this->webHook = $webHook; + $this->host = $host; + $this->pageSize = $pageSize; + } + + public function getCustomerId(): string + { + return $this->customerId; + } + + public function getLicenseKey(): string + { + return $this->licenseKey; + } + + /** + * @return array + */ + public function getItemTypeIdentifierList(): array + { + return $this->itemTypeIdentifierList; + } + + /** + * @return array + */ + public function getLanguages(): array + { + return $this->languages; + } + + public function getSiteaccess(): string + { + return $this->siteaccess; + } + + public function getWebHook(): string + { + return $this->webHook; + } + + public function getHost(): string + { + return $this->host; + } + + public function getPageSize(): int + { + return $this->pageSize; + } + + /** + * @phpstan-param array{ + * customer_id: string, + * license_key: string, + * item_type_identifier_list: string, + * languages: string, + * siteaccess: string, + * web_hook: string, + * host: string, + * page_size: string, + * } $properties + */ + public static function fromArray(array $properties): self + { + return new self( + $properties['customer_id'], + $properties['license_key'], + ParamsConverterHelper::getArrayFromString($properties['item_type_identifier_list']), + ParamsConverterHelper::getArrayFromString($properties['languages']), + $properties['siteaccess'], + $properties['web_hook'], + $properties['host'], + (int)$properties['page_size'], + ); + } +} diff --git a/tests/lib/Factory/Export/ParametersFactoryTest.php b/tests/lib/Factory/Export/ParametersFactoryTest.php new file mode 100644 index 00000000..debefd5a --- /dev/null +++ b/tests/lib/Factory/Export/ParametersFactoryTest.php @@ -0,0 +1,255 @@ +credentialsResolver = $this->createMock(CredentialsResolverInterface::class); + $this->configResolver = $this->createMock(ConfigResolverInterface::class); + $this->siteAccessService = $this->createMock(SiteAccessServiceInterface::class); + $this->parametersFactory = new ParametersFactory( + $this->credentialsResolver, + $this->configResolver, + $this->siteAccessService, + ); + $this->options = [ + 'customer_id' => '12345', + 'license_key' => '12345-12345-12345-12345', + 'siteaccess' => 'test', + 'item_type_identifier_list' => 'article, product, blog', + 'languages' => 'eng-GB', + 'web_hook' => 'https://reco-engine.com/api/12345/items', + 'host' => 'https://127.0.0.1', + 'page_size' => '500', + ]; + } + + /** + * @throws \EzSystems\EzRecommendationClient\Exception\MissingExportParameterException + */ + public function testCreateFromAllOptions(): void + { + $this->configureSiteAccessServiceToReturnAllSiteAccesses(); + + self::assertEquals( + Parameters::fromArray($this->options), + $this->parametersFactory->create($this->options, ParametersFactoryInterface::COMMAND_TYPE) + ); + } + + public function testCreateWithAutocomplete(): void + { + $siteAccess = 'test'; + $options = [ + 'customer_id' => null, + 'license_key' => null, + 'siteaccess' => null, + 'item_type_identifier_list' => 'article, product, blog', + 'languages' => 'eng-GB', + 'page_size' => '500', + 'web_hook' => null, + 'host' => null, + ]; + + $this->credentialsResolver + ->expects(self::atLeastOnce()) + ->method('hasCredentials') + ->with($siteAccess) + ->willReturn(true); + + $this->siteAccessService + ->expects(self::atLeastOnce()) + ->method('getAll') + ->willReturn( + [ + new SiteAccess($siteAccess), + ] + ); + + $this->credentialsResolver + ->expects(self::once()) + ->method('getCredentials') + ->with($siteAccess) + ->willReturn(new EzRecommendationClientCredentials( + 12345, + '12345-12345-12345-12345' + )); + + $this->configureConfigResolverToReturnHostUriAndApiNotifierUri($siteAccess); + + self::assertEquals( + Parameters::fromArray($this->options), + $this->parametersFactory->create($options, ParametersFactoryInterface::COMMAND_TYPE) + ); + } + + public function testCreateForSingleConfiguration(): void + { + $siteAccess = 'test'; + $options = [ + 'customer_id' => '12345', + 'license_key' => '12345-12345-12345-12345', + 'siteaccess' => $siteAccess, + 'item_type_identifier_list' => 'article, product, blog', + 'languages' => 'eng-GB', + 'page_size' => '500', + 'web_hook' => null, + 'host' => null, + ]; + + $this->siteAccessService + ->method('getAll') + ->willReturn( + [ + new SiteAccess($siteAccess), + ] + ); + + $this->credentialsResolver + ->expects(self::once()) + ->method('hasCredentials') + ->with($siteAccess) + ->willReturn(true); + + $this->configureConfigResolverToReturnHostUriAndApiNotifierUri($siteAccess); + + self::assertEquals( + Parameters::fromArray($this->options), + $this->parametersFactory->create($options, ParametersFactoryInterface::COMMAND_TYPE) + ); + } + + public function testCreateForMultiCustomerConfiguration(): void + { + $firstSiteAccess = 'test'; + $secondSiteAccess = 'second_siteaccess'; + + $options = [ + 'customer_id' => '12345', + 'license_key' => '12345-12345-12345-12345', + 'siteaccess' => $firstSiteAccess, + 'item_type_identifier_list' => 'article, product, blog', + 'languages' => 'eng-GB', + 'page_size' => '500', + 'web_hook' => null, + 'host' => null, + ]; + + $this->configureSiteAccessServiceToReturnAllSiteAccesses(); + + $this->credentialsResolver + ->expects(self::at(0)) + ->method('hasCredentials') + ->with($firstSiteAccess) + ->willReturn(true); + + $this->credentialsResolver + ->expects(self::at(1)) + ->method('hasCredentials') + ->with($secondSiteAccess) + ->willReturn(true); + + $this->configureConfigResolverToReturnHostUriAndApiNotifierUri($firstSiteAccess); + + self::assertEquals( + Parameters::fromArray($this->options), + $this->parametersFactory->create($options, ParametersFactoryInterface::COMMAND_TYPE) + ); + } + + /** + * @throws \EzSystems\EzRecommendationClient\Exception\MissingExportParameterException + */ + public function testThrowExportCredentialsNotFoundException(): void + { + $siteAccess = 'invalid'; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('SiteAccess %s doesn\'t exists', $siteAccess)); + + $this->configureSiteAccessServiceToReturnAllSiteAccesses(); + + $this->parametersFactory->create( + [ + 'customer_id' => null, + 'license_key' => null, + 'siteaccess' => $siteAccess, + 'item_type_identifier_list' => 'article, product, blog', + 'languages' => 'eng-GB', + 'page_size' => '500', + 'web_hook' => null, + 'host' => null, + ], + ParametersFactoryInterface::COMMAND_TYPE + ); + } + + private function configureSiteAccessServiceToReturnAllSiteAccesses(): void + { + $this->siteAccessService + ->method('getAll') + ->willReturn( + [ + new SiteAccess('test'), + new SiteAccess('second_siteaccess'), + ] + ); + } + + private function configureConfigResolverToReturnHostUriAndApiNotifierUri(string $siteAccess): void + { + $this->configResolver + ->expects(self::atLeastOnce()) + ->method('getParameter') + ->willReturnMap( + [ + ['host_uri', 'ezrecommendation', $siteAccess, 'https://127.0.0.1'], + ['api.notifier.endpoint', 'ezrecommendation', $siteAccess, 'https://reco-engine.com'], + ] + ); + } +}