Passed
Pull Request — main (#3261)
by Sebastian
48:30 queued 07:03
created

getInitializedSearchResultSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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\ObjectManager;
44
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
45
use UnexpectedValueException;
46
47
/**
48
 * The SearchResultSetService is responsible to build a SearchResultSet from a SearchRequest.
49
 * It encapsulates the logic to trigger a search in order to be able to reuse it in multiple places.
50
 *
51
 * @author Timo Schmidt <[email protected]>
52
 */
53
class SearchResultSetService
54
{
55
    /**
56
     * Track, if the number of results per page has been changed by the current request
57
     *
58
     * @var bool
59
     */
60
    protected bool $resultsPerPageChanged = false;
61
62
    /**
63
     * @var Search
64
     */
65
    protected Search $search;
66
67
    /**
68
     * @var SearchResultSet|null
69
     */
70
    protected ?SearchResultSet $lastResultSet = null;
71
72
    /**
73
     * @var bool
74
     */
75
    protected bool $isSolrAvailable = false;
76
77
    /**
78
     * @var TypoScriptConfiguration
79
     */
80
    protected TypoScriptConfiguration $typoScriptConfiguration;
81
82
    /**
83
     * @var SolrLogManager
84
     */
85
    protected SolrLogManager $logger;
86
87
    /**
88
     * @var SearchResultBuilder
89
     */
90
    protected SearchResultBuilder $searchResultBuilder;
91
92
    /**
93
     * @var QueryBuilder
94
     */
95
    protected QueryBuilder $queryBuilder;
96
97
    /**
98
     * @var ObjectManager
99
     */
100
    protected ObjectManagerInterface $objectManager;
101
102
    /**
103
     * @param TypoScriptConfiguration $configuration
104
     * @param Search $search
105
     * @param SolrLogManager|null $solrLogManager
106
     * @param SearchResultBuilder|null $resultBuilder
107
     * @param QueryBuilder|null $queryBuilder
108
     * @param ObjectManagerInterface|null $objectManager
109
     */
110 52
    public function __construct(
111
        TypoScriptConfiguration $configuration,
112
        Search $search,
113
        ?SolrLogManager $solrLogManager = null,
114
        ?SearchResultBuilder $resultBuilder = null,
115
        ?QueryBuilder $queryBuilder = null,
116
        ?ObjectManagerInterface $objectManager = null
117
    ) {
118 52
        $this->search = $search;
119 52
        $this->typoScriptConfiguration = $configuration;
120 52
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
121 52
        $this->searchResultBuilder = $resultBuilder ?? GeneralUtility::makeInstance(SearchResultBuilder::class);
122 52
        $this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(QueryBuilder::class, /** @scrutinizer ignore-type */ $configuration, /** @scrutinizer ignore-type */ $solrLogManager);
123 52
        $this->objectManager = $objectManager ?? GeneralUtility::makeInstance(ObjectManager::class);
124
    }
125
126
    /**
127
     * @param bool $useCache
128
     * @return bool
129
     * @throws Exception
130
     */
131
    public function getIsSolrAvailable(bool $useCache = true): bool
132
    {
133
        $this->isSolrAvailable = $this->search->ping($useCache);
134
        return $this->isSolrAvailable;
135
    }
136
137
    /**
138
     * Retrieves the used search instance.
139
     *
140
     * @return Search
141
     */
142 1
    public function getSearch(): Search
143
    {
144 1
        return $this->search;
145
    }
146
147
    /**
148
     * @param Query $query
149
     * @param SearchRequest $searchRequest
150
     */
151 41
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
152
    {
153 41
        $searchComponents = $this->getRegisteredSearchComponents();
154
155 41
        foreach ($searchComponents as $searchComponent) {
156
            /** @var Search\SearchComponent $searchComponent */
157 36
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
158
159 36
            if ($searchComponent instanceof QueryAware) {
160 36
                $searchComponent->setQuery($query);
161
            }
162
163 36
            if ($searchComponent instanceof SearchRequestAware) {
164 35
                $searchComponent->setSearchRequest($searchRequest);
165
            }
166
167 36
            $searchComponent->initializeSearchComponent();
168
        }
169
    }
170
171
    /**
172
     * @return string
173
     */
174 49
    protected function getResultSetClassName(): string
175
    {
176 49
        return $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] ?? SearchResultSet::class;
177
    }
178
179
    /**
180
     * Performs a search and returns a SearchResultSet.
181
     *
182
     * @param SearchRequest $searchRequest
183
     * @return SearchResultSet
184
     * @throws Facets\InvalidFacetPackageException
185
     * @throws Exception
186
     */
187 49
    public function search(SearchRequest $searchRequest): SearchResultSet
188
    {
189 49
        $resultSet = $this->getInitializedSearchResultSet($searchRequest);
190 49
        $this->lastResultSet = $resultSet;
191
192 49
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
193 49
        if ($this->shouldReturnEmptyResultSetWithoutExecutedSearch($searchRequest)) {
194 8
            $resultSet->setHasSearched(false);
195 8
            return $resultSet;
196
        }
197
198 41
        $query = $this->queryBuilder->buildSearchQuery(
199 41
            $searchRequest->getRawUserQuery(),
200 41
            $searchRequest->getResultsPerPage(),
201 41
            $searchRequest->getAdditionalFilters()
202
        );
203 41
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
204 41
        $resultSet->setUsedQuery($query);
205
206
        // performing the actual search, sending the query to the Solr server
207 41
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
208 41
        $response = $this->doASearch($query, $searchRequest);
209
210 41
        if ($searchRequest->getResultsPerPage() === 0) {
211
            // when resultPerPage was forced to 0 we also set the numFound to 0 to hide results, e.g.
212
            // when results for the initial search should not be shown.
213
            // @extensionScannerIgnoreLine
214 1
            $response->response->numFound = 0;
215
        }
216
217 41
        $resultSet->setHasSearched(true);
218 41
        $resultSet->setResponse($response);
219
220 41
        $this->getParsedSearchResults($resultSet);
221
222 41
        $resultSet->setUsedAdditionalFilters($this->queryBuilder->getAdditionalFilters());
223
224
        /** @var $variantsProcessor VariantsProcessor */
225 41
        $variantsProcessor = GeneralUtility::makeInstance(
226
            VariantsProcessor::class,
227
            /** @scrutinizer ignore-type */
228 41
            $this->typoScriptConfiguration,
229
            /** @scrutinizer ignore-type */
230 41
            $this->searchResultBuilder
231
        );
232 41
        $variantsProcessor->process($resultSet);
233
234
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
235 41
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
236 41
        $searchResultReconstitutionProcessor->process($resultSet);
237
238 41
        $resultSet = $this->getAutoCorrection($resultSet);
239
240 41
        return $this->handleSearchHook('afterSearch', $resultSet);
241
    }
242
243
    /**
244
     * Uses the configured parser and retrieves the parsed search results.
245
     *
246
     * @param SearchResultSet $resultSet
247
     */
248 41
    protected function getParsedSearchResults(SearchResultSet $resultSet)
249
    {
250
        /** @var ResultParserRegistry $parserRegistry */
251 41
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
252 41
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
253 41
        $parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
254
    }
255
256
    /**
257
     * Evaluates conditions on the request and configuration and returns true if no search should be triggered and an empty
258
     * SearchResultSet should be returned.
259
     *
260
     * @param SearchRequest $searchRequest
261
     * @return bool
262
     */
263 49
    protected function shouldReturnEmptyResultSetWithoutExecutedSearch(SearchRequest $searchRequest): bool
264
    {
265 49
        if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
266
            // when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
267 6
            return true;
268
        }
269
270 43
        if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
271
            // the user entered an empty query string "" or "  " and empty querystring is not allowed
272 2
            return true;
273
        }
