Completed
Push — dev2 ( 410e93...f96365 )
by Gordon
03:11
created

Searchable::getFieldValuesAsArray()   C

Complexity

Conditions 12
Paths 9

Size

Total Lines 62
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 2 Features 0
Metric Value
c 5
b 2
f 0
dl 0
loc 62
rs 6.2073
cc 12
eloc 37
nc 9
nop 1

How to fix   Long Method    Complexity   

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
43
		// The 2 different date types will be stored with different formats
44
		'Date'        => 'date',
45
		'SS_Datetime' => 'date',
46
		'Datetime' => 'date',
47
		'DBLocale'    => 'string'
48
	);
49
50
51
	/**
52
	 * @var ElasticaService associated elastica search service
53
	 */
54
	protected $service;
55
56
57
	/**
58
	 * Array of fields that need HTML parsed
59
	 * @var array
60
	 */
61
	protected $html_fields = array();
62
63
	/**
64
	 * Store a mapping of relationship name to result type
65
	 */
66
	protected $relationship_methods = array();
67
68
69
	/**
70
	 * If importing a large number of items from a fixtures file, or indeed some other source, then
71
	 * it is quicker to set a flag of value IndexingOff => false.  This has the effect of ensuring
72
	 * no indexing happens, a request is normally made per fixture when loading.  One can then run
73
	 * the reindexing teask to bulk index in one HTTP POST request to Elasticsearch
74
	 *
75
	 * @var boolean
76
	 */
77
	private static $IndexingOff = false;
78
79
80
	/**
81
	 * @see getElasticaResult
82
	 * @var \Elastica\Result
83
	 */
84
	protected $elastica_result;
85
86
	public function __construct(ElasticaService $service) {
87
		$this->service = $service;
88
		parent::__construct();
89
	}
90
91
92
	/**
93
	 * Get the elasticsearch type name
94
	 *
95
	 * @return string
96
	 */
97
	public function getElasticaType() {
98
		return get_class($this->owner);
99
	}
100
101
102
	/**
103
	 * If the owner is part of a search result
104
	 * the raw Elastica search result is returned
105
	 * if set via setElasticaResult
106
	 *
107
	 * @return \Elastica\Result
108
	 */
109
	public function getElasticaResult() {
110
		return $this->elastica_result;
111
	}
112
113
114
	/**
115
	 * Set the raw Elastica search result
116
	 *
117
	 * @param \Elastica\Result
118
	 */
119
	public function setElasticaResult(\Elastica\Result $result) {
120
		$this->elastica_result = $result;
121
	}
122
123
124
	/**
125
	 * Gets an array of elastic field definitions.
126
	 *
127
	 * @return array
128
	 */
