Completed
Push — dev2 ( bf3a9f...7a5f92 )
by Gordon
07:26 queued 12s
created

ElasticSearchPage::getCMSFields()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 188
Code Lines 138

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 11.6063
Metric Value
dl 0
loc 188
ccs 2
cts 135
cp 0.0148
rs 8.2857
cc 3
eloc 138
nc 2
nop 0
crap 11.6063

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
* Only show a page with login when not logged in
4
*/
5
use Elastica\Document;
6
use Elastica\Query;
7
use \SilverStripe\Elastica\ResultList;
8
use Elastica\Query\QueryString;
9
use Elastica\Aggregation\Filter;
10
use Elastica\Filter\Term;
11
use Elastica\Filter\BoolAnd;
12
use Elastica\Aggregation\Terms;
13
use Elastica\Query\Filtered;
14
use Elastica\Query\Range;
15
use \SilverStripe\Elastica\ElasticSearcher;
16
use \SilverStripe\Elastica\Searchable;
17
use \SilverStripe\Elastica\QueryGenerator;
18
use \SilverStripe\Elastica\ElasticaUtil;
19
20
//FIXME namespace
21
22
23
class ElasticSearchPage extends Page {
24
	private static $defaults = array(
25
		'ShowInMenus' => 0,
26
		'ShowInSearch' => 0,
27
		'ClassesToSearch' => '',
28
		'ResultsPerPage' => 10,
29
		'SiteTreeOnly' => true,
30
		'MinTermFreq' => 2,
31
		'MaxTermFreq' => 25,
32
		'MinWordLength' => 3,
33
		'MinDocFreq' => 2,
34
		'MaxDocFreq' => 0,
35
		'MinWordLength' => 0,
36
		'MaxWordLength' => 0,
37
		'MinShouldMatch' => '30%'
38
	);
39
40
	private static $db = array(
41
		'ClassesToSearch' => 'Text',
42
		// unique identifier used to find correct search page for results
43
		// e.g. a separate search page for blog, pictures etc
44
		'Identifier' => 'Varchar',
45
		'ResultsPerPage' => 'Int',
46
		'SearchHelper' => 'Varchar',
47
		'SiteTreeOnly' => 'Boolean',
48
		'ContentForEmptySearch' => 'HTMLText',
49
		'MinTermFreq' => 'Int',
50
		'MaxTermFreq' => 'Int',
51
		'MinWordLength' => 'Int',
52
		'MinDocFreq' => 'Int',
53
		'MaxDocFreq' => 'Int',
54
		'MinWordLength' => 'Int',
55
		'MaxWordLength' => 'Int',
56
		'MinShouldMatch' => 'Varchar',
57
		'SimilarityStopWords' => 'Text'
58
	);
59
60
	private static $many_many = array(
61
		'ElasticaSearchableFields' => 'SearchableField'
62
	);
63
64
	private static $many_many_extraFields = array(
65
		'ElasticaSearchableFields' => array(
66
		'Searchable' => 'Boolean', // allows the option of turning off a single field for searching
67
		'SimilarSearchable' => 'Boolean', // allows field to be used in more like this queries.
68
		'Active' => 'Boolean', // preserve previous edits of weighting when classes changed
69
		'EnableAutocomplete' => 'Boolean', // whether or not to show autocomplete search for this field
70
		'Weight' => 'Int' // Weight to apply to this field in a search
71
		)
72
  	);
73
74
75
  	private static $has_one = array(
76
  		'AutoCompleteFunction' => 'AutoCompleteOption',
77
  		'AutoCompleteField' => 'SearchableField'
78
  	);
79
80
81
	/*
82
	Add a tab with details of what to search
83
	 */
84 8
	public function getCMSFields() {
85
		Requirements::javascript('elastica/javascript/elasticaedit.js');
86
		$fields = parent::getCMSFields();
87
88
89
		$fields->addFieldToTab("Root", new TabSet('Search',
90
			new Tab('SearchFor'),
91
			new Tab('Fields'),
92
			new Tab('AutoComplete'),
93
			new Tab('Aggregations'),
94
			new Tab('Similarity')
95
		));
96
97
98
		// ---- similarity tab ----
99
		$html = '<button class="ui-button-text-alternate ui-button-text"
100
		id="MoreLikeThisDefaultsButton"
101
		style="display: block;float: right;">Restore Defaults</button>';
102
		$defaultsButton = new LiteralField('DefaultsButton', $html);
103
				$fields->addFieldToTab("Root.Search.Similarity", $defaultsButton);
104
		$sortedWords = $this->SimilarityStopWords;
105
106
		$stopwordsField = StringTagField::create(
107
			'SimilarityStopWords',
108
			'Stop Words for Similar Search',
109
			explode(',', $sortedWords),
110
			$sortedWords
111
		);
112
113
		$stopwordsField->setShouldLazyLoad(true); // tags should be lazy loaded
114
115
		$fields->addFieldToTab("Root.Search.Similarity", $stopwordsField);
116
117
		$lf = new LiteralField('SimilarityNotes', _t('Elastica.SIMILARITY_NOTES',
118
			'Default values are those used by Elastica'));
119
		$fields->addFieldToTab("Root.Search.Similarity", $lf);
120
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MinTermFreq',
121
			'The minimum term frequency below which the terms will be ignored from the input ' .
122
			'document. Defaults to 2.'));
