Passed
Pull Request — master (#1317)
by
unknown
21:24
created

Search   C

Complexity

Total Complexity 60

Size/Duplication

Total Lines 515
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 45.71%

Importance

Changes 0
Metric Value
wmc 60
lcom 1
cbo 11
dl 0
loc 515
ccs 80
cts 175
cp 0.4571
rs 6.0975
c 0
b 0
f 0

29 Methods

Rating   Name   Duplication   Size   Complexity  
A getDebugResponse() 0 4 1
A __construct() 0 14 2
A getSolrConnection() 0 4 1
A setSolrConnection() 0 4 1
B modifyResponse() 0 27 5
B ping() 0 24 4
A hasSearched() 0 4 1
A getQuery() 0 4 1
A getResponse() 0 4 1
A getRawResponse() 0 4 1
A getResponseHeader() 0 4 1
A getResponseBody() 0 4 1
A getResultDocumentsEscaped() 0 4 1
A applyHtmlSpecialCharsOnAllFields() 0 21 4
A applyHtmlSpecialCharsOnSingleFieldValue() 0 12 3
A getQueryTime() 0 4 1
A getResultsPerPage() 0 4 1
A getFacetRangeOptions() 0 4 1
A getNumberOfResults() 0 4 1
A getMaximumResultScore() 0 4 1
A getHighlightedContent() 0 10 2
B modifyQuery() 0 24 5
A getFacetQueryOptions() 0 21 3
B getFacetCounts() 0 31 5
A getResultOffset() 0 4 1
A getSpellcheckingSuggestions() 0 17 3
B search() 0 52 5
A getResultDocumentsRaw() 0 4 1
A getFacetFieldOptions() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like Search 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 Search, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace ApacheSolrForTypo3\Solr;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2009-2015 Ingo Renner <[email protected]>
8
 *  All rights reserved
9
 *
10
 *  This script is part of the TYPO3 project. The TYPO3 project is
11
 *  free software; you can redistribute it and/or modify
12
 *  it under the terms of the GNU General Public License as published by
13
 *  the Free Software Foundation; either version 2 of the License, or
14
 *  (at your option) any later version.
15
 *
16
 *  The GNU General Public License can be found at
17
 *  http://www.gnu.org/copyleft/gpl.html.
18
 *
19
 *  This script is distributed in the hope that it will be useful,
20
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 *  GNU General Public License for more details.
23
 *
24
 *  This copyright notice MUST APPEAR in all copies of the script!
25
 ***************************************************************/
26
27
use ApacheSolrForTypo3\Solr\Query\Modifier\Modifier;
28
use ApacheSolrForTypo3\Solr\Search\ResponseModifier;
29
use ApacheSolrForTypo3\Solr\Search\FacetsModifier;
30
use ApacheSolrForTypo3\Solr\Search\SearchAware;
31
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
32
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
33
use TYPO3\CMS\Core\SingletonInterface;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
36
/**
37
 * Class to handle solr search requests
38
 *
39
 * @author Ingo Renner <[email protected]>
40
 */
41
class Search implements SingletonInterface
42
{
43
44
    /**
45
     * An instance of the Solr service
46
     *
47
     * @var SolrService
48
     */
49
    protected $solr = null;
50
51
    /**
52
     * The search query
53
     *
54
     * @var Query
55
     */
56
    protected $query = null;
57
58
    /**
59
     * The search response
60
     *
61
     * @var \Apache_Solr_Response
62
     */
63
    protected $response = null;
64
65
    /**
66
     * Flag for marking a search
67
     *
68
     * @var bool
69
     */
70
    protected $hasSearched = false;
71
72
    /**
73
     * @var TypoScriptConfiguration
74
     */
75
    protected $configuration;
76
77
    // TODO Override __clone to reset $response and $hasSearched
78
79
    /**
80
     * @var \ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager
81
     */
82
    protected $logger = null;
83
84
    /**
85
     * Constructor
86
     *
87
     * @param SolrService $solrConnection The Solr connection to use for searching
88
     */
89 35
    public function __construct(SolrService $solrConnection = null)
90
    {
91 35
        $this->logger = GeneralUtility::makeInstance(SolrLogManager::class, __CLASS__);
92
93 35
        $this->solr = $solrConnection;
94
95 35
        if (is_null($solrConnection)) {
96
            /** @var $connectionManager ConnectionManager */
97 1
            $connectionManager = GeneralUtility::makeInstance(ConnectionManager::class);
98 1
            $this->solr = $connectionManager->getConnectionByPageId($GLOBALS['TSFE']->id, $GLOBALS['TSFE']->sys_language_uid);
99
        }
100
101 35
        $this->configuration = Util::getSolrConfiguration();
102 35
    }
103
104
    /**
105
     * Gets the Solr connection used by this search.
106
     *
107
     * @return SolrService Solr connection
108
     */
109
    public function getSolrConnection()
110
    {
111
        return $this->solr;
112
    }
113
114
    /**
115
     * Sets the Solr connection used by this search.
116
     *
117
     * Since ApacheSolrForTypo3\Solr\Search is a \TYPO3\CMS\Core\SingletonInterface, this is needed to
118
     * be able to switch between multiple cores/connections during
119
     * one request
120
     *
121
     * @param SolrService $solrConnection
122
     */
123
    public function setSolrConnection(SolrService $solrConnection)
124
    {
125
        $this->solr = $solrConnection;
126
    }
127
128
    /**
129
     * Executes a query against a Solr server.
130
     *
131
     * 1) Gets the query string
132
     * 2) Conducts the actual search
133
     * 3) Checks debug settings
134
     *
135
     * @param Query $query The query with keywords, filters, and so on.
136
     * @param int $offset Result offset for pagination.
137
     * @param int $limit Maximum number of results to return. If set to NULL, this value is taken from the query object.
138
     * @return \Apache_Solr_Response Solr response
139
     */
140 29
    public function search(Query $query, $offset = 0, $limit = 10)
141
    {
142 29
        $query = $this->modifyQuery($query);
143 29
        $this->query = $query;
144
145 29
        if (empty($limit)) {
146 25
            $limit = $query->getResultsPerPage();
147
        }
148
149
        try {
150 29
            $response = $this->solr->search(
151 29
                $query->getQueryString(),
152
                $offset,
153
                $limit,
154 29
                $query->getQueryParameters()
155
            );
156
157 29
            if ($this->configuration->getLoggingQueryQueryString()) {
158
                $this->logger->log(
159
                    SolrLogManager::INFO,
160
                    'Querying Solr, getting result',
161
                    [
162
                        'query string' => $query->getQueryString(),
163
                        'query parameters' => $query->getQueryParameters(),
164
                        'response' => json_decode($response->getRawResponse(),
165
                            true)
166
                    ]
167
                );
168
            }
169
        } catch (\RuntimeException $e) {
170
            $response = $this->solr->getResponse();
171
172
            if ($this->configuration->getLoggingExceptions()) {
173
                $this->logger->log(
174
                    SolrLogManager::ERROR,
175
                    'Exception while querying Solr',
176
                    [
177
                        'exception' => $e->__toString(),
178
                        'query' => (array)$query,
179
                        'offset' => $offset,
180
                        'limit' => $limit
181
                    ]
182
                );
183
            }
184
        }
185
186 29
        $response = $this->modifyResponse($response);
187 29
        $this->response = $response;
188 29
        $this->hasSearched = true;
189
190 29
        return $this->response;
191
    }
192
193
    /**
194
     * Allows to modify a query before eventually handing it over to Solr.
195
     *
196
     * @param Query $query The current query before it's being handed over to Solr.
197
     * @return Query The modified query that is actually going to be given to Solr.
198
     */
199 29
    protected function modifyQuery(Query $query)
200
    {
201
        // hook to modify the search query
202 29
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
203 25
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
204 25
                $queryModifier = GeneralUtility::getUserObj($classReference);
205
206 25
                if ($queryModifier instanceof Modifier) {
207 25
                    if ($queryModifier instanceof SearchAware) {
208
                        $queryModifier->setSearch($this);
209
                    }
210
211 25
                    $query = $queryModifier->modifyQuery($query);
212
                } else {
213
                    throw new \UnexpectedValueException(
214
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
215
                        1310387414
216
                    );
217
                }
218
            }
219
        }
220
221 29
        return $query;
222
    }
