Passed
Push — master ( 3ce2af...702ea2 )
by Timo
24:17
created

SearchResultSetService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 5
crap 1
1
<?php
2
3
namespace ApacheSolrForTypo3\Solr\Domain\Search\ResultSet;
4
5
/***************************************************************
6
 *  Copyright notice
7
 *
8
 *  (c) 2015-2016 Timo Schmidt <[email protected]>
9
 *  All rights reserved
10
 *
11
 *  This script is part of the TYPO3 project. The TYPO3 project is
12
 *  free software; you can redistribute it and/or modify
13
 *  it under the terms of the GNU General Public License as published by
14
 *  the Free Software Foundation; either version 3 of the License, or
15
 *  (at your option) any later version.
16
 *
17
 *  The GNU General Public License can be found at
18
 *  http://www.gnu.org/copyleft/gpl.html.
19
 *
20
 *  This script is distributed in the hope that it will be useful,
21
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23
 *  GNU General Public License for more details.
24
 *
25
 *  This copyright notice MUST APPEAR in all copies of the script!
26
 ***************************************************************/
27
28
use ApacheSolrForTypo3\Solr\Domain\Search\Query\ParameterBuilder\QueryFields;
29
use ApacheSolrForTypo3\Solr\Domain\Search\Query\Query;
30
use ApacheSolrForTypo3\Solr\Domain\Search\Query\QueryBuilder;
31
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\Parser\ResultParserRegistry;
32
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResultCollection;
33
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
34
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequestAware;
35
use ApacheSolrForTypo3\Solr\Domain\Variants\VariantsProcessor;
36
use ApacheSolrForTypo3\Solr\Query\Modifier\Modifier;
37
use ApacheSolrForTypo3\Solr\Search;
38
use ApacheSolrForTypo3\Solr\Search\QueryAware;
39
use ApacheSolrForTypo3\Solr\Search\SearchAware;
40
use ApacheSolrForTypo3\Solr\Search\SearchComponentManager;
41
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
42
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
43
use ApacheSolrForTypo3\Solr\System\Solr\SolrIncompleteResponseException;
44
use TYPO3\CMS\Core\Utility\GeneralUtility;
45
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResultBuilder;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ApacheSolrForTypo3\Solr\...Set\SearchResultBuilder. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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
    /**
57
     * Track, if the number of results per page has been changed by the current request
58
     *
59
     * @var bool
60
     */
61
    protected $resultsPerPageChanged = false;
62
63
    /**
64
     * @var Search
65
     */
66
    protected $search;
67
68
    /**
69
     * @var SearchResultSet
70
     */
71
    protected $lastResultSet = null;
72
73
    /**
74
     * @var boolean
75
     */
76
    protected $isSolrAvailable = false;
77
78
    /**
79
     * @var TypoScriptConfiguration
80
     */
81
    protected $typoScriptConfiguration;
82
83
    /**
84
     * @var SolrLogManager;
85
     */
86
    protected $logger = null;
87
88
    /**
89
     * @var SearchResultBuilder
90
     */
91
    protected $searchResultBuilder;
92
93
    /**
94
     * @var QueryBuilder
95
     */
96
    protected $queryBuilder;
97
98
    /**
99
     * @param TypoScriptConfiguration $configuration
100
     * @param Search $search
101
     * @param SolrLogManager $solrLogManager
102
     * @param SearchResultBuilder $resultBuilder
103
     * @param QueryBuilder $queryBuilder
104
     */
105 54
    public function __construct(TypoScriptConfiguration $configuration, Search $search, SolrLogManager $solrLogManager = null, SearchResultBuilder $resultBuilder = null, QueryBuilder $queryBuilder = null)
106
    {
107 54
        $this->search = $search;
108 54
        $this->typoScriptConfiguration = $configuration;
109 54
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
110 54
        $this->searchResultBuilder = $resultBuilder ?? GeneralUtility::makeInstance(SearchResultBuilder::class);
111 54
        $this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(QueryBuilder::class, /** @scrutinizer ignore-type */ $configuration, /** @scrutinizer ignore-type */ $solrLogManager);
112 54
    }
113
114
    /**
115
     * @param bool $useCache
116
     * @return bool
117
     */
118
    public function getIsSolrAvailable($useCache = true)
119
    {
120
        $this->isSolrAvailable = $this->search->ping($useCache);
121
        return $this->isSolrAvailable;
122
    }