123
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MaxTermFreq',
124
			'The maximum number of query terms that will be selected. Increasing this value gives ' .
125
			'greater accuracy at the expense of query execution speed. Defaults to 25.'));
126
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MinWordLength',
127
			'The minimum word length below which the terms will be ignored.  Defaults to 0.'));
128
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MinDocFreq',
129
			'The minimum document frequency below which the terms will be ignored from the input ' .
130
			'document. Defaults to 5.'));
131
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MaxDocFreq',
132
			'The maximum document frequency above which the terms will be ignored from the input ' .
133
			'document. This could be useful in order to ignore highly frequent words such as stop ' .
134
			'words. Defaults to unbounded (0).'));
135
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MinWordLength',
136
			'The minimum word length below which the terms will be ignored. The old name min_' .
137
			'word_len is deprecated. Defaults to 0.'));
138
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MaxWordLength',
139
			'The maximum word length above which the terms will be ignored. The old name max_word_' .
140
			'len is deprecated. Defaults to unbounded (0).'));
141
		$fields->addFieldToTab("Root.Search.Similarity", new TextField('MinShouldMatch',
142
			'This parameter controls the number of terms that must match. This can be either a ' .
143
			'number or a percentage.  See ' .
144
			'https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-minimum-should-match.html'));
145
146
		// ---- search details tab ----
147
		$identifierField = new TextField('Identifier',
148
			'Identifier to allow this page to be found in form templates');
149
		$fields->addFieldToTab('Root.Main', $identifierField, 'Content');
150
		$fields->addFieldToTab('Root.Search.SearchFor', new CheckboxField('SiteTreeOnly', 'Show search results for all SiteTree objects only'));
151
152
		$sql = "SELECT DISTINCT ClassName from SiteTree_Live UNION "
153
			 . "SELECT DISTINCT ClassName from SiteTree "
154
			 . "WHERE ClassName != 'ErrorPage'"
155
			 . "ORDER BY ClassName"
156
		;
157
158
		$classes = array();
159
		$records = DB::query($sql);
160
		foreach($records as $record) {
161
			array_push($classes, $record['ClassName']);
162
		}
163
		$list = implode(',', $classes);
164
165
		$clazzes = '';
166
		$clazzes = $this->ClassesToSearch;
167
		$allSearchableClasses = SearchableClass::get()->sort('Name')->map('Name')->toArray();
168
		$classesToSearchField = StringTagField::create(
169
			'ClassesToSearch',
170
			'Choose which SilverStripe classes to search',
171
			$allSearchableClasses,
172
			$clazzes
173
		);