223
224
    /**
225
     * Allows to modify a response returned from Solr before returning it to
226
     * the rest of the extension.
227
     *
228
     * @param \Apache_Solr_Response $response The response as returned by Solr
229
     * @return \Apache_Solr_Response The modified response that is actually going to be returned to the extension.
230
     * @throws \UnexpectedValueException if a response modifier does not implement interface ApacheSolrForTypo3\Solr\Search\ResponseModifier
231
     */
232 29
    protected function modifyResponse(\Apache_Solr_Response $response)
233
    {
234
        // hook to modify the search response
235 29
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchResponse'])) {
236
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchResponse'] as $classReference) {
237
                $responseModifier = GeneralUtility::getUserObj($classReference);
238
239
                if ($responseModifier instanceof ResponseModifier) {
240
                    if ($responseModifier instanceof SearchAware) {
241
                        $responseModifier->setSearch($this);
242
                    }
243
244
                    $response = $responseModifier->modifyResponse($response);
245
                } else {
246
                    throw new \UnexpectedValueException(
247
                        get_class($responseModifier) . ' must implement interface ' . ResponseModifier::class,
248
                        1343147211
249
                    );
250
                }
251
            }
252
253
            // add modification indicator
254
            $response->response->isModified = true;
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...
255
        }
256
257 29
        return $response;
