Completed
Push — master ( 723c0e...7ae703 )
by Timo
05:28
created

SearchResultSetService   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 507
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 89.67%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 65
lcom 1
cbo 17
dl 0
loc 507
ccs 165
cts 184
cp 0.8967
rs 3.3333
c 2
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 3
A getIsSolrAvailable() 0 5 1
A getHasSearched() 0 4 1
A getSearch() 0 4 1
A getResultSetClassName() 0 5 2
A applyPageSectionsRootLineFilter() 0 13 3
C getAdditionalFilters() 0 36 7
B search() 0 65 6
B getPreparedQuery() 0 35 6
A initializeRegisteredSearchComponents() 0 19 4
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
A getQueryInstance() 0 5 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\ResultSet\Result\Parser\ResultParserRegistry;
30
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
31
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequestAware;
32
use ApacheSolrForTypo3\Solr\Domain\Variants\VariantsProcessor;
33
use ApacheSolrForTypo3\Solr\Query;
34
use ApacheSolrForTypo3\Solr\Query\Modifier\Modifier;
35
use ApacheSolrForTypo3\Solr\Search;
36
use ApacheSolrForTypo3\Solr\Search\QueryAware;
37
use ApacheSolrForTypo3\Solr\Search\SearchAware;
38
use ApacheSolrForTypo3\Solr\Search\SearchComponentManager;
39
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
40
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
41
use ApacheSolrForTypo3\Solr\System\Solr\SolrCommunicationException;
42
use ApacheSolrForTypo3\Solr\System\Solr\SolrIncompleteResponseException;
43
use ApacheSolrForTypo3\Solr\System\Solr\SolrInternalServerErrorException;
44
use TYPO3\CMS\Core\Utility\GeneralUtility;
45
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
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
     * Additional filters, which will be added to the query, as well as to
57
     * suggest queries.
58
     *
59
     * @var array
60
     */
61
    protected $additionalFilters = [];
62
63
    /**
64
     * Track, if the number of results per page has been changed by the current request
65
     *
66
     * @var bool
67
     */
68
    protected $resultsPerPageChanged = false;
69
70
    /**
71
     * @var Search
72
     */
73
    protected $search;
74
75
    /**
76
     * @var SearchResultSet
77
     */
78
    protected $lastResultSet = null;
79
80
    /**
81
     * @var boolean
82
     */
83
    protected $isSolrAvailable = false;
84
85
    /**
86
     * @var TypoScriptConfiguration
87
     */
88
    protected $typoScriptConfiguration;
89
90
    /**
91
     * @var SolrLogManager;
92
     */
93
    protected $logger = null;
94
95
    /**
96
     * @var SearchResultBuilder
97
     */
98
    protected $searchResultBuilder;
99
100
    /**
101
     * @param TypoScriptConfiguration $configuration
102
     * @param Search $search
103 45
     * @param SolrLogManager $solrLogManager
104
     * @param SearchResultBuilder $resultBuilder
105 45
     */
106 45
    public function __construct(TypoScriptConfiguration $configuration, Search $search, SolrLogManager $solrLogManager = null, SearchResultBuilder $resultBuilder = null)
107 45
    {
108 45
        $this->search = $search;
109
        $this->typoScriptConfiguration = $configuration;
110
        $this->logger = is_null($solrLogManager) ? GeneralUtility::makeInstance(SolrLogManager::class, __CLASS__) : $solrLogManager;
111
        $this->searchResultBuilder = is_null($resultBuilder) ? GeneralUtility::makeInstance(SearchResultBuilder::class) : $resultBuilder;
112
    }
113
114 29
    /**
115
     * @param bool $useCache
116 29
     * @return bool
117 29
     */
118
    public function getIsSolrAvailable($useCache = true)
119
    {
120
        $this->isSolrAvailable = $this->search->ping($useCache);
121
        return $this->isSolrAvailable;
122
    }
123 29
124
    /**
125 29
     * @return bool
126
     */
127
    public function getHasSearched()
128
    {
129
        return $this->search->hasSearched();
130
    }
131
132
    /**
133 2
     * Retrieves the used search instance.
134
     *
135 2
     * @return Search
136
     */
137
    public function getSearch()
138
    {
139
        return $this->search;
140
    }
141
142
    /**
143
     * Initializes the Query object and SearchComponents and returns
144
     * the initialized query object, when a search should be executed.
145
     *
146
     * @param string|null $rawQuery
147
     * @param int $resultsPerPage
148
     * @return Query
149
     */
150
    protected function getPreparedQuery($rawQuery, $resultsPerPage)