123
124
    /**
125
     * @deprecated Since 8.1.0 will be removed in 9.0.0. This method is deprecated. Use SearchResultSet::getHasSearched instead.
126
     * @return bool
127
     */
128
    public function getHasSearched()
129
    {
130
        trigger_error('Call deprecated method SearchResultSetService::getHasSearched, deprecated since 8.1.0 will be removed in 9.0.0 use SearchResultSet::getHasSearched instead', E_USER_DEPRECATED);
131
132
        return $this->search->hasSearched();
0 ignored issues
show
Deprecated Code introduced by
The function ApacheSolrForTypo3\Solr\Search::hasSearched() has been deprecated: Since 8.1.0 will be removed in 9.0.0. This method is deprecated. Use SearchResultSet::getHasSearched instead. ( Ignorable by Annotation )

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

132
        return /** @scrutinizer ignore-deprecated */ $this->search->hasSearched();

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...
133
    }
134
135
    /**
136
     * Retrieves the used search instance.
137
     *
138
     * @return Search
139
     */
140 2
    public function getSearch()
141
    {
142 2
        return $this->search;
143
    }
144
145
    /**
146
     * @param Query $query
147
     * @param SearchRequest $searchRequest
148
     */
149 43
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
150
    {
151 43
        $searchComponents = $this->getRegisteredSearchComponents();
152
153 43
        foreach ($searchComponents as $searchComponent) {
154
            /** @var Search\SearchComponent $searchComponent */
155 38
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
156
157 38
            if ($searchComponent instanceof QueryAware) {
158 38
                $searchComponent->setQuery($query);
159
            }
160
161 38
            if ($searchComponent instanceof SearchRequestAware) {
162 37
                $searchComponent->setSearchRequest($searchRequest);
163
            }
164
165 38
            $searchComponent->initializeSearchComponent();
166
        }
167 43
    }
168
169
    /**
170
     * @return string
171
     */
172 47
    protected function getResultSetClassName()
173
    {
174 47
        return isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName ']) ?
175 47
            $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] : SearchResultSet::class;
176
    }
177
178
    /**
179
     * Performs a search and returns a SearchResultSet.
180
     *
181
     * @param SearchRequest $searchRequest
182
     * @return SearchResultSet
183
     */
184 47
    public function search(SearchRequest $searchRequest)
185
    {
186 47
        $resultSet = $this->getInitializedSearchResultSet($searchRequest);
187 47
        $this->lastResultSet = $resultSet;
188
189 47
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
190 47
        if ($this->shouldReturnEmptyResultSetWithoutExecutedSearch($searchRequest)) {
191 4
            $resultSet->setHasSearched(false);
192 4
            return $resultSet;
193
        }
194
195 43
        $query = $this->queryBuilder->buildSearchQuery($searchRequest->getRawUserQuery(), (int)$searchRequest->getResultsPerPage(), $searchRequest->getAdditionalFilters());
196 43
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
197 43
        $resultSet->setUsedQuery($query);
198
199
        // performing the actual search, sending the query to the Solr server
200 43
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
201 43
        $response = $this->doASearch($query, $searchRequest);
202
203 42
        if ((int)$searchRequest->getResultsPerPage() === 0) {
204
            // when resultPerPage was forced to 0 we also set the numFound to 0 to hide results, e.g.
205
            // when results for the initial search should not be shown.
206 2
            $response->response->numFound = 0;
207
        }
208
209 42
        $resultSet->setHasSearched(true);
210 42
        $resultSet->setResponse($response);
211 42
        $resultSet->setSearchResults($this->getParsedSearchResults($resultSet));
212 42
        $resultSet->setUsedAdditionalFilters($this->queryBuilder->getAdditionalFilters());
213
214
        /** @var $variantsProcessor VariantsProcessor */
215 42
        $variantsProcessor = GeneralUtility::makeInstance(
216 42
            VariantsProcessor::class,
217 42
            /** @scrutinizer ignore-type */ $this->typoScriptConfiguration,
218 42
            /** @scrutinizer ignore-type */ $this->searchResultBuilder
219
        );
220 42
        $variantsProcessor->process($resultSet);
221
222
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
223 42
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
224 42
        $searchResultReconstitutionProcessor->process($resultSet);
225
226 42
        $resultSet = $this->getAutoCorrection($resultSet);
227
228 42
        return $this->handleSearchHook('afterSearch', $resultSet);
229
    }
