Completed
Push — dev2 ( 764ed3...9e10b9 )
by Gordon
26:05 queued 17:34
created

ResultList   C

Complexity

Total Complexity 61

Size/Duplication

Total Lines 450
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 40.72%

Importance

Changes 5
Bugs 3 Features 0
Metric Value
wmc 61
c 5
b 3
f 0
lcom 1
cbo 10
dl 0
loc 450
ccs 90
cts 221
cp 0.4072
rs 6.0181

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A __clone() 0 3 1
A getService() 0 3 1
A getAggregations() 0 3 1
A getTotalItems() 0 4 1
A getTotalTime() 0 3 1
A getIterator() 0 3 1
A limit() 0 8 1
A toArrayList() 0 3 1
A toNestedArray() 0 9 2
A first() 0 4 1
A last() 0 4 1
A map() 0 3 1
A column() 0 13 3
A each() 0 3 1
A offsetExists() 0 3 1
A offsetGet() 0 3 1
A offsetSet() 0 3 1
A offsetUnset() 0 3 1
A add() 0 3 1
A remove() 0 3 1
A find() 0 3 1
A getQuery() 0 3 1
A count() 0 3 1
A setTypes() 0 3 1
D getResults() 0 142 21
D toArray() 0 86 12

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace SilverStripe\Elastica;
4
5
use Elastica\Index;
6
use Elastica\Query;
7
use Elastica\Filter\GeoDistance;
8
9
10
/**
11
 * A list wrapper around the results from a query. Note that not all operations are implemented.
12
 */