151
    {
152
        /* @var $query Query */
153
        $query = $this->getQueryInstance($rawQuery);
154
155
        $this->applyPageSectionsRootLineFilter($query);
156
157
        if ($this->typoScriptConfiguration->getLoggingQuerySearchWords()) {
158
            $this->logger->log(
159
                SolrLogManager::INFO,
160
                'Received search query',
161
                [
162 37
                    $rawQuery
163
                ]
164
            );
165 37
        }
166
167 37
        $query->setResultsPerPage($resultsPerPage);
168
169 37
        if ($this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
170
            // empty main query, but using a "return everything"
171
            // alternative query in q.alt
172
            $query->setAlternativeQuery('*:*');
173
        }
174
175
        if ($this->typoScriptConfiguration->getSearchInitializeWithQuery()) {
176
            $query->setAlternativeQuery($this->typoScriptConfiguration->getSearchInitializeWithQuery());
177
        }
178
179 37
        foreach ($this->getAdditionalFilters() as $additionalFilter) {
180
            $query->getFilters()->add($additionalFilter);
181 37
        }
182
183
        return $query;
184 30
    }
185
186
    /**
187 37
     * @param Query $query
188 3
     * @param SearchRequest $searchRequest
189
     */
190
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
191 37
    {
192 2
        $searchComponents = $this->getRegisteredSearchComponents();
193
194
        foreach ($searchComponents as $searchComponent) {
195 37
            /** @var Search\SearchComponent $searchComponent */
196
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
197
198
            if ($searchComponent instanceof QueryAware) {
199
                $searchComponent->setQuery($query);
200
            }
201
202 37
            if ($searchComponent instanceof SearchRequestAware) {
203
                $searchComponent->setSearchRequest($searchRequest);
204 37
            }
205
206 37
            $searchComponent->initializeSearchComponent();
207
        }
208 31
    }
209
210 31
    /**
211 31
     * @return string
212
     */
213
    protected function getResultSetClassName()
214 31
    {
215 30
        return isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName ']) ?
216
            $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] : SearchResultSet::class;
217
    }
218 31
219
    /**
220 37
     * Initializes additional filters configured through TypoScript and
221
     * Flexforms for use in regular queries and suggest queries.
222
     *
223
     * @param Query $query
224
     * @return void
225
     */
226
    protected function applyPageSectionsRootLineFilter(Query $query)
227
    {
228
        $searchQueryFilters = $this->typoScriptConfiguration->getSearchQueryFilterConfiguration();
229
        if (count($searchQueryFilters) <= 0) {
230
            return;
231 37
        }
232
233 37
        // special filter to limit search to specific page tree branches
234 37
        if (array_key_exists('__pageSections', $searchQueryFilters)) {
235 37
            $query->setRootlineFilter($searchQueryFilters['__pageSections']);
236 4
            $this->typoScriptConfiguration->removeSearchQueryFilterForPageSections();
237 4
        }
238
    }
239
240 37
    /**
241 37
     * Retrieves the configuration filters from the TypoScript configuration, except the __pageSections filter.
242
     *
243 37
     * @return array
244 37
     */
245 3
    public function getAdditionalFilters()
246
    {
247
        // when we've build the additionalFilter once, we could return them
248 37
        if (count($this->additionalFilters) > 0) {
249
            return $this->additionalFilters;
250
        }
251
252 2
        $searchQueryFilters = $this->typoScriptConfiguration->getSearchQueryFilterConfiguration();
253
        if (count($searchQueryFilters) <= 0) {
254
            return [];
255 37
        }
256
257
        $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
258
259
        // all other regular filters
260
        foreach ($searchQueryFilters as $filterKey => $filter) {
261
            // the __pageSections filter should not be handled as additional filter
262
            if ($filterKey === '__pageSections') {
263
                continue;
264 39
            }
265
266 39
            $filterIsArray = is_array($searchQueryFilters[$filterKey]);
267 39
            if ($filterIsArray) {
268
                continue;
269 39
            }
270 39
271
            $hasSubConfiguration = is_array($searchQueryFilters[$filterKey . '.']);
272
            if ($hasSubConfiguration) {
273
                $filter = $cObj->stdWrap($searchQueryFilters[$filterKey], $searchQueryFilters[$filterKey . '.']);
274
            }
275
276
            $this->additionalFilters[$filterKey] = $filter;
277
        }
278
279 39
        return $this->additionalFilters;
280
    }
281 39
282 1
    /**
283
     * Performs a search and returns a SearchResultSet.
284 1
     *
285 1
     * @param SearchRequest $searchRequest
286 1
     * @return SearchResultSet
287 1
     */
