ResultList::getTotalItems()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 3
nc 1
nop 0
crap 2
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 9
	public function __construct(ElasticaService $service, Query $query, $queryText, $filters = array()) {
55 9
		$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 9
		$this->query = $query;
57 9
		$this->originalQueryText = $queryText;
58 9
		$this->filters = $filters;
59 9
	}
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 9
	public function setTypes($newTypes) {
77 9
		$this->types = $newTypes;
78 9
	}
79
80
81
	/**
82
	 * @return \Elastica\Query
83
	 */
84 2
	public function getQuery() {
85 2
		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 9
	public function getAggregations() {
97 9
		return $this->aggregations;
98
	}
99
100
	/**
101
	 * @return array
102
	 */
103 9
	public function getResults() {
104 9
		if(!isset($this->_cachedResults)) {
105 9
			$ers = $this->service->search($this->query, $this->types);
106
107 9
			if(isset($ers->MoreLikeThisTerms)) {
108
				$this->MoreLikeThisTerms = $ers->MoreLikeThisTerms;
109
			}
110
111 9
			if(isset($ers->getSuggests()['query-phrase-suggestions'])) {
112 9
				$suggest = $ers->getSuggests()['query-phrase-suggestions'];
113 9
				$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 9
				if($suggestedPhraseAndHL) {
115
					$this->SuggestedQuery = $suggestedPhraseAndHL['suggestedQuery'];
116
					$this->SuggestedQueryHighlighted = $suggestedPhraseAndHL['suggestedQueryHighlighted'];
117
				}
118 9
			}
119
120 9
			$this->TotalItems = $ers->getTotalHits();
121 9
			$this->TotalTime = $ers->getTotalTime();
122 9
			$this->_cachedResults = $ers->getResults();
123
			// make the aggregations available to the templating, title casing
124
			// to be consistent with normal templating conventions
125 9
			$aggs = $ers->getAggregations();
126
127
			// array of index field name to human readable title
128 9
			$indexedFieldTitleMapping = array();
129
130
			// optionally remap keys and store chosen aggregations from get params
131 9
			if(isset($this->SearchHelper)) {
132 9
				$manipulator = \Injector::inst()->create($this->SearchHelper);
133 9
				$manipulator->query = $this->query;
134 9
				$manipulator->updateAggregation($aggs);
135
136 9
				$indexedFieldTitleMapping = $manipulator->getIndexFieldTitleMapping();
137 9
			}
138 9
			$aggsTemplate = new \ArrayList();
139
140
			// Convert the buckets into a form suitable for SilverStripe templates
141 9
			$queryText = $this->originalQueryText;
142
143
			// if not search term remove it and aggregate with a blank query
144 9
			if($queryText == '' && sizeof($aggs) > 0) {
145 3
				$params = $this->query->getParams();
146 3
				unset($params['query']);
147 3
				$this->query->setParams($params);
148 3
				$queryText = '';
149 3
			}
150
151
			// get the base URL for the current facets selected
152 9
			$baseURL = \Controller::curr()->Link() . '?';
153 9
			$prefixAmp = false;
154 9
			if($queryText !== '') {
155 7
				$baseURL .= 'q=' . urlencode($queryText);
156 7
				$prefixAmp = true;
157 7
			}
158
159
			// now add the selected facets
160 9
			foreach($this->filters as $key => $value) {
161 4
				if($prefixAmp) {
162 4
					$baseURL .= '&';
163 4
				} else {
164 1
					$prefixAmp = true;
165
				}
166 4
				$baseURL .= $key . '=' . urlencode($value);
167 9
			}
168
169 9
			foreach(array_keys($aggs) as $key) {
170 9
				$aggDO = new \DataObject();
171
				//FIXME - Camel case separate here
172 9
				if(isset($indexedFieldTitleMapping[$key])) {
173 9
					$aggDO->Name = $indexedFieldTitleMapping[$key];
174 9
				} else {
175 9
					$aggDO->Name = $key;
176
				}
177
178
				$aggDO->Slug = preg_replace(
179 9
					'/[^a-zA-Z0-9_]/', '_', strtolower($aggDO->Name)
180 9
				);
181 9
182 9
				// now the buckets
183 9
				if(isset($aggs[$key]['buckets'])) {
184 9
					$bucketsAL = new \ArrayList();
185 9
					foreach($aggs[$key]['buckets'] as $value) {
186 9
						$ct = new \DataObject();
187 9
						$ct->Key = $value['key'];
188 9
						$ct->DocumentCount = $value['doc_count'];
189 2
						$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...
190 2
						if($prefixAmp) {
191
							$url = $baseURL . '&';
192
						} else {
193
							$url = $baseURL;
194 9
							$prefixAmp = true;
195
						}
196 4
197 3
						// check if currently selected
198
						if(isset($this->filters[$key])) {
199
200 3
							if($this->filters[$key] === (string)$value['key']) {
201
								$ct->IsSelected = true;
202 3
								// mark this facet as having been selected, so optional toggling
203
								// of the display of the facet can be done via the template.
204
								$aggDO->IsSelected = true;
205 3
206 3
								$urlParam = $key . '=' . urlencode($this->filters[$key]);
207 3
208 3
								// possible ampersand combos to remove
209 3
								$v2 = '&' . $urlParam;
210 3
								$v3 = $urlParam . '&';
211 3
								$url = str_replace($v2, '', $url);
212 4
								$url = str_replace($v3, '', $url);
213 9
								$url = str_replace($urlParam, '', $url);
214 9
								$ct->URL = $url;
215
							}
216
						} else {
217 9
							$url .= $key . '=' . urlencode($value['key']);
218
							$prefixAmp = true;
219 9
						}
220 9
221 9
						$url = rtrim($url, '&');
222
223
						$ct->URL = $url;
224 9
						$bucketsAL->push($ct);
225 3
					}
226 3
227 3
					// in the case of range queries we wish to remove the non selected ones
228 3
					if($aggDO->IsSelected) {
229 3
						$newList = new \ArrayList();
230
						foreach($bucketsAL->getIterator() as $bucket) {
231 3
							if($bucket->IsSelected) {
232
								$newList->push($bucket);
233 3
								break;
234 3
							}
235 9
						}
236
237
						$bucketsAL = $newList;
238 9
					}
239 9
					$aggDO->Buckets = $bucketsAL;
240 9
241 9
242 9
				}
243 9
				$aggsTemplate->push($aggDO);
244
			}
245
			$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...
246
		}
247 9
		return $this->_cachedResults;
248 9
	}
249 9
250
251
	public function getTotalItems() {
252
		$this->getResults();
253
		return $this->TotalItems;
254
	}
255
256
257
	public function getTotalTime() {
258
		return $this->TotalTime;
259
	}
260
261
	public function getIterator() {
262
		return $this->toArrayList()->getIterator();
263
	}
264
265
	public function limit($limit, $offset = 0) {
266
		$list = clone $this;
267
268
		$list->getQuery()->setSize($limit);
269
		$list->getQuery()->setFrom($offset);
270
271
		return $list;
272
	}
273
274
	/**
275
	 * Converts results of type {@link \Elastica\Result}
276 2
	 * into their respective {@link DataObject} counterparts.
277 2
	 *
278
	 * @return array DataObject[]
279
	 */
280 2
	public function toArray() {
281 2
		$result = array();
282 2
283
		/** @var $found \Elastica\Result[] */
284 2
		$found = $this->getResults();
285 2
		$needed = array();
286
		$retrieved = array();
287 2
288 2
		foreach($found as $item) {
289 2
			$type = $item->getType();
290 2
291 2
			if(!array_key_exists($type, $needed)) {
292
				$needed[$type] = array($item->getId());
293 2
				$retrieved[$type] = array();
294
			} else {
295 2
				$needed[$type][] = $item->getId();
296 2
			}
297 2
		}
298 2
299 2
		foreach($needed as $class => $ids) {
300
			foreach($class::get()->byIDs($ids) as $record) {
301
				$retrieved[$class][$record->ID] = $record;
302 2
			}
303
		}
304 2
305
		// Title and Link are special cases
306 2
		$ignore = array('Title', 'Link', 'Title.standard', 'Link.standard');
307
308 2
		foreach($found as $item) {
309 2
			// Safeguards against indexed items which might no longer be in the DB
310 2
			if(array_key_exists($item->getId(), $retrieved[$item->getType()])) {
311
312
				$data_object = $retrieved[$item->getType()][$item->getId()];
313
				$data_object->setElasticaResult($item);
314 2
				$highlights = $item->getHighlights();
315 2
316
				//$snippets will contain the highlights shown in the body of the search result
317 2
				//$namedSnippets will be used to add highlights to the Link and Title
318
				$snippets = new \ArrayList();
319
				$namedSnippets = new \ArrayList();
320
321
				foreach(array_keys($highlights) as $fieldName) {
322
					$fieldSnippets = new \ArrayList();
323
324
					foreach($highlights[$fieldName] as $snippet) {
325
						$do = new \DataObject();
326
						$do->Snippet = $snippet;
327
328
						// skip title and link in the summary of highlights
329
						if(!in_array($fieldName, $ignore)) {
330
							$snippets->push($do);
331
						}
332
333
						$fieldSnippets->push($do);
334
					}
335
336
					if($fieldSnippets->count() > 0) {
337
						//Fields may have a dot in their name, e.g. Title.standard - take this into account
338
						//As dots are an issue with template syntax, store as Title_standard
339
						$splits = explode('.', $fieldName);
340
						if(sizeof($splits) == 1) {
341
							$namedSnippets->$fieldName = $fieldSnippets;
342
						} else {
343
							// The Title.standard case, for example
344
							$splits = explode('.', $fieldName);
345
							$compositeFielddName = $splits[0] . '_' . $splits[1];
346
							$namedSnippets->$compositeFielddName = $fieldSnippets;
347
						}
348 2
349
					}
350
351 2
352 2
				}
353
354 2
355
				$data_object->SearchHighlights = $snippets;
356 2
				$data_object->SearchHighlightsByField = $namedSnippets;
357 2
358
				$result[] = $data_object;
359
360 2
			}
361
		}
362
363
364
		return $result;
365
	}
366
367
	public function toArrayList() {
368
		return new \ArrayList($this->toArray());
369
	}
370
371
	public function toNestedArray() {
372
		$result = array();
373
374
		foreach($this as $record) {
375
			$result[] = $record->toMap();
376
		}
377
378
		return $result;
379
	}
380
381
	public function first() {
382
		// TODO
383
		throw new \Exception('Not implemented');
384
	}
385
386
	public function last() {
387
		// TODO: Implement last() method
388
		throw new \Exception('Not implemented');
389
	}
390
391
	public function map($key = 'ID', $title = 'Title') {
392
		return $this->toArrayList()->map($key, $title);
393
	}
394
395
	public function column($col = 'ID') {
396
		if($col == 'ID') {
397
			$ids = array();
398
399
			foreach($this->getResults() as $result) {
400
				$ids[] = $result->getId();
401
			}
402
403
			return $ids;
404
		} else {
405
			return $this->toArrayList()->column($col);
406
		}
407
	}
408
409 2
	public function each($callback) {
410 2
		return $this->toArrayList()->each($callback);
411
	}
412
413
	public function count() {
414
		return count($this->toArray());
415
	}
416
417
	/**
418
	 * @ignore
419
	 */
420
	public function offsetExists($offset) {
421
		throw new \Exception('Not implemented');
422
	}
423
424
	/**
425
	 * @ignore
426
	 */
427
	public function offsetGet($offset) {
428
		throw new \Exception('Not implemented');
429
	}
430
431
	/**
432
	 * @ignore
433
	 */
434
	public function offsetSet($offset, $value) {
435
		throw new \Exception('Not implemented');
436
	}
437
438
	/**
439
	 * @ignore
440
	 */
441
	public function offsetUnset($offset) {
442
		throw new \Exception('Not implemented');
443
	}
444
445
	/**
446
	 * @ignore
447
	 */
448
	public function add($item) {
449
		throw new \Exception('Not implemented');
450
	}
451
452
	/**
453
	 * @ignore
454
	 */
455
	public function remove($item) {
456
		throw new \Exception('Not implemented');
457
	}
458
459
	/**
460
	 * @ignore
461
	 */
462
	public function find($key, $value) {
463 1
		throw new \Exception('Not implemented');
464
	}
465
466
}
467