129
	public function getElasticaFields($storeMethodName = false, $recurse = true) {
130
		$db = $this->owner->db();
131
		$fields = $this->getAllSearchableFields();
132
		$result = array();
133
134
		foreach ($fields as $name => $params) {
135
			$spec = array();
136
			$name = str_replace('()', '', $name);
137
138
			if (array_key_exists($name, $db)) {
139
				$class = $db[$name];
140
141
				if (($pos = strpos($class, '('))) {
142
					// Valid in the case of Varchar(255)
143
					$class = substr($class, 0, $pos);
144
				}
145
146
				if (array_key_exists($class, self::$mappings)) {
147
					$spec['type'] = self::$mappings[$class];
148
					if ($spec['type'] === 'date') {
149
						if ($class == 'Date') {
150
							$spec['format'] = 'y-M-d';
151
						} elseif ($class == 'SS_Datetime') {
152
							$spec['format'] = 'y-M-d H:m:s';
153
						} elseif ($class == 'Datetime') {
154
							$spec['format'] = 'y-M-d H:m:s';
155
						} elseif ($class == 'Time') {
156
							$spec['format'] = 'H:m:s';
157
						}
158
					}
159
					if ($class === 'HTMLText' || $class === 'HTMLVarchar') {
160
						array_push($this->html_fields, $name);
161
					}
162
				}
163
				// no need for an extra case here as all SS types checked in tests
164
			} else {
165
				// field name is not in the db, it could be a method
166
				$has_lists = $this->getListRelationshipMethods();
167
				$has_ones = $this->owner->has_one();
168
169
				// check has_many and many_many relations
170
				if (isset($has_lists[$name])) {
171
					// FIX ME how to do nested mapping
172
					// See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-array-type.html
173
174
					// the classes returned by the list method
175
					$resultType = $has_lists[$name];
176
177
					$resultTypeInstance = \Injector::inst()->create($resultType);
178
					$resultTypeMapping = array();
179
180
					// get the fields for the result type, but do not recurse
181
					if ($recurse) {
182
						$resultTypeMapping = $resultTypeInstance->getElasticaFields($storeMethodName, false);
183
					}
184
185
					$resultTypeMapping['ID'] = array('type' => 'integer');
186
187
					if ($storeMethodName) {
188
						$resultTypeMapping['__method'] = $name;
189
					}
190
191
					$spec = array('properties' => $resultTypeMapping);
192
193
194
					// we now change the name to the result type, not the method name
195
					$name = $resultType;
196
				} else if (isset($has_ones[$name])) {
197
					$resultType = $has_ones[$name];
198
					$resultTypeInstance = \Injector::inst()->create($resultType);
199
200
					$resultTypeMapping = array();
201
202
					// get the fields for the result type, but do not recurse
203
					if ($recurse) {
204
						$resultTypeMapping = $resultTypeInstance->getElasticaFields($storeMethodName, false);
205
					}
206
207
					$resultTypeMapping['ID'] = array('type' => 'integer');
208
209
					if ($storeMethodName) {
210
						$resultTypeMapping['__method'] = $name;
211
					}
212
					$spec = array('properties' => $resultTypeMapping);
213
					// we now change the name to the result type, not the method name
214
					$name = $resultType;
215
				}
216
				// otherwise fall back to string - Enum is one such category
217
				else {
218
					$spec["type"] = "string";
219
				}
220
			}
221
222
			// in the case of a relationship type will not be set
223
			if (isset($spec['type'])) {
224
				if ($spec['type'] == 'string') {
225
					$unstemmed = array();
226
					$unstemmed['type'] = "string";
227
					$unstemmed['analyzer'] = "unstemmed";
228
					$unstemmed['term_vector'] = "yes";
229
					$extraFields = array('standard' => $unstemmed);
230
231
					$shingles = array();
232
					$shingles['type'] = "string";
233
					$shingles['analyzer'] = "shingles";
234
					$shingles['term_vector'] = "yes";
235
					$extraFields['shingles'] = $shingles;
236
237
					//Add autocomplete field if so required
238
					$autocomplete = \Config::inst()->get($this->owner->ClassName, 'searchable_autocomplete');
239
240
					if (isset($autocomplete) && in_array($name, $autocomplete)) {
241
						$autocompleteField = array();
242
						$autocompleteField['type'] = "string";
243
						$autocompleteField['index_analyzer'] = "autocomplete_index_analyzer";
244
						$autocompleteField['search_analyzer'] = "autocomplete_search_analyzer";
245
						$autocompleteField['term_vector'] = "yes";
246
						$extraFields['autocomplete'] = $autocompleteField;
247
					}
248
249
					$spec['fields'] = $extraFields;
250
					// FIXME - make index/locale specific, get from settings
251
					$spec['analyzer'] = 'stemmed';
252
					$spec['term_vector'] = "yes";
253
				}
254
			}
255
256
			$result[$name] = $spec;
257
		}
258
259
		if ($this->owner->hasMethod('updateElasticHTMLFields')) {
260
			$this->html_fields = $this->owner->updateElasticHTMLFields($this->html_fields);
261
		}
262
263
		return $result;
264
	}
265
266
267
	/**
268
	 * Get the elasticsearch mapping for the current document/type
269
	 *
270
	 * @return \Elastica\Type\Mapping
271
	 */
272
	public function getElasticaMapping() {
273
		$mapping = new Mapping();
274
275
		$fields = $this->getElasticaFields(false);
276
277
		$localeMapping = array();
278
279
		if ($this->owner->hasField('Locale')) {
280
			$localeMapping['type'] = 'string';
281
			// we wish the locale to be stored as is
282
			$localeMapping['index'] = 'not_analyzed';
283
			$fields['Locale'] = $localeMapping;
284
		}
285
286
		// ADD CUSTOM FIELDS HERE THAT ARE INDEXED BY DEFAULT
287
		// add a mapping to flag whether or not class is in SiteTree
288
		$fields['IsInSiteTree'] = array('type'=>'boolean');
289
		$fields['Link'] = array('type' => 'string', 'index' => 'not_analyzed');
290
291
		$mapping->setProperties($fields);
292
293
		//This concatenates all the fields together into a single field.
294
		//Initially added for suggestions compatibility, in that searching
295
		//_all field picks up all possible suggestions
296
		$mapping->enableAllField();
297
298
		if ($this->owner->hasMethod('updateElasticsearchMapping')) {
299
			$mapping = $this->owner->updateElasticsearchMapping($mapping);
300
		}
301
		return $mapping;
302
	}