274
275 41
        return false;
276
    }
277
278
    /**
279
     * Initializes the SearchResultSet from the SearchRequest
280
     *
281
     * @param SearchRequest $searchRequest
282
     * @return SearchResultSet
283
     */
284 49
    protected function getInitializedSearchResultSet(SearchRequest $searchRequest): SearchResultSet
285
    {
286
        /** @var $resultSet SearchResultSet */
287 49
        $resultSetClass = $this->getResultSetClassName();
288 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

288
        $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...
289
290 49
        $resultSet->setUsedSearchRequest($searchRequest);
291 49
        $resultSet->setUsedPage((int)$searchRequest->getPage());
292 49
        $resultSet->setUsedResultsPerPage($searchRequest->getResultsPerPage());
293 49
        $resultSet->setUsedSearch($this->search);
294 49
        return $resultSet;
295
    }
296
297
    /**
298
     * Executes the search and builds a fake response for a current bug in Apache Solr 6.3
299
     *
300
     * @param Query $query
301
     * @param SearchRequest $searchRequest
302
     * @return ResponseAdapter
303
     * @throws Exception
304
     */
305 41
    protected function doASearch(Query $query, SearchRequest $searchRequest): ResponseAdapter
306
    {
307
        // the offset multiplier is page - 1 but not less than zero
308 41
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
309 41
        $offSet = $offsetMultiplier * $searchRequest->getResultsPerPage();
310
311 41
        $response = $this->search->search($query, $offSet);
312 41
        if ($response === null) {
313
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
314
        }
315
316 41
        return $response;
317
    }
