Passed
Pull Request — release-11.5.x (#3230)
by Markus
41:39 queued 17:21
created

SearchResultSetService   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Test Coverage

Coverage 87.59%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 49
eloc 137
c 1
b 0
f 1
dl 0
loc 454
ccs 127
cts 145
cp 0.8759
rs 8.48

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getInitialSearchIsConfigured() 0 3 4
A handleSearchHook() 0 14 4
A getSearch() 0 3 1
A modifyQuery() 0 27 6
A injectObjectManager() 0 3 1
A getInitializedSearchResultSet() 0 11 1
A getLastSearchWasExecutedWithEmptyQueryString() 0 8 2
A getDocumentById() 0 14 2
A getLastResultSet() 0 3 1
A shouldReturnEmptyResultSetWithoutExecutedSearch() 0 13 5
A doASearch() 0 12 2
A getResultSetClassName() 0 3 1
A initializeRegisteredSearchComponents() 0 17 4
A getAutoCorrection() 0 18 4
A getRegisteredSearchComponents() 0 3 1
A getIsSolrAvailable() 0 4 1
A performAutoCorrection() 0 28 4
A getParsedSearchResults() 0 6 1
A __construct() 0 12 1
A search() 0 54 3

How to fix   Complexity   

Complex Class

Complex classes like SearchResultSetService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SearchResultSetService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace ApacheSolrForTypo3\Solr\Domain\Search\ResultSet;
19
20
use ApacheSolrForTypo3\Solr\Domain\Search\Query\ParameterBuilder\QueryFields;
21
use ApacheSolrForTypo3\Solr\Domain\Search\Query\Query;
22
use ApacheSolrForTypo3\Solr\Domain\Search\Query\QueryBuilder;
23
use ApacheSolrForTypo3\Solr\Domain\Search\Query\SearchQuery;
24
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\Parser\ResultParserRegistry;
25
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResult;
26
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResultBuilder;
27
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
28
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequestAware;
29
use ApacheSolrForTypo3\Solr\Domain\Variants\VariantsProcessor;
30
use ApacheSolrForTypo3\Solr\Query\Modifier\Modifier;
31
use ApacheSolrForTypo3\Solr\Search;
32
use ApacheSolrForTypo3\Solr\Search\QueryAware;
33
use ApacheSolrForTypo3\Solr\Search\SearchAware;
34
use ApacheSolrForTypo3\Solr\Search\SearchComponentManager;
35
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
36
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
37
use ApacheSolrForTypo3\Solr\System\Solr\Document\Document;
38
use ApacheSolrForTypo3\Solr\System\Solr\ResponseAdapter;
39
use ApacheSolrForTypo3\Solr\System\Solr\SolrIncompleteResponseException;
40
use Exception;
41
use function get_class;
42
use TYPO3\CMS\Core\Utility\GeneralUtility;
43
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
44
use UnexpectedValueException;
45
46
/**
47
 * The SearchResultSetService is responsible to build a SearchResultSet from a SearchRequest.
48
 * It encapsulates the logic to trigger a search in order to be able to reuse it in multiple places.
49
 *
50
 * @author Timo Schmidt <[email protected]>
51
 */
52
class SearchResultSetService
53
{
54
    /**
55
     * Track, if the number of results per page has been changed by the current request
56
     *
57
     * @var bool
58
     */
59
    protected bool $resultsPerPageChanged = false;
60
61
    /**
62
     * @var Search
63
     */
64
    protected Search $search;
65
66
    /**
67
     * @var SearchResultSet|null
68
     */
69
    protected ?SearchResultSet $lastResultSet = null;
70
71
    /**
72
     * @var bool
73
     */
74
    protected bool $isSolrAvailable = false;
75
76
    /**
77
     * @var TypoScriptConfiguration
78
     */
79
    protected TypoScriptConfiguration $typoScriptConfiguration;
80
81
    /**
82
     * @var SolrLogManager
83
     */
84
    protected SolrLogManager $logger;
85
86
    /**
87
     * @var SearchResultBuilder
88
     */
89
    protected SearchResultBuilder $searchResultBuilder;
90
91
    /**
92
     * @var QueryBuilder
93
     */
94
    protected QueryBuilder $queryBuilder;
95
96
    /**
97
     * @var ObjectManagerInterface
98
     */
99
    protected ObjectManagerInterface $objectManager;
100
101
    /**
102
     * @param TypoScriptConfiguration $configuration
103
     * @param Search $search
104
     * @param SolrLogManager|null $solrLogManager
105
     * @param SearchResultBuilder|null $resultBuilder
106
     * @param QueryBuilder|null $queryBuilder
107
     */
108 52
    public function __construct(
109
        TypoScriptConfiguration $configuration,
110
        Search $search,
111
        SolrLogManager $solrLogManager = null,
112
        SearchResultBuilder $resultBuilder = null,
113
        QueryBuilder $queryBuilder = null
114
    ) {
115 52
        $this->search = $search;
116 52
        $this->typoScriptConfiguration = $configuration;
117 52
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
118 52
        $this->searchResultBuilder = $resultBuilder ?? GeneralUtility::makeInstance(SearchResultBuilder::class);
119 52
        $this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(QueryBuilder::class, /** @scrutinizer ignore-type */ $configuration, /** @scrutinizer ignore-type */ $solrLogManager);
120
    }
121
122
    /**
123
     * @param ObjectManagerInterface $objectManager
124
     */
125 51
    public function injectObjectManager(ObjectManagerInterface $objectManager)
126
    {
127 51
        $this->objectManager = $objectManager;
128
    }
129
130
    /**
131
     * @param bool $useCache
132
     * @return bool
133
     * @throws Exception
134
     */
135
    public function getIsSolrAvailable(bool $useCache = true): bool
136
    {
137
        $this->isSolrAvailable = $this->search->ping($useCache);
138
        return $this->isSolrAvailable;
139
    }
140
141
    /**
142
     * Retrieves the used search instance.
143
     *
144
     * @return Search
145
     */
146 1
    public function getSearch(): Search
147
    {
148 1
        return $this->search;
149
    }
150
151
    /**
152
     * @param Query $query
153
     * @param SearchRequest $searchRequest
154
     */
155 41
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
156
    {
157 41
        $searchComponents = $this->getRegisteredSearchComponents();
158
159 41
        foreach ($searchComponents as $searchComponent) {
160
            /** @var Search\SearchComponent $searchComponent */
161 36
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
162
163 36
            if ($searchComponent instanceof QueryAware) {
164 36
                $searchComponent->setQuery($query);
165
            }
166
167 36
            if ($searchComponent instanceof SearchRequestAware) {
168 35
                $searchComponent->setSearchRequest($searchRequest);
169
            }
170
171 36
            $searchComponent->initializeSearchComponent();
172
        }
173
    }
174
175
    /**
176
     * @return string
177
     */
178 49
    protected function getResultSetClassName(): string
179
    {
180 49
        return $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] ?? SearchResultSet::class;
181
    }
182
183
    /**
184
     * Performs a search and returns a SearchResultSet.
185
     *
186
     * @param SearchRequest $searchRequest
187
     * @return SearchResultSet
188
     * @throws Facets\InvalidFacetPackageException
189
     * @throws Exception
190
     */
191 49
    public function search(SearchRequest $searchRequest): SearchResultSet
192
    {
193 49
        $resultSet = $this->getInitializedSearchResultSet($searchRequest);
194 49
        $this->lastResultSet = $resultSet;
195
196 49
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
197 49
        if ($this->shouldReturnEmptyResultSetWithoutExecutedSearch($searchRequest)) {
198 8
            $resultSet->setHasSearched(false);
199 8
            return $resultSet;
200
        }
201
202 41
        $query = $this->queryBuilder->buildSearchQuery(
203 41
            $searchRequest->getRawUserQuery(),
204 41
            $searchRequest->getResultsPerPage(),
205 41
            $searchRequest->getAdditionalFilters()
206
        );
207 41
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
208 41
        $resultSet->setUsedQuery($query);
209
210
        // performing the actual search, sending the query to the Solr server
211 41
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
212 41
        $response = $this->doASearch($query, $searchRequest);
213
214 41
        if ($searchRequest->getResultsPerPage() === 0) {
215
            // when resultPerPage was forced to 0 we also set the numFound to 0 to hide results, e.g.
216
            // when results for the initial search should not be shown.
217
            // @extensionScannerIgnoreLine
218 1
            $response->response->numFound = 0;
219
        }
220
221 41
        $resultSet->setHasSearched(true);
222 41
        $resultSet->setResponse($response);
223
224 41
        $this->getParsedSearchResults($resultSet);
225
226 41
        $resultSet->setUsedAdditionalFilters($this->queryBuilder->getAdditionalFilters());
227
228
        /** @var $variantsProcessor VariantsProcessor */
229 41
        $variantsProcessor = GeneralUtility::makeInstance(
230
            VariantsProcessor::class,
231
            /** @scrutinizer ignore-type */
232 41
            $this->typoScriptConfiguration,
233
            /** @scrutinizer ignore-type */
234 41
            $this->searchResultBuilder
235
        );
236 41
        $variantsProcessor->process($resultSet);
237
238
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
239 41
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
240 41
        $searchResultReconstitutionProcessor->process($resultSet);
241
242 41
        $resultSet = $this->getAutoCorrection($resultSet);
243
244 41
        return $this->handleSearchHook('afterSearch', $resultSet);
245
    }
246
247
    /**
248
     * Uses the configured parser and retrieves the parsed search results.
249
     *
250
     * @param SearchResultSet $resultSet
251
     */
252 41
    protected function getParsedSearchResults(SearchResultSet $resultSet)
253
    {
254
        /** @var ResultParserRegistry $parserRegistry */
255 41
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
256 41
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
257 41
        $parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
258
    }
259
260
    /**
261
     * Evaluates conditions on the request and configuration and returns true if no search should be triggered and an empty
262
     * SearchResultSet should be returned.
263
     *
264
     * @param SearchRequest $searchRequest
265
     * @return bool
266
     */
267 49
    protected function shouldReturnEmptyResultSetWithoutExecutedSearch(SearchRequest $searchRequest): bool
268
    {
269 49
        if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
270
            // when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
271 6
            return true;
272
        }
273
274 43
        if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
275
            // the user entered an empty query string "" or "  " and empty querystring is not allowed
276 2
            return true;
277
        }
