diff --git a/src/contracts/Repository/Values/Content/Query/Criterion/ContentName.php b/src/contracts/Repository/Values/Content/Query/Criterion/ContentName.php new file mode 100644 index 0000000000..76518b702d --- /dev/null +++ b/src/contracts/Repository/Values/Content/Query/Criterion/ContentName.php @@ -0,0 +1,30 @@ + + */ + public function getSpecifications(): array + { + return [ + new Specifications(Operator::LIKE, Specifications::FORMAT_SINGLE), + ]; + } +} diff --git a/src/lib/Resources/settings/search_engines/legacy/criterion_handlers_common.yml b/src/lib/Resources/settings/search_engines/legacy/criterion_handlers_common.yml index 84164b1322..e62b350481 100644 --- a/src/lib/Resources/settings/search_engines/legacy/criterion_handlers_common.yml +++ b/src/lib/Resources/settings/search_engines/legacy/criterion_handlers_common.yml @@ -73,6 +73,12 @@ services: - {name: ibexa.search.legacy.gateway.criterion_handler.content} - {name: ibexa.search.legacy.gateway.criterion_handler.location} + Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler\ContentName: + parent: Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler + tags: + - { name: ibexa.search.legacy.gateway.criterion_handler.content } + - { name: ibexa.search.legacy.gateway.criterion_handler.location } + Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler\ContentTypeGroupId: parent: Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler class: Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler\ContentTypeGroupId diff --git a/src/lib/Search/Legacy/Content/Common/Gateway/CriterionHandler/ContentName.php b/src/lib/Search/Legacy/Content/Common/Gateway/CriterionHandler/ContentName.php new file mode 100644 index 0000000000..63314e961c --- /dev/null +++ b/src/lib/Search/Legacy/Content/Common/Gateway/CriterionHandler/ContentName.php @@ -0,0 +1,96 @@ +operator === Criterion\Operator::LIKE; + } + + /** + * @param array{ + * languages: array, + * useAlwaysAvailable: bool, + * } $languageSettings + * + * @throws \Doctrine\DBAL\Exception + */ + public function handle( + CriteriaConverter $converter, + QueryBuilder $queryBuilder, + Criterion $criterion, + array $languageSettings + ): string { + $subQuery = $this->connection->createQueryBuilder(); + $subQuery + ->select('1') + ->from(Gateway::CONTENT_NAME_TABLE, self::CONTENTOBJECT_NAME_ALIAS) + ->andWhere( + $queryBuilder->expr()->eq( + self::CONTENTOBJECT_NAME_ALIAS . '.contentobject_id', + self::CONTENTOBJECT_ALIAS . '.id' + ), + $queryBuilder->expr()->eq( + self::CONTENTOBJECT_NAME_ALIAS . '.content_version', + self::CONTENTOBJECT_ALIAS . '.current_version' + ), + $queryBuilder->expr()->like( + $this->toLowerCase(self::CONTENTOBJECT_NAME_ALIAS . '.name'), + $this->toLowerCase( + $queryBuilder->createNamedParameter( + $this->prepareValue($criterion) + ) + ) + ) + ); + + return sprintf( + 'EXISTS (%s)', + $subQuery->getSQL() + ); + } + + private function prepareValue(Criterion $criterion): string + { + /** @var string $value */ + $value = $criterion->value; + + return str_replace( + '*', + '%', + addcslashes( + $value, + '%_' + ) + ); + } + + /** + * @throws \Doctrine\DBAL\Exception + */ + private function toLowerCase(string $value): string + { + return $this->connection->getDatabasePlatform()->getLowerExpression($value); + } +} diff --git a/tests/integration/Core/Repository/SearchServiceContentNameTest.php b/tests/integration/Core/Repository/SearchServiceContentNameTest.php new file mode 100644 index 0000000000..b333212898 --- /dev/null +++ b/tests/integration/Core/Repository/SearchServiceContentNameTest.php @@ -0,0 +1,301 @@ + self::LANGUAGE_CODE_ENG, + 'name' => self::CAR_ENG, + 'translations' => [ + self::LANGUAGE_CODE_GER => self::CAR_GER, + ], + ], + [ + 'mainLanguageCode' => self::LANGUAGE_CODE_ENG, + 'name' => self::SPORTS_CAR_ENG, + 'translations' => [ + self::LANGUAGE_CODE_GER => self::SPORTS_CAR_GER, + ], + ], + [ + 'mainLanguageCode' => self::LANGUAGE_CODE_ENG, + 'name' => self::TRUCK_ENG, + 'translations' => [ + self::LANGUAGE_CODE_GER => self::TRUCK_GER, + ], + ], + ]; + + protected function setUp(): void + { + parent::setUp(); + + $this->createTestContentItems(); + + $this->refreshSearch(); + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + */ + public function testCriterionFindAllContentItems(): void + { + $query = $this->createQuery( + $this->createContentNameCriterion('*') + ); + + self::assertSame( + self::TOTAL_COUNT, + self::getSearchService()->findContent($query)->totalCount + ); + } + + /** + * @dataProvider provideDataForTestCriterion + * + * @param array $expectedContentItemTitles + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidCriterionArgumentException + */ + public function testCriterion( + Criterion $criterion, + ?string $languageCode, + array $expectedContentItemTitles, + int $expectedCount + ): void { + $result = self::getSearchService()->findContent( + $this->createQuery($criterion), + $this->getLanguageFilter($languageCode) + ); + + self::assertEquals( + $expectedContentItemTitles, + array_map( + static function (SearchHit $searchHit) use ($languageCode): ?string { + $content = $searchHit->valueObject; + if ($content instanceof Content) { + return $content->getName($languageCode); + } + + return null; + }, + $result->searchHits + ) + ); + + self::assertSame( + $expectedCount, + $result->totalCount + ); + } + + /** + * @return iterable, + * int, + * }> + */ + public function provideDataForTestCriterion(): iterable + { + yield 'Content items not found' => [ + $this->createContentNameCriterion('foo'), + self::LANGUAGE_CODE_ENG, + [], + 0, + ]; + + yield 'Return content items in default language (English) that contain "car" in name' => [ + $this->createContentNameCriterion('*car*'), + null, + [ + self::CAR_ENG, + self::SPORTS_CAR_ENG, + ], + 2, + ]; + + yield 'Return content item in default language (English) whose name starts with "car"' => [ + $this->createContentNameCriterion('Car*'), + null, + [ + self::CAR_ENG, + ], + 1, + ]; + + yield 'Return content item in English that contain "Spo*t*" in name' => [ + $this->createContentNameCriterion('Spo*t*'), + self::LANGUAGE_CODE_ENG, + [ + self::SPORTS_CAR_ENG, + ], + 1, + ]; + + yield 'Return content item in English with name "sports car"' => [ + $this->createContentNameCriterion('sports car'), + self::LANGUAGE_CODE_ENG, + [ + self::SPORTS_CAR_ENG, + ], + 1, + ]; + + yield 'Return content item in English that contain "**ruc*" in name' => [ + $this->createContentNameCriterion('**ruc*'), + self::LANGUAGE_CODE_ENG, + [ + self::TRUCK_ENG, + ], + 1, + ]; + + yield 'Return content item in German that contain "aut*" in name' => [ + $this->createContentNameCriterion('aut*'), + self::LANGUAGE_CODE_GER, + [ + self::CAR_GER, + ], + 1, + ]; + + yield 'Return content items in German that contain "*wagen" in name' => [ + $this->createContentNameCriterion('*wagen'), + self::LANGUAGE_CODE_GER, + [ + self::SPORTS_CAR_GER, + self::TRUCK_GER, + ], + 2, + ]; + + yield 'Return content item in German with name "lastwagen"' => [ + $this->createContentNameCriterion('lastwagen'), + self::LANGUAGE_CODE_GER, + [ + self::TRUCK_GER, + ], + 1, + ]; + } + + private function createTestContentItems(): void + { + foreach (self::CONTENT_ITEMS_MAP as $contentItem) { + $this->createContent( + $contentItem['name'], + $contentItem['mainLanguageCode'], + $contentItem['translations'] + ); + } + } + + /** + * @param array $translations + * + * @return \Ibexa\Contracts\Core\Repository\Values\Content\Content + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\ContentFieldValidationException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\ContentValidationException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException + */ + private function createContent( + string $title, + string $mainLanguageCode, + array $translations + ): Content { + $contentService = self::getContentService(); + $createStruct = $contentService->newContentCreateStruct( + $this->loadContentType('article'), + $mainLanguageCode + ); + + $createStruct->setField('title', new Value($title)); + + if (!empty($translations)) { + foreach ($translations as $languageCode => $translatedName) { + $createStruct->setField('title', new Value($translatedName), $languageCode); + } + } + + $content = $contentService->createContent($createStruct); + + $contentService->publishVersion($content->getVersionInfo()); + + return $content; + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + private function loadContentType(string $contentTypeIdentifier): ContentType + { + return self::getContentTypeService() + ->loadContentTypeByIdentifier($contentTypeIdentifier); + } + + private function createContentNameCriterion(string $value): Criterion + { + return new Criterion\ContentName($value); + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidCriterionArgumentException + */ + private function createQuery(Criterion $criterion): Query + { + $query = new Query(); + $query->filter = new Criterion\LogicalAnd( + [$criterion] + ); + + return $query; + } + + /** + * @return array{}|array{ + * languages: array + * } + */ + public function getLanguageFilter(?string $languageCode): array + { + return null !== $languageCode + ? ['languages' => [$languageCode]] + : []; + } +}