318
319
    /**
320
     * @param SearchResultSet $searchResultSet
321
     * @return SearchResultSet
322
     * @throws Facets\InvalidFacetPackageException
323
     */
324 41
    protected function getAutoCorrection(SearchResultSet $searchResultSet): SearchResultSet
325
    {
326
        // no secondary search configured
327 41
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
328 40
            return $searchResultSet;
329
        }
330
331
        // if more as zero results
332 1
        if ($searchResultSet->getAllResultCount() > 0) {
333 1
            return $searchResultSet;
334
        }
335
336
        // no corrections present
337 1
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
338
            return $searchResultSet;
339
        }
340
341 1
        return $this->performAutoCorrection($searchResultSet);
342
    }
343
344
    /**
345
     * @param SearchResultSet $searchResultSet
346
     * @return SearchResultSet
347
     *
348
     * @throws Facets\InvalidFacetPackageException
349
     */
350 1
    protected function performAutoCorrection(SearchResultSet $searchResultSet): SearchResultSet
351
    {
352 1
        $searchRequest = $searchResultSet->getUsedSearchRequest();
353 1
        $suggestions = $searchResultSet->getSpellCheckingSuggestions();
354
355 1
        $maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry();
356 1
        $runs = 0;
357
358 1
        foreach ($suggestions as $suggestion) {
359 1
            $runs++;
360
361 1
            $correction = $suggestion->getSuggestion();
362 1
            $initialQuery = $searchRequest->getRawUserQuery();
363
364 1
            $searchRequest->setRawQueryString($correction);
365 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

365
            $searchResultSet = $this->search(/** @scrutinizer ignore-type */ $searchRequest);
Loading history...
366 1
            if ($searchResultSet->getAllResultCount() > 0) {
367 1
                $searchResultSet->setIsAutoCorrected(true);
368 1
                $searchResultSet->setCorrectedQueryString($correction);
369 1
                $searchResultSet->setInitialQueryString($initialQuery);
370 1
                break;
371
            }
372
373
            if ($runs > $maximumRuns) {
374
                break;
375
            }
376
        }
377 1
        return $searchResultSet;
378
    }
379
380
    /**
381
     * Allows to modify a query before eventually handing it over to Solr.
382
     *
383
     * @param Query $query The current query before it's being handed over to Solr.
384
     * @param SearchRequest $searchRequest The searchRequest, relevant in the current context
385
     * @param Search $search The search, relevant in the current context
386
     * @throws UnexpectedValueException
387
     * @return Query The modified query that is actually going to be given to Solr.
388
     */
389 41
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search): Query
390
    {
391
        // hook to modify the search query
392 41
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] ?? null)) {
393 35
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
394 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

394
                $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...
395
396 35
                if ($queryModifier instanceof Modifier) {
397 35
                    if ($queryModifier instanceof SearchAware) {
398
                        $queryModifier->setSearch($search);
399
                    }
400
401 35
                    if ($queryModifier instanceof SearchRequestAware) {
402 17
                        $queryModifier->setSearchRequest($searchRequest);
403
                    }
404
405 35
                    $query = $queryModifier->modifyQuery($query);
406
                } else {
407
                    throw new UnexpectedValueException(
408
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
409
                        1310387414
410
                    );
411
                }
412
            }
413
        }
414
415 41
        return $query;
416
    }
417
418
    /**
419
     * Retrieves a single document from solr by document id.
420
     *
421
     * @param string $documentId
422
     * @return SearchResult
423
     * @throws Exception
424
     */
425 2
    public function getDocumentById(string $documentId): SearchResult
426
    {
427
        /* @var $query SearchQuery */
428 2
        $query = $this->queryBuilder->newSearchQuery($documentId)->useQueryFields(QueryFields::fromString('id'))->getQuery();
429 2
        $response = $this->search->search($query, 0, 1);
430 2
        $parsedData = $response->getParsedData();
431
        // @extensionScannerIgnoreLine
432 2
        $resultDocument = $parsedData->response->docs[0] ?? null;
433
434 2
        if (!$resultDocument instanceof Document) {
435
            throw new UnexpectedValueException('Response did not contain a valid Document object');
436
        }
437
438 2
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
439
    }
440
441
    /**
442
     * This method is used to call the registered hooks during the search execution.
443
     *
444
     * @param string $eventName
445
     * @param SearchResultSet $resultSet
446
     * @return SearchResultSet
447
     */
448 49
    private function handleSearchHook(string $eventName, SearchResultSet $resultSet): SearchResultSet
449
    {
450 49
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] ?? null)) {
451 49
            return $resultSet;
452
        }
453
454 3
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
455 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

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