Completed
Push — master ( cefe26...80523f )
by Timo
23:41
created

SearchResultSetService::search()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 65
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 6.0007

Importance

Changes 0
Metric Value
dl 0
loc 65
ccs 35
cts 36
cp 0.9722
rs 8.6195
c 0
b 0
f 0
cc 6
eloc 36
nc 4
nop 1
crap 6.0007

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 2 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\QueryBuilder;
30
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\Parser\ResultParserRegistry;
31
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
32
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequestAware;
33
use ApacheSolrForTypo3\Solr\Domain\Variants\VariantsProcessor;
34
use ApacheSolrForTypo3\Solr\Query;
35
use ApacheSolrForTypo3\Solr\Query\Modifier\Modifier;
36
use ApacheSolrForTypo3\Solr\Search;
37
use ApacheSolrForTypo3\Solr\Search\QueryAware;
38
use ApacheSolrForTypo3\Solr\Search\SearchAware;
39
use ApacheSolrForTypo3\Solr\Search\SearchComponentManager;
40
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
41
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
42
use ApacheSolrForTypo3\Solr\System\Solr\SolrIncompleteResponseException;
43
use TYPO3\CMS\Core\Utility\GeneralUtility;
44
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.

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...
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
    /**
56
     * Track, if the number of results per page has been changed by the current request
57
     *
58
     * @var bool
59
     */
60
    protected $resultsPerPageChanged = false;
61
62
    /**
63
     * @var Search
64
     */
65
    protected $search;
66
67
    /**
68
     * @var SearchResultSet
69
     */
70
    protected $lastResultSet = null;
71
72
    /**
73
     * @var boolean
74
     */
75
    protected $isSolrAvailable = false;
76
77
    /**
78
     * @var TypoScriptConfiguration
79
     */
80
    protected $typoScriptConfiguration;
81
82
    /**
83
     * @var SolrLogManager;
84
     */
85
    protected $logger = null;
86
87
    /**
88
     * @var SearchResultBuilder
89
     */
90
    protected $searchResultBuilder;
91
92
    /**
93
     * @var QueryBuilder
94
     */
95
    protected $queryBuilder;
96
97
    /**
98
     * @param TypoScriptConfiguration $configuration
99
     * @param Search $search
100
     * @param SolrLogManager $solrLogManager
101
     * @param SearchResultBuilder $resultBuilder
102
     * @param QueryBuilder $queryBuilder
103
     */
104 48
    public function __construct(TypoScriptConfiguration $configuration, Search $search, SolrLogManager $solrLogManager = null, SearchResultBuilder $resultBuilder = null, QueryBuilder $queryBuilder = null)
105
    {
106 48
        $this->search = $search;
107 48
        $this->typoScriptConfiguration = $configuration;
108 48
        $this->logger = is_null($solrLogManager) ? GeneralUtility::makeInstance(SolrLogManager::class, __CLASS__) : $solrLogManager;
109 48
        $this->searchResultBuilder = is_null($resultBuilder) ? GeneralUtility::makeInstance(SearchResultBuilder::class) : $resultBuilder;
110 48
        $this->queryBuilder = is_null($queryBuilder) ? GeneralUtility::makeInstance(QueryBuilder::class, $configuration, $solrLogManager) : $queryBuilder;
111 48
    }
112
113
    /**
114
     * @param bool $useCache
115
     * @return bool
116
     */
117
    public function getIsSolrAvailable($useCache = true)
118
    {
119
        $this->isSolrAvailable = $this->search->ping($useCache);
120
        return $this->isSolrAvailable;
121
    }
122
123
    /**
124
     * @return bool
125
     */
126 31
    public function getHasSearched()
127
    {
128 31
        return $this->search->hasSearched();
129
    }
130
131
    /**
132
     * Retrieves the used search instance.
133
     *
134
     * @return Search
135
     */
136 2
    public function getSearch()
137
    {
138 2
        return $this->search;
139
    }
140
141
    /**
142
     * @param Query $query
143
     * @param SearchRequest $searchRequest
144
     */
145 39
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
146
    {
147 39
        $searchComponents = $this->getRegisteredSearchComponents();
148
149 39
        foreach ($searchComponents as $searchComponent) {
150
            /** @var Search\SearchComponent $searchComponent */
151 34
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
152
153 34
            if ($searchComponent instanceof QueryAware) {
154 34
                $searchComponent->setQuery($query);
155
            }
156
157 34
            if ($searchComponent instanceof SearchRequestAware) {
158 33
                $searchComponent->setSearchRequest($searchRequest);
159
            }
160
161 34
            $searchComponent->initializeSearchComponent();
162
        }
163 39
    }
164
165
    /**
166
     * @return string
167
     */