303
304
305
	/**
306
	* Get an elasticsearch document
307
	*
308
	* @return \Elastica\Document
309
	*/
310
	public function getElasticaDocument() {
0 ignored issues
show
Coding Style introduced by
getElasticaDocument uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
311
		self::$index_ctr++;
312
		$fields = $this->getFieldValuesAsArray();
313
314
		if (isset($_GET['progress'])) {
315
			$progress = $_GET['progress'];
316
			self::$progressInterval = (int) $progress;
317
		}
318
319
		if (self::$progressInterval > 0) {
320
			if (self::$index_ctr % self::$progressInterval === 0) {
321
				ElasticaUtil::message("\t".$this->owner->ClassName." - Prepared ".self::$index_ctr." for indexing...");
322
			}
323
		}
324
325
		// Optionally update the document
326
		$document = new Document($this->owner->ID, $fields);
327
		if ($this->owner->hasMethod('updateElasticsearchDocument')) {
328
			$document = $this->owner->updateElasticsearchDocument($document);
329
		}
330
331
		// Check if the current classname is part of the site tree or not
332
		// Results are cached to save reprocessing the same
333
		$classname = $this->owner->ClassName;
334
		$inSiteTree = $this->isInSiteTree($classname);
335
336
		$document->set('IsInSiteTree', $inSiteTree);
337
338
		if ($inSiteTree) {
339
			$document->set('Link', $this->owner->AbsoluteLink());
340
		}
341
342
		if (isset($this->owner->Locale)) {
343
			$document->set('Locale', $this->owner->Locale);
344
		}
345
346
		return $document;
347
	}
348
349
350
	public function getFieldValuesAsArray($recurse = true) {
351
		$fields = array();
352
		$has_ones = $this->owner->has_one();
353
354
		foreach ($this->getElasticaFields($recurse) as $field => $config) {
355
			if (null === $this->owner->$field && is_callable(get_class($this->owner) . "::" . $field)) {
356
				// call a method to get a field value
357
				if (in_array($field, $this->html_fields)) {
358
					// Parse short codes in HTML, and then convert to text
359
					$fields[$field] = $this->owner->$field;
360
					$html = ShortcodeParser::get_active()->parse($this->owner->$field());
361
					$txt = \Convert::html2raw($html);
362
					$fields[$field] = $txt;
363
				} else {
364
					// Plain text
365
					$fields[$field] = $this->owner->$field();
366
				}
367
368
			} else {
369
				if (in_array($field, $this->html_fields)) {
370
					$fields[$field] = $this->owner->$field;
371
					if (gettype($this->owner->$field) !== 'NULL') {
372
						$html = ShortcodeParser::get_active()->parse($this->owner->$field);
373
						$txt = \Convert::html2raw($html);
374
						$fields[$field] = $txt;
375
					}
376
				} else {
377
					if (isset($config['properties']['__method'])) {
378
						$methodName = $config['properties']['__method'];
379
						$data = $this->owner->$methodName();
380
						$relArray = array();
381
382
						// get the fields of a has_one relational object
383
						if (isset($has_ones[$methodName])) {
384
							if ($data->ID > 0) {
385
								$item = $data->getFieldValuesAsArray(false);
386
								$relArray = $item;
387
							}
388
389
						// get the fields for a has_many or many_many relational list
390
						} else {
391
							foreach ($data->getIterator() as $item) {
392
								if ($recurse) {
393
									// populate the subitem but do not recurse any further if more relationships
394
									$itemDoc = $item->getFieldValuesAsArray(false);
395
									array_push($relArray, $itemDoc);
396
								}
397
							}
398
						}
399
						// save the relation as an array (for now)
400
						$fields[$methodName] = $relArray;
401
					} else {
402
						$fields[$field] = $this->owner->$field;
403
					}
404
405
				}
406
407
			}
408
		}
409
410
		return $fields;
411
	}
412
413
414
	/**
415
	 * Returns whether to include the document into the search index.
416
	 * All documents are added unless they have a field "ShowInSearch" which is set to false
417
	 *
418
	 * @return boolean
419
	 */
420
	public function showRecordInSearch() {
421
		return !($this->owner->hasField('ShowInSearch') && false == $this->owner->ShowInSearch);
422
	}