258
    }
259
260
    /**
261
     * Sends a ping to the solr server to see whether it is available.
262
     *
263
     * @param bool $useCache Set to true if the cache should be used.
264
     * @return bool Returns TRUE on successful ping.
265
     * @throws \Exception Throws an exception in case ping was not successful.
266
     */
267 24
    public function ping($useCache = true)
268
    {
269 24
        $solrAvailable = false;
270
271
        try {
272 24
            if (!$this->solr->ping(2, $useCache)) {
273
                throw new \Exception('Solr Server not responding.', 1237475791);
274
            }
275
276 24
            $solrAvailable = true;
277
        } catch (\Exception $e) {
278
            if ($this->configuration->getLoggingExceptions()) {
279
                $this->logger->log(
280
                    SolrLogManager::ERROR,
281
                    'Exception while trying to ping the solr server',
282
                    [
283
                        $e->__toString()
284
                    ]
285
                );
286
            }
287
        }
288
289 24
        return $solrAvailable;
290
    }
291
292
    /**
293
     * checks whether a search has been executed
294
     *
295
     * @return bool    TRUE if there was a search, FALSE otherwise (if the user just visited the search page f.e.)
296
     */
297 24
    public function hasSearched()
298
    {
299 24
        return $this->hasSearched;
300
    }
301
302
    /**
303
     * Gets the query object.
304
     *
305
     * @return Query Query
306
     */
307 21
    public function getQuery()
308
    {
309 21
        return $this->query;
310
    }
311
312
    /**
313
     * Gets the Solr response
314
     *
315
     * @return \Apache_Solr_Response
316
     */
317 22
    public function getResponse()
318
    {
319 22
        return $this->response;
320
    }
321
322
    public function getRawResponse()
323
    {
324
        return $this->response->getRawResponse();
325
    }
326
327 19
    public function getResponseHeader()
328
    {
329 19
        return $this->getResponse()->responseHeader;
330
    }
331
332 22
    public function getResponseBody()
333
    {
334 22
        return $this->getResponse()->response;
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...
335
    }
336
337
    /**
338
     * Returns all results documents raw. Use with caution!
339
     *
340
     * @return \Apache_Solr_Document[]
341
     */
342 21
    public function getResultDocumentsRaw()
343
    {
344 21
        return $this->getResponseBody()->docs;
345
    }
346
347
    /**
348
     * Returns all result documents but applies htmlspecialchars() on all fields retrieved
349
     * from solr except the configured fields in plugin.tx_solr.search.trustedFields
350
     *
351
     * @return \Apache_Solr_Document[]
352
     */
353 2
    public function getResultDocumentsEscaped()
354
    {
355 2
        return $this->applyHtmlSpecialCharsOnAllFields($this->getResponseBody()->docs);
356
    }
357
358
    /**
359
     * This method is used to apply htmlspecialchars on all document fields that
360
     * are not configured to be secure. Secure mean that we know where the content is coming from.
361
     *
362
     * @param array $documents
363
     * @return \Apache_Solr_Document[]
364
     */
365 2
    protected function applyHtmlSpecialCharsOnAllFields(array $documents)
366
    {
367 2
        $trustedSolrFields = $this->configuration->getSearchTrustedFieldsArray();
368
369 2
        foreach ($documents as $key => $document) {
370 2
            $fieldNames = $document->getFieldNames();
371
372 2
            foreach ($fieldNames as $fieldName) {
373 2
                if (in_array($fieldName, $trustedSolrFields)) {
374
                    // we skip this field, since it was marked as secure
375 2
                    continue;
376
                }
377
378 2
                $document->{$fieldName} = $this->applyHtmlSpecialCharsOnSingleFieldValue($document->{$fieldName});
379
            }
380
381 2
            $documents[$key] = $document;
382
        }
383
384 2
        return $documents;
385
    }
386
387
    /**
388
     * Applies htmlspecialchars on all items of an array of a single value.
389
     *
390
     * @param $fieldValue
391
     * @return array|string
392
     */
393 2
    protected function applyHtmlSpecialCharsOnSingleFieldValue($fieldValue)
394
    {
395 2
        if (is_array($fieldValue)) {
396 2
            foreach ($fieldValue as $key => $fieldValueItem) {
397 2
                $fieldValue[$key] = htmlspecialchars($fieldValueItem, null, null, false);
398
            }
399
        } else {
400 2
            $fieldValue = htmlspecialchars($fieldValue, null, null, false);
401
        }
402
403 2
        return $fieldValue;
404
    }
