Passed
Pull Request — release-11.5.x (#3206)
by Michael
40:59 queued 01:38
created

SearchResultSetService   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Test Coverage

Coverage 86.9%

Importance

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

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 51
    public function __construct(
109
        TypoScriptConfiguration $configuration,
110
        Search $search,
111
        SolrLogManager $solrLogManager = null,
112
        SearchResultBuilder $resultBuilder = null,
113
        QueryBuilder $queryBuilder = null
114
    ) {
115 51
        $this->search = $search;
116 51
        $this->typoScriptConfiguration = $configuration;
117 51
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
118 51
        $this->searchResultBuilder = $resultBuilder ?? GeneralUtility::makeInstance(SearchResultBuilder::class);
119 51
        $this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(QueryBuilder::class, /** @scrutinizer ignore-type */ $configuration, /** @scrutinizer ignore-type */ $solrLogManager);
120
    }
121
122
    /**
123
     * @param ObjectManagerInterface $objectManager
124
     */
125 50
    public function injectObjectManager(ObjectManagerInterface $objectManager)
126
    {
127 50
        $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 40
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
156
    {
157 40
        $searchComponents = $this->getRegisteredSearchComponents();
158
159 40
        foreach ($searchComponents as $searchComponent) {
160
            /** @var Search\SearchComponent $searchComponent */
161 35
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
162
163 35
            if ($searchComponent instanceof QueryAware) {
164 35
                $searchComponent->setQuery($query);
165
            }
166
167 35
            if ($searchComponent instanceof SearchRequestAware) {
168 34
                $searchComponent->setSearchRequest($searchRequest);
169
            }
170
171 35
            $searchComponent->initializeSearchComponent();
172
        }
173
    }
174
175
    /**
176
     * @return string
177
     */
178 48
    protected function getResultSetClassName(): string
179
    {
180 48
        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 48
    public function search(SearchRequest $searchRequest): SearchResultSet
192
    {
193 48
        $resultSet = $this->getInitializedSearchResultSet($searchRequest);
194 48
        $this->lastResultSet = $resultSet;
195
196 48
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
197 48
        if ($this->shouldReturnEmptyResultSetWithoutExecutedSearch($searchRequest)) {
198 8
            $resultSet->setHasSearched(false);
199 8
            return $resultSet;
200
        }
201
202 40
        $query = $this->queryBuilder->buildSearchQuery(
203 40
            $searchRequest->getRawUserQuery(),
204 40
            $searchRequest->getResultsPerPage(),
205 40
            $searchRequest->getAdditionalFilters()
206
        );
207 40
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
208 40
        $resultSet->setUsedQuery($query);
209
210
        // performing the actual search, sending the query to the Solr server
211 40
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
212 40
        $response = $this->doASearch($query, $searchRequest);
213
214 40
        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
            $response->response->numFound = 0;
219
        }
220
221 40
        $resultSet->setHasSearched(true);
222 40
        $resultSet->setResponse($response);
223
224 40
        $this->getParsedSearchResults($resultSet);
225
226 40
        $resultSet->setUsedAdditionalFilters($this->queryBuilder->getAdditionalFilters());
227
228
        /** @var $variantsProcessor VariantsProcessor */
229 40
        $variantsProcessor = GeneralUtility::makeInstance(
230
            VariantsProcessor::class,
231
            /** @scrutinizer ignore-type */
232 40
            $this->typoScriptConfiguration,
233
            /** @scrutinizer ignore-type */
234 40
            $this->searchResultBuilder
235
        );
236 40
        $variantsProcessor->process($resultSet);
237
238
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
239 40
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
240 40
        $searchResultReconstitutionProcessor->process($resultSet);
241
242 40
        $resultSet = $this->getAutoCorrection($resultSet);
243
244 40
        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 40
    protected function getParsedSearchResults(SearchResultSet $resultSet)
253
    {
254
        /** @var ResultParserRegistry $parserRegistry */
255 40
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
256 40
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
257 40
        $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 48
    protected function shouldReturnEmptyResultSetWithoutExecutedSearch(SearchRequest $searchRequest): bool
268
    {
269 48
        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 42
        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 40
        return false;
280
    }
281
282
    /**
283
     * Initializes the SearchResultSet from the SearchRequest
284
     *
285
     * @param SearchRequest $searchRequest
286
     * @return SearchResultSet
287
     */
288 48
    protected function getInitializedSearchResultSet(SearchRequest $searchRequest): SearchResultSet
289
    {
290
        /** @var $resultSet SearchResultSet */
291 48
        $resultSetClass = $this->getResultSetClassName();
292 48
        $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 48
        $resultSet->setUsedSearchRequest($searchRequest);
295 48
        $resultSet->setUsedPage((int)$searchRequest->getPage());
296 48
        $resultSet->setUsedResultsPerPage($searchRequest->getResultsPerPage());
297 48
        $resultSet->setUsedSearch($this->search);
298 48
        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 40
    protected function doASearch(Query $query, SearchRequest $searchRequest): ResponseAdapter
310
    {
311
        // the offset multiplier is page - 1 but not less than zero
312 40
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
313 40
        $offSet = $offsetMultiplier * $searchRequest->getResultsPerPage();
314
315 40
        $response = $this->search->search($query, $offSet);
316 40
        if ($response === null) {
317
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
318
        }
319
320 40
        return $response;
321
    }
322
323
    /**
324
     * @param SearchResultSet $searchResultSet
325
     * @return SearchResultSet
326
     * @throws Facets\InvalidFacetPackageException
327
     */
328 40
    protected function getAutoCorrection(SearchResultSet $searchResultSet): SearchResultSet
329
    {
330
        // no secondary search configured
331 40
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
332 39
            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 40
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search): Query
394
    {
395
        // hook to modify the search query
396 40
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] ?? null)) {
397 34
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
398 34
                $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 34
                if ($queryModifier instanceof Modifier) {
401 34
                    if ($queryModifier instanceof SearchAware) {
402
                        $queryModifier->setSearch($search);
403
                    }
404
405 34
                    if ($queryModifier instanceof SearchRequestAware) {
406 16
                        $queryModifier->setSearchRequest($searchRequest);
407
                    }
408
409 34
                    $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 40
        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 48
    private function handleSearchHook(string $eventName, SearchResultSet $resultSet): SearchResultSet
453
    {
454 48
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] ?? null)) {
455 48
            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 8
    protected function getInitialSearchIsConfigured(): bool
496
    {
497 8
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
498
    }
499
500
    /**
501
     * @return mixed
502
     */
503 34
    protected function getRegisteredSearchComponents()
504
    {
505 34
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
506
    }
507
}
508