Passed
Pull Request — release-11.5.x (#3243)
by Markus
70:57 queued 25:59
created

SearchResultSetService::getDocumentById()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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

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

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

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

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