423
424
425
	/**
426
	 * Delete the record from the search index if ShowInSearch is deactivated (non-SiteTree).
427
	 */
428
	public function onBeforeWrite() {
429
		if (($this->owner instanceof \SiteTree)) {
430
			if ($this->owner->hasField('ShowInSearch') &&
431
				$this->owner->isChanged('ShowInSearch', 2) && false == $this->owner->ShowInSearch) {
432
				$this->doDeleteDocument();
433
			}
434
		}
435
	}
436
437
438
	/**
439
	 * Delete the record from the search index if ShowInSearch is deactivated (SiteTree).
440
	 */
441
	public function onBeforePublish() {
442
		if (false == $this->owner->ShowInSearch) {
443
			if ($this->owner->isPublished()) {
444
				$liveRecord = \Versioned::get_by_stage(get_class($this->owner), 'Live')->
445
					byID($this->owner->ID);
446
				if ($liveRecord->ShowInSearch != $this->owner->ShowInSearch) {
447
					$this->doDeleteDocument();
448
				}
449
			}
450
		}
451
	}
452
453
454
	/**
455
	 * Updates the record in the search index (non-SiteTree).
456
	 */
457
	public function onAfterWrite() {
458
		$this->doIndexDocument();
459
	}
460
461
462
	/**
463
	 * Updates the record in the search index (SiteTree).
464
	 */
465
	public function onAfterPublish() {
466
		$this->doIndexDocument();
467
	}
468
469
470
	/**
471
	 * Updates the record in the search index.
472
	 */
473
	protected function doIndexDocument() {
474
		if ($this->showRecordInSearch()) {
475
			if (!$this->owner->IndexingOff) {
476
				$this->service->index($this->owner);
477
			}
478
		}
479
	}
480
481
482
	/**
483
	 * Removes the record from the search index (non-SiteTree).
484
	 */
485
	public function onAfterDelete() {
486
		$this->doDeleteDocumentIfInSearch();
487
	}
488
489
490
	/**
491
	 * Removes the record from the search index (non-SiteTree).
492
	 */
493
	public function onAfterUnpublish() {
494
		$this->doDeleteDocumentIfInSearch();
495
	}
496
497
498
	/**
499
	 * Removes the record from the search index if the "ShowInSearch" attribute is set to true.
500
	 */
501
	protected function doDeleteDocumentIfInSearch() {
502
		if ($this->showRecordInSearch()) {
503
			$this->doDeleteDocument();
504
		}
505
	}
506
507
508
	/**
509
	 * Removes the record from the search index.
510
	 */
511
	protected function doDeleteDocument() {
512
		try{
513
			if (!$this->owner->IndexingOff) {
514
				// this goes to elastica service
515
				$this->service->remove($this->owner);
516
			}
517
		}
518
		catch(\Elastica\Exception\NotFoundException $e) {
519
			trigger_error("Deleted document ".$this->owner->ClassName." (".$this->owner->ID.
520
				") not found in search index.", E_USER_NOTICE);
521
		}
522
523
	}
524
525
526
	/**
527
	 * Return all of the searchable fields defined in $this->owner::$searchable_fields and all the parent classes.
528
	 *
529
	 * @param  $recuse Whether or not to traverse relationships. First time round yes, subsequently no
530
	 * @return array searchable fields
531
	 */
532
	public function getAllSearchableFields($recurse = true) {
533
		$fields = \Config::inst()->get(get_class($this->owner), 'searchable_fields');
534
535
		// fallback to default method
536
		if(!$fields) user_error('The field $searchable_fields must be set for the class '.$this->owner->ClassName);
537
538
		// get the values of these fields
539
		$elasticaMapping = $this->fieldsToElasticaConfig($fields);
540
541
		if ($recurse) {
542
			// now for the associated methods and their results
543
			$methodDescs = \Config::inst()->get(get_class($this->owner), 'searchable_relationships');
544
			$has_ones = $this->owner->has_one();
545
			$has_lists = $this->getListRelationshipMethods();
546
547
			if (isset($methodDescs) && is_array($methodDescs)) {
548
				foreach ($methodDescs as $methodDesc) {
549
					// split before the brackets which can optionally list which fields to index
550
					$splits = explode('(', $methodDesc);
551
					$methodName = $splits[0];
552
553
					if (isset($has_lists[$methodName])) {
554
555
						$relClass = $has_lists[$methodName];
556
						$fields = \Config::inst()->get($relClass, 'searchable_fields');
557
						if(!$fields) user_error('The field $searchable_fields must be set for the class '.$relClass);
558
						$rewrite = $this->fieldsToElasticaConfig($fields);
559
560
						// mark as a method, the resultant fields are correct
561
						$elasticaMapping[$methodName.'()'] = $rewrite;
562
					} else if (isset($has_ones[$methodName])) {
563
						$relClass = $has_ones[$methodName];
564
						$fields = \Config::inst()->get($relClass, 'searchable_fields');
565
						if(!$fields) user_error('The field $searchable_fields must be set for the class '.$relClass);
566
						$rewrite = $this->fieldsToElasticaConfig($fields);
567
568
						// mark as a method, the resultant fields are correct
569
						$elasticaMapping[$methodName.'()'] = $rewrite;
570
					} else {
571
						user_error('The method '.$methodName.' not found in class '.$this->owner->ClassName.
572
								', please check configuration');
573
					}
574
				}
575
			}
576
		}
577
578
		return $elasticaMapping;
579
	}