230
231
    /**
232
     * Uses the configured parser and retrieves the parsed search resutls.
233
     *
234
     * @param SearchResultSet $resultSet
235
     * @return Result\SearchResultCollection
236
     */
237 42
    protected function getParsedSearchResults($resultSet): SearchResultCollection
238
    {
239
        /** @var ResultParserRegistry $parserRegistry */
240 42
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
241 42
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
242 42
        $searchResults = $parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
243 42
        return $searchResults;
244
    }
245
246
    /**
247
     * Evaluates conditions on the request and configuration and returns true if no search should be triggered and an empty
248
     * SearchResultSet should be returned.
249
     *
250
     * @param SearchRequest $searchRequest
251
     * @return bool
252
     */
253 47
    protected function shouldReturnEmptyResultSetWithoutExecutedSearch(SearchRequest $searchRequest)
254
    {
255 47
        if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
256
            // when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
257 3
            return true;
258
        }
259
260 44
        if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
261
            // the user entered an empty query string "" or "  " and empty querystring is not allowed
262 1
            return true;
263
        }
264
265 43
        return false;
266
    }
267
268
    /**
269
     * Initializes the SearchResultSet from the SearchRequest
270
     *
271
     * @param SearchRequest $searchRequest
272
     * @return SearchResultSet
273
     */
274 47
    protected function getInitializedSearchResultSet(SearchRequest $searchRequest):SearchResultSet
275
    {
276
        /** @var $resultSet SearchResultSet */
277 47
        $resultSetClass = $this->getResultSetClassName();
278 47
        $resultSet = GeneralUtility::makeInstance($resultSetClass);
279
280 47
        $resultSet->setUsedSearchRequest($searchRequest);
281 47
        $resultSet->setUsedPage((int)$searchRequest->getPage());
282 47
        $resultSet->setUsedResultsPerPage((int)$searchRequest->getResultsPerPage());
283 47
        $resultSet->setUsedSearch($this->search);
284 47
        return $resultSet;
285
    }
286
287
    /**
288
     * Retrieves the configuration filters from the TypoScript configuration, except the __pageSections filter.
289
     *
290
     * @return array
291
     */
292 37
    public function getAdditionalFilters()
293
    {
294 37
        return $this->queryBuilder->getAdditionalFilters();
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 \Apache_Solr_Response
303
     */
304 43
    protected function doASearch($query, $searchRequest)
305
    {
306
        // the offset mulitplier is page - 1 but not less then zero
307 43
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
308 43
        $offSet = $offsetMultiplier * (int)$searchRequest->getResultsPerPage();
309
310 43
        $response = $this->search->search($query, $offSet, null);
311 42
        if($response === null) {
312
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
313
        }
314
315 42
        return $response;
316
    }
317
318
    /**
319
     * @param SearchResultSet $searchResultSet
320
     * @return SearchResultSet
321
     */
322 42
    protected function getAutoCorrection(SearchResultSet $searchResultSet)
323
    {
324
        // no secondary search configured
325 42
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
326 41
            return $searchResultSet;
327
        }
328
329
        // more then zero results
330 1
        if ($searchResultSet->getAllResultCount() > 0) {
331 1
            return $searchResultSet;
332
        }
333
334
        // no corrections present
335 1
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
336
            return $searchResultSet;
337
        }
338
339 1
        $searchResultSet = $this->peformAutoCorrection($searchResultSet);
340
341 1
        return $searchResultSet;
342
    }
343
344
    /**
345
     * @param SearchResultSet $searchResultSet
346
     * @return SearchResultSet
347
     */
348 1
    protected function peformAutoCorrection(SearchResultSet $searchResultSet)
349
    {
350 1
        $searchRequest = $searchResultSet->getUsedSearchRequest();
351 1
        $suggestions = $searchResultSet->getSpellCheckingSuggestions();
352
353 1
        $maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry(1);
354 1
        $runs = 0;
355
356 1
        foreach ($suggestions as $suggestion) {
357 1
            $runs++;
358
359 1
            $correction = $suggestion->getSuggestion();
360 1
            $initialQuery = $searchRequest->getRawUserQuery();
361
362 1
            $searchRequest->setRawQueryString($correction);
363 1
            $searchResultSet = $this->search($searchRequest);
364 1
            if ($searchResultSet->getAllResultCount() > 0) {
365 1
                $searchResultSet->setIsAutoCorrected(true);
366 1
                $searchResultSet->setCorrectedQueryString($correction);
367 1
                $searchResultSet->setInitialQueryString($initialQuery);
368 1
                break;
369
            }
370
371
            if ($runs > $maximumRuns) {
372
                break;
373
            }
374
        }
375 1
        return $searchResultSet;
376
    }