278
279 41
        return false;
280
    }
281
282
    /**
283
     * Initializes the SearchResultSet from the SearchRequest
284
     *
285
     * @param SearchRequest $searchRequest
286
     * @return SearchResultSet
287
     */
288 49
    protected function getInitializedSearchResultSet(SearchRequest $searchRequest): SearchResultSet
289
    {
290
        /** @var $resultSet SearchResultSet */
291 49
        $resultSetClass = $this->getResultSetClassName();
292 49
        $resultSet = $this->objectManager->get($resultSetClass);
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Extbase\Object...ManagerInterface::get() has been deprecated: since TYPO3 10.4, will be removed in version 12.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

292
        $resultSet = /** @scrutinizer ignore-deprecated */ $this->objectManager->get($resultSetClass);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
293
294 49
        $resultSet->setUsedSearchRequest($searchRequest);
295 49
        $resultSet->setUsedPage((int)$searchRequest->getPage());
296 49
        $resultSet->setUsedResultsPerPage($searchRequest->getResultsPerPage());
297 49
        $resultSet->setUsedSearch($this->search);
298 49
        return $resultSet;
299
    }
300
301
    /**
302
     * Executes the search and builds a fake response for a current bug in Apache Solr 6.3
303
     *
304
     * @param Query $query
305
     * @param SearchRequest $searchRequest
306
     * @return ResponseAdapter
307
     * @throws Exception
308
     */