174
175
		$fields->addFieldToTab('Root.Search.SearchFor', $classesToSearchField);
176
177
178
		$html = '<div class="field text" id="SiteTreeOnlyInfo">';
179
		$html .= "<p>Copy the following into the above field to ensure that all SiteTree classes are searched</p>";
180
		$html .= '<p class="message">' . $list;
181
		$html .= "</p></div>";
182
		$infoField = new LiteralField('InfoField', $html);
183
		$fields->addFieldToTab('Root.Search.SearchFor', $infoField);
184
185
		$fields->addFieldToTab('Root.Main', new HTMLEditorField('ContentForEmptySearch'));
186
187
188
		$fields->addFieldToTab('Root.Search.SearchFor', new NumericField('ResultsPerPage',
189
											'The number of results to return on a page'));
190
		$fields->addFieldToTab('Root.Search.Aggregations', new TextField('SearchHelper',
191 8
			'ClassName of object to manipulate search details and results.  Leave blank for standard search'));
192
193
		$ottos = AutoCompleteOption::get()->Filter('Locale', $this->Locale)->map('ID', 'Name')->
194
											toArray();
195
		$df = DropdownField::create('AutoCompleteFunctionID', 'Autocomplete Function')->
196
									setSource($ottos);
197
		$df->setEmptyString('-- Please select what do do after find as you type has occurred --');
198
199
		$ottos = $this->ElasticaSearchableFields()->filter('EnableAutocomplete', 1)->Map('ID', 'Name')->toArray();
200
		$autoCompleteFieldDF = DropDownField::create('AutoCompleteFieldID', 'Field to use for autocomplete')->setSource($ottos);
201
		$autoCompleteFieldDF->setEmptyString('-- Please select which field to use for autocomplete --');
202
203
		$fields->addFieldToTab("Root.Search.AutoComplete",
204
		  		FieldGroup::create(
205
		  			$autoCompleteFieldDF,
206
		 			$df
207
		 		)->setTitle('Autocomplete')
208
		 );
209
210
		// ---- grid of searchable fields ----
211
		$html = '<p id="SearchFieldIntro">' . _t('SiteConfig.ELASTICA_SEARCH_INFO',
212
				"Select a field to edit it's properties") . '</p>';
213
		$fields->addFieldToTab('Root.Search.Fields', $h1 = new LiteralField('SearchInfo', $html));
214
		$searchPicker = new PickerField('ElasticaSearchableFields', 'Searchable Fields',
215
			$this->ElasticaSearchableFields()->filter('Active', 1)->sort('Name'));
216
217
		$fields->addFieldToTab('Root.Search.Fields', $searchPicker);
218
219
		$pickerConfig = $searchPicker->getConfig();
220
221
		$pickerConfig->removeComponentsByType(new GridFieldAddNewButton());
222
		$pickerConfig->removeComponentsByType(new GridFieldDeleteAction());
223
		$pickerConfig->removeComponentsByType(new PickerFieldAddExistingSearchButton());
224
		$pickerConfig->getComponentByType('GridFieldPaginator')->setItemsPerPage(100);
225
226
		$searchPicker->enableEdit();
227
		$edittest = $pickerConfig->getComponentByType('GridFieldDetailForm');
228
		$edittest->setFields(FieldList::create(
229
			TextField::create('Name', 'Field Name'),
230
			TextField::create('ClazzName', 'Class'),
231
			HiddenField::create('Autocomplete', 'This can be autocompleted'),
232
			CheckboxField::create('ManyMany[Searchable]', 'Use for normal searching'),
233
			CheckboxField::create('ManyMany[SimilarSearchable]', 'Use for similar search'),
234
			NumericField::create('ManyMany[Weight]', 'Weighting'),
235
			CheckboxField::create('ShowHighlights', 'Show highlights from search in results for this field'),
236
			CheckboxField::create('ManyMany[EnableAutocomplete]', 'Enable Autocomplete')
237
		));