580
581
582
	/*
583
	Evaluate each field, e.g. 'Title', 'Member.Name'
584
	 */
585
	private function fieldsToElasticaConfig($fields) {
586
		// Copied from DataObject::searchableFields() as there is no separate accessible method
587
		$rewrite = array();
588
		foreach($fields as $name => $specOrName) {
589
			$identifer = (is_int($name)) ? $specOrName : $name;
590
			$rewrite[$identifer] = array();
591
			if(!isset($rewrite[$identifer]['title'])) {
592
				$rewrite[$identifer]['title'] = (isset($labels[$identifer]))
593
					? $labels[$identifer] : \FormField::name_to_label($identifer);
594
			}
595
			if(!isset($rewrite[$identifer]['filter'])) {
596
				$rewrite[$identifer]['filter'] = 'PartialMatchFilter';
597
			}
598
		}
599
600
		return $rewrite;
601
	}
602
603
604
	public function requireDefaultRecords() {
605
		parent::requireDefaultRecords();
606
607
		$searchableFields = $this->getElasticaFields(true,true);
608
609
610
		$doSC = \SearchableClass::get()->filter(array('Name' => $this->owner->ClassName))->first();
611
		if (!$doSC) {
612
			$doSC = new \SearchableClass();
613
			$doSC->Name = $this->owner->ClassName;
1 ignored issue
show
Documentation introduced by
The property Name does not exist on object<SearchableClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
614
615
			$inSiteTree = $this->isInSiteTree($this->owner->ClassName);
616
			$doSC->InSiteTree = $inSiteTree;
1 ignored issue
show
Documentation introduced by
The property InSiteTree does not exist on object<SearchableClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
617
618
			$doSC->write();
619
		}
620
621
		foreach ($searchableFields as $name => $searchableField) {
622
			// check for existence of methods and if they exist use that as the name
623
			if (!isset($searchableField['type'])) {
624
				$name = $searchableField['properties']['__method'];
625
			}
626
627
			$filter = array('ClazzName' => $this->owner->ClassName, 'Name' => $name);
628
			$doSF = \SearchableField::get()->filter($filter)->first();
629
630
631
			if (!$doSF) {
632
				$doSF = new \SearchableField();
633
				$doSF->ClazzName = $this->owner->ClassName;
1 ignored issue
show
Documentation introduced by
The property ClazzName does not exist on object<SearchableField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
634
				$doSF->Name = $name;
1 ignored issue
show
Documentation introduced by
The property Name does not exist on object<SearchableField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
635
636
				if (isset($searchableField['type'])) {
637
					$doSF->Type = $searchableField['type'];
1 ignored issue
show
Documentation introduced by
The property Type does not exist on object<SearchableField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
638
				}  else {
639
					$doSF->Name = $searchableField['properties']['__method'];
1 ignored issue
show
Documentation introduced by
The property Name does not exist on object<SearchableField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
640
					$doSF->Type = 'relationship';
1 ignored issue
show
Documentation introduced by
The property Type does not exist on object<SearchableField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
641
				}
642
				$doSF->SearchableClassID = $doSC->ID;
1 ignored issue
show
Documentation introduced by
The property SearchableClassID does not exist on object<SearchableField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
643
644
				if (isset($searchableField['fields']['autocomplete'])) {
645
					$doSF->Autocomplete = true;
1 ignored issue
show
Documentation introduced by
The property Autocomplete does not exist on object<SearchableField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
646
				}
647
648
				$doSF->write();
649
				\DB::alteration_message("Created new searchable editable field ".$name,"changed");
650
			}
651
652
			// FIXME deal with deletions
653
		}
654
	}