13
class ResultList extends \ViewableData implements \SS_Limitable, \SS_List {
14
15
	/**
16
	 * @var \Elastica\Index
17
	 */
18
	private $service;
19
20
	/**
21
	 * @var \Elastica\Query
22
	 */
23
	private $query;
24
25
	/**
26
	 * List of types to search for, default (blank) returns all
27
	 * @var string
28
	 */
29
30
	private $types = '';
31
32
	/**
33
	 * Filters, i.e. selected aggregations, to apply to the search
34
	 */
35
	private $filters = array();
36
37
	/**
38
	 * An array list of aggregations from this search
39
	 * @var ArrayList
40
	 */
41
	private $aggregations;
42
43
44
	/**
45
	 * Create a search and then optionally tweak it.  Actual search is only performed against
46
	 * Elasticsearch when the getResults() method is called.
47
	 *
48
	 * @param ElasticaService $service object used to communicate with Elasticsearch
49
	 * @param Query           $query   Elastica query object, created via QueryGenerator
50
	 * @param string          $queryText       the text from the query
51
	 * @param array           $filters Selected filters, used for aggregation purposes only
52
	 *                                 (i.e. query already filtered prior to this)
53
	 */
54 1
	public function __construct(ElasticaService $service, Query $query, $queryText, $filters = array()) {
55 1
		$this->service = $service;
0 ignored issues
show
Documentation Bug introduced by
It seems like $service of type object<SilverStripe\Elastica\ElasticaService> is incompatible with the declared type object<Elastica\Index> of property $service.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
56 1
		$this->query = $query;
57 1
		$this->originalQueryText = $queryText;
58 1
		$this->filters = $filters;
59 1
	}
60
61
	public function __clone() {
62
		$this->query = clone $this->query;
63
	}
64
65
	/**
66
	 * @return \Elastica\Index
67
	 */
68
	public function getService() {
69
		return $this->service;
70
	}
71
72
	/**
73
	 * Set a new list of types (SilverStripe classes) to search for
74
	 * @param string $newTypes comma separated list of types to search for
75
	 */
76
	public function setTypes($newTypes) {
77
		$this->types = $newTypes;
78
	}
79
80
81
	/**
82
	 * @return \Elastica\Query
83
	 */
84
	public function getQuery() {
85
		return $this->query;
86
	}
87
88
89
	/**
90
	 * Get the aggregation results for this query.  Should only be called
91
	 * after $this->getResults() has been executed.
92
	 * Note this will be an empty array list if there is no aggregation
93
	 *
94
	 * @return ArrayList ArrayList of the aggregated results for this query
95
	 */
96 1
	public function getAggregations() {
97 1
		return $this->aggregations;
98
	}
99
100
	/**
101
	 * @return array
102
	 */
103 1
	public function getResults() {
104 1
		if(!isset($this->_cachedResults)) {
105 1
			$ers = $this->service->search($this->query, $this->types);
106
107 1
			if(isset($ers->MoreLikeThisTerms)) {
108 1
				$this->MoreLikeThisTerms = $ers->MoreLikeThisTerms;
109 1
			}
110
111 1
			if(isset($ers->getSuggests()['query-phrase-suggestions'])) {
112
				$suggest = $ers->getSuggests()['query-phrase-suggestions'];
113
				$suggestedPhraseAndHL = ElasticaUtil::getPhraseSuggestion($suggest);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $suggestedPhraseAndHL is correct as \SilverStripe\Elastica\E...aseSuggestion($suggest) (which targets SilverStripe\Elastica\El...::getPhraseSuggestion()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
114
				if($suggestedPhraseAndHL) {
115
					$this->SuggestedQuery = $suggestedPhraseAndHL['suggestedQuery'];
116
					$this->SuggestedQueryHighlighted = $suggestedPhraseAndHL['suggestedQueryHighlighted'];
117
				}
118
			}
119
120 1
			$this->TotalItems = $ers->getTotalHits();
121 1
			$this->TotalTime = $ers->getTotalTime();
122 1
			$this->_cachedResults = $ers->getResults();
123
			// make the aggregations available to the templating, title casing
124
			// to be consistent with normal templating conventions
125 1
			$aggs = $ers->getAggregations();
126
127
			// array of index field name to human readable title
128 1
			$indexedFieldTitleMapping = array();
129
130
			// optionally remap keys and store chosen aggregations from get params
131 1
			if(isset($this->SearchHelper)) {
132
				$manipulator = \Injector::inst()->create($this->SearchHelper);
133
				$manipulator->query = $this->query;
134
				$manipulator->updateAggregation($aggs);
135
136
				$indexedFieldTitleMapping = $manipulator->getIndexFieldTitleMapping();
137
			}
138 1
			$aggsTemplate = new \ArrayList();
139
140
			// Convert the buckets into a form suitable for SilverStripe templates
141 1
			$queryText = $this->originalQueryText;
142
143
			// if not search term remove it and aggregate with a blank query
144 1
			if($queryText == '' && sizeof($aggs) > 0) {
145
				$params = $this->query->getParams();
146
				unset($params['query']);
147
				$this->query->setParams($params);
148
				$queryText = '';
149
			}
150
151
			// get the base URL for the current facets selected
152 1
			$baseURL = \Controller::curr()->Link() . '?';
153 1
			$prefixAmp = false;
154 1
			if($queryText !== '') {
155 1
				$baseURL .= 'q=' . urlencode($queryText);
156 1
				$prefixAmp = true;
157 1
			}
158
159
			// now add the selected facets
160 1
			foreach($this->filters as $key => $value) {
161
				if($prefixAmp) {
162
					$baseURL .= '&';
163
				} else {
164
					$prefixAmp = true;
165
				}
166
				$baseURL .= $key . '=' . urlencode($value);
167 1
			}
168
169 1
			foreach(array_keys($aggs) as $key) {
170
				$aggDO = new \DataObject();
171
				//FIXME - Camel case separate here
172
				if(isset($indexedFieldTitleMapping[$key])) {
173
					$aggDO->Name = $indexedFieldTitleMapping[$key];
174
				} else {
175
					$aggDO->Name = $key;
176
				}
177
178
				// now the buckets
179
				if(isset($aggs[$key]['buckets'])) {
180
					$bucketsAL = new \ArrayList();
181
					foreach($aggs[$key]['buckets'] as $value) {
182
						$ct = new \DataObject();
183
						$ct->Key = $value['key'];
184
						$ct->DocumentCount = $value['doc_count'];
185
						$query[$key] = $value;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$query was never initialized. Although not strictly required by PHP, it is generally a good practice to add $query = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
186
						if($prefixAmp) {
187
							$url = $baseURL . '&';
188
						} else {
189
							$url = $baseURL;
190
							$prefixAmp = true;
191
						}
192
193
						// check if currently selected
194
						if(isset($this->filters[$key])) {
195
196
							if($this->filters[$key] === (string)$value['key']) {
197
								$ct->IsSelected = true;
198
								// mark this facet as having been selected, so optional toggling
199
								// of the display of the facet can be done via the template.
200
								$aggDO->IsSelected = true;
201
202
								$urlParam = $key . '=' . urlencode($this->filters[$key]);
203
204
								// possible ampersand combos to remove
205
								$v2 = '&' . $urlParam;
206
								$v3 = $urlParam . '&';
207
								$url = str_replace($v2, '', $url);
208
								$url = str_replace($v3, '', $url);
209
								$url = str_replace($urlParam, '', $url);
210
								$ct->URL = $url;
211
							}
212
						} else {
213
							$url .= $key . '=' . urlencode($value['key']);
214
							$prefixAmp = true;
215
						}
216
217
						$url = rtrim($url, '&');
218
219
						$ct->URL = $url;
220
						$bucketsAL->push($ct);
221
					}
222
223
					// in the case of range queries we wish to remove the non selected ones
224
					if($aggDO->IsSelected) {
225
						$newList = new \ArrayList();
226
						foreach($bucketsAL->getIterator() as $bucket) {
227
							if($bucket->IsSelected) {
228
								$newList->push($bucket);
229
								break;
230
							}
231
						}
232
233
						$bucketsAL = $newList;
234
					}
235
					$aggDO->Buckets = $bucketsAL;
236
237
238
				}
239
				$aggsTemplate->push($aggDO);
240 1
			}
241 1
			$this->aggregations = $aggsTemplate;
0 ignored issues
show
Documentation Bug introduced by
It seems like $aggsTemplate of type object<ArrayList> is incompatible with the declared type object<SilverStripe\Elastica\ArrayList> of property $aggregations.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
242 1
		}
243 1
		return $this->_cachedResults;
244
	}
245
246
247 1
	public function getTotalItems() {
248 1
		$this->getResults();
249 1
		return $this->TotalItems;
250
	}
251
252
253
	public function getTotalTime() {
254
		return $this->TotalTime;
255
	}
256
257
	public function getIterator() {
258
		return $this->toArrayList()->getIterator();
259
	}
260
261
	public function limit($limit, $offset = 0) {
262
		$list = clone $this;
263
264
		$list->getQuery()->setSize($limit);
265
		$list->getQuery()->setFrom($offset);
266
267
		return $list;
268
	}
269
270
	/**
271
	 * Converts results of type {@link \Elastica\Result}
272
	 * into their respective {@link DataObject} counterparts.
273
	 *
274
	 * @return array DataObject[]
275
	 */
276 1
	public function toArray() {
277 1
		$result = array();
278
279
		/** @var $found \Elastica\Result[] */
280 1
		$found = $this->getResults();
281 1
		$needed = array();
282 1
		$retrieved = array();
283
284 1
		foreach($found as $item) {
285 1
			$type = $item->getType();
286
287 1
			if(!array_key_exists($type, $needed)) {
288 1
				$needed[$type] = array($item->getId());
289 1
				$retrieved[$type] = array();
290 1
			} else {
291 1
				$needed[$type][] = $item->getId();
292
			}
293 1
		}
294
295 1
		foreach($needed as $class => $ids) {
296 1
			foreach($class::get()->byIDs($ids) as $record) {
297 1
				$retrieved[$class][$record->ID] = $record;
298 1
			}
299 1
		}
300
301
		// Title and Link are special cases
302 1
		$ignore = array('Title', 'Link', 'Title.standard', 'Link.standard');
303
304 1
		foreach($found as $item) {
305
			// Safeguards against indexed items which might no longer be in the DB
306 1
			if(array_key_exists($item->getId(), $retrieved[$item->getType()])) {
307
308 1
				$data_object = $retrieved[$item->getType()][$item->getId()];
309 1
				$data_object->setElasticaResult($item);
310 1
				$highlights = $item->getHighlights();
311
312
				//$snippets will contain the highlights shown in the body of the search result
313
				//$namedSnippets will be used to add highlights to the Link and Title
314 1
				$snippets = new \ArrayList();
315 1
				$namedSnippets = new \ArrayList();
316
317 1
				foreach(array_keys($highlights) as $fieldName) {
318 1
					$fieldSnippets = new \ArrayList();
319
320 1
					foreach($highlights[$fieldName] as $snippet) {
321 1
						$do = new \DataObject();
322 1
						$do->Snippet = $snippet;
323
324
						// skip title and link in the summary of highlights
325 1
						if(!in_array($fieldName, $ignore)) {
326 1
							$snippets->push($do);
327 1
						}
328
329 1
						$fieldSnippets->push($do);
330 1
					}
331
332 1
					if($fieldSnippets->count() > 0) {
333
						//Fields may have a dot in their name, e.g. Title.standard - take this into account
334
						//As dots are an issue with template syntax, store as Title_standard
335 1
						$splits = explode('.', $fieldName);
336 1
						if(sizeof($splits) == 1) {
337
							$namedSnippets->$fieldName = $fieldSnippets;
338
						} else {
339
							// The Title.standard case, for example
340 1
							$splits = explode('.', $fieldName);
341 1
							$compositeFielddName = $splits[0] . '_' . $splits[1];
342 1
							$namedSnippets->$compositeFielddName = $fieldSnippets;
343
						}
344
345 1
					}
346
347
348 1
				}
349
350
351 1
				$data_object->SearchHighlights = $snippets;
352 1
				$data_object->SearchHighlightsByField = $namedSnippets;
353
354 1
				$result[] = $data_object;
355
356 1
			}
357 1
		}
358
359
360 1
		return $result;
361
	}
362
363
	public function toArrayList() {
364
		return new \ArrayList($this->toArray());
365
	}
366
367
	public function toNestedArray() {
368
		$result = array();
369
370
		foreach($this as $record) {
371
			$result[] = $record->toMap();
372
		}
373
374
		return $result;
375
	}
376
377
	public function first() {
378
		// TODO
379
		throw new \Exception('Not implemented');
380
	}
381
382
	public function last() {
383
		// TODO: Implement last() method
384
		throw new \Exception('Not implemented');
385
	}
386
387
	public function map($key = 'ID', $title = 'Title') {
388
		return $this->toArrayList()->map($key, $title);
389
	}
390
391
	public function column($col = 'ID') {
392
		if($col == 'ID') {
393
			$ids = array();
394
395
			foreach($this->getResults() as $result) {
396
				$ids[] = $result->getId();
397
			}
398
399
			return $ids;
400
		} else {
401
			return $this->toArrayList()->column($col);
402
		}
403
	}
404
405
	public function each($callback) {
406
		return $this->toArrayList()->each($callback);
407
	}
408
409
	public function count() {
410
		return count($this->toArray());
411
	}
412
413
	/**
414
	 * @ignore
415
	 */
416
	public function offsetExists($offset) {
417
		throw new \Exception('Not implemented');
418
	}
419
420
	/**
421
	 * @ignore
422
	 */
423
	public function offsetGet($offset) {
424
		throw new \Exception('Not implemented');
425
	}
426
427
	/**
428
	 * @ignore
429
	 */
430
	public function offsetSet($offset, $value) {
431
		throw new \Exception('Not implemented');
432
	}
433
434
	/**
435
	 * @ignore
436
	 */
437
	public function offsetUnset($offset) {
438
		throw new \Exception('Not implemented');
439
	}
440
441
	/**
442
	 * @ignore
443
	 */
444
	public function add($item) {
445
		throw new \Exception('Not implemented');
446
	}
447
448
	/**
449
	 * @ignore
450
	 */
451
	public function remove($item) {
452
		throw new \Exception('Not implemented');
453
	}
454
455
	/**
456
	 * @ignore
457
	 */
458
	public function find($key, $value) {
459
		throw new \Exception('Not implemented');
460
	}
461
462
}
463