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/lib/Exception/CredentialsNotFoundException.php b/src/lib/Exception/CredentialsNotFoundException.php index e9e87eba..75290217 100644 --- a/src/lib/Exception/CredentialsNotFoundException.php +++ b/src/lib/Exception/CredentialsNotFoundException.php @@ -8,10 +8,24 @@ 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 .= sprintf( + ' for siteAccess: %s', $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..dc28761f --- /dev/null +++ b/src/lib/Factory/Export/ParametersFactory.php @@ -0,0 +1,341 @@ +credentialsResolver = $credentialsResolver; + $this->configResolver = $configResolver; + $this->siteAccessService = $siteAccessService; + } + + 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 (null !== $options['customer_id'] && null !== $options['license_key'] && null !== $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( + 'host_uri', + 'ezrecommendation', + $siteAccess + ); + } + + private function getWebHook(int $customerId, string $siteAccess): string + { + return $this->configResolver->getParameter( + 'api.notifier.endpoint', + 'ezrecommendation', + $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 + { + $missingOptions = []; + + if (!isset($options['customer_id'])) { + $missingOptions[] = 'customer_id'; + } + + if (!isset($options['license_key'])) { + $missingOptions[] = 'license_key'; + } + + if (!isset($options['siteaccess'])) { + $missingOptions[] = 'siteaccess'; + } + + return $missingOptions; + } +} 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/tests/lib/Factory/Export/ParametersFactoryTest.php b/tests/lib/Factory/Export/ParametersFactoryTest.php new file mode 100644 index 00000000..cf2798da --- /dev/null +++ b/tests/lib/Factory/Export/ParametersFactoryTest.php @@ -0,0 +1,265 @@ +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->getAllSiteAccesses(); + + 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->getHostUriAndApiNotifierUri($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->getHostUriAndApiNotifierUri($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->getAllSiteAccesses(); + + $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->getHostUriAndApiNotifierUri($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->getAllSiteAccesses(); + + $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 getAllSiteAccesses(): void + { + $this->siteAccessService + ->method('getAll') + ->willReturn( + [ + new SiteAccess('test'), + new SiteAccess('second_siteaccess'), + ] + ); + } + + private function getHostUriAndApiNotifierUri(string $siteAccess): void + { + $this->configResolver + ->expects(self::at(0)) + ->method('getParameter') + ->with( + 'host_uri', + 'ezrecommendation', + $siteAccess + ) + ->willReturn('https://127.0.0.1'); + + $this->configResolver + ->expects(self::at(1)) + ->method('getParameter') + ->with( + 'api.notifier.endpoint', + 'ezrecommendation', + $siteAccess + ) + ->willReturn('https://reco-engine.com'); + } +}