168 41
    protected function getResultSetClassName()
169
    {
170 41
        return isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName ']) ?
171 41
            $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] : SearchResultSet::class;
172
    }
173
174
    /**
175
     * Performs a search and returns a SearchResultSet.
176
     *
177
     * @param SearchRequest $searchRequest
178
     * @return SearchResultSet
179
     */
180 41
    public function search(SearchRequest $searchRequest)
181
    {
182
        /** @var $resultSet SearchResultSet */
183 41
        $resultSetClass = $this->getResultSetClassName();
184 41
        $resultSet = GeneralUtility::makeInstance($resultSetClass);
185 41
        $resultSet->setUsedSearchRequest($searchRequest);
186 41
        $this->lastResultSet = $resultSet;
187
188 41
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
189
190 41
        if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
191
            // when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
192 2
            return $resultSet;
193
        }
194
195 39
        if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
196
            // the user entered an empty query string "" or "  " and empty querystring is not allowed
197
            return $resultSet;
198
        }
199
200 39
        $rawQuery = $searchRequest->getRawUserQuery();
201 39
        $resultsPerPage = (int)$searchRequest->getResultsPerPage();
202 39
        $query = $this->queryBuilder->buildSearchQuery($rawQuery, $resultsPerPage);
203 39
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
204 39
        $resultSet->setUsedQuery($query);
205
206
        // the offset mulitplier is page - 1 but not less then zero
207 39
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
208 39
        $offSet = $offsetMultiplier * $resultsPerPage;
209
210
        // performing the actual search, sending the query to the Solr server
211 39
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
212 39
        $response = $this->doASearch($query, $offSet);
213
214 38
        if ($resultsPerPage === 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 2
            $response->response->numFound = 0;
0 ignored issues
show
Bug introduced by
The property response does not seem to exist. Did you mean _response?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
218
        }
219
220 38
        $resultSet->setUsedSearch($this->search);
221 38
        $resultSet->setResponse($response);
222
223
            /** @var ResultParserRegistry $parserRegistry */
224 38
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, $this->typoScriptConfiguration);
225 38
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
226 38
        $searchResults = $parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
227 38
        $resultSet->setSearchResults($searchResults);
228
229 38
        $resultSet->setUsedPage((int)$searchRequest->getPage());
230 38
        $resultSet->setUsedResultsPerPage($resultsPerPage);
231 38
        $resultSet->setUsedAdditionalFilters($this->queryBuilder->getAdditionalFilters());
232
233
        /** @var $variantsProcessor VariantsProcessor */
234 38
        $variantsProcessor = GeneralUtility::makeInstance(VariantsProcessor::class, $this->typoScriptConfiguration, $this->searchResultBuilder);
235 38
        $variantsProcessor->process($resultSet);
236
237
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
238 38
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
239 38
        $searchResultReconstitutionProcessor->process($resultSet);
240
241 38
        $resultSet = $this->getAutoCorrection($resultSet);
242
243 38
        return $this->handleSearchHook('afterSearch', $resultSet);
244
    }
245
246
    /**
247
     * Retrieves the configuration filters from the TypoScript configuration, except the __pageSections filter.
248
     *
249
     * @return array
250
     */
251 33
    public function getAdditionalFilters()
252
    {
253 33
        return $this->queryBuilder->getAdditionalFilters();
254
    }
255
256
    /**
257
     * Executes the search and builds a fake response for a current bug in Apache Solr 6.3
258
     *
259
     * @param Query $query
260
     * @param int $offSet
261
     * @return \Apache_Solr_Response
262
     */
263 39
    protected function doASearch($query, $offSet)
264
    {
265 39
        $response = $this->search->search($query, $offSet, null);
266 38
        if($response === null) {
267
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
268
        }
269
270 38
        return $response;
271
    }
272
273
    /**
274
     * @param SearchResultSet $searchResultSet
275
     * @return SearchResultSet
276
     */
277 38
    protected function getAutoCorrection(SearchResultSet $searchResultSet)
278
    {
279
        // no secondary search configured
280 38
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
281 37
            return $searchResultSet;
282
        }
283
284
        // more then zero results
285 1
        if ($searchResultSet->getAllResultCount() > 0) {
286 1
            return $searchResultSet;
287
        }
288
289
        // no corrections present
290 1
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
291
            return $searchResultSet;
292
        }
293
294 1
        $searchResultSet = $this->peformAutoCorrection($searchResultSet);
295
296 1
        return $searchResultSet;
297
    }
298
299
    /**
300
     * @param SearchResultSet $searchResultSet
301
     * @return SearchResultSet
302
     */
303 1
    protected function peformAutoCorrection(SearchResultSet $searchResultSet)
