From 4c1efd78aa04e195af7c66ccb03eb223de12bf2a Mon Sep 17 00:00:00 2001 From: konradoboza Date: Thu, 25 May 2023 09:50:39 +0200 Subject: [PATCH] IBX-5111: Introduced mapper registry for customizing value object building on search hits --- .../Core/ApiLoader/RepositoryFactory.php | 2 + .../Decorator/SearchServiceDecorator.php | 11 +- src/contracts/Repository/SearchService.php | 8 +- src/contracts/Search/Subject/Content.php | 11 + .../Search/Subject/SearchSubjectInterface.php | 15 ++ .../Container/ApiLoader/RepositoryFactory.php | 3 + .../Repository/Mapper/ContentDomainMapper.php | 18 +- .../Mapper/SearchResultMapperInterface.php | 22 ++ .../Mapper/SearchResultMapperRegistry.php | 58 +++++ .../SearchResultMapperRegistryInterface.php | 16 ++ src/lib/Repository/Repository.php | 6 + src/lib/Repository/SearchService.php | 32 ++- .../SiteAccessAware/SearchService.php | 16 +- .../Resources/settings/repository/inner.yml | 9 + tests/lib/Persistence/Legacy/HandlerTest.php | 2 +- .../Mapper/SearchResultMapperRegistryTest.php | 70 ++++++ tests/lib/Repository/Service/Mock/Base.php | 24 +++ .../Repository/Service/Mock/SearchTest.php | 203 +++++++++++------- 18 files changed, 440 insertions(+), 86 deletions(-) create mode 100644 src/contracts/Search/Subject/Content.php create mode 100644 src/contracts/Search/Subject/SearchSubjectInterface.php create mode 100644 src/lib/Repository/Mapper/SearchResultMapperInterface.php create mode 100644 src/lib/Repository/Mapper/SearchResultMapperRegistry.php create mode 100644 src/lib/Repository/Mapper/SearchResultMapperRegistryInterface.php create mode 100644 tests/lib/Repository/Mapper/SearchResultMapperRegistryTest.php diff --git a/src/bundle/Core/ApiLoader/RepositoryFactory.php b/src/bundle/Core/ApiLoader/RepositoryFactory.php index 45df9b0283..b8e7f9f8df 100644 --- a/src/bundle/Core/ApiLoader/RepositoryFactory.php +++ b/src/bundle/Core/ApiLoader/RepositoryFactory.php @@ -76,6 +76,7 @@ public function buildRepository( PersistenceHandler $persistenceHandler, SearchHandler $searchHandler, BackgroundIndexer $backgroundIndexer, + Mapper\SearchResultMapperRegistryInterface $searchResultMapperRegistry, RelationProcessor $relationProcessor, FieldTypeRegistry $fieldTypeRegistry, PasswordHashService $passwordHashService, @@ -99,6 +100,7 @@ public function buildRepository( $persistenceHandler, $searchHandler, $backgroundIndexer, + $searchResultMapperRegistry, $relationProcessor, $fieldTypeRegistry, $passwordHashService, diff --git a/src/contracts/Repository/Decorator/SearchServiceDecorator.php b/src/contracts/Repository/Decorator/SearchServiceDecorator.php index 82f75f5f55..5592223433 100644 --- a/src/contracts/Repository/Decorator/SearchServiceDecorator.php +++ b/src/contracts/Repository/Decorator/SearchServiceDecorator.php @@ -14,6 +14,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; +use Ibexa\Contracts\Core\Search\Subject\SearchSubjectInterface; abstract class SearchServiceDecorator implements SearchService { @@ -28,9 +29,15 @@ public function __construct(SearchService $innerService) public function findContent( Query $query, array $languageFilter = [], - bool $filterOnUserPermissions = true + bool $filterOnUserPermissions = true, + SearchSubjectInterface $searchSubject = null ): SearchResult { - return $this->innerService->findContent($query, $languageFilter, $filterOnUserPermissions); + return $this->innerService->findContent( + $query, + $languageFilter, + $filterOnUserPermissions, + $searchSubject + ); } public function findContentInfo( diff --git a/src/contracts/Repository/SearchService.php b/src/contracts/Repository/SearchService.php index 585c5ae894..a651278cf9 100644 --- a/src/contracts/Repository/SearchService.php +++ b/src/contracts/Repository/SearchService.php @@ -13,6 +13,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; +use Ibexa\Contracts\Core\Search\Subject\SearchSubjectInterface; /** * Search service. @@ -128,7 +129,12 @@ interface SearchService * * @return \Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult */ - public function findContent(Query $query, array $languageFilter = [], bool $filterOnUserPermissions = true): SearchResult; + public function findContent( + Query $query, + array $languageFilter = [], + bool $filterOnUserPermissions = true, + SearchSubjectInterface $searchSubject = null + ): SearchResult; /** * Finds contentInfo objects for the given query. diff --git a/src/contracts/Search/Subject/Content.php b/src/contracts/Search/Subject/Content.php new file mode 100644 index 0000000000..35585389dc --- /dev/null +++ b/src/contracts/Search/Subject/Content.php @@ -0,0 +1,11 @@ +valueObject; $contentIds[] = $info->id; $contentTypeIds[] = $info->contentTypeId; - // Unless we are told to load all languages, we add main language to translations so they are loaded too - // Might in some case load more languages then intended, but prioritised handling will pick right one + // Unless we are told to load all languages, we add main language to translations, so they are loaded too + // Might in some case load more languages than intended, but prioritised handling will pick right one if (!empty($languageFilter['languages']) && $useAlwaysAvailable && $info->alwaysAvailable) { $translations[] = $info->mainLanguageCode; } @@ -893,6 +895,16 @@ private function isRootLocation(SPILocation $spiLocation): bool { return $spiLocation->id === $spiLocation->parentId; } + + public function buildObjectsOnSearchResult(SearchResult $result, array $languageFilter = []): array + { + return $this->buildContentDomainObjectsOnSearchResult($result, $languageFilter); + } + + public function supports(SearchSubjectInterface $searchSubject): bool + { + return $searchSubject instanceof ContentSearchSubject; + } } class_alias(ContentDomainMapper::class, 'eZ\Publish\Core\Repository\Mapper\ContentDomainMapper'); diff --git a/src/lib/Repository/Mapper/SearchResultMapperInterface.php b/src/lib/Repository/Mapper/SearchResultMapperInterface.php new file mode 100644 index 0000000000..abe50951f4 --- /dev/null +++ b/src/lib/Repository/Mapper/SearchResultMapperInterface.php @@ -0,0 +1,22 @@ + $languageFilter + * + * @return array<\Ibexa\Contracts\Core\Persistence\ValueObject> + */ + public function buildObjectsOnSearchResult(SearchResult $result, array $languageFilter = []): array; + + public function supports(SearchSubjectInterface $searchSubject): bool; +} diff --git a/src/lib/Repository/Mapper/SearchResultMapperRegistry.php b/src/lib/Repository/Mapper/SearchResultMapperRegistry.php new file mode 100644 index 0000000000..a4f882a412 --- /dev/null +++ b/src/lib/Repository/Mapper/SearchResultMapperRegistry.php @@ -0,0 +1,58 @@ + */ + private iterable $mappers; + + /** + * @param iterable<\Ibexa\Core\Repository\Mapper\SearchResultMapperInterface> $mappers + */ + public function __construct(iterable $mappers) + { + $this->mappers = $mappers; + } + + public function hasMapper(SearchSubjectInterface $searchSubject): bool + { + return $this->findMappers($searchSubject) !== null; + } + + public function getMapper(SearchSubjectInterface $searchSubject): SearchResultMapperInterface + { + $mapper = $this->findMappers($searchSubject); + + if ($mapper === null) { + throw new InvalidArgumentException( + '$hitValueObject', + sprintf( + 'undefined %s for search subject %s', + SearchResultMapperInterface::class, + get_debug_type($searchSubject) + ) + ); + } + + return $mapper; + } + + private function findMappers(SearchSubjectInterface $searchSubject): ?SearchResultMapperInterface + { + foreach ($this->mappers as $mapper) { + if ($mapper->supports($searchSubject)) { + return $mapper; + } + } + + return null; + } +} diff --git a/src/lib/Repository/Mapper/SearchResultMapperRegistryInterface.php b/src/lib/Repository/Mapper/SearchResultMapperRegistryInterface.php new file mode 100644 index 0000000000..78b6adb019 --- /dev/null +++ b/src/lib/Repository/Mapper/SearchResultMapperRegistryInterface.php @@ -0,0 +1,16 @@ +persistenceHandler = $persistenceHandler; $this->searchHandler = $searchHandler; $this->backgroundIndexer = $backgroundIndexer; + $this->searchResultMapperRegistry = $searchResultMapperRegistry; $this->relationProcessor = $relationProcessor; $this->fieldTypeRegistry = $fieldTypeRegistry; $this->passwordHashService = $passwordHashGenerator; @@ -694,6 +699,7 @@ public function getSearchService(): SearchServiceInterface $this->contentDomainMapper, $this->getPermissionCriterionResolver(), $this->backgroundIndexer, + $this->searchResultMapperRegistry, $this->serviceSettings['search'] ); diff --git a/src/lib/Repository/SearchService.php b/src/lib/Repository/SearchService.php index a296a7710a..d81fb38373 100644 --- a/src/lib/Repository/SearchService.php +++ b/src/lib/Repository/SearchService.php @@ -8,6 +8,7 @@ namespace Ibexa\Core\Repository; +use Ibexa\Contracts\Core\Persistence\Content\ContentInfo; use Ibexa\Contracts\Core\Repository\PermissionCriterionResolver; use Ibexa\Contracts\Core\Repository\Repository as RepositoryInterface; use Ibexa\Contracts\Core\Repository\SearchService as SearchServiceInterface; @@ -22,10 +23,13 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; use Ibexa\Contracts\Core\Search\Capable; use Ibexa\Contracts\Core\Search\Handler; +use Ibexa\Contracts\Core\Search\Subject\Content as ContentSearchSubject; +use Ibexa\Contracts\Core\Search\Subject\SearchSubjectInterface; use Ibexa\Core\Base\Exceptions\InvalidArgumentException; use Ibexa\Core\Base\Exceptions\InvalidArgumentType; use Ibexa\Core\Base\Exceptions\NotFoundException; use Ibexa\Core\Repository\Mapper\ContentDomainMapper; +use Ibexa\Core\Repository\Mapper\SearchResultMapperRegistryInterface; use Ibexa\Core\Search\Common\BackgroundIndexer; /** @@ -51,6 +55,8 @@ class SearchService implements SearchServiceInterface /** @var \Ibexa\Core\Search\Common\BackgroundIndexer */ protected $backgroundIndexer; + protected SearchResultMapperRegistryInterface $searchResultMapperRegistry; + /** * Setups service with reference to repository object that created it & corresponding handler. * @@ -67,6 +73,7 @@ public function __construct( ContentDomainMapper $contentDomainMapper, PermissionCriterionResolver $permissionCriterionResolver, BackgroundIndexer $backgroundIndexer, + SearchResultMapperRegistryInterface $searchResultMapperRegistry, array $settings = [] ) { $this->repository = $repository; @@ -78,6 +85,7 @@ public function __construct( ]; $this->permissionCriterionResolver = $permissionCriterionResolver; $this->backgroundIndexer = $backgroundIndexer; + $this->searchResultMapperRegistry = $searchResultMapperRegistry; } /** @@ -93,11 +101,29 @@ public function __construct( * * @return \Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult */ - public function findContent(Query $query, array $languageFilter = [], bool $filterOnUserPermissions = true): SearchResult - { + public function findContent( + Query $query, + array $languageFilter = [], + bool $filterOnUserPermissions = true, + SearchSubjectInterface $searchSubject = null + ): SearchResult { + if ($searchSubject === null) { + $searchSubject = new ContentSearchSubject(); + } + + $missingContentList = []; $result = $this->internalFindContentInfo($query, $languageFilter, $filterOnUserPermissions); - $missingContentList = $this->contentDomainMapper->buildContentDomainObjectsOnSearchResult($result, $languageFilter); + + if ($this->searchResultMapperRegistry->hasMapper($searchSubject)) { + $mapper = $this->searchResultMapperRegistry->getMapper($searchSubject); + $missingContentList = $mapper->buildObjectsOnSearchResult($result, $languageFilter); + } + foreach ($missingContentList as $missingContent) { + if (!$missingContent instanceof ContentInfo) { + continue; + } + $this->backgroundIndexer->registerContent($missingContent); } diff --git a/src/lib/Repository/SiteAccessAware/SearchService.php b/src/lib/Repository/SiteAccessAware/SearchService.php index 64b2177377..d7087e98b1 100644 --- a/src/lib/Repository/SiteAccessAware/SearchService.php +++ b/src/lib/Repository/SiteAccessAware/SearchService.php @@ -15,6 +15,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; +use Ibexa\Contracts\Core\Search\Subject\SearchSubjectInterface; /** * SiteAccess aware implementation of SearchService injecting languages where needed. @@ -41,8 +42,12 @@ public function __construct( $this->languageResolver = $languageResolver; } - public function findContent(Query $query, array $languageFilter = [], bool $filterOnUserPermissions = true): SearchResult - { + public function findContent( + Query $query, + array $languageFilter = [], + bool $filterOnUserPermissions = true, + SearchSubjectInterface $searchSubject = null + ): SearchResult { $languageFilter['languages'] = $this->languageResolver->getPrioritizedLanguages( $languageFilter['languages'] ?? null ); @@ -51,7 +56,12 @@ public function findContent(Query $query, array $languageFilter = [], bool $filt $languageFilter['useAlwaysAvailable'] ?? null ); - return $this->service->findContent($query, $languageFilter, $filterOnUserPermissions); + return $this->service->findContent( + $query, + $languageFilter, + $filterOnUserPermissions, + $searchSubject + ); } public function findContentInfo(Query $query, array $languageFilter = [], bool $filterOnUserPermissions = true): SearchResult diff --git a/src/lib/Resources/settings/repository/inner.yml b/src/lib/Resources/settings/repository/inner.yml index 50eb0fd3ce..2d03e60110 100644 --- a/src/lib/Resources/settings/repository/inner.yml +++ b/src/lib/Resources/settings/repository/inner.yml @@ -18,6 +18,7 @@ services: - '@ibexa.api.persistence_handler' - '@ibexa.spi.search' - '@Ibexa\Bundle\Core\EventListener\BackgroundIndexingTerminateListener' + - '@Ibexa\Core\Repository\Mapper\SearchResultMapperRegistryInterface' - '@Ibexa\Core\Repository\Helper\RelationProcessor' - '@Ibexa\Core\FieldType\FieldTypeRegistry' - '@Ibexa\Core\Repository\User\PasswordHashService' @@ -164,6 +165,13 @@ services: alias: 'Ibexa\Core\Repository\ProxyFactory\ProxyDomainMapper' # Mappers + Ibexa\Core\Repository\Mapper\SearchResultMapperRegistryInterface: + alias: Ibexa\Core\Repository\Mapper\SearchResultMapperRegistry + + Ibexa\Core\Repository\Mapper\SearchResultMapperRegistry: + arguments: + $mappers: !tagged_iterator { tag: ibexa.search_result.mapper } + Ibexa\Core\Repository\Mapper\ProxyAwareDomainMapper: abstract: true calls: @@ -197,6 +205,7 @@ services: - [setLogger, ['@?logger']] tags: - { name: 'monolog.logger', channel: 'ibexa.core' } + - { name: 'ibexa.search_result.mapper' } Ibexa\Core\Repository\Mapper\RoleDomainMapper: arguments: diff --git a/tests/lib/Persistence/Legacy/HandlerTest.php b/tests/lib/Persistence/Legacy/HandlerTest.php index 104d926ea3..82af0ae659 100644 --- a/tests/lib/Persistence/Legacy/HandlerTest.php +++ b/tests/lib/Persistence/Legacy/HandlerTest.php @@ -234,7 +234,7 @@ protected function getHandlerFixture() if (!isset(self::$legacyHandler)) { $container = $this->getContainer(); - self::$legacyHandler = $container->get(\Ibexa\Core\Persistence\Legacy\Handler::class); + self::$legacyHandler = $container->get(Handler::class); } return self::$legacyHandler; diff --git a/tests/lib/Repository/Mapper/SearchResultMapperRegistryTest.php b/tests/lib/Repository/Mapper/SearchResultMapperRegistryTest.php new file mode 100644 index 0000000000..881e1bc676 --- /dev/null +++ b/tests/lib/Repository/Mapper/SearchResultMapperRegistryTest.php @@ -0,0 +1,70 @@ +getSearchResultMapperMock(), + ]); + + self::assertTrue($registry->hasMapper(new Content())); + } + + public function testDoesntHaveMapper(): void + { + $registry = new SearchResultMapperRegistry([]); + $fooSearchSubject = new class() implements SearchSubjectInterface { + }; + + self::assertFalse($registry->hasMapper($fooSearchSubject)); + } + + public function testGetMapper(): void + { + $exampleMapper = $this->getSearchResultMapperMock(); + + $registry = new SearchResultMapperRegistry([ + $exampleMapper, + ]); + + self::assertSame($exampleMapper, $registry->getMapper(new Content())); + } + + public function testGetMapperThrowsInvalidArgumentException(): void + { + $message = "Argument '\$hitValueObject' is invalid: undefined Ibexa\Core\Repository\Mapper\SearchResultMapperInterface for search subject Ibexa\Contracts\Core\Search\Subject\Content"; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($message); + + $registry = new SearchResultMapperRegistry([/* Empty registry */]); + $registry->getMapper(new Content()); + } + + private function getSearchResultMapperMock(): SearchResultMapperInterface + { + $mock = $this->createMock(SearchResultMapperInterface::class); + $mock + ->method('supports') + ->with(new Content()) + ->willReturn(true); + + return $mock; + } +} diff --git a/tests/lib/Repository/Service/Mock/Base.php b/tests/lib/Repository/Service/Mock/Base.php index a2df11176d..e47c94998b 100644 --- a/tests/lib/Repository/Service/Mock/Base.php +++ b/tests/lib/Repository/Service/Mock/Base.php @@ -24,6 +24,8 @@ use Ibexa\Core\Repository\Mapper\ContentMapper; use Ibexa\Core\Repository\Mapper\ContentTypeDomainMapper; use Ibexa\Core\Repository\Mapper\RoleDomainMapper; +use Ibexa\Core\Repository\Mapper\SearchResultMapperInterface; +use Ibexa\Core\Repository\Mapper\SearchResultMapperRegistryInterface; use Ibexa\Core\Repository\Permission\LimitationService; use Ibexa\Core\Repository\ProxyFactory\ProxyDomainMapperFactoryInterface; use Ibexa\Core\Repository\Repository; @@ -109,6 +111,7 @@ protected function getRepository(array $serviceSettings = []) $this->getPersistenceMock(), $this->getSPIMockHandler('Search\\Handler'), new NullIndexer(), + $this->getSearchResultMapperRegistryMock(), $this->getRelationProcessorMock(), $this->getFieldTypeRegistryMock(), $this->createMock(PasswordHashService::class), @@ -433,6 +436,27 @@ protected function getContentFilteringHandlerMock(): ContentFilteringHandler return $this->contentFilteringHandlerMock; } + /** + * @return \Ibexa\Core\Repository\Mapper\SearchResultMapperRegistryInterface|\PHPUnit\Framework\MockObject\MockObject + */ + protected function getSearchResultMapperRegistryMock(): SearchResultMapperRegistryInterface + { + $domainMapper = $this->createMock(SearchResultMapperInterface::class); + $searchResultMapperRegistryMock = $this->createMock( + SearchResultMapperRegistryInterface::class + ); + + $searchResultMapperRegistryMock + ->method('hasMapper') + ->willReturn(true); + + $searchResultMapperRegistryMock + ->method('getMapper') + ->willReturn($domainMapper); + + return $searchResultMapperRegistryMock; + } + private function getLocationFilteringHandlerMock(): LocationFilteringHandler { if (null === $this->locationFilteringHandlerMock) { diff --git a/tests/lib/Repository/Service/Mock/SearchTest.php b/tests/lib/Repository/Service/Mock/SearchTest.php index 848c431a57..63ef19fc35 100644 --- a/tests/lib/Repository/Service/Mock/SearchTest.php +++ b/tests/lib/Repository/Service/Mock/SearchTest.php @@ -19,6 +19,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchHit; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; +use Ibexa\Contracts\Core\Search\Subject\SearchSubjectInterface; use Ibexa\Core\Repository\ContentService; use Ibexa\Core\Repository\Permission\PermissionCriterionResolver; use Ibexa\Core\Repository\SearchService; @@ -42,13 +43,14 @@ class SearchTest extends BaseServiceMockTest * * @covers \Ibexa\Contracts\Core\Repository\SearchService::__construct */ - public function testConstructor() + public function testConstructor(): void { $repositoryMock = $this->getRepositoryMock(); /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $contentDomainMapperMock = $this->getContentDomainMapperMock(); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); $settings = ['teh setting']; $service = new SearchService( @@ -57,11 +59,12 @@ public function testConstructor() $contentDomainMapperMock, $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, $settings ); } - public function providerForFindContentValidatesLocationCriteriaAndSortClauses() + public function providerForFindContentValidatesLocationCriteriaAndSortClauses(): array { return [ [ @@ -94,7 +97,7 @@ public function providerForFindContentValidatesLocationCriteriaAndSortClauses() /** * @dataProvider providerForFindContentValidatesLocationCriteriaAndSortClauses */ - public function testFindContentValidatesLocationCriteriaAndSortClauses($query, $exceptionMessage) + public function testFindContentValidatesLocationCriteriaAndSortClauses($query, $exceptionMessage): void { $this->expectException(InvalidArgumentException::class); @@ -102,6 +105,7 @@ public function testFindContentValidatesLocationCriteriaAndSortClauses($query, $ /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); $service = new SearchService( $repositoryMock, @@ -109,6 +113,7 @@ public function testFindContentValidatesLocationCriteriaAndSortClauses($query, $ $this->getContentDomainMapperMock(), $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -119,10 +124,10 @@ public function testFindContentValidatesLocationCriteriaAndSortClauses($query, $ throw $e; } - $this->fail('Expected exception was not thrown'); + self::fail('Expected exception was not thrown'); } - public function providerForFindSingleValidatesLocationCriteria() + public function providerForFindSingleValidatesLocationCriteria(): array { return [ [ @@ -143,7 +148,7 @@ public function providerForFindSingleValidatesLocationCriteria() /** * @dataProvider providerForFindSingleValidatesLocationCriteria */ - public function testFindSingleValidatesLocationCriteria($criterion, $exceptionMessage) + public function testFindSingleValidatesLocationCriteria($criterion, $exceptionMessage): void { $this->expectException(InvalidArgumentException::class); @@ -151,23 +156,26 @@ public function testFindSingleValidatesLocationCriteria($criterion, $exceptionMe /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $repositoryMock, $searchHandlerMock, $this->getContentDomainMapperMock(), $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); try { $service->findSingle($criterion); } catch (InvalidArgumentException $e) { - $this->assertEquals($exceptionMessage, $e->getMessage()); + self::assertEquals($exceptionMessage, $e->getMessage()); throw $e; } - $this->fail('Expected exception was not thrown'); + self::fail('Expected exception was not thrown'); } /** @@ -176,7 +184,7 @@ public function testFindSingleValidatesLocationCriteria($criterion, $exceptionMe * @covers \Ibexa\Contracts\Core\Repository\SearchService::addPermissionsCriterion * @covers \Ibexa\Contracts\Core\Repository\SearchService::findContent */ - public function testFindContentThrowsHandlerException() + public function testFindContentThrowsHandlerException(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('Handler threw an exception'); @@ -185,6 +193,7 @@ public function testFindContentThrowsHandlerException() /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); $service = new SearchService( $repositoryMock, @@ -192,6 +201,7 @@ public function testFindContentThrowsHandlerException() $this->getContentDomainMapperMock(), $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -205,9 +215,9 @@ public function testFindContentThrowsHandlerException() $permissionsCriterionResolverMock->expects($this->once()) ->method('getPermissionsCriterion') ->with('content', 'read') - ->will($this->throwException(new Exception('Handler threw an exception'))); + ->will(self::throwException(new Exception('Handler threw an exception'))); - $service->findContent($query, [], true); + $service->findContent($query); } /** @@ -215,34 +225,39 @@ public function testFindContentThrowsHandlerException() * * @covers \Ibexa\Contracts\Core\Repository\SearchService::findContent */ - public function testFindContentWhenDomainMapperThrowsException() + public function testFindContentWhenDomainMapperThrowsException(): void { $indexer = $this->createMock(BackgroundIndexer::class); - $indexer->expects($this->once()) + $indexer->expects(self::once()) ->method('registerContent') - ->with($this->isInstanceOf(SPIContentInfo::class)); + ->with(self::isInstanceOf(SPIContentInfo::class)); + + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); $service = $this->getMockBuilder(SearchService::class) ->setConstructorArgs([ $this->getRepositoryMock(), $this->getSPIMockHandler('Search\\Handler'), - $mapper = $this->getContentDomainMapperMock(), + $this->getContentDomainMapperMock(), $this->getPermissionCriterionResolverMock(), $indexer, + $searchResultMapperRegistryMock, ])->setMethods(['internalFindContentInfo']) ->getMock(); $info = new SPIContentInfo(['id' => 33]); $result = new SearchResult(['searchHits' => [new SearchHit(['valueObject' => $info])], 'totalCount' => 2]); - $service->expects($this->once()) + $service->expects(self::once()) ->method('internalFindContentInfo') - ->with($this->isInstanceOf(Query::class)) + ->with(self::isInstanceOf(Query::class)) ->willReturn($result); - $mapper->expects($this->once()) - ->method('buildContentDomainObjectsOnSearchResult') + $searchResultMapperRegistryMock + ->getMapper($this->createMock(SearchSubjectInterface::class)) + ->expects($this->once()) + ->method('buildObjectsOnSearchResult') ->with($this->equalTo($result), $this->equalTo([])) - ->willReturnCallback(static function (SearchResult $spiResult) use ($info) { + ->willReturnCallback(static function (SearchResult $spiResult) use ($info): array { unset($spiResult->searchHits[0]); --$spiResult->totalCount; @@ -251,8 +266,8 @@ public function testFindContentWhenDomainMapperThrowsException() $finalResult = $service->findContent(new Query()); - $this->assertEmpty($finalResult->searchHits, 'Expected search hits to be empty'); - $this->assertEquals(1, $finalResult->totalCount, 'Expected total count to be 1'); + self::assertEmpty($finalResult->searchHits, 'Expected search hits to be empty'); + self::assertEquals(1, $finalResult->totalCount, 'Expected total count to be 1'); } /** @@ -261,21 +276,24 @@ public function testFindContentWhenDomainMapperThrowsException() * @covers \Ibexa\Contracts\Core\Repository\SearchService::addPermissionsCriterion * @covers \Ibexa\Contracts\Core\Repository\SearchService::findContent */ - public function testFindContentNoPermissionsFilter() + public function testFindContentNoPermissionsFilter(): void { /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $repositoryMock = $this->getRepositoryMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $repositoryMock, $searchHandlerMock, - $mapper = $this->getContentDomainMapperMock(), - $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(), + $this->getContentDomainMapperMock(), + $this->getPermissionCriterionResolverMock(), new NullIndexer(), + $searchResultMapperRegistryMock, [] ); - $repositoryMock->expects($this->never())->method('getPermissionResolver'); + $repositoryMock->expects(self::never())->method('getPermissionResolver'); $serviceQuery = new Query(); $handlerQuery = new Query(['filter' => new Criterion\MatchAll(), 'limit' => 25]); @@ -284,11 +302,11 @@ public function testFindContentNoPermissionsFilter() $contentMock = $this->getMockForAbstractClass(Content::class); /* @var \PHPUnit\Framework\MockObject\MockObject $searchHandlerMock */ - $searchHandlerMock->expects($this->once()) + $searchHandlerMock->expects(self::once()) ->method('findContent') - ->with($this->equalTo($handlerQuery), $this->equalTo($languageFilter)) + ->with(self::equalTo($handlerQuery), self::equalTo($languageFilter)) ->will( - $this->returnValue( + self::returnValue( new SearchResult( [ 'searchHits' => [new SearchHit(['valueObject' => $spiContentInfo])], @@ -298,10 +316,12 @@ public function testFindContentNoPermissionsFilter() ) ); - $mapper->expects($this->once()) - ->method('buildContentDomainObjectsOnSearchResult') - ->with($this->isInstanceOf(SearchResult::class), $this->equalTo([])) - ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock) { + $searchResultMapperRegistryMock + ->getMapper($this->createMock(SearchSubjectInterface::class)) + ->expects(self::once()) + ->method('buildObjectsOnSearchResult') + ->with(self::isInstanceOf(SearchResult::class), self::equalTo([])) + ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock): array { $spiResult->searchHits[0]->valueObject = $contentMock; return []; @@ -309,7 +329,7 @@ public function testFindContentNoPermissionsFilter() $result = $service->findContent($serviceQuery, $languageFilter, false); - $this->assertEquals( + self::assertEquals( new SearchResult( [ 'searchHits' => [new SearchHit(['valueObject' => $contentMock])], @@ -326,18 +346,21 @@ public function testFindContentNoPermissionsFilter() * @covers \Ibexa\Contracts\Core\Repository\SearchService::addPermissionsCriterion * @covers \Ibexa\Contracts\Core\Repository\SearchService::findContent */ - public function testFindContentWithPermission() + public function testFindContentWithPermission(): void { /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $domainMapperMock = $this->getContentDomainMapperMock(); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $this->getRepositoryMock(), $searchHandlerMock, $domainMapperMock, $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -345,20 +368,21 @@ public function testFindContentWithPermission() ->getMockBuilder(Criterion::class) ->disableOriginalConstructor() ->getMock(); + $query = new Query(['filter' => $criterionMock, 'limit' => 10]); $languageFilter = []; $spiContentInfo = new SPIContentInfo(); $contentMock = $this->getMockForAbstractClass(Content::class); - $permissionsCriterionResolverMock->expects($this->once()) + $permissionsCriterionResolverMock->expects(self::once()) ->method('getPermissionsCriterion') ->with('content', 'read') - ->will($this->returnValue(true)); + ->will(self::returnValue(true)); /* @var \PHPUnit\Framework\MockObject\MockObject $searchHandlerMock */ - $searchHandlerMock->expects($this->once()) + $searchHandlerMock->expects(self::once()) ->method('findContent') - ->with($this->equalTo($query), $this->equalTo($languageFilter)) + ->with(self::equalTo($query), self::equalTo($languageFilter)) ->will( $this->returnValue( new SearchResult( @@ -370,11 +394,12 @@ public function testFindContentWithPermission() ) ); - $domainMapperMock - ->expects($this->once()) - ->method('buildContentDomainObjectsOnSearchResult') - ->with($this->isInstanceOf(SearchResult::class), $this->equalTo([])) - ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock) { + $searchResultMapperRegistryMock + ->getMapper($this->createMock(SearchSubjectInterface::class)) + ->expects(self::once()) + ->method('buildObjectsOnSearchResult') + ->with(self::isInstanceOf(SearchResult::class), self::equalTo([])) + ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock): array { $spiResult->searchHits[0]->valueObject = $contentMock; return []; @@ -382,7 +407,7 @@ public function testFindContentWithPermission() $result = $service->findContent($query, $languageFilter, true); - $this->assertEquals( + self::assertEquals( new SearchResult( [ 'searchHits' => [new SearchHit(['valueObject' => $contentMock])], @@ -399,22 +424,25 @@ public function testFindContentWithPermission() * @covers \Ibexa\Contracts\Core\Repository\SearchService::addPermissionsCriterion * @covers \Ibexa\Contracts\Core\Repository\SearchService::findContent */ - public function testFindContentWithNoPermission() + public function testFindContentWithNoPermission(): void { /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $this->getRepositoryMock(), $searchHandlerMock, - $mapper = $this->getContentDomainMapperMock(), + $this->getContentDomainMapperMock(), $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); /* @var \PHPUnit\Framework\MockObject\MockObject $searchHandlerMock */ - $searchHandlerMock->expects($this->never())->method('findContent'); + $searchHandlerMock->expects(self::never())->method('findContent'); $criterionMock = $this ->getMockBuilder(Criterion::class) @@ -422,19 +450,21 @@ public function testFindContentWithNoPermission() ->getMock(); $query = new Query(['filter' => $criterionMock]); - $permissionsCriterionResolverMock->expects($this->once()) + $permissionsCriterionResolverMock->expects(self::once()) ->method('getPermissionsCriterion') ->with('content', 'read') ->will($this->returnValue(false)); - $mapper->expects($this->once()) - ->method('buildContentDomainObjectsOnSearchResult') - ->with($this->isInstanceOf(SearchResult::class), $this->equalTo([])) + $searchResultMapperRegistryMock + ->getMapper($this->createMock(SearchSubjectInterface::class)) + ->expects(self::once()) + ->method('buildObjectsOnSearchResult') + ->with(self::isInstanceOf(SearchResult::class), self::equalTo([])) ->willReturn([]); - $result = $service->findContent($query, [], true); + $result = $service->findContent($query); - $this->assertEquals( + self::assertEquals( new SearchResult(['time' => 0, 'totalCount' => 0]), $result ); @@ -443,17 +473,20 @@ public function testFindContentWithNoPermission() /** * Test for the findContent() method. */ - public function testFindContentWithDefaultQueryValues() + public function testFindContentWithDefaultQueryValues(): void { /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $domainMapperMock = $this->getContentDomainMapperMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $this->getRepositoryMock(), $searchHandlerMock, $domainMapperMock, $this->getPermissionCriterionResolverMock(), new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -463,7 +496,7 @@ public function testFindContentWithDefaultQueryValues() /* @var \PHPUnit\Framework\MockObject\MockObject $searchHandlerMock */ $searchHandlerMock - ->expects($this->once()) + ->expects(self::once()) ->method('findContent') ->with( new Query( @@ -475,7 +508,7 @@ public function testFindContentWithDefaultQueryValues() [] ) ->will( - $this->returnValue( + self::returnValue( new SearchResult( [ 'searchHits' => [new SearchHit(['valueObject' => $spiContentInfo])], @@ -485,11 +518,12 @@ public function testFindContentWithDefaultQueryValues() ) ); - $domainMapperMock - ->expects($this->once()) - ->method('buildContentDomainObjectsOnSearchResult') - ->with($this->isInstanceOf(SearchResult::class), $this->equalTo([])) - ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock) { + $searchResultMapperRegistryMock + ->getMapper($this->createMock(SearchSubjectInterface::class)) + ->expects(self::once()) + ->method('buildObjectsOnSearchResult') + ->with(self::isInstanceOf(SearchResult::class), self::equalTo([])) + ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock): array { $spiResult->searchHits[0]->valueObject = $contentMock; return []; @@ -497,7 +531,7 @@ public function testFindContentWithDefaultQueryValues() $result = $service->findContent(new Query(), $languageFilter, false); - $this->assertEquals( + self::assertEquals( new SearchResult( [ 'searchHits' => [new SearchHit(['valueObject' => $contentMock])], @@ -514,19 +548,22 @@ public function testFindContentWithDefaultQueryValues() * @covers \Ibexa\Contracts\Core\Repository\SearchService::addPermissionsCriterion * @covers \Ibexa\Contracts\Core\Repository\SearchService::findSingle */ - public function testFindSingleThrowsNotFoundException() + public function testFindSingleThrowsNotFoundException(): void { $this->expectException(NotFoundException::class); $repositoryMock = $this->getRepositoryMock(); /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $repositoryMock, $searchHandlerMock, $this->getContentDomainMapperMock(), $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(), new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -550,7 +587,7 @@ public function testFindSingleThrowsNotFoundException() * @covers \Ibexa\Contracts\Core\Repository\SearchService::addPermissionsCriterion * @covers \Ibexa\Contracts\Core\Repository\SearchService::findSingle */ - public function testFindSingleThrowsHandlerException() + public function testFindSingleThrowsHandlerException(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('Handler threw an exception'); @@ -559,12 +596,15 @@ public function testFindSingleThrowsHandlerException() /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $repositoryMock, $searchHandlerMock, $this->getContentDomainMapperMock(), $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -588,19 +628,22 @@ public function testFindSingleThrowsHandlerException() * @covers \Ibexa\Contracts\Core\Repository\SearchService::addPermissionsCriterion * @covers \Ibexa\Contracts\Core\Repository\SearchService::findSingle */ - public function testFindSingle() + public function testFindSingle(): void { $repositoryMock = $this->getRepositoryMock(); /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $domainMapperMock = $this->getContentDomainMapperMock(); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $repositoryMock, $searchHandlerMock, $domainMapperMock, $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -654,19 +697,22 @@ public function testFindSingle() /** * Test for the findLocations() method. */ - public function testFindLocationsWithPermission() + public function testFindLocationsWithPermission(): void { $repositoryMock = $this->getRepositoryMock(); /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $domainMapperMock = $this->getContentDomainMapperMock(); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $repositoryMock, $searchHandlerMock, $domainMapperMock, $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -714,7 +760,7 @@ public function testFindLocationsWithPermission() return []; }); - $result = $service->findLocations($query, [], true); + $result = $service->findLocations($query); $this->assertEquals( $endResult, @@ -725,19 +771,22 @@ public function testFindLocationsWithPermission() /** * Test for the findLocations() method. */ - public function testFindLocationsWithNoPermissionsFilter() + public function testFindLocationsWithNoPermissionsFilter(): void { $repositoryMock = $this->getRepositoryMock(); /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $domainMapperMock = $this->getContentDomainMapperMock(); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $repositoryMock, $searchHandlerMock, $domainMapperMock, $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -794,13 +843,15 @@ public function testFindLocationsWithNoPermissionsFilter() * * @covers \Ibexa\Contracts\Core\Repository\SearchService::findLocations */ - public function testFindLocationsBackgroundIndexerWhenDomainMapperThrowsException() + public function testFindLocationsBackgroundIndexerWhenDomainMapperThrowsException(): void { $indexer = $this->createMock(BackgroundIndexer::class); $indexer->expects($this->once()) ->method('registerLocation') ->with($this->isInstanceOf(SPILocation::class)); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = $this->getMockBuilder(SearchService::class) ->setConstructorArgs([ $this->getRepositoryMock(), @@ -808,6 +859,7 @@ public function testFindLocationsBackgroundIndexerWhenDomainMapperThrowsExceptio $mapper = $this->getContentDomainMapperMock(), $this->getPermissionCriterionResolverMock(), $indexer, + $searchResultMapperRegistryMock, ])->setMethods(['addPermissionsCriterion']) ->getMock(); @@ -842,7 +894,7 @@ public function testFindLocationsBackgroundIndexerWhenDomainMapperThrowsExceptio /** * Test for the findLocations() method. */ - public function testFindLocationsThrowsHandlerException() + public function testFindLocationsThrowsHandlerException(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('Handler threw an exception'); @@ -851,6 +903,7 @@ public function testFindLocationsThrowsHandlerException() /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $permissionsCriterionResolverMock = $this->getPermissionCriterionResolverMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); $service = new SearchService( $repositoryMock, @@ -858,6 +911,7 @@ public function testFindLocationsThrowsHandlerException() $this->getContentDomainMapperMock(), $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -879,18 +933,21 @@ public function testFindLocationsThrowsHandlerException() /** * Test for the findLocations() method. */ - public function testFindLocationsWithDefaultQueryValues() + public function testFindLocationsWithDefaultQueryValues(): void { $repositoryMock = $this->getRepositoryMock(); /** @var \Ibexa\Contracts\Core\Search\Handler $searchHandlerMock */ $searchHandlerMock = $this->getSPIMockHandler('Search\\Handler'); $domainMapperMock = $this->getContentDomainMapperMock(); + $searchResultMapperRegistryMock = $this->getSearchResultMapperRegistryMock(); + $service = new SearchService( $repositoryMock, $searchHandlerMock, $domainMapperMock, $this->getPermissionCriterionResolverMock(), new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -947,7 +1004,7 @@ public function testFindLocationsWithDefaultQueryValues() /** * @return \PHPUnit\Framework\MockObject\MockObject|\Ibexa\Contracts\Core\Repository\PermissionCriterionResolver */ - protected function getPermissionCriterionResolverMock() + protected function getPermissionCriterionResolverMock(): PermissionCriterionResolver { if (!isset($this->permissionsCriterionResolverMock)) { $this->permissionsCriterionResolverMock = $this