Completed
Push — dev2 ( 12047b...a70cfe )
by Gordon
05:03
created

Searchable::requireDefaultRecords()   C

Complexity

Conditions 7
Paths 22

Size

Total Lines 51
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 3 Features 0
Metric Value
c 5
b 3
f 0
dl 0
loc 51
rs 6.9744
cc 7
eloc 29
nc 22
nop 0

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
namespace SilverStripe\Elastica;
4
5
use Elastica\Document;
6
use Elastica\Type\Mapping;
7
use ShortcodeParser;
8
9
/**
10
 * Adds elastic search integration to a data object.
11
 */
12
class Searchable extends \DataExtension {
13
14
	/**
15
	 * Counter used to display progress of indexing
16
	 * @var integer
17
	 */
18
	public static $index_ctr = 0;
19
20
	/**
21
	 * Everytime progressInterval divides $index_ctr exactly display progress
22
	 * @var integer
23
	 */
24
	private static $progressInterval = 0;
25
26
	public static $mappings = array(
27
		'Boolean'     => 'boolean',
28
		'Decimal'     => 'double',
29
		'Currency'    => 'double',
30
		'Double'      => 'double',
31
		'Enum'        => 'string',
32
		'Float'       => 'float',
33
		'HTMLText'    => 'string',
34
		'HTMLVarchar' => 'string',
35
		'Int'         => 'integer',
36
		'Text'        => 'string',
37
		'VarChar'     => 'string',
38
		'Varchar'     => 'string',
39
		'Year'        => 'integer',
40
		'Percentage'  => 'double',
41
		'Time'  => 'date',
42
		// The 2 different date types will be stored with different formats
43
		'Date'        => 'date',
44
		'SS_Datetime' => 'date',
45
		'Datetime' => 'date',
46
		'DBLocale'    => 'string'
47
	);
48
49
50
	/**
51
	 * @var ElasticaService associated elastica search service
52
	 */
53
	protected $service;
54
55
56
	/**
57
	 * Array of fields that need HTML parsed
58
	 * @var array
59
	 */
60
	protected $html_fields = array();
61
62
	/**
63
	 * Store a mapping of relationship name to result type
64
	 */
65
	protected $relationship_methods = array();
66
67
68
	/**
69
	 * If importing a large number of items from a fixtures file, or indeed some other source, then
70
	 * it is quicker to set a flag of value IndexingOff => false.  This has the effect of ensuring
71
	 * no indexing happens, a request is normally made per fixture when loading.  One can then run
72
	 * the reindexing teask to bulk index in one HTTP POST request to Elasticsearch
73
	 *
74
	 * @var boolean
75
	 */
76
	private static $IndexingOff = false;
77
78
79
	/**
80
	 * @see getElasticaResult
81
	 * @var \Elastica\Result
82
	 */
83
	protected $elastica_result;
84
85
	public function __construct(ElasticaService $service) {
86
		$this->service = $service;
87
		parent::__construct();
88
	}
89
90
91
	/**
92
	 * Get the elasticsearch type name
93
	 *
94
	 * @return string
95
	 */
96
	public function getElasticaType() {
97
		return get_class($this->owner);
98
	}
99
100
101
	/**
102
	 * If the owner is part of a search result
103
	 * the raw Elastica search result is returned
104
	 * if set via setElasticaResult
105
	 *
106
	 * @return \Elastica\Result
107
	 */
108
	public function getElasticaResult() {
109
		return $this->elastica_result;
110
	}
111
112
113
	/**
114
	 * Set the raw Elastica search result
115
	 *
116
	 * @param \Elastica\Result
117
	 */
118
	public function setElasticaResult(\Elastica\Result $result) {
119
		$this->elastica_result = $result;
120
	}
121
122
123
	/**
124
	 * Gets an array of elastic field definitions.
125
	 *
126
	 * @return array
127
	 */
128
	public function getElasticaFields($storeMethodName = false, $recurse = true) {
129
		$db = $this->owner->db();
130
		$fields = $this->getAllSearchableFields();
131
		$result = array();
132
133
		foreach($fields as $name => $params) {
134
			$spec = array();
135
			$name = str_replace('()', '', $name);
136
137
			if(array_key_exists($name, $db)) {
138
				$class = $db[$name];
139
				SearchableHelper::assignSpecForStandardFieldType($name, $class, $spec, $this->html_fields, self::$mappings);
140
			} else {
141
				// field name is not in the db, it could be a method
142
				$has_lists = SearchableHelper::getListRelationshipMethods($this->owner);
143
				$has_ones = $this->owner->has_one();
144
145
				// check has_many and many_many relations
146
				if(isset($has_lists[$name])) {
147
					// the classes returned by the list method
148
					$resultType = $has_lists[$name];
149
					SearchableHelper::assignSpecForRelationship($name, $resultType, $spec, $storeMethodName, $recurse);
150
				} else if(isset($has_ones[$name])) {
151
					$resultType = $has_ones[$name];
152
					SearchableHelper::assignSpecForRelationship($name, $resultType, $spec, $storeMethodName, $recurse);
153
				}
154
				// otherwise fall back to string - Enum is one such category
155
				else {
156
					$spec["type"] = "string";
157
				}
158
			}
159
160
			SearchableHelper::addIndexedFields($name, $spec, $this->owner->ClassName);
161
			$result[$name] = $spec;
162
		}
163
164
		if($this->owner->hasMethod('updateElasticHTMLFields')) {
165
			$this->html_fields = $this->owner->updateElasticHTMLFields($this->html_fields);
166
		}
167
168
		return $result;
169
	}
170
171
172
173
	/**
174
	 * Get the elasticsearch mapping for the current document/type
175
	 *
176
	 * @return \Elastica\Type\Mapping
177
	 */
178
	public function getElasticaMapping() {
179
		$mapping = new Mapping();
180
181
		$fields = $this->getElasticaFields(false);
182
183
		$localeMapping = array();
184
185
		if($this->owner->hasField('Locale')) {
186
			$localeMapping['type'] = 'string';
187
			// we wish the locale to be stored as is
188
			$localeMapping['index'] = 'not_analyzed';
189
			$fields['Locale'] = $localeMapping;
190
		}
191
192
		// ADD CUSTOM FIELDS HERE THAT ARE INDEXED BY DEFAULT
193
		// add a mapping to flag whether or not class is in SiteTree
194
		$fields['IsInSiteTree'] = array('type'=>'boolean');
195
		$fields['Link'] = array('type' => 'string', 'index' => 'not_analyzed');
196
197
		$mapping->setProperties($fields);
198
199
		//This concatenates all the fields together into a single field.
200
		//Initially added for suggestions compatibility, in that searching
201
		//_all field picks up all possible suggestions
202
		$mapping->enableAllField();
203
204
		if($this->owner->hasMethod('updateElasticsearchMapping')) {
205
			$mapping = $this->owner->updateElasticsearchMapping($mapping);
206
		}
207
		return $mapping;
208
	}
209
210
211
	/**
212
	 * Get an elasticsearch document
213
	 *
214
	 * @return \Elastica\Document
215
	 */
216
	public function getElasticaDocument() {
217
		self::$index_ctr++;
218
		$fields = $this->getFieldValuesAsArray();
219
		$progress = \Controller::curr()->request->getVar('progress');
220
		if(!empty($progress)) {
221
			self::$progressInterval = (int)$progress;
222
		}
223
224
		if(self::$progressInterval > 0) {
225
			if(self::$index_ctr % self::$progressInterval === 0) {
226
				ElasticaUtil::message("\t" . $this->owner->ClassName . " - Prepared " . self::$index_ctr . " for indexing...");
227
			}
228
		}
229
230
		// Optionally update the document
231
		$document = new Document($this->owner->ID, $fields);
232
		if($this->owner->hasMethod('updateElasticsearchDocument')) {
233
			$document = $this->owner->updateElasticsearchDocument($document);
234
		}
235
236
		// Check if the current classname is part of the site tree or not
237
		// Results are cached to save reprocessing the same
238
		$classname = $this->owner->ClassName;
239
		$inSiteTree = SearchableHelper::isInSiteTree($classname);
240
241
		$document->set('IsInSiteTree', $inSiteTree);
242
		if($inSiteTree) {
243
			$document->set('Link', $this->owner->AbsoluteLink());
244
		}
245
246
		if(isset($this->owner->Locale)) {
247
			$document->set('Locale', $this->owner->Locale);
248
		}
249
250
		return $document;
251
	}
252
253
254
255
256
257
	public function getFieldValuesAsArray($recurse = true) {
258
		$fields = array();
259
260
		foreach($this->getElasticaFields($recurse) as $field => $config) {
261
262
			//This is the case of calling a method to get a value, the field does not exist in the DB
263
			if(null === $this->owner->$field && is_callable(get_class($this->owner) . "::" . $field)) {
264
				// call a method to get a field value
265
				SearchableHelper::storeMethodTextValue($this->owner, $field, $fields, $this->html_fields);
266
267
			} else {
268
				if(in_array($field, $this->html_fields)) {
269
					$fields[$field] = $this->owner->$field;
270
					if(gettype($this->owner->$field) !== 'NULL') {
271
						$html = ShortcodeParser::get_active()->parse($this->owner->$field);
272
						$txt = \Convert::html2raw($html);
273
						$fields[$field] = $txt;
274
					}
275
				} else {
276
					if(isset($config['properties']['__method'])) {
277
						$methodName = $config['properties']['__method'];
278
						$data = $this->owner->$methodName();
279
						$relArray = array();
280
281
						$has_ones = $this->owner->has_one();
282
						// get the fields of a has_one relational object
283
						if(isset($has_ones[$methodName])) {
284
							if($data->ID > 0) {
285
								$item = $data->getFieldValuesAsArray(false);
286
								$relArray = $item;
287
							}
288
289
						// get the fields for a has_many or many_many relational list
290
						} else {
291
							foreach($data->getIterator() as $item) {
292
								if($recurse) {
293
									// populate the subitem but do not recurse any further if more relationships
294
									$itemDoc = $item->getFieldValuesAsArray(false);
295
									array_push($relArray, $itemDoc);
296
								}
297
							}
298
						}
299
						// save the relation as an array (for now)
300
						$fields[$methodName] = $relArray;
301
					} else {
302
						$fields[$field] = $this->owner->$field;
303
					}
304
305
				}
306
307
			}
308
		}
309
310
		return $fields;
311
	}
312
313
314
	/**
315
	 * Returns whether to include the document into the search index.
316
	 * All documents are added unless they have a field "ShowInSearch" which is set to false
317
	 *
318
	 * @return boolean
319
	 */
320
	public function showRecordInSearch() {
321
		return !($this->owner->hasField('ShowInSearch') && false == $this->owner->ShowInSearch);
322
	}
323
324
325
	/**
326
	 * Delete the record from the search index if ShowInSearch is deactivated (non-SiteTree).
327
	 */
328
	public function onBeforeWrite() {
329
		if(($this->owner instanceof \SiteTree)) {
330
			if($this->owner->hasField('ShowInSearch') &&
331
				$this->owner->isChanged('ShowInSearch', 2) && false == $this->owner->ShowInSearch) {
332
				$this->doDeleteDocument();
333
			}
334
		}
335
	}
336
337
338
	/**
339
	 * Delete the record from the search index if ShowInSearch is deactivated (SiteTree).
340
	 */
341
	public function onBeforePublish() {
342
		if(false == $this->owner->ShowInSearch) {
343
			if($this->owner->isPublished()) {
344
				$liveRecord = \Versioned::get_by_stage(get_class($this->owner), 'Live')->
345
					byID($this->owner->ID);
346
				if($liveRecord->ShowInSearch != $this->owner->ShowInSearch) {
347
					$this->doDeleteDocument();
348
				}
349
			}
350
		}
351
	}
352
353
354
	/**
355
	 * Updates the record in the search index (non-SiteTree).
356
	 */
357
	public function onAfterWrite() {
358
		$this->doIndexDocument();
359
	}
360
361
362
	/**
363
	 * Updates the record in the search index (SiteTree).
364
	 */
365
	public function onAfterPublish() {
366
		$this->doIndexDocument();
367
	}
368
369
370
	/**
371
	 * Updates the record in the search index.
372
	 */
373
	protected function doIndexDocument() {
374
		if($this->showRecordInSearch()) {
375
			if(!$this->owner->IndexingOff) {
376
				$this->service->index($this->owner);
377
			}
378
		}
379
	}
380
381
382
	/**
383
	 * Removes the record from the search index (non-SiteTree).
384
	 */
385
	public function onAfterDelete() {
386
		$this->doDeleteDocumentIfInSearch();
387
	}
388
389
390
	/**
391
	 * Removes the record from the search index (non-SiteTree).
392
	 */
393
	public function onAfterUnpublish() {
394
		$this->doDeleteDocumentIfInSearch();
395
	}
396
397
398
	/**
399
	 * Removes the record from the search index if the "ShowInSearch" attribute is set to true.
400
	 */
401
	protected function doDeleteDocumentIfInSearch() {
402
		if($this->showRecordInSearch()) {
403
			$this->doDeleteDocument();
404
		}
405
	}
406
407
408
	/**
409
	 * Removes the record from the search index.
410
	 */
411
	protected function doDeleteDocument() {
412
		try {
413
			if(!$this->owner->IndexingOff) {
414
				// this goes to elastica service
415
				$this->service->remove($this->owner);
416
			}
417
		} catch (\Elastica\Exception\NotFoundException $e) {
418
			trigger_error("Deleted document " . $this->owner->ClassName . " (" . $this->owner->ID .
419
				") not found in search index.", E_USER_NOTICE);
420
		}
421
422
	}
423
424
425
	/**
426
	 * Return all of the searchable fields defined in $this->owner::$searchable_fields and all the parent classes.
427
	 *
428
	 * @param  $recuse Whether or not to traverse relationships. First time round yes, subsequently no
429
	 * @return array searchable fields
430
	 */
431
	public function getAllSearchableFields($recurse = true) {
432
		$fields = \Config::inst()->get(get_class($this->owner), 'searchable_fields');
433
434
		// fallback to default method
435
		if(!$fields) {
436
			user_error('The field $searchable_fields must be set for the class ' . $this->owner->ClassName);
437
		}
438
439
		// get the values of these fields
440
		$elasticaMapping = SearchableHelper::fieldsToElasticaConfig($fields);
441
442
		if($recurse) {
443
			// now for the associated methods and their results
444
			$methodDescs = \Config::inst()->get(get_class($this->owner), 'searchable_relationships');
445
			$has_ones = $this->owner->has_one();
446
			$has_lists = SearchableHelper::getListRelationshipMethods($this->owner);
447
448
			if(isset($methodDescs) && is_array($methodDescs)) {
449
				foreach($methodDescs as $methodDesc) {
450
					// split before the brackets which can optionally list which fields to index
451
					$splits = explode('(', $methodDesc);
452
					$methodName = $splits[0];
453
454
					if(isset($has_lists[$methodName])) {
455
456
						$relClass = $has_lists[$methodName];
457
						$fields = \Config::inst()->get($relClass, 'searchable_fields');
458
						if(!$fields) {
459
							user_error('The field $searchable_fields must be set for the class ' . $relClass);
460
						}
461
						$rewrite = SearchableHelper::fieldsToElasticaConfig($fields);
462
463
						// mark as a method, the resultant fields are correct
464
						$elasticaMapping[$methodName . '()'] = $rewrite;
465
					} else if(isset($has_ones[$methodName])) {
466
						$relClass = $has_ones[$methodName];
467
						$fields = \Config::inst()->get($relClass, 'searchable_fields');
468
						if(!$fields) {
469
							user_error('The field $searchable_fields must be set for the class ' . $relClass);
470
						}
471
						$rewrite = SearchableHelper::fieldsToElasticaConfig($fields);
472
473
						// mark as a method, the resultant fields are correct
474
						$elasticaMapping[$methodName . '()'] = $rewrite;
475
					} else {
476
						user_error('The method ' . $methodName . ' not found in class ' . $this->owner->ClassName .
477
								', please check configuration');
478
					}
479
				}
480
			}
481
		}
482
483
		return $elasticaMapping;
484
	}
485
486
487
488
489
	public function requireDefaultRecords() {
490
		parent::requireDefaultRecords();
491
492
		$searchableFields = $this->getElasticaFields(true, true);
493
494
495
		$doSC = \SearchableClass::get()->filter(array('Name' => $this->owner->ClassName))->first();
496
		if(!$doSC) {
497
			$doSC = new \SearchableClass();
498
			$doSC->Name = $this->owner->ClassName;
499
500
			$inSiteTree = SearchableHelper::isInSiteTree($this->owner->ClassName);
501
			$doSC->InSiteTree = $inSiteTree;
502
503
			$doSC->write();
504
		}
505
506
		foreach($searchableFields as $name => $searchableField) {
507
			// check for existence of methods and if they exist use that as the name
508
			if(!isset($searchableField['type'])) {
509
				$name = $searchableField['properties']['__method'];
510
			}
511
512
			$filter = array('ClazzName' => $this->owner->ClassName, 'Name' => $name);
513
			$doSF = \SearchableField::get()->filter($filter)->first();
514
515
516
			if(!$doSF) {
517
				$doSF = new \SearchableField();
518
				$doSF->ClazzName = $this->owner->ClassName;
519
				$doSF->Name = $name;
520
521
				if(isset($searchableField['type'])) {
522
					$doSF->Type = $searchableField['type'];
523
				} else {
524
					$doSF->Name = $searchableField['properties']['__method'];
525
					$doSF->Type = 'relationship';
526
				}
527
				$doSF->SearchableClassID = $doSC->ID;
528
529
				if(isset($searchableField['fields']['autocomplete'])) {
530
					$doSF->Autocomplete = true;
531
				}
532
533
				$doSF->write();
534
				\DB::alteration_message("Created new searchable editable field " . $name, "changed");
535
			}
536
537
			// FIXME deal with deletions
538
		}
539
	}
540
541
542
	/*
543
	Allow the option of overriding the default template with one of <ClassName>ElasticSearchResult
544
	 */
545
	public function RenderResult($linkToContainer = '') {
546
		$vars = new \ArrayData(array('SearchResult' => $this->owner, 'ContainerLink' => $linkToContainer));
547
		$possibleTemplates = array($this->owner->ClassName . 'ElasticSearchResult', 'ElasticSearchResult');
548
		return $this->owner->customise($vars)->renderWith($possibleTemplates);
549
	}
550
551
552
553
	public function getTermVectors() {
554
		return $this->service->getTermVectors($this->owner);
555
	}
556
557
558
	public function updateCMSFields(\FieldList $fields) {
559
		$isIndexed = false;
560
		// SIteTree object must have a live record, ShowInSearch = true
561
		if(SearchableHelper::isInSiteTree($this->owner->ClassName)) {
562
			$liveRecord = \Versioned::get_by_stage(get_class($this->owner), 'Live')->
563
				byID($this->owner->ID);
564
			if($liveRecord->ShowInSearch) {
565
				$isIndexed = true;
566
			} else {
567
				$isIndexed = false;
568
			}
569
		} else {
570
			// In the case of a DataObject we use the ShowInSearchFlag
571
			$isIndexed = true;
572
		}
573
574
		if($isIndexed) {
575
			$termVectors = $this->getTermVectors();
576
			$termFields = array_keys($termVectors);
577
			sort($termFields);
578
579
			foreach($termFields as $field) {
580
				$terms = new \ArrayList();
581
582
				foreach(array_keys($termVectors[$field]['terms']) as $term) {
583
					$do = new \DataObject();
584
					$do->Term = $term;
585
					$stats = $termVectors[$field]['terms'][$term];
586
					if(isset($stats['ttf'])) {
587
						$do->TTF = $stats['ttf'];
588
					}
589
590
					if(isset($stats['doc_freq'])) {
591
						$do->DocFreq = $stats['doc_freq'];
592
					}
593
594
					if(isset($stats['term_freq'])) {
595
						$do->TermFreq = $stats['term_freq'];
596
					}
597
					$terms->push($do);
598
				}
599
600
				$config = \GridFieldConfig_RecordViewer::create(100);
601
				$config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
602
					'Term' => 'Term',
603
					'TTF' => 'Total term frequency (how often a term occurs in all documents)',
604
					'DocFreq' => 'n documents with this term',
605
					'TermFreq'=> 'n times this term appears in this field'
606
				));
607
608
			   $underscored = str_replace('.', '_', $field);
609
610
				$gridField = new \GridField(
611
					'TermsFor' . $underscored, // Field name
612
					$field . 'TITLE' . $field, // Field title
613
					$terms,
614
					$config
615
				);
616
			   $fields->addFieldToTab('Root.ElasticaTerms.' . $underscored, $gridField);
617
			}
618
619
		}
620
621
		return $fields;
622
	}
623
624
625
}
626