diff --git a/src/bundle/Core/ApiLoader/RepositoryFactory.php b/src/bundle/Core/ApiLoader/RepositoryFactory.php index 45df9b0283..25f11e3fa3 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\SearchHitMapperRegistryInterface $searchHitMapperRegistry, RelationProcessor $relationProcessor, FieldTypeRegistry $fieldTypeRegistry, PasswordHashService $passwordHashService, @@ -99,6 +100,7 @@ public function buildRepository( $persistenceHandler, $searchHandler, $backgroundIndexer, + $searchHitMapperRegistry, $relationProcessor, $fieldTypeRegistry, $passwordHashService, diff --git a/src/contracts/Repository/Decorator/SearchServiceDecorator.php b/src/contracts/Repository/Decorator/SearchServiceDecorator.php index 82f75f5f55..b1364c05cc 100644 --- a/src/contracts/Repository/Decorator/SearchServiceDecorator.php +++ b/src/contracts/Repository/Decorator/SearchServiceDecorator.php @@ -14,23 +14,32 @@ 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\Repository\Values\Search\SearchContextInterface; abstract class SearchServiceDecorator implements SearchService { - /** @var \Ibexa\Contracts\Core\Repository\SearchService */ - protected $innerService; + protected SearchService $innerService; public function __construct(SearchService $innerService) { $this->innerService = $innerService; } + public function find(Query $query, SearchContextInterface $context = null): SearchResult + { + return $this->innerService->find($query, $context); + } + public function findContent( Query $query, array $languageFilter = [], bool $filterOnUserPermissions = true ): SearchResult { - return $this->innerService->findContent($query, $languageFilter, $filterOnUserPermissions); + return $this->innerService->findContent( + $query, + $languageFilter, + $filterOnUserPermissions + ); } public function findContentInfo( diff --git a/src/contracts/Repository/SearchService.php b/src/contracts/Repository/SearchService.php index 585c5ae894..a54fd82f2a 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\Repository\Values\Search\SearchContextInterface; /** * Search service. @@ -114,6 +115,8 @@ interface SearchService */ public const CAPABILITY_AGGREGATIONS = 128; + public function find(Query $query, SearchContextInterface $context = null): SearchResult; + /** * Finds content objects for the given query. * diff --git a/src/contracts/Repository/Values/Search/SearchContext.php b/src/contracts/Repository/Values/Search/SearchContext.php new file mode 100644 index 0000000000..c1cb9229ba --- /dev/null +++ b/src/contracts/Repository/Values/Search/SearchContext.php @@ -0,0 +1,62 @@ +, useAlwaysAvailable: bool} */ + private array $languageFilter; + + private bool $filterOnUserPermissions; + + /** @var array */ + private array $documentTypeIdentifiers; + + /** + * @param array{}|array{languages: array, useAlwaysAvailable: bool} $languageFilter + * @param array $documentTypeIdentifiers + */ + public function __construct( + array $languageFilter = [], + bool $filterOnUserPermissions = true, + array $documentTypeIdentifiers = [] + ) { + $this->languageFilter = $languageFilter; + $this->filterOnUserPermissions = $filterOnUserPermissions; + $this->documentTypeIdentifiers = $documentTypeIdentifiers; + } + + public function setLanguageFilter(array $languageFilter): void + { + $this->languageFilter = $languageFilter; + } + + public function getLanguageFilter(): array + { + return $this->languageFilter; + } + + public function setFilterOnUserPermissions(bool $filterOnUserPermissions): void + { + $this->filterOnUserPermissions = $filterOnUserPermissions; + } + + public function doesFilterOnUserPermissions(): bool + { + return $this->filterOnUserPermissions; + } + + /** + * @return array + */ + public function getDocumentTypeIdentifiers(): array + { + return $this->documentTypeIdentifiers; + } +} diff --git a/src/contracts/Repository/Values/Search/SearchContextInterface.php b/src/contracts/Repository/Values/Search/SearchContextInterface.php new file mode 100644 index 0000000000..868e21b48f --- /dev/null +++ b/src/contracts/Repository/Values/Search/SearchContextInterface.php @@ -0,0 +1,31 @@ +, useAlwaysAvailable: bool} $languageFilter + */ + public function setLanguageFilter(array $languageFilter): void; + + /** + * @return array{}|array{languages: array, useAlwaysAvailable: bool} + */ + public function getLanguageFilter(): array; + + public function setFilterOnUserPermissions(bool $filterOnUserPermissions): void; + + public function doesFilterOnUserPermissions(): bool; + + /** + * @return array + */ + public function getDocumentTypeIdentifiers(): array; +} diff --git a/src/contracts/Search/Handler.php b/src/contracts/Search/Handler.php index 593d3f8b89..a16f25f745 100644 --- a/src/contracts/Search/Handler.php +++ b/src/contracts/Search/Handler.php @@ -11,6 +11,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery; use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; +use Ibexa\Contracts\Core\Repository\Values\Search\SearchContextInterface; /** * The Search handler retrieves sets of of Content objects, based on a @@ -18,6 +19,13 @@ */ interface Handler { + /** + * Finds objects for the given query. + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException if Query criterion is not applicable to its target + */ + public function find(Query $query, SearchContextInterface $context = null); + /** * Finds content objects for the given query. * diff --git a/src/lib/Base/Container/ApiLoader/RepositoryFactory.php b/src/lib/Base/Container/ApiLoader/RepositoryFactory.php index c3703ee72b..d9de10e0ad 100644 --- a/src/lib/Base/Container/ApiLoader/RepositoryFactory.php +++ b/src/lib/Base/Container/ApiLoader/RepositoryFactory.php @@ -21,6 +21,7 @@ use Ibexa\Core\FieldType\FieldTypeRegistry; use Ibexa\Core\Repository\Helper\RelationProcessor; use Ibexa\Core\Repository\Mapper; +use Ibexa\Core\Repository\Mapper\SearchHitMapperRegistryInterface; use Ibexa\Core\Repository\Permission\LimitationService; use Ibexa\Core\Repository\ProxyFactory\ProxyDomainMapperFactoryInterface; use Ibexa\Core\Repository\User\PasswordValidatorInterface; @@ -67,6 +68,7 @@ public function buildRepository( PersistenceHandler $persistenceHandler, SearchHandler $searchHandler, BackgroundIndexer $backgroundIndexer, + SearchHitMapperRegistryInterface $searchHitMapperRegistry, RelationProcessor $relationProcessor, FieldTypeRegistry $fieldTypeRegistry, PasswordHashService $passwordHashService, @@ -89,6 +91,7 @@ public function buildRepository( $persistenceHandler, $searchHandler, $backgroundIndexer, + $searchHitMapperRegistry, $relationProcessor, $fieldTypeRegistry, $passwordHashService, diff --git a/src/lib/Repository/Mapper/ContentDomainMapper.php b/src/lib/Repository/Mapper/ContentDomainMapper.php index 45cc8cf94b..c5e0eb7e9b 100644 --- a/src/lib/Repository/Mapper/ContentDomainMapper.php +++ b/src/lib/Repository/Mapper/ContentDomainMapper.php @@ -24,9 +24,11 @@ use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo; use Ibexa\Contracts\Core\Repository\Values\Content\Field; use Ibexa\Contracts\Core\Repository\Values\Content\Location as APILocation; +use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchHit; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; use Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo as APIVersionInfo; use Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType; +use Ibexa\Contracts\Core\Repository\Values\Search\SearchContextInterface; use Ibexa\Core\Base\Exceptions\InvalidArgumentException; use Ibexa\Core\Base\Exceptions\InvalidArgumentType; use Ibexa\Core\Base\Exceptions\InvalidArgumentValue; @@ -46,7 +48,7 @@ * * @internal Meant for internal use by Repository. */ -class ContentDomainMapper extends ProxyAwareDomainMapper implements LoggerAwareInterface +class ContentDomainMapper extends ProxyAwareDomainMapper implements LoggerAwareInterface, SearchHitMapperInterface { use LoggerAwareTrait; @@ -610,8 +612,8 @@ public function buildContentDomainObjectsOnSearchResult(SearchResult $result, ar $info = $hit->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,52 @@ private function isRootLocation(SPILocation $spiLocation): bool { return $spiLocation->id === $spiLocation->parentId; } + + public function buildObjectOnSearchHit(SearchHit $hit, SearchContextInterface $context = null) + { + return $this->buildContentDomainObjectsOnSearchHit($hit, $context); + } + + public function supports(SearchHit $hit, SearchContextInterface $context = null): bool + { + return $hit->valueObject instanceof SPIContent\ContentInfo; + } + + private function buildContentDomainObjectsOnSearchHit(SearchHit $hit, SearchContextInterface $context = null): ?Content + { + /** @var \Ibexa\Contracts\Core\Persistence\Content\ContentInfo $info */ + $info = $hit->valueObject; + $languageFilter = []; + + if ($context !== null) { + $languageFilter = $context->getLanguageFilter(); + } + + $translations = $languageFilter['languages'] ?? []; + $useAlwaysAvailable = $languageFilter['useAlwaysAvailable'] ?? true; + + if (!empty($languageFilter['languages']) && $useAlwaysAvailable && $info->alwaysAvailable) { + $translations[] = $info->mainLanguageCode; + } + + try { + $content = $this->contentHandler->load($info->id, null, $translations); + $contentType = $this->contentTypeHandler->load($info->contentTypeId); + $languages = $languageFilter['languages'] ?? []; + + return $this->buildContentDomainObject( + $content, + $this->contentTypeDomainMapper->buildContentTypeDomainObject( + $contentType, + $languages + ), + $languages, + $useAlwaysAvailable ? $info->mainLanguageCode : null + ); + } catch (NotFoundException $e) { + return null; + } + } } class_alias(ContentDomainMapper::class, 'eZ\Publish\Core\Repository\Mapper\ContentDomainMapper'); diff --git a/src/lib/Repository/Mapper/SearchHitMapperInterface.php b/src/lib/Repository/Mapper/SearchHitMapperInterface.php new file mode 100644 index 0000000000..00653ea1b4 --- /dev/null +++ b/src/lib/Repository/Mapper/SearchHitMapperInterface.php @@ -0,0 +1,20 @@ + */ + private iterable $mappers; + + /** + * @param iterable<\Ibexa\Core\Repository\Mapper\SearchHitMapperInterface> $mappers + */ + public function __construct(iterable $mappers) + { + $this->mappers = $mappers; + } + + public function hasMapper(SearchHit $hit): bool + { + return $this->findMappers($hit) !== null; + } + + public function getMapper(SearchHit $hit): SearchHitMapperInterface + { + if (!$this->hasMapper($hit)) { + throw new InvalidArgumentException( + '$hit', + sprintf( + 'undefined %s for search hit %s', + SearchHitMapperInterface::class, + get_debug_type($hit->valueObject) + ) + ); + } + + return $this->findMappers($hit); + } + + private function findMappers(SearchHit $hit): ?SearchHitMapperInterface + { + foreach ($this->mappers as $mapper) { + if ($mapper->supports($hit)) { + return $mapper; + } + } + + return null; + } +} diff --git a/src/lib/Repository/Mapper/SearchHitMapperRegistryInterface.php b/src/lib/Repository/Mapper/SearchHitMapperRegistryInterface.php new file mode 100644 index 0000000000..7de8e48472 --- /dev/null +++ b/src/lib/Repository/Mapper/SearchHitMapperRegistryInterface.php @@ -0,0 +1,16 @@ +persistenceHandler = $persistenceHandler; $this->searchHandler = $searchHandler; $this->backgroundIndexer = $backgroundIndexer; + $this->searchHitMapperRegistry = $searchHitMapperRegistry; $this->relationProcessor = $relationProcessor; $this->fieldTypeRegistry = $fieldTypeRegistry; $this->passwordHashService = $passwordHashGenerator; @@ -694,6 +699,7 @@ public function getSearchService(): SearchServiceInterface $this->contentDomainMapper, $this->getPermissionCriterionResolver(), $this->backgroundIndexer, + $this->searchHitMapperRegistry, $this->serviceSettings['search'] ); diff --git a/src/lib/Repository/SearchService.php b/src/lib/Repository/SearchService.php index a296a7710a..8898553f21 100644 --- a/src/lib/Repository/SearchService.php +++ b/src/lib/Repository/SearchService.php @@ -20,12 +20,14 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion\LogicalOperator; use Ibexa\Contracts\Core\Repository\Values\Content\Query\SortClause\Location as LocationSortClause; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; +use Ibexa\Contracts\Core\Repository\Values\Search\SearchContextInterface; use Ibexa\Contracts\Core\Search\Capable; use Ibexa\Contracts\Core\Search\Handler; 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\SearchHitMapperRegistryInterface; use Ibexa\Core\Search\Common\BackgroundIndexer; /** @@ -33,23 +35,19 @@ */ class SearchService implements SearchServiceInterface { - /** @var \Ibexa\Core\Repository\Repository */ - protected $repository; + protected RepositoryInterface $repository; - /** @var \Ibexa\Contracts\Core\Search\Handler */ - protected $searchHandler; + protected Handler $searchHandler; - /** @var array */ - protected $settings; + protected array $settings; - /** @var \Ibexa\Core\Repository\Mapper\ContentDomainMapper */ - protected $contentDomainMapper; + protected ContentDomainMapper $contentDomainMapper; - /** @var \Ibexa\Contracts\Core\Repository\PermissionCriterionResolver */ - protected $permissionCriterionResolver; + protected PermissionCriterionResolver $permissionCriterionResolver; - /** @var \Ibexa\Core\Search\Common\BackgroundIndexer */ - protected $backgroundIndexer; + protected BackgroundIndexer $backgroundIndexer; + + protected SearchHitMapperRegistryInterface $searchHitMapperRegistry; /** * Setups service with reference to repository object that created it & corresponding handler. @@ -67,6 +65,7 @@ public function __construct( ContentDomainMapper $contentDomainMapper, PermissionCriterionResolver $permissionCriterionResolver, BackgroundIndexer $backgroundIndexer, + SearchHitMapperRegistryInterface $searchHitMapperRegistry, array $settings = [] ) { $this->repository = $repository; @@ -78,6 +77,70 @@ public function __construct( ]; $this->permissionCriterionResolver = $permissionCriterionResolver; $this->backgroundIndexer = $backgroundIndexer; + $this->searchHitMapperRegistry = $searchHitMapperRegistry; + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function find(Query $query, SearchContextInterface $context = null): SearchResult + { + $domainObjects = []; + $result = $this->internalFind($query, $context); + + foreach ($result->searchHits as $hit) { + $domainObject = $this->searchHitMapperRegistry + ->getMapper($hit) + ->buildObjectOnSearchHit($hit, $context); + + if ($domainObject !== null) { + $domainObjects[] = $domainObject; + } + } + + $result->searchHits = $domainObjects; + + return $result; + } + + /** + * Finds persistence objects for the given query. + * + * Internal for use by {@link find}. + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException if query is not valid + */ + protected function internalFind(Query $query, SearchContextInterface $context = null): SearchResult + { + if (!is_int($query->offset)) { + throw new InvalidArgumentType( + '$query->offset', + 'integer', + $query->offset + ); + } + + if (!is_int($query->limit)) { + throw new InvalidArgumentType( + '$query->limit', + 'integer', + $query->limit + ); + } + + $query = clone $query; + $query->filter = $query->filter ?: new Criterion\MatchAll(); + + $this->validateContentCriteria([$query->query], '$query'); + $this->validateContentCriteria([$query->filter], '$query'); + $this->validateContentSortClauses($query); + + $doesFilterOnUserPermissions = $context !== null && $context->doesFilterOnUserPermissions(); + if ($doesFilterOnUserPermissions && !$this->addPermissionsCriterion($query->filter)) { + return new SearchResult(['time' => 0, 'totalCount' => 0]); + } + + return $this->searchHandler->find($query, $context); } /** diff --git a/src/lib/Repository/SiteAccessAware/SearchService.php b/src/lib/Repository/SiteAccessAware/SearchService.php index 64b2177377..773b4fda85 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\Repository\Values\Search\SearchContextInterface; /** * SiteAccess aware implementation of SearchService injecting languages where needed. @@ -41,6 +42,22 @@ public function __construct( $this->languageResolver = $languageResolver; } + public function find(Query $query, SearchContextInterface $context = null): SearchResult + { + $languageFilter = $context !== null ? $context->getLanguageFilter() : []; + $languageFilter['languages'] = $this->languageResolver->getPrioritizedLanguages( + $languageFilter['languages'] ?? null + ); + + $languageFilter['useAlwaysAvailable'] = $this->languageResolver->getUseAlwaysAvailable( + $languageFilter['useAlwaysAvailable'] ?? null + ); + + $context->setLanguageFilter($languageFilter); + + return $this->service->find($query, $context); + } + public function findContent(Query $query, array $languageFilter = [], bool $filterOnUserPermissions = true): SearchResult { $languageFilter['languages'] = $this->languageResolver->getPrioritizedLanguages( diff --git a/src/lib/Resources/settings/repository/inner.yml b/src/lib/Resources/settings/repository/inner.yml index 50eb0fd3ce..7d191c2fa6 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\SearchHitMapperRegistryInterface' - '@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\SearchHitMapperRegistryInterface: + alias: Ibexa\Core\Repository\Mapper\SearchHitMapperRegistry + + Ibexa\Core\Repository\Mapper\SearchHitMapperRegistry: + arguments: + $mappers: !tagged_iterator { tag: ibexa.search_hit.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_hit.mapper', priority: -110 } Ibexa\Core\Repository\Mapper\RoleDomainMapper: arguments: diff --git a/src/lib/Search/Legacy/Content/Handler.php b/src/lib/Search/Legacy/Content/Handler.php index e00265b39b..61f2e66a03 100644 --- a/src/lib/Search/Legacy/Content/Handler.php +++ b/src/lib/Search/Legacy/Content/Handler.php @@ -15,6 +15,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchHit; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; +use Ibexa\Contracts\Core\Repository\Values\Search\SearchContextInterface; use Ibexa\Contracts\Core\Search\VersatileHandler as SearchHandlerInterface; use Ibexa\Core\Base\Exceptions\InvalidArgumentException; use Ibexa\Core\Base\Exceptions\NotFoundException; @@ -114,6 +115,16 @@ public function __construct( $this->mapper = $mapper; } + public function find( + Query $query, + SearchContextInterface $context = null + ): SearchResult { + $languageFilter = $context !== null ? $context->getLanguageFilter() : []; + + //fallback to findContent method for the time being to keep BC + return $this->findContent($query, $languageFilter); + } + /** * Finds content objects for the given query. * 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..77989b8472 --- /dev/null +++ b/tests/lib/Repository/Mapper/SearchResultMapperRegistryTest.php @@ -0,0 +1,87 @@ +getSearchResultMapperMock(), + ]); + + $searchHit = new SearchHit([ + 'valueObject' => new Content(), + ]); + + self::assertTrue($registry->hasMapper($searchHit)); + } + + public function testDoesntHaveMapper(): void + { + $registry = new SearchHitMapperRegistry([]); + $searchHit = new SearchHit([ + 'valueObject' => new Location(), + ]); + + self::assertFalse($registry->hasMapper($searchHit)); + } + + public function testGetMapper(): void + { + $exampleMapper = $this->getSearchResultMapperMock(); + $searchHit = new SearchHit([ + 'valueObject' => new Content(), + ]); + + $registry = new SearchHitMapperRegistry([ + $exampleMapper, + ]); + + self::assertSame($exampleMapper, $registry->getMapper($searchHit)); + } + + public function testGetMapperThrowsInvalidArgumentException(): void + { + $message = "Argument '\$hit' is invalid: undefined Ibexa\Core\Repository\Mapper\SearchHitMapperInterface for search hit Ibexa\Core\Repository\Values\Content\Location"; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($message); + + $registry = new SearchHitMapperRegistry([/* Empty registry */]); + $registry->getMapper( + new SearchHit([ + 'valueObject' => new Location(), + ]) + ); + } + + private function getSearchResultMapperMock(): SearchHitMapperInterface + { + $searchHit = new SearchHit([ + 'valueObject' => new Content(), + ]); + + $mock = $this->createMock(SearchHitMapperInterface::class); + $mock + ->method('supports') + ->with($searchHit) + ->willReturn(true); + + return $mock; + } +} diff --git a/tests/lib/Repository/Service/Mock/Base.php b/tests/lib/Repository/Service/Mock/Base.php index a2df11176d..9c38facbdd 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\SearchHitMapperInterface; +use Ibexa\Core\Repository\Mapper\SearchHitMapperRegistryInterface; 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\SearchHitMapperRegistryInterface|\PHPUnit\Framework\MockObject\MockObject + */ + protected function getSearchResultMapperRegistryMock(): SearchHitMapperRegistryInterface + { + $domainMapper = $this->createMock(SearchHitMapperInterface::class); + $searchResultMapperRegistryMock = $this->createMock( + SearchHitMapperRegistryInterface::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..8bea4dd791 100644 --- a/tests/lib/Repository/Service/Mock/SearchTest.php +++ b/tests/lib/Repository/Service/Mock/SearchTest.php @@ -42,13 +42,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 +58,12 @@ public function testConstructor() $contentDomainMapperMock, $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, $settings ); } - public function providerForFindContentValidatesLocationCriteriaAndSortClauses() + public function providerForFindContentValidatesLocationCriteriaAndSortClauses(): array { return [ [ @@ -94,7 +96,7 @@ public function providerForFindContentValidatesLocationCriteriaAndSortClauses() /** * @dataProvider providerForFindContentValidatesLocationCriteriaAndSortClauses */ - public function testFindContentValidatesLocationCriteriaAndSortClauses($query, $exceptionMessage) + public function testFindContentValidatesLocationCriteriaAndSortClauses($query, $exceptionMessage): void { $this->expectException(InvalidArgumentException::class); @@ -102,6 +104,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 +112,7 @@ public function testFindContentValidatesLocationCriteriaAndSortClauses($query, $ $this->getContentDomainMapperMock(), $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -119,10 +123,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 +147,7 @@ public function providerForFindSingleValidatesLocationCriteria() /** * @dataProvider providerForFindSingleValidatesLocationCriteria */ - public function testFindSingleValidatesLocationCriteria($criterion, $exceptionMessage) + public function testFindSingleValidatesLocationCriteria($criterion, $exceptionMessage): void { $this->expectException(InvalidArgumentException::class); @@ -151,23 +155,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 +183,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 +192,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 +200,7 @@ public function testFindContentThrowsHandlerException() $this->getContentDomainMapperMock(), $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -205,9 +214,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,12 +224,14 @@ 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([ @@ -229,20 +240,21 @@ public function testFindContentWhenDomainMapperThrowsException() $mapper = $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()) + $mapper->expects(self::once()) ->method('buildContentDomainObjectsOnSearchResult') - ->with($this->equalTo($result), $this->equalTo([])) - ->willReturnCallback(static function (SearchResult $spiResult) use ($info) { + ->with(self::equalTo($result), self::equalTo([])) + ->willReturnCallback(static function (SearchResult $spiResult) use ($info): array { unset($spiResult->searchHits[0]); --$spiResult->totalCount; @@ -251,8 +263,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 +273,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->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 +299,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 +313,10 @@ public function testFindContentNoPermissionsFilter() ) ); - $mapper->expects($this->once()) + $mapper->expects(self::once()) ->method('buildContentDomainObjectsOnSearchResult') - ->with($this->isInstanceOf(SearchResult::class), $this->equalTo([])) - ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock) { + ->with($this->isInstanceOf(SearchResult::class), self::equalTo([])) + ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock): array { $spiResult->searchHits[0]->valueObject = $contentMock; return []; @@ -309,7 +324,7 @@ public function testFindContentNoPermissionsFilter() $result = $service->findContent($serviceQuery, $languageFilter, false); - $this->assertEquals( + self::assertEquals( new SearchResult( [ 'searchHits' => [new SearchHit(['valueObject' => $contentMock])], @@ -326,18 +341,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,22 +363,23 @@ 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( + self::returnValue( new SearchResult( [ 'searchHits' => [new SearchHit(['valueObject' => $spiContentInfo])], @@ -371,10 +390,10 @@ public function testFindContentWithPermission() ); $domainMapperMock - ->expects($this->once()) + ->expects(self::once()) ->method('buildContentDomainObjectsOnSearchResult') - ->with($this->isInstanceOf(SearchResult::class), $this->equalTo([])) - ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock) { + ->with(self::isInstanceOf(SearchResult::class), self::equalTo([])) + ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock): array { $spiResult->searchHits[0]->valueObject = $contentMock; return []; @@ -382,7 +401,7 @@ public function testFindContentWithPermission() $result = $service->findContent($query, $languageFilter, true); - $this->assertEquals( + self::assertEquals( new SearchResult( [ 'searchHits' => [new SearchHit(['valueObject' => $contentMock])], @@ -399,22 +418,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(), $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 +444,19 @@ 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)); + ->will(self::returnValue(false)); - $mapper->expects($this->once()) + $mapper->expects(self::once()) ->method('buildContentDomainObjectsOnSearchResult') - ->with($this->isInstanceOf(SearchResult::class), $this->equalTo([])) + ->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 ); @@ -448,12 +470,15 @@ public function testFindContentWithDefaultQueryValues() /** @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 +488,7 @@ public function testFindContentWithDefaultQueryValues() /* @var \PHPUnit\Framework\MockObject\MockObject $searchHandlerMock */ $searchHandlerMock - ->expects($this->once()) + ->expects(self::once()) ->method('findContent') ->with( new Query( @@ -475,7 +500,7 @@ public function testFindContentWithDefaultQueryValues() [] ) ->will( - $this->returnValue( + self::returnValue( new SearchResult( [ 'searchHits' => [new SearchHit(['valueObject' => $spiContentInfo])], @@ -489,7 +514,7 @@ public function testFindContentWithDefaultQueryValues() ->expects($this->once()) ->method('buildContentDomainObjectsOnSearchResult') ->with($this->isInstanceOf(SearchResult::class), $this->equalTo([])) - ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock) { + ->willReturnCallback(static function (SearchResult $spiResult) use ($contentMock): array { $spiResult->searchHits[0]->valueObject = $contentMock; return []; @@ -497,7 +522,7 @@ public function testFindContentWithDefaultQueryValues() $result = $service->findContent(new Query(), $languageFilter, false); - $this->assertEquals( + self::assertEquals( new SearchResult( [ 'searchHits' => [new SearchHit(['valueObject' => $contentMock])], @@ -514,19 +539,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 +578,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 +587,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 +619,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 +688,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, [] ); @@ -708,13 +745,13 @@ public function testFindLocationsWithPermission() $domainMapperMock->expects($this->once()) ->method('buildLocationDomainObjectsOnSearchResult') ->with($this->equalTo($spiResult)) - ->willReturnCallback(static function (SearchResult $spiResult) use ($endResult) { + ->willReturnCallback(static function (SearchResult $spiResult) use ($endResult): array { $spiResult->searchHits[0] = $endResult->searchHits[0]; return []; }); - $result = $service->findLocations($query, [], true); + $result = $service->findLocations($query); $this->assertEquals( $endResult, @@ -725,19 +762,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 +834,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 +850,7 @@ public function testFindLocationsBackgroundIndexerWhenDomainMapperThrowsExceptio $mapper = $this->getContentDomainMapperMock(), $this->getPermissionCriterionResolverMock(), $indexer, + $searchResultMapperRegistryMock, ])->setMethods(['addPermissionsCriterion']) ->getMock(); @@ -826,7 +869,7 @@ public function testFindLocationsBackgroundIndexerWhenDomainMapperThrowsExceptio $mapper->expects($this->once()) ->method('buildLocationDomainObjectsOnSearchResult') ->with($this->equalTo($result)) - ->willReturnCallback(static function (SearchResult $spiResult) use ($location) { + ->willReturnCallback(static function (SearchResult $spiResult) use ($location): array { unset($spiResult->searchHits[0]); --$spiResult->totalCount; @@ -842,7 +885,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 +894,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 +902,7 @@ public function testFindLocationsThrowsHandlerException() $this->getContentDomainMapperMock(), $permissionsCriterionResolverMock, new NullIndexer(), + $searchResultMapperRegistryMock, [] ); @@ -873,24 +918,27 @@ public function testFindLocationsThrowsHandlerException() ->with('content', 'read') ->will($this->throwException(new Exception('Handler threw an exception'))); - $service->findLocations($query, [], true); + $service->findLocations($query); } /** * 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, [] ); @@ -930,7 +978,7 @@ public function testFindLocationsWithDefaultQueryValues() $domainMapperMock->expects($this->once()) ->method('buildLocationDomainObjectsOnSearchResult') ->with($this->equalTo($spiResult)) - ->willReturnCallback(static function (SearchResult $spiResult) use ($endResult) { + ->willReturnCallback(static function (SearchResult $spiResult) use ($endResult): array { $spiResult->searchHits[0] = $endResult->searchHits[0]; return []; @@ -947,7 +995,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