238
239
		$edittest->setItemEditFormCallback(function($form) {
240
			$fields = $form->Fields();
241
			$fieldAutocomplete = $fields->dataFieldByName('Autocomplete');
242
			$fieldEnableAutcomplete = $fields->dataFieldByName('ManyMany[EnableAutocomplete]');
243
244
			$fields->dataFieldByName('ClazzName')->setReadOnly(true);
245
			$fields->dataFieldByName('ClazzName')->setDisabled(true);
246
			$fields->dataFieldByName('Name')->setReadOnly(true);
247
			$fields->dataFieldByName('Name')->setDisabled(true);
248
249
			if(!$fieldAutocomplete->Value() == '1') {
250
				$fieldEnableAutcomplete->setDisabled(true);
251
				$fieldEnableAutcomplete->setReadOnly(true);
252
				$fieldEnableAutcomplete->setTitle("Autcomplete is not available for this field");
253
			}
254
255
		});
256
257
258
		// What do display on the grid of searchable fields
259
		$dataColumns = $pickerConfig->getComponentByType('GridFieldDataColumns');
260
		$dataColumns->setDisplayFields(array(
261
			'Name' => 'Name',
262
			'ClazzName' => 'Class',
263
			'Type' => 'Type',
264
			'Searchable' => 'Use for Search?',
265
			'SimilarSearchable' => 'Use for Similar Search?',
266
			'ShowHighlights' => 'Show Search Highlights',
267
			'Weight' => 'Weighting'
268
		));
269
270
		return $fields;
271
	}
272
273
274
	public function getCMSValidator() {
275
		return new ElasticSearchPage_Validator();
276
	}
277
278
279
	/**
280
	 * Avoid duplicate identifiers, and check that ClassesToSearch actually exist and are Searchable
281
	 * @return DataObject result with or without error
282
	 */
283 8
	public function validate() {
284 8
		$result = parent::validate();
285 8
		$mode = Versioned::get_reading_mode();
286 8
		$suffix = '';
287 8
		if($mode == 'Stage.Live') {
288 8
			$suffix = '_Live';
289 8
		}
290
291 8
		if(!$this->Identifier) {
292
			$result->error('The identifier cannot be blank');
293
		}
294
295 8
		$where = 'ElasticSearchPage' . $suffix . '.ID != ' . $this->ID . " AND `Identifier` = '{$this->Identifier}'";
296 8
		$existing = ElasticSearchPage::get()->where($where)->count();
1 ignored issue
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
297 8
		if($existing > 0) {
298
			$result->error('The identifier ' . $this->Identifier . ' already exists');
299
		}
300
301
		// now check classes to search actually exist, assuming in site tree not set
302 8
		if(!$this->SiteTreeOnly) {
303
			if($this->ClassesToSearch == '') {
304
				$result->error('At least one searchable class must be available, or SiteTreeOnly flag set');
305
			} else {
306
				$toSearch = explode(',', $this->ClassesToSearch);
307
				foreach($toSearch as $clazz) {
308
					try {
309
						$instance = Injector::inst()->create($clazz);
310
						if(!$instance->hasExtension('SilverStripe\Elastica\Searchable')) {
311
							$result->error('The class ' . $clazz . ' must have the Searchable extension');
312
						}
313
					} catch (ReflectionException $e) {
314
						$result->error('The class ' . $clazz . ' does not exist');
315
					}
316
				}
317
			}
318
		}
319
320
321 8
		foreach($this->ElasticaSearchableFields() as $esf) {
322 8
			if($esf->Weight == 0) {
323 1
				$result->error("The field {$esf->ClazzName}.{$esf->Name} has a zero weight. ");
324 8
			} else if($esf->Weight < 0) {
325 1
				$result->error("The field {$esf->ClazzName}.{$esf->Name} has a negative weight. ");
326 1
			}
327 8
		}
328
329 8
		return $result;
330
	}
