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 |
||
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; |
||
|
|||
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; |
|
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; |
||
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; |
|
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; |
||
515 | } |
||
516 | |||
517 | 19 | public function getMaximumResultScore() |
|
518 | { |
||
519 | 19 | return $this->response->response->maxScore; |
|
520 | } |
||
521 | |||
522 | 2 | public function getDebugResponse() |
|
526 | |||
527 | 19 | public function getHighlightedContent() |
|
528 | { |
||
529 | 19 | $highlightedContent = false; |
|
530 | |||
531 | 19 | if ($this->response->highlighting) { |
|
532 | 19 | $highlightedContent = $this->response->highlighting; |
|
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 | } |
||
555 | } |
||
556 |
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.