655
656
657
	private function getListRelationshipMethods() {
658
		$has_manys = $this->owner->has_many();
659
		$many_manys = $this->owner->many_many();
660
661
		// array of method name to retuned object ClassName for relationships returning lists
662
		$has_lists = $has_manys;
663
		foreach (array_keys($many_manys) as $key) {
664
			$has_lists[$key] = $many_manys[$key];
665
		}
666
667
		return $has_lists;
668
	}
669
670
671
	private function isInSiteTree($classname) {
672
		$inSiteTree = $classname === 'SiteTree' ? true : false;
673
674
		if (!$inSiteTree) {
675
			$class = new \ReflectionClass($this->owner->ClassName);
676 View Code Duplication
			while ($class = $class->getParentClass()) {
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...
677
				$parentClass = $class->getName();
678
				if ($parentClass == 'SiteTree') {
679
					$inSiteTree = true;
680
					break;
681
				}
682
			}
683
		}
684
685
		return $inSiteTree;
686
	}
687
688
689
	/*
690
	Allow the option of overriding the default template with one of <ClassName>ElasticSearchResult
691
	 */
692
	public function RenderResult($linkToContainer = '') {
693
		$vars = new \ArrayData(array('SearchResult' => $this->owner, 'ContainerLink' => $linkToContainer));
694
		$possibleTemplates = array($this->owner->ClassName.'ElasticSearchResult', 'ElasticSearchResult');
695
		return $this->owner->customise($vars)->renderWith($possibleTemplates);
696
	}
697
698
699
700
	public function getTermVectors() {
701
		return $this->service->getTermVectors($this->owner);
702
	}
703
704
705
    public function updateCMSFields(\FieldList $fields) {
706
		$isIndexed = false;
707
		// SIteTree object must have a live record, ShowInSearch = true
708
		if ($this->isInSiteTree($this->owner->ClassName)) {
709
			$liveRecord = \Versioned::get_by_stage(get_class($this->owner), 'Live')->
710
				byID($this->owner->ID);
711
			if ($liveRecord->ShowInSearch) {
712
				$isIndexed = true;
713
			} else {
714
				$isIndexed = false;
715
			}
716
		} else {
717
			// In the case of a DataObject we use the ShowInSearchFlag
718
			$isIndexed = true;
719
		}
720
721
		if ($isIndexed) {
722
			$termVectors = $this->getTermVectors();
723
			$termFields = array_keys($termVectors);
724
			sort($termFields);
725
726
			foreach ($termFields as $field) {
727
				$terms = new \ArrayList();
728
729
				foreach (array_keys($termVectors[$field]['terms']) as $term) {
730
		        	$do = new \DataObject();
731
			        $do->Term = $term;
732
			        $stats = $termVectors[$field]['terms'][$term];
733
			        if (isset($stats['ttf'])) {
734
			        	$do->TTF = $stats['ttf'];
735
			        }
736
737
			        if (isset($stats['doc_freq'])) {
738
			        	$do->DocFreq = $stats['doc_freq'];
739
			        }
740
741
			        if (isset($stats['term_freq'])) {
742
			        	$do->TermFreq = $stats['term_freq'];
743
			        }
744
			        $terms->push($do);
745
		        }
746
747
		        $config = \GridFieldConfig_RecordViewer::create(100);
748
				$config->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
1 ignored issue
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setDisplayFields() does only exist in the following implementations of said interface: GridFieldDataColumns.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
749
		            'Term' => 'Term',
750
		            'TTF' => 'Total term frequency (how often a term occurs in all documents)',
751
		            'DocFreq' => 'n documents with this term',
752
		            'TermFreq'=> 'n times this term appears in this field'
753
		        ));
754
755
		       $underscored = str_replace('.', '_', $field);
756
757
		        $gridField = new \GridField(
758
		            'TermsFor'.$underscored, // Field name
759
		            $field.'TITLE'.$field, // Field title
760
		            $terms,
761
		            $config
762
		        );
763
764
		      // $tab = new \Tab($field, new \TextField('Test'.$field, 'Testing'));
0 ignored issues
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
765
		       $fields->addFieldToTab('Root.ElasticaTerms.'.$underscored, $gridField);
766
			}
767
768
		}
769
770
	    return $fields;
771
	}
772
773
774
}
775