331
332
333 8
	public function onAfterWrite() {
334
		// FIXME - move to a separate testable method and call at build time also
335 8
		$nameToMapping = QueryGenerator::getSearchFieldsMappingForClasses($this->ClassesToSearch);
336 8
		$names = array_keys($nameToMapping);
337
338
		#FIXME -  SiteTree only
339 8
		$relevantClasses = $this->ClassesToSearch; // due to validation this will be valid
340 8
		if($this->SiteTreeOnly) {
341 8
			$relevantClasses = SearchableClass::get()->filter('InSiteTree', true)->Map('Name')->toArray();
342
343 8
		}
344 8
		$quotedClasses = QueryGenerator::convertToQuotedCSV($relevantClasses);
345 8
		$quotedNames = QueryGenerator::convertToQuotedCSV($names);
346
347 8
		$where = "Name in ($quotedNames) AND ClazzName IN ($quotedClasses)";
348
349
		// Get the searchfields for the ClassNames searched
350 8
		$sfs = SearchableField::get()->where($where);
351
352
353
		// Get the searchable fields associated with this search page
354 8
		$esfs = $this->ElasticaSearchableFields();
355
356
		// Remove existing searchable fields for this page from the list of all available
357 8
		$delta = array_keys($esfs->map()->toArray());
358 8
		$newSearchableFields = $sfs->exclude('ID', $delta);
359
360 8
		if($newSearchableFields->count() > 0) {
361 8
			foreach($newSearchableFields->getIterator() as $newSearchableField) {
362 8
				$newSearchableField->Active = true;
363 8
				$newSearchableField->Weight = 1;
364
365 8
				$esfs->add($newSearchableField);
366
367
				// Note 1 used instead of true for SQLite3 testing compatibility
368 8
				$sql = "UPDATE ElasticSearchPage_ElasticaSearchableFields SET ";
369 8
				$sql .= 'Active=1, Weight=1 WHERE ElasticSearchPageID = ' . $this->ID;
370 8
				DB::query($sql);
371 8
			}
372 8
		}
373
374
375
376
		// Mark all the fields for this page as inactive initially
377 8
		$sql = "UPDATE ElasticSearchPage_ElasticaSearchableFields SET ACTIVE=0 WHERE ";
378 8
		$sql .= "ElasticSearchPageID={$this->ID}";
379 8
		DB::query($sql);
380
381 8
		$activeIDs = array_keys($sfs->map()->toArray());
382 8
		$activeIDs = implode(',', $activeIDs);
383
384
		//Mark as active the relevant ones
385 8
		$sql = "UPDATE ElasticSearchPage_ElasticaSearchableFields SET ACTIVE=1 WHERE ";
386 8
		$sql .= "ElasticSearchPageID={$this->ID} AND SearchableFieldID IN (";
387 8
		$sql .= "$activeIDs)";
388 8
		DB::query($sql);
389 8
	}
390
391
392
	/*
393
	Obtain an instance of the form - this is need for rendering the search box in the header
394
	*/
395
	public function SearchForm($buttonTextOverride = null) {
396
		$result = new ElasticSearchForm($this, 'SearchForm');
397
		$fields = $result->Fields();
398
		$identifierField = new HiddenField('identifier');
399
		$identifierField->setValue($this->Identifier);
400
		$fields->push($identifierField);
401
		$qField = $fields->fieldByName('q');
402
403
404
		if($buttonTextOverride) {
405
			$result->setButtonText($buttonTextOverride);
406
		}
407
408
		if($this->AutoCompleteFieldID > 0) {
409
			ElasticaUtil::addAutocompleteToQueryField(
410
				$qField,
411
				$this->ClassesToSearch,
412
				$this->SiteTreeOnly,
413
				$this->Link(),
414
				$this->AutocompleteFunction()->Slug
415
			);
416
		}
417
		return $result;
418
	}
419
420
421
	/*
422
	If a manipulator object is set, assume aggregations are present.  Used to add the column
423
	for aggregates
424
	 */
425 8
	public function HasAggregations() {
426
		return $this->SearchHelper != null;
427 8
	}
428
429
}
430