405
406
    /**
407
     * Gets the time Solr took to execute the query and return the result.
408
     *
409
     * @return int Query time in milliseconds
410
     */
411 19
    public function getQueryTime()
412
    {
413 19
        return $this->getResponseHeader()->QTime;
414
    }
415
416
    /**
417
     * Gets the number of results per page.
418
     *
419
     * @return int Number of results per page
420
     */
421
    public function getResultsPerPage()
422
    {
423
        return $this->getResponseHeader()->params->rows;
424
    }
425
426
    /**
427
     * Gets all facets with their fields, options, and counts.
428
     *
429
     * @return array
430
     */
431
    public function getFacetCounts()
432
    {
433
        static $facetCountsModified = false;
434
        static $facetCounts = null;
435
436
        $unmodifiedFacetCounts = $this->response->facet_counts;
0 ignored issues
show
Bug introduced by
The property facet_counts does not seem to exist in Apache_Solr_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...
437
438
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifyFacets'])) {
439
            if (!$facetCountsModified) {
440
                $facetCounts = $unmodifiedFacetCounts;
441
442
                foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifyFacets'] as $classReference) {
443
                    $facetsModifier = GeneralUtility::getUserObj($classReference);
444
445
                    if ($facetsModifier instanceof FacetsModifier) {
446
                        $facetCounts = $facetsModifier->modifyFacets($facetCounts);
447
                        $facetCountsModified = true;
448
                    } else {
449
                        throw new \UnexpectedValueException(
450
                            get_class($facetsModifier) . ' must implement interface ' . FacetsModifier::class,
451
                            1310387526
452
                        );
453
                    }
454
                }
455
            }
456
        } else {
457
            $facetCounts = $unmodifiedFacetCounts;
458
        }
459
460
        return $facetCounts;
461
    }
462
463
    public function getFacetFieldOptions($facetField)
464
    {
465
        $facetOptions = null;
466
467
        if (property_exists($this->getFacetCounts()->facet_fields,
468
            $facetField)) {
469
            $facetOptions = get_object_vars($this->getFacetCounts()->facet_fields->$facetField);
470
        }
471
472
        return $facetOptions;
473
    }
474
475
    public function getFacetQueryOptions($facetField)
476
    {
477
        $options = [];
478
479
        $facetQueries = get_object_vars($this->getFacetCounts()->facet_queries);
480
        foreach ($facetQueries as $facetQuery => $numberOfResults) {
481
            // remove tags from the facet.query response, for facet.field
482
            // and facet.range Solr does that on its own automatically
483
            $facetQuery = preg_replace('/^\{!ex=[^\}]*\}(.*)/', '\\1',
484
                $facetQuery);
485
486
            if (GeneralUtility::isFirstPartOfStr($facetQuery, $facetField)) {
487
                $options[$facetQuery] = $numberOfResults;
488
            }
489
        }
490
491
        // filter out queries with no results
492
        $options = array_filter($options);
493
494
        return $options;
495
    }
496
497
    public function getFacetRangeOptions($rangeFacetField)
498
    {
499
        return get_object_vars($this->getFacetCounts()->facet_ranges->$rangeFacetField);
500
    }
501
502 21
    public function getNumberOfResults()
503
    {
504 21
        return $this->response->response->numFound;
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...
505
    }
506
507
    /**
508
     * Gets the result offset.
509
     *
510
     * @return int Result offset
511
     */
512
    public function getResultOffset()
513
    {
514
        return $this->response->response->start;
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...
515
    }
516
517 19
    public function getMaximumResultScore()
518
    {
519 19
        return $this->response->response->maxScore;
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...
520
    }
521
522 2
    public function getDebugResponse()
523
    {
524 2
        return $this->response->debug;
525
    }
526
527 19
    public function getHighlightedContent()
528
    {
529 19
        $highlightedContent = false;
530
531 19
        if ($this->response->highlighting) {
532 19
            $highlightedContent = $this->response->highlighting;
0 ignored issues
show
Bug introduced by
The property highlighting does not seem to exist in Apache_Solr_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...
533
        }
534
535 19
        return $highlightedContent;
536
    }
537
538
    public function getSpellcheckingSuggestions()
539
    {
540
        $spellcheckingSuggestions = false;
541
542
        $suggestions = (array)$this->response->spellcheck->suggestions;
543
544
        if (!empty($suggestions)) {
545
            $spellcheckingSuggestions = $suggestions;
546
547
            if (isset($this->response->spellcheck->collations)) {
548
                $collections = (array)$this->response->spellcheck->collations;
549
                $spellcheckingSuggestions['collation'] = $collections['collation'];
550
            }
551
        }
552
553
        return $spellcheckingSuggestions;
554
    }
555
}
556