288
    public function search(SearchRequest $searchRequest)
289
    {
290
        /** @var $resultSet SearchResultSet */
291 39
        $resultSetClass = $this->getResultSetClassName();
292
        $resultSet = GeneralUtility::makeInstance($resultSetClass);
293
        $resultSet->setUsedSearchRequest($searchRequest);
294
        $this->lastResultSet = $resultSet;
295
296
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
297
298
        if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
299 39
            // when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
300
            return $resultSet;
301 39
        }
302 5
303
        if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
304
            // the user entered an empty query string "" or "  " and empty querystring is not allowed
305 34
            return $resultSet;
306 31
        }
307
308
        $rawQuery = $searchRequest->getRawUserQuery();
309 3
        $resultsPerPage = (int)$searchRequest->getResultsPerPage();
310 3
        $query = $this->getPreparedQuery($rawQuery, $resultsPerPage);
311
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
312 2
        $resultSet->setUsedQuery($query);
313 2
314
        // the offset mulitplier is page - 1 but not less then zero
315
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
316 2
        $offSet = $offsetMultiplier * $resultsPerPage;
317
318
        // performing the actual search, sending the query to the Solr server
319
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
320 2
        $response = $this->doASearch($query, $offSet);
321 2
322
        if ($resultsPerPage === 0) {
323
            // when resultPerPage was forced to 0 we also set the numFound to 0 to hide results, e.g.
324
            // when results for the initial search should not be shown.
325 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...
326 2
        }
327 2
328 2
        $resultSet->setUsedSearch($this->search);
329
        $resultSet->setResponse($response);
330 2
331 2
            /** @var ResultParserRegistry $parserRegistry */
332 2
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, $this->typoScriptConfiguration);
333
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
334 2
        $searchResults = $parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
335
        $resultSet->setSearchResults($searchResults);
336
337 3
        $resultSet->setUsedPage((int)$searchRequest->getPage());
338
        $resultSet->setUsedResultsPerPage($resultsPerPage);
339
        $resultSet->setUsedAdditionalFilters($this->getAdditionalFilters());
340
341
        /** @var $variantsProcessor VariantsProcessor */
342
        $variantsProcessor = GeneralUtility::makeInstance(VariantsProcessor::class, $this->typoScriptConfiguration, $this->searchResultBuilder);
343
        $variantsProcessor->process($resultSet);
344
345
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
346
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
347
        $searchResultReconstitutionProcessor->process($resultSet);
348
349
        $resultSet = $this->getAutoCorrection($resultSet);
350
351 39
        return $this->handleSearchHook('afterSearch', $resultSet);
352
    }
353
354 39
    /**
355 1
     * Executes the search and builds a fake response for a current bug in Apache Solr 6.3
356
     *
357
     * @param Query $query
358
     * @param int $offSet
359
     * @return \Apache_Solr_Response
360 1
     */
361
    protected function doASearch($query, $offSet)
362
    {
363
        $response = $this->search->search($query, $offSet, null);
364 1
        if($response === null) {
365 1
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
366 1
        }
367 1
368 1
        return $response;
369
    }
370 1
371
    /**
372
     * @param SearchResultSet $searchResultSet
373 39
     * @return SearchResultSet
374 5
     */
375
    protected function getAutoCorrection(SearchResultSet $searchResultSet)
376
    {
377 34
        // no secondary search configured
378 28
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
379 28
            return $searchResultSet;
380
        }
381
382 34
        // more then zero results
383 34
        if ($searchResultSet->getAllResultCount() > 0) {
384
            return $searchResultSet;
385
        }
386
387
        // no corrections present
388
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
389
            return $searchResultSet;
390
        }
391
392
        $searchResultSet = $this->peformAutoCorrection($searchResultSet);
393 28
394
        return $searchResultSet;
395 28
    }
396 28
397 28
    /**
398
     * @param SearchResultSet $searchResultSet
399
     * @return SearchResultSet
400
     */
401 28
    protected function peformAutoCorrection(SearchResultSet $searchResultSet)