309 41
    protected function doASearch(Query $query, SearchRequest $searchRequest): ResponseAdapter
310
    {
311
        // the offset multiplier is page - 1 but not less than zero
312 41
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
313 41
        $offSet = $offsetMultiplier * $searchRequest->getResultsPerPage();
314
315 41
        $response = $this->search->search($query, $offSet);
316 41
        if ($response === null) {
317
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
318
        }
319
320 41
        return $response;
321
    }
322
323
    /**
324
     * @param SearchResultSet $searchResultSet
325
     * @return SearchResultSet
326
     * @throws Facets\InvalidFacetPackageException
327
     */
328 41
    protected function getAutoCorrection(SearchResultSet $searchResultSet): SearchResultSet
329
    {
330
        // no secondary search configured
331 41
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
332 40
            return $searchResultSet;
333
        }
334
335
        // if more as zero results
336 1
        if ($searchResultSet->getAllResultCount() > 0) {
337 1
            return $searchResultSet;
338
        }
339
340
        // no corrections present
341 1
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
342
            return $searchResultSet;
343
        }
344
345 1
        return $this->performAutoCorrection($searchResultSet);
346
    }
347
348
    /**
349
     * @param SearchResultSet $searchResultSet
350
     * @return SearchResultSet
351
     *
352
     * @throws Facets\InvalidFacetPackageException
353
     */
354 1
    protected function performAutoCorrection(SearchResultSet $searchResultSet): SearchResultSet
355
    {
356 1
        $searchRequest = $searchResultSet->getUsedSearchRequest();
357 1
        $suggestions = $searchResultSet->getSpellCheckingSuggestions();
358
359 1
        $maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry();
360 1
        $runs = 0;
361
362 1
        foreach ($suggestions as $suggestion) {
363 1
            $runs++;
364
365 1
            $correction = $suggestion->getSuggestion();
366 1
            $initialQuery = $searchRequest->getRawUserQuery();
367
368 1
            $searchRequest->setRawQueryString($correction);
369 1
            $searchResultSet = $this->search($searchRequest);
0 ignored issues
show
Bug introduced by
It seems like $searchRequest can also be of type null; however, parameter $searchRequest of ApacheSolrForTypo3\Solr\...ultSetService::search() does only seem to accept ApacheSolrForTypo3\Solr\...in\Search\SearchRequest, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

369
            $searchResultSet = $this->search(/** @scrutinizer ignore-type */ $searchRequest);
Loading history...
370 1
            if ($searchResultSet->getAllResultCount() > 0) {
371 1
                $searchResultSet->setIsAutoCorrected(true);
372 1
                $searchResultSet->setCorrectedQueryString($correction);
373 1
                $searchResultSet->setInitialQueryString($initialQuery);
374 1
                break;
375
            }
376
377
            if ($runs > $maximumRuns) {
378
                break;
379
            }
380
        }
381 1
        return $searchResultSet;
382
    }
