Completed
Push — dev2 ( 971ce9...880119 )
by Gordon
02:58
created

ElasticSearchPage_Controller::init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 8
rs 9.4286
cc 1
eloc 6
nc 1
nop 0
1
<?php
2
3
use Elastica\Document;
4
use Elastica\Query;
5
use \SilverStripe\Elastica\ResultList;
6
use Elastica\Query\QueryString;
7
use Elastica\Aggregation\Filter;
8
use Elastica\Filter\Term;
9
use Elastica\Filter\BoolAnd;
10
use Elastica\Aggregation\Terms;
11
use Elastica\Query\Filtered;
12
use Elastica\Query\Range;
13
use \SilverStripe\Elastica\ElasticSearcher;
14
use \SilverStripe\Elastica\Searchable;
15
use \SilverStripe\Elastica\QueryGenerator;
16
use \SilverStripe\Elastica\ElasticaUtil;
17
18
class ElasticSearchPage_Controller extends Page_Controller {
19
20
	private static $allowed_actions = array('SearchForm', 'submit', 'index', 'similar');
21
22
	public function init() {
23
		parent::init();
24
25
		Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
26
		Requirements::javascript("elastica/javascript/jquery.autocomplete.js");
27
		Requirements::javascript("elastica/javascript/elastica.js");
28
		Requirements::css("elastica/css/elastica.css");
29
	}
30
31
32
33
	/*
34
	Find DataObjects in Elasticsearch similar to the one selected.  Note that aggregations are not
35
	taken into account, merely the text of the selected document.
36
	 */
37
	public function similar() {
38
		//FIXME double check security, ie if escaping needed
39
		$class = $this->request->param('ID');
40
		$instanceID = $this->request->param('OtherID');
41
42
		$data = array(
43
			'Content' => $this->Content,
44
			'Title' => $this->Title,
45
			'SearchPerformed' => false
46
		);
47
48
		// record the time
49
		$startTime = microtime(true);
50
51
		//instance of ElasticPage associated with this controller
52
		$ep = Controller::curr()->dataRecord;
53
54
		// use an Elastic Searcher, which needs primed from URL params
55
		$es = new ElasticSearcher();
56
57
		// start, and page length, i.e. pagination
58
		$startParam = $this->request->getVar('start');
59
		$start = isset($startParam) ? $startParam : 0;
60
		$es->setStart($start);
61
		$es->setPageLength($ep->ResultsPerPage);
62
63
64
		$es->setMinTermFreq($this->MinTermFreq);
65
		$es->setMaxTermFreq($this->MaxTermFreq);
66
		$es->setMinDocFreq($this->MinDocFreq);
67
		$es->setMaxDocFreq($this->MaxDocFreq);
68
		$es->setMinWordLength($this->MinWordLength);
69
		$es->setMaxWordLength($this->MaxWordLength);
70
		$es->setMinShouldMatch($this->MinShouldMatch);
71
		$es->setSimilarityStopWords($this->SimilarityStopWords);
72
73
74
		// filter by class or site tree
75
		if($ep->SiteTreeOnly) {
76
			T7; //FIXME test missing
77
			$es->addFilter('IsInSiteTree', true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
78
		} else {
79
			$es->setClasses($ep->ClassesToSearch);
80
		}
81
82
83
		// get the edited fields to search from the database for this search page
84
		// Convert this into a name => weighting array
85
		$fieldsToSearch = array();
86
		$editedSearchFields = $this->ElasticaSearchableFields()->filter(array(
87
			'Active' => true,
88
			'SimilarSearchable' => true
89
		));
90
91
		foreach($editedSearchFields->getIterator() as $searchField) {
92
			$fieldsToSearch[$searchField->Name] = $searchField->Weight;
93
		}
94
95
		// Use the standard field for more like this, ie not stemmed
96
		foreach($fieldsToSearch as $field => $value) {
97
			$fieldsToSearch[$field . '.standard'] = $value;
98
			unset($fieldsToSearch[$field]);
99
		}
100
101
		try {
102
			// Simulate server being down for testing purposes
103
			if($this->request->getVar('ServerDown')) {
104
				throw new Elastica\Exception\Connection\HttpException('Unable to reach search server');
105
			}
106
			if(class_exists($class)) {
107
				$instance = \DataObject::get_by_id($class, $instanceID);
108
109
				$paginated = $es->moreLikeThis($instance, $fieldsToSearch);
0 ignored issues
show
Documentation introduced by
$instance is of type object<DataObject>, but the function expects a object<SilverStripe\Elastica\DataObject>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
110
111
				$this->Aggregations = $es->getAggregations();
112
				$data['SearchResults'] = $paginated;
113
				$data['SearchPerformed'] = true;
114
				$data['SearchPageLink'] = $ep->Link();
115
				$data['SimilarTo'] = $instance;
116
				$data['NumberOfResults'] = $paginated->getTotalItems();
117
118
119
				$moreLikeThisTerms = $paginated->getList()->MoreLikeThisTerms;
1 ignored issue
show
Bug introduced by
Accessing MoreLikeThisTerms on the interface SS_List suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
120
				$fieldToTerms = new ArrayList();
121
				foreach(array_keys($moreLikeThisTerms) as $fieldName) {
122
					$readableFieldName = str_replace('.standard', '', $fieldName);
123
					$fieldTerms = new ArrayList();
124
					foreach($moreLikeThisTerms[$fieldName] as $value) {
125
						$do = new DataObject();
126
						$do->Term = $value;
127
						$fieldTerms->push($do);
128
					}
129
130
					$do = new DataObject();
131
					$do->FieldName = $readableFieldName;
132
					$do->Terms = $fieldTerms;
133
					$fieldToTerms->push($do);
134
				}
135
136
				$data['SimilarSearchTerms'] = $fieldToTerms;
137
			} else {
138
				// class does not exist
139
				$data['ErrorMessage'] = "Class $class is either not found or not searchable\n";
140
			}
141
		} catch (\InvalidArgumentException $e) {
142
			$data['ErrorMessage'] = "Class $class is either not found or not searchable\n";
143
		} catch (Elastica\Exception\Connection\HttpException $e) {
144
			$data['ErrorMessage'] = 'Unable to connect to search server';
145
			$data['SearchPerformed'] = false;
146
		}
147
148
149
		// calculate time
150
		$endTime = microtime(true);
151
		$elapsed = round(100 * ($endTime - $startTime)) / 100;
152
153
		// store variables for the template to use
154
		$data['ElapsedTime'] = $elapsed;
155
		$data['Elapsed'] = $elapsed;
156
157
		// allow the optional use of overriding the search result page, e.g. for photos, maps or facets
158
		if($this->hasExtension('PageControllerTemplateOverrideExtension')) {
159
			return $this->useTemplateOverride($data);
160
		} else {
161
			return $data;
162
		}
163
	}
164
165
166
	/*
167
	Display the search form. If the query parameter exists, search against Elastica
168
	and render results accordingly.
169
	 */
170
	public function index() {
171
		$data = array(
172
			'Content' => $this->Content,
173
			'Title' => $this->Title,
174
			'SearchPerformed' => false
175
		);
176
177
		// record the time
178
		$startTime = microtime(true);
179
180
		//instance of ElasticPage associated with this controller
181
		$ep = Controller::curr()->dataRecord;
182
183
		// use an Elastic Searcher, which needs primed from URL params
184
		$es = new ElasticSearcher();
185
186
		// start, and page length, i.e. pagination
187
		$startParam = $this->request->getVar('start');
188
		$start = isset($startParam) ? $startParam : 0;
189
		$es->setStart($start);
190
		$es->setPageLength($ep->ResultsPerPage);
191
192
193
		// Do not show suggestions if this flag is set
194
		$ignoreSuggestions = null !== $this->request->getVar('is');
195
196
197
		// query string
198
		$queryTextParam = $this->request->getVar('q');
199
		$queryText = !empty($queryTextParam) ? $queryTextParam : '';
200
201
		$testMode = !empty($this->request->getVar('TestMode'));
202
203
		// filters for aggregations
204
		$ignore = \Config::inst()->get('Elastica', 'BlackList');
205
		foreach($this->request->getVars() as $key => $value) {
206
			if(!in_array($key, $ignore)) {
207
				$es->addFilter($key, $value);
208
			}
209
		}
210
211
		// filter by class or site tree
212
		if($ep->SiteTreeOnly) {
213
			$es->addFilter('IsInSiteTree', true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
214
		} else {
215
			$es->setClasses($ep->ClassesToSearch);
216
		}
217
218
		// set the optional aggregation manipulator
219
		// In the event of a manipulator being present, show all the results for search
220
		// Otherwise aggregations are all zero
221
		if($this->SearchHelper) {
222
			$es->setQueryResultManipulator($this->SearchHelper);
223
			$es->showResultsForEmptySearch();
224
		} else {
225
			$es->hideResultsForEmptySearch();
226
		}
227
228
		// get the edited fields to search from the database for this search page
229
		// Convert this into a name => weighting array
230
		$fieldsToSearch = array();
231
		$editedSearchFields = $this->ElasticaSearchableFields()->filter(array(
232
			'Active' => true,
233
			'Searchable' => true
234
		));
235
236
		foreach($editedSearchFields->getIterator() as $searchField) {
237
			$fieldsToSearch[$searchField->Name] = $searchField->Weight;
238
		}
239
240
		$paginated = null;
241
		try {
242
			// Simulate server being down for testing purposes
243
			if(!empty($this->request->getVar('ServerDown'))) {
244
				throw new Elastica\Exception\Connection\HttpException('Unable to reach search server');
245
			}
246
247
			// now actually perform the search using the original query
248
			$paginated = $es->search($queryText, $fieldsToSearch, $testMode);
249
250
			// This is the case of the original query having a better one suggested.  Do a
251
			// second search for the suggested query, throwing away the original
252
			if($es->hasSuggestedQuery() && !$ignoreSuggestions) {
253
				$data['SuggestedQuery'] = $es->getSuggestedQuery();
254
				$data['SuggestedQueryHighlighted'] = $es->getSuggestedQueryHighlighted();
255
				//Link for if the user really wants to try their original query
256
				$sifLink = rtrim($this->Link(), '/') . '?q=' . $queryText . '&is=1';
257
				$data['SearchInsteadForLink'] = $sifLink;
258
				$paginated = $es->search($es->getSuggestedQuery(), $fieldsToSearch);
259
260
			}
261
262
			// calculate time
263
			$endTime = microtime(true);
264
			$elapsed = round(100 * ($endTime - $startTime)) / 100;
265
266
			// store variables for the template to use
267
			$data['ElapsedTime'] = $elapsed;
268
			$this->Aggregations = $es->getAggregations();
269
			$data['SearchResults'] = $paginated;
270
			$data['SearchPerformed'] = true;
271
			$data['NumberOfResults'] = $paginated->getTotalItems();
272
273
		} catch (Elastica\Exception\Connection\HttpException $e) {
274
			$data['ErrorMessage'] = 'Unable to connect to search server';
275
			$data['SearchPerformed'] = false;
276
		}
277
278
		$data['OriginalQuery'] = $queryText;
279
		$data['IgnoreSuggestions'] = $ignoreSuggestions;
280
281
		if($this->has_extension('PageControllerTemplateOverrideExtension')) {
282
			return $this->useTemplateOverride($data);
283
		} else {
284
			return $data;
285
		}
286
	}
287
288
289
290
	/*
291
	Return true if the query is not empty
292
	 */
293
	public function QueryIsEmpty() {
294
		return empty($this->request->getVar('q'));
295
	}
296
297
298
	/**
299
	 * Process submission of the search form, redirecting to a URL that will render search results
300
	 * @param  array $data form data
301
	 * @param  Form $form form
302
	 */
303
	public function submit($data, $form) {
304
		$queryText = $data['q'];
305
		$url = $this->Link();
306
		$url = rtrim($url, '/');
307
		$link = rtrim($url, '/') . '?q=' . $queryText . '&sfid=' . $data['identifier'];
308
		$this->redirect($link);
309
	}
310
311
	/*
312
	Obtain an instance of the form
313
	*/
314
315
	public function SearchForm() {
316
		$form = new ElasticSearchForm($this, 'SearchForm');
317
		$fields = $form->Fields();
318
		$ep = Controller::curr()->dataRecord;
319
		$identifierField = new HiddenField('identifier');
320
		$identifierField->setValue($ep->Identifier);
321
		$fields->push($identifierField);
322
		$queryField = $fields->fieldByName('q');
323
324
		 if($this->isParamSet('q') && $this->isParamSet('sfid')) {
325
		 	$sfid = $this->request->getVar('sfid');
326
			if($sfid == $ep->Identifier) {
327
				$queryText = $this->request->getVar('q');
328
				$queryField->setValue($queryText);
329
			}
330
331
		}
332
333
		if($this->action == 'similar') {
334
			$queryField->setDisabled(true);
335
			$actions = $form->Actions();
336
			foreach($actions as $field) {
0 ignored issues
show
Bug introduced by
The expression $actions of type object<FieldList>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
337
				$field->setDisabled(true);
338
			}
339
		}
340
341
		/*
342
		A field needs to be chosen for autocompletion, if not no autocomplete
343
		 */
344 View Code Duplication
		if($this->AutoCompleteFieldID > 0) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
345
			$queryField->setAttribute('data-autocomplete', 'true');
346
			$queryField->setAttribute('data-autocomplete-field', 'Title');
347
			$queryField->setAttribute('data-autocomplete-classes', $this->ClassesToSearch);
348
			$queryField->setAttribute('data-autocomplete-sitetree', $this->SiteTreeOnly);
349
			$queryField->setAttribute('data-autocomplete-source', $this->Link());
350
			$queryField->setAttribute('data-autocomplete-function',
351
			$this->AutocompleteFunction()->Slug);
352
		}
353
354
		return $form;
355
	}
356
357
358
	private function isParamSet($paramName) {
359
		return !empty($this->request->getVar($paramName));
360
	}
361
362
}
363