402
    {
403
        $searchRequest = $searchResultSet->getUsedSearchRequest();
404
        $suggestions = $searchResultSet->getSpellCheckingSuggestions();
405
406
        $maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry(1);
407 28
        $runs = 0;
408
409 28
        foreach ($suggestions as $suggestion) {
410 28
            $runs++;
411
412
            $correction = $suggestion->getSuggestion();
413
            $initialQuery = $searchRequest->getRawUserQuery();
414
415
            $searchRequest->setRawQueryString($correction);
416 39
            $searchResultSet = $this->search($searchRequest);
417
            if ($searchResultSet->getAllResultCount() > 0) {
418 39
                $searchResultSet->setIsAutoCorrected(true);
419 39
                $searchResultSet->setCorrectedQueryString($correction);
420
                $searchResultSet->setInitialQueryString($initialQuery);
421
                break;
422
            }
423
424
            if ($runs > $maximumRuns) {
425
                break;
426
            }
427
        }
428 37
        return $searchResultSet;
429
    }
430 37
431
    /**
432
     * Allows to modify a query before eventually handing it over to Solr.
433
     *
434
     * @param Query $query The current query before it's being handed over to Solr.
435
     * @param SearchRequest $searchRequest The searchRequest, relevant in the current context
436
     * @param Search $search The search, relevant in the current context
437
     * @throws \UnexpectedValueException
438
     * @return Query The modified query that is actually going to be given to Solr.
439
     */
440 37
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search)
441
    {
442 37
        // hook to modify the search query
443 37
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
444 35
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
445
                $queryModifier = GeneralUtility::getUserObj($classReference);
446
447
                if ($queryModifier instanceof Modifier) {
448 2
                    if ($queryModifier instanceof SearchAware) {
449
                        $queryModifier->setSearch($search);
450
                    }
451
452 2
                    if ($queryModifier instanceof SearchRequestAware) {
453
                        $queryModifier->setSearchRequest($searchRequest);
454
                    }
455
456
                    $query = $queryModifier->modifyQuery($query);
457
                } else {
458
                    throw new \UnexpectedValueException(
459 42
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
460
                        1310387414
461
                    );
462 42
                }
463 2
            }
464
        }
465
466 42
        return $query;
467 42
    }
468 40
469
    /**
470
     * Retrieves a single document from solr by document id.
471 2
     *
472
     * @param string $documentId
473
     * @return SearchResult
474 2
     */
475
    public function getDocumentById($documentId)
476 2
    {
477
        /* @var $query Query */
478
        $query = GeneralUtility::makeInstance(Query::class, $documentId);
479
        $query->setQueryFields(QueryFields::fromString('id'));
480 2
        $response = $this->search->search($query, 0, 1);
481 2
        $parsedData = $response->getParsedData();
482
        $resultDocument = isset($parsedData->response->docs[0]) ? $parsedData->response->docs[0] : null;
483
484
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
485 2
    }
486 2
487
    /**
488
     * This method is used to call the registered hooks during the search execution.
489
     *
490 2
     * @param string $eventName
491
     * @param SearchResultSet $resultSet
492
     * @return SearchResultSet
493 2
     */
494
    private function handleSearchHook($eventName, SearchResultSet $resultSet)
495
    {
496
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName])) {
497
            return $resultSet;
498
        }
499
500
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
501
            $afterSearchProcessor = GeneralUtility::getUserObj($classReference);
502 39
            if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
503
                $afterSearchProcessor->process($resultSet);
504
            }
505 39
        }
506 39
507 39
        return $resultSet;
508 39
    }
509
510 39
    /**
511
     * @return SearchResultSet
512 39
     */
513
    public function getLastResultSet()
514 2
    {
515
        return $this->lastResultSet;
516
    }
517 37
518
    /**
519
     * This method returns true when the last search was executed with an empty query
520
     * string or whitespaces only. When no search was triggered it will return false.
521
     *
522 37
     * @return bool
523 37
     */
524 37
    public function getLastSearchWasExecutedWithEmptyQueryString()
525 37
    {
526 37
        $wasEmptyQueryString = false;
527
        if ($this->lastResultSet != null) {
528 37
            $wasEmptyQueryString = $this->lastResultSet->getUsedSearchRequest()->getRawUserQueryIsEmptyString();
529
        }
530 37
531 4
        return $wasEmptyQueryString;
532
    }
533
534 37
    /**
535
     * @return bool
536
     */
537 37
    protected function getInitialSearchIsConfigured()
538 37
    {
539 37
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
540
    }
541 37
542
    /**
543
     * @return mixed
544 2
     */
545
    protected function getRegisteredSearchComponents()
546
    {
547 37
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
548
    }
549 37
550
    /**
551 37
     * @param string $rawQuery
552 37
     * @return Query|object
553 37
     */
554 37
    protected function getQueryInstance($rawQuery)
555 37
    {
556
        $query = GeneralUtility::makeInstance(Query::class, $rawQuery, $this->typoScriptConfiguration);
557
        return $query;
558 37
    }
559
}
560