diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e5f48eb6..c3fbbc36 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -20,40 +20,15 @@ parameters: count: 1 path: src/bundle/Command/UserAttributesUpdateCommand.php - - - message: "#^PHPDoc tag @param has invalid value \\(\\\\Symfony\\\\Component\\\\HttpFoundation\\\\ParameterBag ParameterBag \\$parameterBag\\)\\: Unexpected token \"ParameterBag\", expected variable at offset 65$#" - count: 1 - path: src/bundle/Controller/ContentController.php - - - - message: "#^Parameter \\#1 \\$string of static method EzSystems\\\\EzRecommendationClient\\\\Helper\\\\ParamsConverterHelper\\:\\:getArrayFromString\\(\\) expects string, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/bundle/Controller/ContentController.php - - - - message: "#^Property EzSystems\\\\EzRecommendationClientBundle\\\\Controller\\\\ContentController\\:\\:\\$searchService \\(eZ\\\\Publish\\\\Core\\\\Repository\\\\SearchService\\) does not accept eZ\\\\Publish\\\\API\\\\Repository\\\\SearchService\\.$#" - count: 1 - path: src/bundle/Controller/ContentController.php - - - - message: "#^Property EzSystems\\\\EzRecommendationClient\\\\SPI\\\\Content\\:\\:\\$fields \\(array\\\\) does not accept array\\|null\\.$#" - count: 1 - path: src/bundle/Controller/ContentController.php - - - - message: "#^Property EzSystems\\\\EzRecommendationClient\\\\SPI\\\\Content\\:\\:\\$lang \\(string\\|null\\) does not accept bool\\|float\\|int\\|string\\|null\\.$#" - count: 1 - path: src/bundle/Controller/ContentController.php - - message: "#^Call to an undefined method EzSystems\\\\EzRecommendationClient\\\\Authentication\\\\AuthenticatorInterface\\:\\:authenticateByFile\\(\\)\\.$#" count: 1 - path: src/bundle/Controller/ExportController.php + path: src/bundle/Controller/REST/ExportController.php - message: "#^Parameter \\#2 \\$values of method Symfony\\\\Component\\\\HttpFoundation\\\\ResponseHeaderBag\\:\\:set\\(\\) expects array\\\\|string\\|null, int\\|false given\\.$#" count: 1 - path: src/bundle/Controller/ExportController.php + path: src/bundle/Controller/REST/ExportController.php - message: "#^Method EzSystems\\\\EzRecommendationClientBundle\\\\Controller\\\\RecommendationController\\:\\:getTemplate\\(\\) should return string but returns string\\|null\\.$#" @@ -690,11 +665,6 @@ parameters: count: 1 path: src/lib/Helper/ContentHelper.php - - - message: "#^Interface eZ\\\\Publish\\\\API\\\\Repository\\\\SearchService referenced with incorrect case\\: eZ\\\\Publish\\\\Api\\\\Repository\\\\SearchService\\.$#" - count: 1 - path: src/lib/Helper/ContentHelper.php - - message: "#^Method EzSystems\\\\EzRecommendationClient\\\\Helper\\\\ContentHelper\\:\\:countContentItemsByContentTypeId\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 diff --git a/phpstan.neon b/phpstan.neon index 3a36a395..e39037e7 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,8 @@ includes: - phpstan-baseline.neon parameters: + ignoreErrors: + - "#^Cannot call method (log|debug|info|notice|warning|error|critical|alert|emergency)\\(\\) on Psr\\\\Log\\\\LoggerInterface\\|null\\.$#" level: max paths: - src diff --git a/src/bundle/Controller/ContentController.php b/src/bundle/Controller/ContentController.php deleted file mode 100644 index d8c4696f..00000000 --- a/src/bundle/Controller/ContentController.php +++ /dev/null @@ -1,106 +0,0 @@ -repository = $repository; - $this->searchService = $searchService; - $this->authenticator = $authenticator; - $this->contentService = $contentService; - } - - /** - * Prepares content for ContentData class. - * - * @ParamConverter("list_converter") - * - * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException - */ - public function getContentAction(IdList $idList, Request $request): ContentData - { - if (!$this->authenticator->authenticate()) { - throw new AuthenticationFailedException('Access denied: wrong credentials', Response::HTTP_UNAUTHORIZED); - } - - $requestQuery = $request->query; - - $content = new Content(); - $content->lang = $requestQuery->get('lang'); - $content->fields = $requestQuery->get('fields') - ? ParamsConverterHelper::getArrayFromString($requestQuery->get('fields')) - : null; - - $contentItems = $this->repository->sudo(function () use ($requestQuery, $idList, $content) { - return $this->searchService->findContent( - $this->getQuery($requestQuery, $idList), - (!empty($content->lang) ? ['languages' => [$content->lang]] : []) - )->searchHits; - }); - $contentData = $this->contentService->prepareContent([$contentItems], $content); - - return new ContentData($contentData); - } - - /** - * @param \Symfony\Component\HttpFoundation\ParameterBag ParameterBag $parameterBag - */ - private function getQuery(ParameterBag $parameterBag, IdList $idList): Query - { - $criteria = [new Criterion\ContentId($idList->list)]; - - if (!$parameterBag->get('hidden')) { - $criteria[] = new Criterion\Visibility(Criterion\Visibility::VISIBLE); - } - - if ($parameterBag->has('lang')) { - $criteria[] = new Criterion\LanguageCode($parameterBag->get('lang')); - } - - $query = new Query(); - $query->query = new Criterion\LogicalAnd($criteria); - - return $query; - } -} diff --git a/src/bundle/Controller/REST/ContentController.php b/src/bundle/Controller/REST/ContentController.php new file mode 100644 index 00000000..cda93018 --- /dev/null +++ b/src/bundle/Controller/REST/ContentController.php @@ -0,0 +1,110 @@ +authenticator = $authenticator; + $this->contentQueryType = $contentQueryType; + $this->contentService = $contentService; + $this->repository = $repository; + $this->searchService = $searchService; + } + + /** + * @throws \Exception + */ + public function getContentByIdAction(int $contentId, Request $request): ContentData + { + if (!$this->authenticator->authenticate()) { + throw new AuthenticationFailedException('Access denied: wrong credentials', Response::HTTP_UNAUTHORIZED); + } + + $requestQuery = $request->query; + $contentItems = $this->getContentItems( + $this->contentQueryType->getQueryByContentId($contentId, (string) $requestQuery->get('lang')) + ); + + return $this->getContentData($contentItems); + } + + /** + * @throws \Exception + */ + public function getContentByRemoteIdAction(string $remoteId, Request $request): ContentData + { + if (!$this->authenticator->authenticate()) { + throw new AuthenticationFailedException('Access denied: wrong credentials', Response::HTTP_UNAUTHORIZED); + } + + $requestQuery = $request->query; + $contentItems = $this->getContentItems( + $this->contentQueryType->getQueryByContentRemoteId($remoteId, (string) $requestQuery->get('lang')) + ); + + return $this->getContentData($contentItems); + } + + /** + * @return array<\eZ\Publish\API\Repository\Values\Content\Search\SearchHit> + * + * @throws \Exception + */ + private function getContentItems(Query $query): array + { + return $this->repository->sudo(function () use ($query) { + return $this->searchService->findContent($query)->searchHits; + }); + } + + /** + * @param array<\eZ\Publish\API\Repository\Values\Content\Search\SearchHit> $contentItems + */ + private function getContentData(array $contentItems): ContentData + { + $contentData = $this->contentService->prepareContent( + [$contentItems], + new Content() + ); + + return new ContentData($contentData); + } +} diff --git a/src/bundle/Controller/ExportController.php b/src/bundle/Controller/REST/ExportController.php similarity index 96% rename from src/bundle/Controller/ExportController.php rename to src/bundle/Controller/REST/ExportController.php index 6ec6eb93..f7286d44 100644 --- a/src/bundle/Controller/ExportController.php +++ b/src/bundle/Controller/REST/ExportController.php @@ -6,7 +6,7 @@ */ declare(strict_types=1); -namespace EzSystems\EzRecommendationClientBundle\Controller; +namespace EzSystems\EzRecommendationClientBundle\Controller\REST; use EzSystems\EzPlatformRest\Server\Controller; use EzSystems\EzRecommendationClient\Authentication\AuthenticatorInterface; diff --git a/src/bundle/ParamConverter/ListParamConverter.php b/src/bundle/ParamConverter/ListParamConverter.php deleted file mode 100644 index 1128b3dc..00000000 --- a/src/bundle/ParamConverter/ListParamConverter.php +++ /dev/null @@ -1,52 +0,0 @@ -getName(); - - if (!$request->attributes->has($paramName)) { - return false; - } - - try { - $idListAsString = $request->attributes->get($paramName); - $idList = new IdList(); - $idList->list = ParamsConverterHelper::getIdListFromString($idListAsString); - $request->attributes->set($paramName, $idList); - - return true; - } catch (InvalidArgumentException $e) { - throw new BadRequestException('Bad Request', Response::HTTP_BAD_REQUEST); - } - } - - /** - * {@inheritdoc} - */ - public function supports(ParamConverter $configuration): bool - { - return IdList::class === $configuration->getClass(); - } -} diff --git a/src/bundle/Resources/config/routing_rest.yaml b/src/bundle/Resources/config/routing_rest.yaml index 8f3c35a0..2cc9ffa7 100644 --- a/src/bundle/Resources/config/routing_rest.yaml +++ b/src/bundle/Resources/config/routing_rest.yaml @@ -1,12 +1,22 @@ -ez_recommendation.content_type.get_content: - path: /ez_recommendation/v1/content/{idList} +ez_recommendation.rest.content.get_by_id: + path: /ez_recommendation/v1/content/id/{contentId} defaults: - _controller: 'EzSystems\EzRecommendationClientBundle\Controller\ContentController::getContentAction' + _controller: 'EzSystems\EzRecommendationClientBundle\Controller\REST\ContentController::getContentByIdAction' + methods: [GET] + requirements: + contentId: '\d+' + +ez_recommendation.rest.content.get_by_remote_id: + path: /ez_recommendation/v1/content/remote-id/{remoteId} + defaults: + _controller: 'EzSystems\EzRecommendationClientBundle\Controller\REST\ContentController::getContentByRemoteIdAction' + requirements: + remoteId: '[a-zA-Z0-9\_\-\/]+' methods: [GET] -ez_recommendation.export.download: - path: /ez_recommendation/v1/exportDownload/{filePath} +ez_recommendation.rest.export.download: + path: /ez_recommendation/v1/export/download/{filePath} defaults: - _controller: 'EzSystems\EzRecommendationClientBundle\Controller\ExportController::downloadAction' + _controller: 'EzSystems\EzRecommendationClientBundle\Controller\REST\ExportController::downloadAction' requirements: filePath: '[a-zA-Z0-9\_\-\/]+' diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml index 6020010c..517fb3b9 100644 --- a/src/bundle/Resources/config/services.yaml +++ b/src/bundle/Resources/config/services.yaml @@ -5,7 +5,6 @@ imports: - { resource: services/commands.yaml } - { resource: services/config.yaml } - { resource: services/controllers.yaml } - - { resource: services/converters.yaml } - { resource: services/events.yaml } - { resource: services/exporter.yaml } - { resource: services/factory.yaml } @@ -15,6 +14,7 @@ imports: - { resource: services/helpers.yaml } - { resource: services/http.yaml } - { resource: services/mappers.yaml } + - { resource: services/query_types.yaml } - { resource: services/response.yaml } - { resource: services/services.yaml } - { resource: services/twig.yaml } diff --git a/src/bundle/Resources/config/services/controllers.yaml b/src/bundle/Resources/config/services/controllers.yaml index 102c25ed..4f94351e 100644 --- a/src/bundle/Resources/config/services/controllers.yaml +++ b/src/bundle/Resources/config/services/controllers.yaml @@ -16,11 +16,12 @@ services: alias: EzSystems\EzRecommendationClientBundle\Controller\RecommendationController public: true - EzSystems\EzRecommendationClientBundle\Controller\ContentController: + # REST + EzSystems\EzRecommendationClientBundle\Controller\REST\ContentController: arguments: $authenticator: '@EzSystems\EzRecommendationClient\Authentication\ExportAuthenticator' - EzSystems\EzRecommendationClientBundle\Controller\ExportController: + EzSystems\EzRecommendationClientBundle\Controller\REST\ExportController: arguments: $authenticator: '@EzSystems\EzRecommendationClient\Authentication\ExportAuthenticator' tags: diff --git a/src/bundle/Resources/config/services/converters.yaml b/src/bundle/Resources/config/services/converters.yaml deleted file mode 100644 index cda705d7..00000000 --- a/src/bundle/Resources/config/services/converters.yaml +++ /dev/null @@ -1,12 +0,0 @@ -services: - _defaults: - autowire: true - autoconfigure: true - public: true - - EzSystems\EzRecommendationClientBundle\ParamConverter\: - resource: '../../../../src/bundle/ParamConverter/*' - - EzSystems\EzRecommendationClientBundle\ParamConverter\ListParamConverter: - tags: - - { name: request.param_converter, priority: -2, converter: list_converter } diff --git a/src/bundle/Resources/config/services/query_types.yaml b/src/bundle/Resources/config/services/query_types.yaml new file mode 100644 index 00000000..b410e34d --- /dev/null +++ b/src/bundle/Resources/config/services/query_types.yaml @@ -0,0 +1,9 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Ibexa\Personalization\QueryType\ContentQueryType: + tags: + - { name: ezplatform.query_type } diff --git a/src/lib/Exporter/Exporter.php b/src/lib/Exporter/Exporter.php index 97bdc924..8958d8af 100644 --- a/src/lib/Exporter/Exporter.php +++ b/src/lib/Exporter/Exporter.php @@ -22,7 +22,7 @@ */ final class Exporter implements ExporterInterface { - private const API_ENDPOINT_URL = '%s/api/ezp/v2/ez_recommendation/v1/exportDownload/%s'; + private const API_ENDPOINT_URL = '%s/api/ezp/v2/ez_recommendation/v1/export/download/%s'; /** @var \eZ\Publish\API\Repository\Repository */ private $repository; diff --git a/src/lib/Helper/ContentHelper.php b/src/lib/Helper/ContentHelper.php index 21a86c12..b736bf5a 100644 --- a/src/lib/Helper/ContentHelper.php +++ b/src/lib/Helper/ContentHelper.php @@ -18,47 +18,59 @@ use eZ\Publish\API\Repository\Values\Content\Query\Criterion; use eZ\Publish\Core\MVC\ConfigResolverInterface; use EzSystems\EzRecommendationClient\Value\Parameters; +use Ibexa\Personalization\Config\Repository\RepositoryConfigResolverInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; -final class ContentHelper +final class ContentHelper implements LoggerAwareInterface { - /** @var \eZ\Publish\Api\Repository\SearchService */ - private $searchService; + use LoggerAwareTrait; + + private const UPDATE_CONTENT_URL_SUFFIX = '%s/api/ezp/v2/ez_recommendation/v1/content/%s/%s%s'; + private const CONTENT_ID_URL_PREFIX = 'id'; + private const CONTENT_REMOTE_ID_URL_PREFIX = 'remote-id'; + + /** @var \eZ\Publish\Core\MVC\ConfigResolverInterface */ + private $configResolver; /** @var \eZ\Publish\API\Repository\ContentService */ private $contentService; + /** @var \EzSystems\EzRecommendationClient\Helper\ContentTypeHelper */ + private $contentTypeHelper; + /** @var \eZ\Publish\API\Repository\LocationService */ private $locationService; - /** @var \eZ\Publish\Core\MVC\ConfigResolverInterface */ - private $configResolver; + /** @var \Ibexa\Personalization\Config\Repository\RepositoryConfigResolverInterface */ + private $repositoryConfigResolver; - /** @var \EzSystems\EzRecommendationClient\Helper\ContentTypeHelper */ - private $contentTypeHelper; + /** @var \eZ\Publish\API\Repository\SearchService */ + private $searchService; /** @var \EzSystems\EzRecommendationClient\Helper\SiteAccessHelper */ private $siteAccessHelper; - /** @var \Psr\Log\LoggerInterface */ - private $logger; - public function __construct( + ConfigResolverInterface $configResolver, ContentServiceInterface $contentService, + ContentTypeHelper $contentTypeHelper, LocationServiceInterface $locationService, + RepositoryConfigResolverInterface $repositoryConfigResolver, SearchServiceInterface $searchService, - ConfigResolverInterface $configResolver, - ContentTypeHelper $contentTypeHelper, SiteAccessHelper $siteAccessHelper, - LoggerInterface $logger + ?LoggerInterface $logger = null ) { + $this->configResolver = $configResolver; $this->contentService = $contentService; + $this->contentTypeHelper = $contentTypeHelper; $this->locationService = $locationService; + $this->repositoryConfigResolver = $repositoryConfigResolver; $this->searchService = $searchService; - $this->configResolver = $configResolver; - $this->contentTypeHelper = $contentTypeHelper; $this->siteAccessHelper = $siteAccessHelper; - $this->logger = $logger; + $this->logger = $logger ?? new NullLogger(); } /** @@ -79,11 +91,17 @@ public function getLanguageCodes(ContentInfo $contentInfo, ?int $versionNo = nul */ public function getContentUri(ContentInfo $contentInfo, ?string $lang = null): string { + $useRemoteId = $this->repositoryConfigResolver->useRemoteId(); + $contentId = $useRemoteId ? $contentInfo->remoteId : $contentInfo->id; + $prefix = $useRemoteId ? self::CONTENT_REMOTE_ID_URL_PREFIX : self::CONTENT_ID_URL_PREFIX; + $language = isset($lang) ? '?lang=' . $lang : ''; + return sprintf( - '%s/api/ezp/v2/ez_recommendation/v1/content/%s%s', + self::UPDATE_CONTENT_URL_SUFFIX, $this->configResolver->getParameter('host_uri', Parameters::NAMESPACE), - $contentInfo->id, - isset($lang) ? '?lang=' . $lang : '' + $prefix, + $contentId, + $language ); } diff --git a/src/lib/QueryType/ContentQueryType.php b/src/lib/QueryType/ContentQueryType.php new file mode 100644 index 00000000..6c4efa0e --- /dev/null +++ b/src/lib/QueryType/ContentQueryType.php @@ -0,0 +1,94 @@ +getQuery( + [ + 'criteria' => [new Criterion\ContentId($contentId)], + 'language' => $language, + ] + ); + } + + public function getQueryByContentRemoteId(string $remoteId, ?string $language = null): Query + { + return $this->getQuery( + [ + 'criteria' => [new Criterion\RemoteId($remoteId)], + 'language' => $language, + ] + ); + } + + protected function configureOptions(OptionsResolver $optionsResolver): void + { + $optionsResolver + ->setDefaults( + [ + 'criteria' => [], + 'language' => null, + ] + ) + ->setAllowedTypes('criteria', ['array']) + ->setAllowedTypes('language', ['null', 'string']); + } + + /** + * @phpstan-param array{ + * 'criteria': array<\eZ\Publish\API\Repository\Values\Content\Query\Criterion>, + * 'language': ?string, + * } $parameters + */ + protected function doGetQuery(array $parameters): Query + { + $query = new Query(); + + $query->filter = new Criterion\LogicalAnd( + $this->buildCriteria( + $parameters['criteria'], + $parameters['language'] + ) + ); + + return $query; + } + + /** + * @param array<\eZ\Publish\API\Repository\Values\Content\Query\Criterion> $criteria + * + * @return array<\eZ\Publish\API\Repository\Values\Content\Query\Criterion> + */ + private function buildCriteria(array $criteria, ?string $language = null): array + { + $additionalCriteria[] = new Query\Criterion\Visibility(Query\Criterion\Visibility::VISIBLE); + + if (!empty($language)) { + $additionalCriteria[] = new Criterion\LanguageCode($language); + } + + return array_merge( + $additionalCriteria, + $criteria, + ); + } +} diff --git a/src/lib/Service/ContentService.php b/src/lib/Service/ContentService.php index f08b1bc5..8b26437d 100644 --- a/src/lib/Service/ContentService.php +++ b/src/lib/Service/ContentService.php @@ -20,6 +20,7 @@ use EzSystems\EzRecommendationClient\Helper\ContentHelper; use EzSystems\EzRecommendationClient\SPI\Content as ContentOptions; use EzSystems\EzRecommendationClient\Value\ExportParameters; +use Ibexa\Personalization\Config\Repository\RepositoryConfigResolverInterface; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; @@ -27,6 +28,9 @@ final class ContentService implements ContentServiceInterface { + /** @var \EzSystems\EzRecommendationClient\Helper\ContentHelper */ + private $contentHelper; + /** @var \eZ\Publish\API\Repository\ContentService */ private $contentService; @@ -36,12 +40,12 @@ final class ContentService implements ContentServiceInterface /** @var \eZ\Publish\API\Repository\LocationService */ private $locationService; + /** @var \Ibexa\Personalization\Config\Repository\RepositoryConfigResolverInterface */ + private $repositoryConfigResolver; + /** @var \Symfony\Component\Routing\RouterInterface */ private $router; - /** @var \EzSystems\EzRecommendationClient\Helper\ContentHelper */ - private $contentHelper; - /** @var \EzSystems\EzRecommendationClient\Field\Value */ private $value; @@ -53,19 +57,21 @@ final class ContentService implements ContentServiceInterface public function __construct( APIContentServiceInterface $contentService, + ContentHelper $contentHelper, ContentTypeServiceInterface $contentTypeService, LocationServiceInterface $locationService, + RepositoryConfigResolverInterface $repositoryConfigResolver, RouterInterface $router, - ContentHelper $contentHelper, Value $value, int $defaultAuthorId, string $defaultSiteAccess ) { + $this->contentHelper = $contentHelper; $this->contentService = $contentService; $this->contentTypeService = $contentTypeService; $this->locationService = $locationService; + $this->repositoryConfigResolver = $repositoryConfigResolver; $this->router = $router; - $this->contentHelper = $contentHelper; $this->value = $value; $this->defaultAuthorId = $defaultAuthorId; $this->defaultSiteAccess = $defaultSiteAccess; @@ -166,7 +172,7 @@ public function setContent(CoreContent $content, ContentOptions $options): array ]; return [ - 'contentId' => $content->id, + 'contentId' => $this->repositoryConfigResolver->useRemoteId() ? $contentInfo->remoteId : $content->id, 'contentTypeId' => $contentType->id, 'identifier' => $contentType->identifier, 'language' => $language,