From 31dc70a1803fe3e42609b7e4429c0519f5cecc45 Mon Sep 17 00:00:00 2001 From: Imko Schumacher Date: Sun, 17 Dec 2023 18:05:57 +0100 Subject: [PATCH] Fix search with categories on translated pages Results from repositories have the uid of the default language, even when the rest is translated. However, the foreign uid filter for the index table needs the translated uid. Additionally, the category filters are now translated and provided as objects. --- .../Domain/Repository/CategoryRepository.php | 32 ++++++++ Classes/Domain/Repository/EventRepository.php | 9 +-- .../CategoryFilterEventListener.php | 75 ++++++------------- ....php => SearchConstraintEventListener.php} | 23 +++--- Configuration/Services.yaml | 2 +- .../Fixtures/EventsWithCategories.csv | 34 +++++++++ .../Domain/Repository/IndexRepositoryTest.php | 73 ++++++++++++++++++ 7 files changed, 177 insertions(+), 71 deletions(-) create mode 100644 Classes/Domain/Repository/CategoryRepository.php rename Classes/EventListener/{DefaultEventSearchListener.php => SearchConstraintEventListener.php} (78%) create mode 100644 Tests/Functional/Domain/Repository/Fixtures/EventsWithCategories.csv create mode 100644 Tests/Functional/Domain/Repository/IndexRepositoryTest.php diff --git a/Classes/Domain/Repository/CategoryRepository.php b/Classes/Domain/Repository/CategoryRepository.php new file mode 100644 index 00000000..402cdafb --- /dev/null +++ b/Classes/Domain/Repository/CategoryRepository.php @@ -0,0 +1,32 @@ +objectType = Category::class; + } + + public function findByIds(array $categoryIds, array $orderings = []): array|QueryResultInterface + { + $query = $this->createQuery(); + $query->getQuerySettings()->setRespectStoragePage(false); + $query->getQuerySettings()->setRespectSysLanguage(false); + + $query->matching($query->in('uid', $categoryIds)); + + if (!empty($orderings)) { + $query->setOrderings($orderings); + } + + return $query->execute(); + } +} diff --git a/Classes/Domain/Repository/EventRepository.php b/Classes/Domain/Repository/EventRepository.php index cd415f4c..54cb8056 100644 --- a/Classes/Domain/Repository/EventRepository.php +++ b/Classes/Domain/Repository/EventRepository.php @@ -27,6 +27,8 @@ public function injectIndexRepository(IndexRepository $indexRepository): void public function findBySearch(Search $search): array { $query = $this->createQuery(); + $query->getQuerySettings()->setRespectStoragePage(false); + $constraints = []; if ($search->getFullText()) { $constraints['fullText'] = $query->logicalOr( @@ -44,12 +46,7 @@ public function findBySearch(Search $search): array $query->matching($query->logicalAnd(...$constraints)); $rows = $query->execute(true); - $ids = []; - foreach ($rows as $row) { - $ids[] = (int)$row['uid']; - } - - return $ids; + return array_map(static fn ($row) => (int)($row['_LOCALIZED_UID'] ?? $row['uid']), $rows); } public function findOneByImportId(string $importId, int $pid = null): ?object diff --git a/Classes/EventListener/CategoryFilterEventListener.php b/Classes/EventListener/CategoryFilterEventListener.php index 3dcc5789..38ce0530 100644 --- a/Classes/EventListener/CategoryFilterEventListener.php +++ b/Classes/EventListener/CategoryFilterEventListener.php @@ -4,14 +4,11 @@ namespace HDNET\Calendarize\EventListener; -use Doctrine\DBAL\ArrayParameterType; use HDNET\Calendarize\Controller\CalendarController; +use HDNET\Calendarize\Domain\Repository\CategoryRepository; use HDNET\Calendarize\Event\GenericActionAssignmentEvent; -use TYPO3\CMS\Core\Context\Context; -use TYPO3\CMS\Core\Context\LanguageAspect; use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer; -use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Persistence\QueryInterface; /** * Gets all used categories from the default Event and assigns it to extended.categories in fluid. @@ -23,12 +20,18 @@ class CategoryFilterEventListener protected string $itemFieldName = 'categories'; + public function __construct( + private readonly ConnectionPool $connectionPool, + private readonly CategoryRepository $categoryRepository, + ) { + } + public function __invoke(GenericActionAssignmentEvent $event): void { if (CalendarController::class !== $event->getClassName() || 'searchAction' !== $event->getFunctionName()) { return; } - if (!$this->checkConfiguration($event->getVariables()['configurations'] ?? [], $this->itemTableName)) { + if (!\in_array($this->itemTableName, array_column($event->getVariables()['configurations'] ?? [], 'tableName'), true)) { return; } $variables = $event->getVariables(); @@ -40,51 +43,15 @@ public function __invoke(GenericActionAssignmentEvent $event): void $event->setVariables($variables); } - /** - * Check if the event configuration is active. - */ - protected function checkConfiguration(array $configurations, string $tableName): bool - { - foreach ($configurations as $config) { - if (($config['tableName'] ?? '') === $tableName) { - return true; - } - } - - return false; - } - /** * Gets all used categories of the default Event (self::itemTableName). */ protected function getCategories(string $tableName, string $fieldName): array { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_category'); - - $queryBuilder - ->getRestrictions() - ->removeAll() - ->add(GeneralUtility::makeInstance(FrontendRestrictionContainer::class)); - - /** @var Context $context */ - $context = GeneralUtility::makeInstance(Context::class); - /** @var LanguageAspect $languageAspect */ - $languageAspect = $context->getAspect('language'); - $languageUid = $languageAspect->getId(); - - $queryBuilder->select('sys_category.*') - ->groupBy('sys_category.uid') - ->from('sys_category') - ->join( - 'sys_category', - 'sys_category_record_mm', - 'sys_category_record_mm', - $queryBuilder->expr()->eq( - 'sys_category_record_mm.uid_local', - $queryBuilder->quoteIdentifier('sys_category.uid') - ) - ) + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_category'); + $queryBuilder->distinct() + ->select('uid_local') + ->from('sys_category_record_mm') ->where( $queryBuilder->expr()->and( $queryBuilder->expr()->eq( @@ -94,17 +61,17 @@ protected function getCategories(string $tableName, string $fieldName): array $queryBuilder->expr()->eq( 'sys_category_record_mm.fieldname', $queryBuilder->createNamedParameter($fieldName, \PDO::PARAM_STR) - ), - $queryBuilder->expr()->in( - 'sys_category.sys_language_uid', - $queryBuilder->createNamedParameter([-1, $languageUid], ArrayParameterType::INTEGER) ) ) - ) - ->orderBy('sys_category.title', 'ASC'); + ); - return $queryBuilder + $categoryIds = $queryBuilder ->executeQuery() - ->fetchAllAssociative(); + ->fetchFirstColumn(); + + return $this->categoryRepository->findByIds( + $categoryIds, + ['title' => QueryInterface::ORDER_ASCENDING] + )->toArray(); } } diff --git a/Classes/EventListener/DefaultEventSearchListener.php b/Classes/EventListener/SearchConstraintEventListener.php similarity index 78% rename from Classes/EventListener/DefaultEventSearchListener.php rename to Classes/EventListener/SearchConstraintEventListener.php index 164a05ea..753e714d 100644 --- a/Classes/EventListener/DefaultEventSearchListener.php +++ b/Classes/EventListener/SearchConstraintEventListener.php @@ -8,11 +8,12 @@ use HDNET\Calendarize\Domain\Repository\EventRepository; use HDNET\Calendarize\Event\IndexRepositoryFindBySearchEvent; use HDNET\Calendarize\Register; -use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; -use TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings; -class DefaultEventSearchListener +/** + * Filters Event records by the search query ('categories' and 'fullText'). + */ +class SearchConstraintEventListener { public function __construct(protected EventRepository $eventRepository) { @@ -23,23 +24,21 @@ public function __invoke(IndexRepositoryFindBySearchEvent $event): void if (!\in_array(Register::UNIQUE_REGISTER_KEY, $event->getIndexTypes(), true)) { return; } + $foreignIds = $event->getForeignIds(); + if (!empty($foreignIds['tx_calendarize_domain_model_event'])) { + // Skip if there are already ids (e.g. by other extensions) + return; + } $search = $this->getSearchDto($event); - if (!$search->isSearch()) { return; } - /** @var Typo3QuerySettings $querySettings */ - $querySettings = GeneralUtility::makeInstance(Typo3QuerySettings::class); - $querySettings->setRespectStoragePage(false); - $this->eventRepository->setDefaultQuerySettings($querySettings); $searchTermIds = $this->eventRepository->findBySearch($search); - // Blocks result (displaying no event) on no search match (empty id array) $searchTermIds[] = -1; - $foreignIds = $event->getForeignIds(); $foreignIds['tx_calendarize_domain_model_event'] = $searchTermIds; $event->setForeignIds($foreignIds); } @@ -56,6 +55,10 @@ protected function getSearchDto(IndexRepositoryFindBySearchEvent $event): Search $search->setCategories($categories); } elseif (MathUtility::canBeInterpretedAsInteger($customSearch['category'] ?? '')) { // Fallback for previous mode + @trigger_error( + 'Search request with the parameter \'category\' is deprecated. Use \'categories\' instead.', + \E_USER_DEPRECATED + ); $search->setCategories([(int)$customSearch['category']]); } diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index b97fa662..e46393d5 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -38,7 +38,7 @@ services: identifier: 'categoryFilter' event: HDNET\Calendarize\Event\GenericActionAssignmentEvent - HDNET\Calendarize\EventListener\DefaultEventSearchListener: + HDNET\Calendarize\EventListener\SearchConstraintEventListener: tags: - name: event.listener identifier: 'defaultEventSearch' diff --git a/Tests/Functional/Domain/Repository/Fixtures/EventsWithCategories.csv b/Tests/Functional/Domain/Repository/Fixtures/EventsWithCategories.csv new file mode 100644 index 00000000..37f47503 --- /dev/null +++ b/Tests/Functional/Domain/Repository/Fixtures/EventsWithCategories.csv @@ -0,0 +1,34 @@ +"pages" +,"uid","pid",doktype,"sorting","deleted","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title" +,1,0,1,256,0,0,0,0,0,0,"Parent page" +,2,1,254,256,0,0,0,0,0,0,"Events" +,3,2,254,256,0,0,0,0,0,0,"Events with Categories" + +"sys_category", +,"uid","pid","sorting","deleted","sys_language_uid","l10n_parent","t3_origuid","t3ver_wsid","t3ver_state","t3ver_stage","t3ver_oid","title","parent","items","l10n_diffsource","description" +,28,0,256,0,0,0,0,0,0,0,0,"Category A",0,0,, +,29,0,512,0,0,0,0,0,0,0,0,"Category B",0,0,, +,30,0,768,0,0,0,0,0,0,0,0,"Category C",0,0,, +,31,0,1024,0,0,0,0,0,0,0,0,"Category A.A",28,0,, + +"sys_category_record_mm", +,"uid_local","uid_foreign","tablenames","sorting","sorting_foreign","fieldname" +,28,20,"tx_calendarize_domain_model_event",0,1,"categories" +,28,21,"tx_calendarize_domain_model_event",0,2,"categories" +,29,30,"tx_calendarize_domain_model_event",0,3,"categories" + +"tx_calendarize_domain_model_event" +,uid,pid,sys_language_uid,l10n_parent,title,calendarize,categories +,10,2,0,0,No Category,10,0 +,11,2,1,10,[Translated to German] No Category,11,0 +,20,3,0,0,Only Category A,20,1 +,21,3,1,20,[Translated to German] Only Category A,21,1 +,30,3,0,0,Only Category B,30,0 + +"tx_calendarize_domain_model_configuration" +,"uid","pid","type","handling","start_date","all_day","t3ver_oid","t3ver_wsid","t3ver_state" +,10,2,"time","include","2025-03-01",1,0,0,0 +,11,2,"time","include","2025-03-01",1,0,0,0 +,20,3,"time","include","2025-03-02",1,0,0,0 +,21,3,"time","include","2025-03-02",1,0,0,0 +,30,3,"time","include","2025-03-03",1,0,0,0 diff --git a/Tests/Functional/Domain/Repository/IndexRepositoryTest.php b/Tests/Functional/Domain/Repository/IndexRepositoryTest.php new file mode 100644 index 00000000..af010998 --- /dev/null +++ b/Tests/Functional/Domain/Repository/IndexRepositoryTest.php @@ -0,0 +1,73 @@ +importCSVDataSet(__DIR__ . '/Fixtures/EventsWithCategories.csv'); + + $this->indexerService = GeneralUtility::makeInstance(IndexerService::class); + $this->indexerService->reindexAll(); + } + + /** + * @dataProvider findBySearchDataProvider + */ + public function testFindBySearch( + int $language, + ?\DateTimeInterface $startDate, + ?\DateTimeInterface $endDate, + array $customSearch, + int $limit, + array $expectedEventIds + ): void { + $context = GeneralUtility::makeInstance(Context::class); + $context->setAspect('language', new LanguageAspect($language, $language, LanguageAspect::OVERLAYS_ON)); + + $subject = GeneralUtility::makeInstance(IndexRepository::class); + $subject->setIndexTypes([Register::UNIQUE_REGISTER_KEY]); + $subject->setOverridePageIds([2, 3]); + + $result = $subject->findBySearch($startDate, $endDate, $customSearch, $limit)->toArray(); + $eventIds = array_map(static fn ($i) => $i->getForeignUid(), $result); + + self::assertEquals($expectedEventIds, $eventIds); + } + + public static function findBySearchDataProvider(): array + { + return [ + 'no filter' => [0, null, null, [], 0, [10, 20, 30]], + 'category' => [0, null, null, ['categories' => ['28']], 0, [20]], + 'category translated' => [1, null, null, ['categories' => ['28']], 0, [21]], + 'fullText' => [0, null, null, ['fullText' => 'Only'], 0, [20, 30]], + 'fullText translated' => [1, null, null, ['fullText' => 'No'], 0, [11]], + 'start and end date' => [0, new \DateTime('2025-02-28'), new \DateTime('2025-03-01'), [], 0, [10]], + 'dates + categories + text' => [ + 0, + new \DateTime('2025-02-28'), + new \DateTime('2025-03-05'), + ['categories' => ['28', '29'], 'fullText' => 'B'], + 0, + [30], + ], + ]; + } +}