383
384
    /**
385
     * Allows to modify a query before eventually handing it over to Solr.
386
     *
387
     * @param Query $query The current query before it's being handed over to Solr.
388
     * @param SearchRequest $searchRequest The searchRequest, relevant in the current context
389
     * @param Search $search The search, relevant in the current context
390
     * @throws UnexpectedValueException
391
     * @return Query The modified query that is actually going to be given to Solr.
392
     */
393 41
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search): Query
394
    {
395
        // hook to modify the search query
396 41
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] ?? null)) {
397 35
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
398 35
                $queryModifier = $this->objectManager->get($classReference);
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Extbase\Object...ManagerInterface::get() has been deprecated: since TYPO3 10.4, will be removed in version 12.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

398
                $queryModifier = /** @scrutinizer ignore-deprecated */ $this->objectManager->get($classReference);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
399
400 35
                if ($queryModifier instanceof Modifier) {
401 35
                    if ($queryModifier instanceof SearchAware) {
402
                        $queryModifier->setSearch($search);
403
                    }
404
405 35
                    if ($queryModifier instanceof SearchRequestAware) {
406 17
                        $queryModifier->setSearchRequest($searchRequest);
407
                    }
408
409 35
                    $query = $queryModifier->modifyQuery($query);
410
                } else {
411
                    throw new UnexpectedValueException(
412
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
413
                        1310387414
414
                    );
415
                }
416
            }
417
        }
418
419 41
        return $query;
420
    }
421
422
    /**
423
     * Retrieves a single document from solr by document id.
424
     *
425
     * @param string $documentId
426
     * @return SearchResult
427
     * @throws Exception
428
     */
429 2
    public function getDocumentById(string $documentId): SearchResult
430
    {
431
        /* @var $query SearchQuery */
432 2
        $query = $this->queryBuilder->newSearchQuery($documentId)->useQueryFields(QueryFields::fromString('id'))->getQuery();
433 2
        $response = $this->search->search($query, 0, 1);
434 2
        $parsedData = $response->getParsedData();
435
        // @extensionScannerIgnoreLine
436 2
        $resultDocument = $parsedData->response->docs[0] ?? null;
437
438 2
        if (!$resultDocument instanceof Document) {
439
            throw new UnexpectedValueException('Response did not contain a valid Document object');
440
        }
441
442 2
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
443
    }
444
445
    /**
446
     * This method is used to call the registered hooks during the search execution.
447
     *
448
     * @param string $eventName
449
     * @param SearchResultSet $resultSet
450
     * @return SearchResultSet
451
     */
452 49
    private function handleSearchHook(string $eventName, SearchResultSet $resultSet): SearchResultSet
453
    {
454 49
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] ?? null)) {
455 49
            return $resultSet;
456
        }
457
458 3
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
459 3
            $afterSearchProcessor = $this->objectManager->get($classReference);
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Extbase\Object...ManagerInterface::get() has been deprecated: since TYPO3 10.4, will be removed in version 12.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

459
            $afterSearchProcessor = /** @scrutinizer ignore-deprecated */ $this->objectManager->get($classReference);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
460 3
            if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
461 3
                $afterSearchProcessor->process($resultSet);
462
            }
463
        }
464
465 3
        return $resultSet;
466
    }
467
468
    /**
469
     * @return SearchResultSet
470
     */
471
    public function getLastResultSet(): ?SearchResultSet
472
    {
473
        return $this->lastResultSet;
474
    }
475
476
    /**
477
     * This method returns true when the last search was executed with an empty query
478
     * string or whitespaces only. When no search was triggered it will return false.
479
     *
480
     * @return bool
481
     */
482
    public function getLastSearchWasExecutedWithEmptyQueryString(): bool
483
    {
484
        $wasEmptyQueryString = false;
485
        if ($this->lastResultSet != null) {
486
            $wasEmptyQueryString = $this->lastResultSet->getUsedSearchRequest()->getRawUserQueryIsEmptyString();
487
        }
488
489
        return $wasEmptyQueryString;
490
    }
491
492
    /**
493
     * @return bool
494
     */
495 9
    protected function getInitialSearchIsConfigured(): bool
496
    {
497 9
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
498
    }
499
500
    /**
501
     * @return mixed
502
     */
503 35
    protected function getRegisteredSearchComponents()
504
    {
505 35
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
506
    }
507
}
508