Passed
Push — master ( 39eae3...2e34de )
by Timo
05:33
created

SearchResultSetService   C

Complexity

Total Complexity 50

Size/Duplication

Total Lines 400
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 87.5%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 50
c 2
b 0
f 0
lcom 1
cbo 17
dl 0
loc 400
ccs 126
cts 144
cp 0.875
rs 5.511

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 4
A getIsSolrAvailable() 0 5 1
A getHasSearched() 0 4 1
A getSearch() 0 4 1
A initializeRegisteredSearchComponents() 0 19 4
A getResultSetClassName() 0 5 2
B search() 0 65 6
A getAdditionalFilters() 0 4 1
A doASearch() 0 9 2
A getAutoCorrection() 0 21 4
B peformAutoCorrection() 0 29 4
B modifyQuery() 0 28 6
A getDocumentById() 0 11 2
A handleSearchHook() 0 15 4
A getLastResultSet() 0 4 1
A getLastSearchWasExecutedWithEmptyQueryString() 0 9 2
A getInitialSearchIsConfigured() 0 4 4
A getRegisteredSearchComponents() 0 4 1

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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