304
    {
305 1
        $searchRequest = $searchResultSet->getUsedSearchRequest();
306 1
        $suggestions = $searchResultSet->getSpellCheckingSuggestions();
307
308 1
        $maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry(1);
309 1
        $runs = 0;
310
311 1
        foreach ($suggestions as $suggestion) {
312 1
            $runs++;
313
314 1
            $correction = $suggestion->getSuggestion();
315 1
            $initialQuery = $searchRequest->getRawUserQuery();
316
317 1
            $searchRequest->setRawQueryString($correction);
318 1
            $searchResultSet = $this->search($searchRequest);
319 1
            if ($searchResultSet->getAllResultCount() > 0) {
320 1
                $searchResultSet->setIsAutoCorrected(true);
321 1
                $searchResultSet->setCorrectedQueryString($correction);
322 1
                $searchResultSet->setInitialQueryString($initialQuery);
323 1
                break;
324
            }
325
326
            if ($runs > $maximumRuns) {
327
                break;
328
            }
329
        }
330 1
        return $searchResultSet;
331
    }
332
333
    /**
334
     * Allows to modify a query before eventually handing it over to Solr.
335
     *
336
     * @param Query $query The current query before it's being handed over to Solr.
337
     * @param SearchRequest $searchRequest The searchRequest, relevant in the current context
338
     * @param Search $search The search, relevant in the current context
339
     * @throws \UnexpectedValueException
340
     * @return Query The modified query that is actually going to be given to Solr.
341
     */
342 39
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search)
343
    {
344
        // hook to modify the search query
345 39
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
346 33
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
347 33
                $queryModifier = GeneralUtility::makeInstance($classReference);
348
349 33
                if ($queryModifier instanceof Modifier) {
350 33
                    if ($queryModifier instanceof SearchAware) {
351
                        $queryModifier->setSearch($search);
352
                    }
353
354 33
                    if ($queryModifier instanceof SearchRequestAware) {
355 33
                        $queryModifier->setSearchRequest($searchRequest);
356
                    }
357
358 33
                    $query = $queryModifier->modifyQuery($query);
359
                } else {
360
                    throw new \UnexpectedValueException(
361
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
362 33
                        1310387414
363
                    );
364
                }
365
            }
366
        }
367
368 39
        return $query;
369
    }
370
371
    /**
372
     * Retrieves a single document from solr by document id.
373
     *
374
     * @param string $documentId
375
     * @return SearchResult
376
     */
377 3
    public function getDocumentById($documentId)
378
    {
379
        /* @var $query Query */
380 3
        $query = GeneralUtility::makeInstance(Query::class, $documentId);
381 3
        $query->setQueryFields(QueryFields::fromString('id'));
382 3
        $response = $this->search->search($query, 0, 1);
383 2
        $parsedData = $response->getParsedData();
384 2
        $resultDocument = isset($parsedData->response->docs[0]) ? $parsedData->response->docs[0] : null;
385
386 2
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
387
    }
388
389
    /**
390
     * This method is used to call the registered hooks during the search execution.
391
     *
392
     * @param string $eventName
393
     * @param SearchResultSet $resultSet
394
     * @return SearchResultSet
395
     */
396 41
    private function handleSearchHook($eventName, SearchResultSet $resultSet)
397
    {
398 41
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName])) {
399 41
            return $resultSet;
400
        }
401
402 29
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
403 29
            $afterSearchProcessor = GeneralUtility::makeInstance($classReference);
404 29
            if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
405 29
                $afterSearchProcessor->process($resultSet);
406
            }
407
        }
408
409 29
        return $resultSet;
410
    }
411
412
    /**
413
     * @return SearchResultSet
414
     */
415 30
    public function getLastResultSet()
416
    {
417 30
        return $this->lastResultSet;
418
    }
419
420
    /**
421
     * This method returns true when the last search was executed with an empty query
422
     * string or whitespaces only. When no search was triggered it will return false.
423
     *
424
     * @return bool
425
     */
426
    public function getLastSearchWasExecutedWithEmptyQueryString()
427
    {
428
        $wasEmptyQueryString = false;
429
        if ($this->lastResultSet != null) {
430
            $wasEmptyQueryString = $this->lastResultSet->getUsedSearchRequest()->getRawUserQueryIsEmptyString();
431
        }
432
433
        return $wasEmptyQueryString;
434
    }
435
436
    /**
437
     * @return bool
438
     */
439 6
    protected function getInitialSearchIsConfigured()
440
    {
441 6
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
442
    }
443
444
    /**
445
     * @return mixed
446
     */
447 33
    protected function getRegisteredSearchComponents()
448
    {
449 33
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
450
    }
451
}
452