377
378
    /**
379
     * Allows to modify a query before eventually handing it over to Solr.
380
     *
381
     * @param Query $query The current query before it's being handed over to Solr.
382
     * @param SearchRequest $searchRequest The searchRequest, relevant in the current context
383
     * @param Search $search The search, relevant in the current context
384
     * @throws \UnexpectedValueException
385
     * @return Query The modified query that is actually going to be given to Solr.
386
     */
387 43
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search)
388
    {
389
        // hook to modify the search query
390 43
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
391 37
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
392 37
                $queryModifier = GeneralUtility::makeInstance($classReference);
393
394 37
                if ($queryModifier instanceof Modifier) {
395 37
                    if ($queryModifier instanceof SearchAware) {
396
                        $queryModifier->setSearch($search);
397
                    }
398
399 37
                    if ($queryModifier instanceof SearchRequestAware) {
400 33
                        $queryModifier->setSearchRequest($searchRequest);
401
                    }
402
403 37
                    $query = $queryModifier->modifyQuery($query);
404
                } else {
405
                    throw new \UnexpectedValueException(
406
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
407 37
                        1310387414
408
                    );
409
                }
410
            }
411
        }
412
413 43
        return $query;
414
    }
415
416
    /**
417
     * Retrieves a single document from solr by document id.
418
     *
419
     * @param string $documentId
420
     * @return SearchResult
421
     */
422 3
    public function getDocumentById($documentId)
423
    {
424
        /* @var $query Query */
425 3
        $query = GeneralUtility::makeInstance(Query::class, /** @scrutinizer ignore-type */ $documentId);
426 3
        $query->setQueryFields(QueryFields::fromString('id'));
427 3
        $response = $this->search->search($query, 0, 1);
428 2
        $parsedData = $response->getParsedData();
429 2
        $resultDocument = isset($parsedData->response->docs[0]) ? $parsedData->response->docs[0] : null;
430
431 2
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
0 ignored issues
show
Bug introduced by
It seems like $resultDocument can also be of type null; however, parameter $originalDocument of ApacheSolrForTypo3\Solr\...romApacheSolrDocument() does only seem to accept Apache_Solr_Document, 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

431
        return $this->searchResultBuilder->fromApacheSolrDocument(/** @scrutinizer ignore-type */ $resultDocument);
Loading history...
432
    }
433
434
    /**
435
     * This method is used to call the registered hooks during the search execution.
436
     *
437
     * @param string $eventName
438
     * @param SearchResultSet $resultSet
439
     * @return SearchResultSet
440
     */
441 47
    private function handleSearchHook($eventName, SearchResultSet $resultSet)
442
    {
443 47
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName])) {
444 47
            return $resultSet;
445
        }
446
447 32
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
448 32
            $afterSearchProcessor = GeneralUtility::makeInstance($classReference);
449 32
            if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
450 32
                $afterSearchProcessor->process($resultSet);
451
            }
452
        }
453
454 32
        return $resultSet;
455
    }
456
457
    /**
458
     * @return SearchResultSet
459
     */
460 33
    public function getLastResultSet()
461
    {
462 33
        return $this->lastResultSet;
463
    }
464
465
    /**
466
     * This method returns true when the last search was executed with an empty query
467
     * string or whitespaces only. When no search was triggered it will return false.
468
     *
469
     * @return bool
470
     */
471
    public function getLastSearchWasExecutedWithEmptyQueryString()
472
    {
473
        $wasEmptyQueryString = false;
474
        if ($this->lastResultSet != null) {
475
            $wasEmptyQueryString = $this->lastResultSet->getUsedSearchRequest()->getRawUserQueryIsEmptyString();
476
        }
477
478
        return $wasEmptyQueryString;
479
    }
480
481
    /**
482
     * @return bool
483
     */
484 7
    protected function getInitialSearchIsConfigured()
485
    {
486 7
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
487
    }
488
489
    /**
490
     * @return mixed
491
     */
492 37
    protected function getRegisteredSearchComponents()
493
    {
494 37
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
495
    }
496
}
497