Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

DataObject::getManyManyComponents()   D

Complexity

Conditions 9
Paths 12

Size

Total Lines 40
Code Lines 22

Duplication

Lines 12
Ratio 30 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 9
eloc 22
nc 12
nop 5
dl 12
loc 40
rs 4.909
c 1
b 1
f 0
1
<?php
2
/**
3
 * A single database record & abstract class for the data-access-model.
4
 *
5
 * <h2>Extensions</h2>
6
 *
7
 * See {@link Extension} and {@link DataExtension}.
8
 *
9
 * <h2>Permission Control</h2>
10
 *
11
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
12
 * strings which can be selected on a group-by-group basis.
13
 *
14
 * <code>
15
 * class Article extends DataObject implements PermissionProvider {
16
 *  static $api_access = true;
17
 *
18
 *  function canView($member = false) {
19
 *    return Permission::check('ARTICLE_VIEW');
20
 *  }
21
 *  function canEdit($member = false) {
22
 *    return Permission::check('ARTICLE_EDIT');
23
 *  }
24
 *  function canDelete() {
25
 *    return Permission::check('ARTICLE_DELETE');
26
 *  }
27
 *  function canCreate() {
28
 *    return Permission::check('ARTICLE_CREATE');
29
 *  }
30
 *  function providePermissions() {
31
 *    return array(
32
 *      'ARTICLE_VIEW' => 'Read an article object',
33
 *      'ARTICLE_EDIT' => 'Edit an article object',
34
 *      'ARTICLE_DELETE' => 'Delete an article object',
35
 *      'ARTICLE_CREATE' => 'Create an article object',
36
 *    );
37
 *  }
38
 * }
39
 * </code>
40
 *
41
 * Object-level access control by {@link Group} membership:
42
 * <code>
43
 * class Article extends DataObject {
44
 *   static $api_access = true;
45
 *
46
 *   function canView($member = false) {
47
 *     if(!$member) $member = Member::currentUser();
48
 *     return $member->inGroup('Subscribers');
49
 *   }
50
 *   function canEdit($member = false) {
51
 *     if(!$member) $member = Member::currentUser();
52
 *     return $member->inGroup('Editors');
53
 *   }
54
 *
55
 *   // ...
56
 * }
57
 * </code>
58
 *
59
 * If any public method on this class is prefixed with an underscore,
60
 * the results are cached in memory through {@link cachedCall()}.
61
 *
62
 *
63
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
64
 *  and defineMethods()
65
 *
66
 * @package framework
67
 * @subpackage model
68
 *
69
 * @property integer ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
70
 * @property string ClassName Class name of the DataObject
71
 * @property string LastEdited Date and time of DataObject's last modification.
72
 * @property string Created Date and time of DataObject creation.
73
 */
74
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider {
75
76
	/**
77
	 * Human-readable singular name.
78
	 * @var string
79
	 * @config
80
	 */
81
	private static $singular_name = null;
82
83
	/**
84
	 * Human-readable plural name
85
	 * @var string
86
	 * @config
87
	 */
88
	private static $plural_name = null;
89
90
	/**
91
	 * Allow API access to this object?
92
	 * @todo Define the options that can be set here
93
	 * @config
94
	 */
95
	private static $api_access = false;
96
97
	/**
98
	 * True if this DataObject has been destroyed.
99
	 * @var boolean
100
	 */
101
	public $destroyed = false;
102
103
	/**
104
	 * The DataModel from this this object comes
105
	 */
106
	protected $model;
107
108
	/**
109
	 * Data stored in this objects database record. An array indexed by fieldname.
110
	 *
111
	 * Use {@link toMap()} if you want an array representation
112
	 * of this object, as the $record array might contain lazy loaded field aliases.
113
	 *
114
	 * @var array
115
	 */
116
	protected $record;
117
118
	/**
119
	 * Represents a field that hasn't changed (before === after, thus before == after)
120
	 */
121
	const CHANGE_NONE = 0;
122
123
	/**
124
	 * Represents a field that has changed type, although not the loosely defined value.
125
	 * (before !== after && before == after)
126
	 * E.g. change 1 to true or "true" to true, but not true to 0.
127
	 * Value changes are by nature also considered strict changes.
128
	 */
129
	const CHANGE_STRICT = 1;
130
131
	/**
132
	 * Represents a field that has changed the loosely defined value
133
	 * (before != after, thus, before !== after))
134
	 * E.g. change false to true, but not false to 0
135
	 */
136
	const CHANGE_VALUE = 2;
137
138
	/**
139
	 * An array indexed by fieldname, true if the field has been changed.
140
	 * Use {@link getChangedFields()} and {@link isChanged()} to inspect
141
	 * the changed state.
142
	 *
143
	 * @var array
144
	 */
145
	private $changed;
146
147
	/**
148
	 * The database record (in the same format as $record), before
149
	 * any changes.
150
	 * @var array
151
	 */
152
	protected $original;
153
154
	/**
155
	 * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
156
	 * @var boolean
157
	 */
158
	protected $brokenOnDelete = false;
159
160
	/**
161
	 * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
162
	 * @var boolean
163
	 */
164
	protected $brokenOnWrite = false;
165
166
	/**
167
	 * @config
168
	 * @var boolean Should dataobjects be validated before they are written?
169
	 * Caution: Validation can contain safeguards against invalid/malicious data,
170
	 * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
171
	 * to only disable validation for very specific use cases.
172
	 */
173
	private static $validation_enabled = true;
174
175
	/**
176
	 * Static caches used by relevant functions.
177
	 */
178
	public static $cache_has_own_table = array();
179
	protected static $_cache_db = array();
180
	protected static $_cache_get_one;
181
	protected static $_cache_get_class_ancestry;
182
	protected static $_cache_composite_fields = array();
183
	protected static $_cache_is_composite_field = array();
184
	protected static $_cache_custom_database_fields = array();
185
	protected static $_cache_field_labels = array();
186
187
	// base fields which are not defined in static $db
188
	private static $fixed_fields = array(
189
		'ID' => 'Int',
190
		'ClassName' => 'Enum',
191
		'LastEdited' => 'SS_Datetime',
192
		'Created' => 'SS_Datetime',
193
	);
194
195
	/**
196
	 * Non-static relationship cache, indexed by component name.
197
	 */
198
	protected $components;
199
200
	/**
201
	 * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
202
	 */
203
	protected $unsavedRelations;
204
205
	/**
206
	 * Returns when validation on DataObjects is enabled.
207
	 *
208
	 * @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
209
	 * @return bool
210
	 */
211
	public static function get_validation_enabled() {
212
		Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead');
213
		return Config::inst()->get('DataObject', 'validation_enabled');
214
	}
215
216
	/**
217
	 * Set whether DataObjects should be validated before they are written.
218
	 *
219
	 * Caution: Validation can contain safeguards against invalid/malicious data,
220
	 * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
221
	 * to only disable validation for very specific use cases.
222
	 *
223
	 * @param $enable bool
224
	 * @see DataObject::validate()
225
	 * @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
226
	 */
227
	public static function set_validation_enabled($enable) {
228
		Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead');
229
		Config::inst()->update('DataObject', 'validation_enabled', (bool)$enable);
230
	}
231
232
	/**
233
	 * @var [string] - class => ClassName field definition cache for self::database_fields
234
	 */
235
	private static $classname_spec_cache = array();
236
237
	/**
238
	 * Clear all cached classname specs. It's necessary to clear all cached subclassed names
239
	 * for any classes if a new class manifest is generated.
240
	 */
241
	public static function clear_classname_spec_cache() {
242
		self::$classname_spec_cache = array();
243
		PolymorphicForeignKey::clear_classname_spec_cache();
244
	}
245
246
	/**
247
	 * Determines the specification for the ClassName field for the given class
248
	 *
249
	 * @param string $class
250
	 * @param boolean $queryDB Determine if the DB may be queried for additional information
251
	 * @return string Resulting ClassName spec. If $queryDB is true this will include all
252
	 * legacy types that no longer have concrete classes in PHP
253
	 */
254
	public static function get_classname_spec($class, $queryDB = true) {
255
		// Check cache
256
		if(!empty(self::$classname_spec_cache[$class])) return self::$classname_spec_cache[$class];
257
258
		// Build known class names
259
		$classNames = ClassInfo::subclassesFor($class);
260
261
		// Enhance with existing classes in order to prevent legacy details being lost
262
		if($queryDB && DB::get_schema()->hasField($class, 'ClassName')) {
263
			$existing = DB::query("SELECT DISTINCT \"ClassName\" FROM \"{$class}\"")->column();
264
			$classNames = array_unique(array_merge($classNames, $existing));
265
		}
266
		$spec = "Enum('" . implode(', ', $classNames) . "')";
267
268
		// Only cache full information if queried
269
		if($queryDB) self::$classname_spec_cache[$class] = $spec;
270
		return $spec;
271
	}
272
273
	/**
274
	 * Return the complete map of fields on this object, including "Created", "LastEdited" and "ClassName".
275
	 * See {@link custom_database_fields()} for a getter that excludes these "base fields".
276
	 *
277
	 * @param string $class
278
	 * @param boolean $queryDB Determine if the DB may be queried for additional information
279
	 * @return array
280
	 */
281
	public static function database_fields($class, $queryDB = true) {
282
		if(get_parent_class($class) == 'DataObject') {
283
			// Merge fixed with ClassName spec and custom db fields
284
			$fixed = self::$fixed_fields;
285
			unset($fixed['ID']);
286
			return array_merge(
287
				$fixed,
288
				array('ClassName' => self::get_classname_spec($class, $queryDB)),
289
				self::custom_database_fields($class)
290
			);
291
		}
292
293
		return self::custom_database_fields($class);
294
	}
295
296
	/**
297
	 * Get all database columns explicitly defined on a class in {@link DataObject::$db}
298
	 * and {@link DataObject::$has_one}. Resolves instances of {@link CompositeDBField}
299
	 * into the actual database fields, rather than the name of the field which
300
	 * might not equate a database column.
301
	 *
302
	 * Does not include "base fields" like "ID", "ClassName", "Created", "LastEdited",
303
	 * see {@link database_fields()}.
304
	 *
305
	 * @uses CompositeDBField->compositeDatabaseFields()
306
	 *
307
	 * @param string $class
308
	 * @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
309
	 */
310
	public static function custom_database_fields($class) {
311
		if(isset(self::$_cache_custom_database_fields[$class])) {
312
			return self::$_cache_custom_database_fields[$class];
313
		}
314
315
		$fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
316
317
		foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) {
318
			// Remove the original fieldname, it's not an actual database column
319
			unset($fields[$fieldName]);
320
321
			// Add all composite columns
322
			$compositeFields = singleton($fieldClass)->compositeDatabaseFields();
323
			if($compositeFields) foreach($compositeFields as $compositeName => $spec) {
324
				$fields["{$fieldName}{$compositeName}"] = $spec;
325
			}
326
		}
327
328
		// Add has_one relationships
329
		$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
330
		if($hasOne) foreach(array_keys($hasOne) as $field) {
331
332
			// Check if this is a polymorphic relation, in which case the relation
333
			// is a composite field
334
			if($hasOne[$field] === 'DataObject') {
335
				$relationField = DBField::create_field('PolymorphicForeignKey', null, $field);
336
				$relationField->setTable($class);
337
				if($compositeFields = $relationField->compositeDatabaseFields()) {
338
					foreach($compositeFields as $compositeName => $spec) {
339
						$fields["{$field}{$compositeName}"] = $spec;
340
					}
341
				}
342
			} else {
343
				$fields[$field . 'ID'] = 'ForeignKey';
344
			}
345
		}
346
347
		$output = (array) $fields;
348
349
		self::$_cache_custom_database_fields[$class] = $output;
350
351
		return $output;
352
	}
353
354
	/**
355
	 * Returns the field class if the given db field on the class is a composite field.
356
	 * Will check all applicable ancestor classes and aggregate results.
357
	 *
358
	 * @param string $class Class to check
359
	 * @param string $name Field to check
360
	 * @param boolean $aggregated True if parent classes should be checked, or false to limit to this class
361
	 * @return string Class name of composite field if it exists
362
	 */
363
	public static function is_composite_field($class, $name, $aggregated = true) {
364
		$key = $class . '_' . $name . '_' . (string)$aggregated;
365
366
		if(!isset(DataObject::$_cache_is_composite_field[$key])) {
367
			$isComposite = null;
368
369
			if(!isset(DataObject::$_cache_composite_fields[$class])) {
370
				self::cache_composite_fields($class);
371
			}
372
373
			if(isset(DataObject::$_cache_composite_fields[$class][$name])) {
374
				$isComposite = DataObject::$_cache_composite_fields[$class][$name];
375
			} elseif($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
376
				$isComposite = self::is_composite_field($parentClass, $name);
377
			}
378
379
			DataObject::$_cache_is_composite_field[$key] = ($isComposite) ? $isComposite : false;
380
		}
381
382
		return DataObject::$_cache_is_composite_field[$key] ?: null;
383
	}
384
385
	/**
386
	 * Returns a list of all the composite if the given db field on the class is a composite field.
387
	 * Will check all applicable ancestor classes and aggregate results.
388
	 */
389
	public static function composite_fields($class, $aggregated = true) {
390
		if(!isset(DataObject::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
391
392
		$compositeFields = DataObject::$_cache_composite_fields[$class];
393
394
		if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
395
			$compositeFields = array_merge($compositeFields,
396
				self::composite_fields($parentClass));
397
		}
398
399
		return $compositeFields;
400
	}
401
402
	/**
403
	 * Internal cacher for the composite field information
404
	 */
405
	private static function cache_composite_fields($class) {
406
		$compositeFields = array();
407
408
		$fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
409
		if($fields) foreach($fields as $fieldName => $fieldClass) {
0 ignored issues
show
Bug introduced by
The expression $fields of type array|integer|double|string|boolean 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...
410
			if(!is_string($fieldClass)) continue;
411
412
			// Strip off any parameters
413
			$bPos = strpos($fieldClass, '(');
414
			if($bPos !== FALSE) $fieldClass = substr($fieldClass, 0, $bPos);
415
416
			// Test to see if it implements CompositeDBField
417
			if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
418
				$compositeFields[$fieldName] = $fieldClass;
419
			}
420
		}
421
422
		DataObject::$_cache_composite_fields[$class] = $compositeFields;
423
	}
424
425
	/**
426
	 * Construct a new DataObject.
427
	 *
428
	 * @param array|null $record This will be null for a new database record.  Alternatively, you can pass an array of
429
	 * field values.  Normally this contructor is only used by the internal systems that get objects from the database.
430
	 * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
431
	 *                             Singletons don't have their defaults set.
432
	 */
433
	public function __construct($record = null, $isSingleton = false, $model = null) {
434
		parent::__construct();
435
436
		// Set the fields data.
437
		if(!$record) {
438
			$record = array(
439
				'ID' => 0,
440
				'ClassName' => get_class($this),
441
				'RecordClassName' => get_class($this)
442
			);
443
		}
444
445
		if(!is_array($record) && !is_a($record, "stdClass")) {
446
			if(is_object($record)) $passed = "an object of type '$record->class'";
447
			else $passed = "The value '$record'";
448
449
			user_error("DataObject::__construct passed $passed.  It's supposed to be passed an array,"
450
				. " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
451
				E_USER_WARNING);
452
			$record = null;
453
		}
454
455
		if(is_a($record, "stdClass")) {
456
			$record = (array)$record;
457
		}
458
459
		// Set $this->record to $record, but ignore NULLs
460
		$this->record = array();
461
		foreach($record as $k => $v) {
0 ignored issues
show
Bug introduced by
The expression $record of type null|array 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...
462
			// Ensure that ID is stored as a number and not a string
463
			// To do: this kind of clean-up should be done on all numeric fields, in some relatively
464
			// performant manner
465
			if($v !== null) {
466
				if($k == 'ID' && is_numeric($v)) $this->record[$k] = (int)$v;
467
				else $this->record[$k] = $v;
468
			}
469
		}
470
471
		// Identify fields that should be lazy loaded, but only on existing records
472
		if(!empty($record['ID'])) {
473
			$currentObj = get_class($this);
474
			while($currentObj != 'DataObject') {
475
				$fields = self::custom_database_fields($currentObj);
476
				foreach($fields as $field => $type) {
477
					if(!array_key_exists($field, $record)) $this->record[$field.'_Lazy'] = $currentObj;
478
				}
479
				$currentObj = get_parent_class($currentObj);
480
			}
481
		}
482
483
		$this->original = $this->record;
484
485
		// Keep track of the modification date of all the data sourced to make this page
486
		// From this we create a Last-Modified HTTP header
487
		if(isset($record['LastEdited'])) {
488
			HTTP::register_modification_date($record['LastEdited']);
489
		}
490
491
		// this must be called before populateDefaults(), as field getters on a DataObject
492
		// may call getComponent() and others, which rely on $this->model being set.
493
		$this->model = $model ? $model : DataModel::inst();
494
495
		// Must be called after parent constructor
496
		if(!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
497
			$this->populateDefaults();
498
		}
499
500
		// prevent populateDefaults() and setField() from marking overwritten defaults as changed
501
		$this->changed = array();
502
	}
503
504
	/**
505
	 * Set the DataModel
506
	 * @param DataModel $model
507
	 * @return DataObject $this
508
	 */
509
	public function setDataModel(DataModel $model) {
510
		$this->model = $model;
511
		return $this;
512
	}
513
514
	/**
515
	 * Destroy all of this objects dependant objects and local caches.
516
	 * You'll need to call this to get the memory of an object that has components or extensions freed.
517
	 */
518
	public function destroy() {
519
		//$this->destroyed = true;
520
		gc_collect_cycles();
521
		$this->flushCache(false);
522
	}
523
524
	/**
525
	 * Create a duplicate of this node.
526
	 * Note: now also duplicates relations.
527
	 *
528
	 * @param $doWrite Perform a write() operation before returning the object.  If this is true, it will create the
529
	 *                 duplicate in the database.
530
	 * @return DataObject A duplicate of this node. The exact type will be the type of this node.
531
	 */
532
	public function duplicate($doWrite = true) {
533
		$className = $this->class;
534
		$clone = new $className( $this->toMap(), false, $this->model );
535
		$clone->ID = 0;
536
537
		$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
538
		if($doWrite) {
539
			$clone->write();
540
			$this->duplicateManyManyRelations($this, $clone);
541
		}
542
		$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
543
544
		return $clone;
545
	}
546
547
	/**
548
	 * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object
549
	 * The destinationObject must be written to the database already and have an ID. Writing is performed
550
	 * automatically when adding the new relations.
551
	 *
552
	 * @param $sourceObject the source object to duplicate from
553
	 * @param $destinationObject the destination object to populate with the duplicated relations
554
	 * @return DataObject with the new many_many relations copied in
555
	 */
556
	protected function duplicateManyManyRelations($sourceObject, $destinationObject) {
557
		if (!$destinationObject || $destinationObject->ID < 1) {
558
			user_error("Can't duplicate relations for an object that has not been written to the database",
559
				E_USER_ERROR);
560
		}
561
562
		//duplicate complex relations
563
		// DO NOT copy has_many relations, because copying the relation would result in us changing the has_one
564
		// relation on the other side of this relation to point at the copy and no longer the original (being a
565
		// has_one, it can only point at one thing at a time). So, all relations except has_many can and are copied
566
		if ($sourceObject->hasOne()) foreach($sourceObject->hasOne() as $name => $type) {
567
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
568
		}
569
		if ($sourceObject->manyMany()) foreach($sourceObject->manyMany() as $name => $type) {
570
			//many_many include belongs_many_many
571
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
572
		}
573
574
		return $destinationObject;
575
	}
576
577
	/**
578
	 * Helper function to duplicate relations from one object to another
579
	 * @param $sourceObject the source object to duplicate from
580
	 * @param $destinationObject the destination object to populate with the duplicated relations
581
	 * @param $name the name of the relation to duplicate (e.g. members)
582
	 */
583
	private function duplicateRelations($sourceObject, $destinationObject, $name) {
584
		$relations = $sourceObject->$name();
585
		if ($relations) {
586
			if ($relations instanceOf RelationList) {   //many-to-something relation
587
				if ($relations->Count() > 0) {  //with more than one thing it is related to
588
					foreach($relations as $relation) {
589
						$destinationObject->$name()->add($relation);
590
					}
591
				}
592
			} else {    //one-to-one relation
593
				$destinationObject->{"{$name}ID"} = $relations->ID;
594
			}
595
		}
596
	}
597
598
	public function getObsoleteClassName() {
599
		$className = $this->getField("ClassName");
600
		if (!ClassInfo::exists($className)) return $className;
601
	}
602
603
	public function getClassName() {
604
		$className = $this->getField("ClassName");
605
		if (!ClassInfo::exists($className)) return get_class($this);
606
		return $className;
607
	}
608
609
	/**
610
	 * Set the ClassName attribute. {@link $class} is also updated.
611
	 * Warning: This will produce an inconsistent record, as the object
612
	 * instance will not automatically switch to the new subclass.
613
	 * Please use {@link newClassInstance()} for this purpose,
614
	 * or destroy and reinstanciate the record.
615
	 *
616
	 * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
617
	 * @return DataObject $this
618
	 */
619
	public function setClassName($className) {
620
		$className = trim($className);
621
		if(!$className || !is_subclass_of($className, 'DataObject')) return;
622
623
		$this->class = $className;
624
		$this->setField("ClassName", $className);
625
		return $this;
626
	}
627
628
	/**
629
	 * Create a new instance of a different class from this object's record.
630
	 * This is useful when dynamically changing the type of an instance. Specifically,
631
	 * it ensures that the instance of the class is a match for the className of the
632
	 * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
633
	 * property manually before calling this method, as it will confuse change detection.
634
	 *
635
	 * If the new class is different to the original class, defaults are populated again
636
	 * because this will only occur automatically on instantiation of a DataObject if
637
	 * there is no record, or the record has no ID. In this case, we do have an ID but
638
	 * we still need to repopulate the defaults.
639
	 *
640
	 * @param string $newClassName The name of the new class
641
	 *
642
	 * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
643
	 */
644
	public function newClassInstance($newClassName) {
645
		$originalClass = $this->ClassName;
646
		$newInstance = new $newClassName(array_merge(
647
			$this->record,
648
			array(
649
				'ClassName' => $originalClass,
650
				'RecordClassName' => $originalClass,
651
			)
652
		), false, $this->model);
653
654
		if($newClassName != $originalClass) {
655
			$newInstance->setClassName($newClassName);
656
			$newInstance->populateDefaults();
657
			$newInstance->forceChange();
658
		}
659
660
		return $newInstance;
661
	}
662
663
	/**
664
	 * Adds methods from the extensions.
665
	 * Called by Object::__construct() once per class.
666
	 */
667
	public function defineMethods() {
668
		parent::defineMethods();
669
670
		// Define the extra db fields - this is only necessary for extensions added in the
671
		// class definition.  Object::add_extension() will call this at definition time for
672
		// those objects, which is a better mechanism.  Perhaps extensions defined inside the
673
		// class def can somehow be applied at definiton time also?
674
		if($this->extension_instances) foreach($this->extension_instances as $i => $instance) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extension_instances of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
675
			if(!$instance->class) {
676
				$class = get_class($instance);
677
				user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls"
678
					. " parent::__construct()", E_USER_ERROR);
679
			}
680
		}
681
682
		if($this->class == 'DataObject') return;
683
684
		// Set up accessors for joined items
685
		if($manyMany = $this->manyMany()) {
686
			foreach($manyMany as $relationship => $class) {
687
				$this->addWrapperMethod($relationship, 'getManyManyComponents');
688
			}
689
		}
690
		if($hasMany = $this->hasMany()) {
691
692
			foreach($hasMany as $relationship => $class) {
0 ignored issues
show
Bug introduced by
The expression $hasMany of type string|array 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...
693
				$this->addWrapperMethod($relationship, 'getComponents');
694
			}
695
696
		}
697
		if($hasOne = $this->hasOne()) {
698
			foreach($hasOne as $relationship => $class) {
0 ignored issues
show
Bug introduced by
The expression $hasOne of type string|array 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...
699
				$this->addWrapperMethod($relationship, 'getComponent');
700
			}
701
		}
702
		if($belongsTo = $this->belongsTo()) foreach(array_keys($belongsTo) as $relationship) {
703
			$this->addWrapperMethod($relationship, 'getComponent');
704
		}
705
	}
706
707
	/**
708
	 * Returns true if this object "exists", i.e., has a sensible value.
709
	 * The default behaviour for a DataObject is to return true if
710
	 * the object exists in the database, you can override this in subclasses.
711
	 *
712
	 * @return boolean true if this object exists
713
	 */
714
	public function exists() {
715
		return (isset($this->record['ID']) && $this->record['ID'] > 0);
716
	}
717
718
	/**
719
	 * Returns TRUE if all values (other than "ID") are
720
	 * considered empty (by weak boolean comparison).
721
	 * Only checks for fields listed in {@link custom_database_fields()}
722
	 *
723
	 * @todo Use DBField->hasValue()
724
	 *
725
	 * @return boolean
726
	 */
727
	public function isEmpty(){
728
		$isEmpty = true;
729
		$customFields = self::custom_database_fields(get_class($this));
730
		if($map = $this->toMap()){
731
			foreach($map as $k=>$v){
732
				// only look at custom fields
733
				if(!array_key_exists($k, $customFields)) continue;
734
735
				$dbObj = ($v instanceof DBField) ? $v : $this->dbObject($k);
736
				$isEmpty = ($isEmpty && !$dbObj->exists());
737
			}
738
		}
739
		return $isEmpty;
740
	}
741
742
	/**
743
	 * Get the user friendly singular name of this DataObject.
744
	 * If the name is not defined (by redefining $singular_name in the subclass),
745
	 * this returns the class name.
746
	 *
747
	 * @return string User friendly singular name of this DataObject
748
	 */
749
	public function singular_name() {
750
		if(!$name = $this->stat('singular_name')) {
751
			$name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $this->class))));
752
		}
753
754
		return $name;
755
	}
756
757
	/**
758
	 * Get the translated user friendly singular name of this DataObject
759
	 * same as singular_name() but runs it through the translating function
760
	 *
761
	 * Translating string is in the form:
762
	 *     $this->class.SINGULARNAME
763
	 * Example:
764
	 *     Page.SINGULARNAME
765
	 *
766
	 * @return string User friendly translated singular name of this DataObject
767
	 */
768
	public function i18n_singular_name() {
769
		return _t($this->class.'.SINGULARNAME', $this->singular_name());
770
	}
771
772
	/**
773
	 * Get the user friendly plural name of this DataObject
774
	 * If the name is not defined (by renaming $plural_name in the subclass),
775
	 * this returns a pluralised version of the class name.
776
	 *
777
	 * @return string User friendly plural name of this DataObject
778
	 */
779
	public function plural_name() {
780
		if($name = $this->stat('plural_name')) {
781
			return $name;
782
		} else {
783
			$name = $this->singular_name();
784
			//if the penultimate character is not a vowel, replace "y" with "ies"
785
			if (preg_match('/[^aeiou]y$/i', $name)) {
786
				$name = substr($name,0,-1) . 'ie';
787
			}
788
			return ucfirst($name . 's');
789
		}
790
	}
791
792
	/**
793
	 * Get the translated user friendly plural name of this DataObject
794
	 * Same as plural_name but runs it through the translation function
795
	 * Translation string is in the form:
796
	 *      $this->class.PLURALNAME
797
	 * Example:
798
	 *      Page.PLURALNAME
799
	 *
800
	 * @return string User friendly translated plural name of this DataObject
801
	 */
802
	public function i18n_plural_name()
803
	{
804
		$name = $this->plural_name();
805
		return _t($this->class.'.PLURALNAME', $name);
806
	}
807
808
	/**
809
	 * Standard implementation of a title/label for a specific
810
	 * record. Tries to find properties 'Title' or 'Name',
811
	 * and falls back to the 'ID'. Useful to provide
812
	 * user-friendly identification of a record, e.g. in errormessages
813
	 * or UI-selections.
814
	 *
815
	 * Overload this method to have a more specialized implementation,
816
	 * e.g. for an Address record this could be:
817
	 * <code>
818
	 * function getTitle() {
819
	 *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
820
	 * }
821
	 * </code>
822
	 *
823
	 * @return string
824
	 */
825
	public function getTitle() {
826
		if($this->hasDatabaseField('Title')) return $this->getField('Title');
827
		if($this->hasDatabaseField('Name')) return $this->getField('Name');
828
829
		return "#{$this->ID}";
830
	}
831
832
	/**
833
	 * Returns the associated database record - in this case, the object itself.
834
	 * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
835
	 *
836
	 * @return DataObject Associated database record
837
	 */
838
	public function data() {
839
		return $this;
840
	}
841
842
	/**
843
	 * Convert this object to a map.
844
	 *
845
	 * @return array The data as a map.
846
	 */
847
	public function toMap() {
848
		$this->loadLazyFields();
849
		return $this->record;
850
	}
851
852
	/**
853
	 * Return all currently fetched database fields.
854
	 *
855
	 * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
856
	 * Obviously, this makes it a lot faster.
857
	 *
858
	 * @return array The data as a map.
859
	 */
860
	public function getQueriedDatabaseFields() {
861
		return $this->record;
862
	}
863
864
	/**
865
	 * Update a number of fields on this object, given a map of the desired changes.
866
	 *
867
	 * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
868
	 * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
869
	 *
870
	 * update() doesn't write the main object, but if you use the dot syntax, it will write()
871
	 * the related objects that it alters.
872
	 *
873
	 * @param array $data A map of field name to data values to update.
874
	 * @return DataObject $this
875
	 */
876
	public function update($data) {
877
		foreach($data as $k => $v) {
878
			// Implement dot syntax for updates
879
			if(strpos($k,'.') !== false) {
880
				$relations = explode('.', $k);
881
				$fieldName = array_pop($relations);
882
				$relObj = $this;
883
				foreach($relations as $i=>$relation) {
884
					// no support for has_many or many_many relationships,
885
					// as the updater wouldn't know which object to write to (or create)
886
					if($relObj->$relation() instanceof DataObject) {
887
						$parentObj = $relObj;
888
						$relObj = $relObj->$relation();
889
						// If the intermediate relationship objects have been created, then write them
890
						if($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
891
							$relObj->write();
892
							$relatedFieldName = $relation."ID";
893
							$parentObj->$relatedFieldName = $relObj->ID;
894
							$parentObj->write();
895
						}
896
					} else {
897
						user_error(
898
							"DataObject::update(): Can't traverse relationship '$relation'," .
899
							"it has to be a has_one relationship or return a single DataObject",
900
							E_USER_NOTICE
901
						);
902
						// unset relation object so we don't write properties to the wrong object
903
						unset($relObj);
904
						break;
905
					}
906
				}
907
908
				if($relObj) {
909
					$relObj->$fieldName = $v;
910
					$relObj->write();
911
					$relatedFieldName = $relation."ID";
0 ignored issues
show
Bug introduced by
The variable $relation seems to be defined by a foreach iteration on line 883. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
912
					$this->$relatedFieldName = $relObj->ID;
913
					$relObj->flushCache();
914
				} else {
915
					user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
916
				}
917
			} else {
918
				$this->$k = $v;
919
			}
920
		}
921
		return $this;
922
	}
923
924
	/**
925
	 * Pass changes as a map, and try to
926
	 * get automatic casting for these fields.
927
	 * Doesn't write to the database. To write the data,
928
	 * use the write() method.
929
	 *
930
	 * @param array $data A map of field name to data values to update.
931
	 * @return DataObject $this
932
	 */
933
	public function castedUpdate($data) {
934
		foreach($data as $k => $v) {
935
			$this->setCastedField($k,$v);
936
		}
937
		return $this;
938
	}
939
940
	/**
941
	 * Merges data and relations from another object of same class,
942
	 * without conflict resolution. Allows to specify which
943
	 * dataset takes priority in case its not empty.
944
	 * has_one-relations are just transferred with priority 'right'.
945
	 * has_many and many_many-relations are added regardless of priority.
946
	 *
947
	 * Caution: has_many/many_many relations are moved rather than duplicated,
948
	 * meaning they are not connected to the merged object any longer.
949
	 * Caution: Just saves updated has_many/many_many relations to the database,
950
	 * doesn't write the updated object itself (just writes the object-properties).
951
	 * Caution: Does not delete the merged object.
952
	 * Caution: Does now overwrite Created date on the original object.
953
	 *
954
	 * @param $obj DataObject
955
	 * @param $priority String left|right Determines who wins in case of a conflict (optional)
956
	 * @param $includeRelations Boolean Merge any existing relations (optional)
957
	 * @param $overwriteWithEmpty Boolean Overwrite existing left values with empty right values.
958
	 *                            Only applicable with $priority='right'. (optional)
959
	 * @return Boolean
960
	 */
961
	public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) {
962
		$leftObj = $this;
963
964
		if($leftObj->ClassName != $rightObj->ClassName) {
965
			// we can't merge similiar subclasses because they might have additional relations
966
			user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
967
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
968
			return false;
969
		}
970
971
		if(!$rightObj->ID) {
972
			user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
973
				to make sure all relations are transferred properly.').", E_USER_WARNING);
974
			return false;
975
		}
976
977
		// makes sure we don't merge data like ID or ClassName
978
		$leftData = $leftObj->inheritedDatabaseFields();
0 ignored issues
show
Unused Code introduced by
$leftData is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
979
		$rightData = $rightObj->inheritedDatabaseFields();
980
981
		foreach($rightData as $key=>$rightVal) {
982
			// don't merge conflicting values if priority is 'left'
983
			if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) continue;
984
985
			// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
986
			if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) continue;
987
988
			// TODO remove redundant merge of has_one fields
989
			$leftObj->{$key} = $rightObj->{$key};
990
		}
991
992
		// merge relations
993
		if($includeRelations) {
994 View Code Duplication
			if($manyMany = $this->manyMany()) {
0 ignored issues
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...
995
				foreach($manyMany as $relationship => $class) {
996
					$leftComponents = $leftObj->getManyManyComponents($relationship);
997
					$rightComponents = $rightObj->getManyManyComponents($relationship);
998
					if($rightComponents && $rightComponents->exists()) {
999
						$leftComponents->addMany($rightComponents->column('ID'));
1000
					}
1001
					$leftComponents->write();
1002
				}
1003
			}
1004
1005 View Code Duplication
			if($hasMany = $this->hasMany()) {
0 ignored issues
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...
1006
				foreach($hasMany as $relationship => $class) {
0 ignored issues
show
Bug introduced by
The expression $hasMany of type string|array 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...
1007
					$leftComponents = $leftObj->getComponents($relationship);
1008
					$rightComponents = $rightObj->getComponents($relationship);
1009
					if($rightComponents && $rightComponents->exists()) {
1010
						$leftComponents->addMany($rightComponents->column('ID'));
1011
					}
1012
					$leftComponents->write();
1013
				}
1014
1015
			}
1016
1017
			if($hasOne = $this->hasOne()) {
1018
				foreach($hasOne as $relationship => $class) {
0 ignored issues
show
Bug introduced by
The expression $hasOne of type string|array 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...
1019
					$leftComponent = $leftObj->getComponent($relationship);
1020
					$rightComponent = $rightObj->getComponent($relationship);
1021
					if($leftComponent->exists() && $rightComponent->exists() && $priority == 'right') {
1022
						$leftObj->{$relationship . 'ID'} = $rightObj->{$relationship . 'ID'};
1023
					}
1024
				}
1025
			}
1026
		}
1027
1028
		return true;
1029
	}
1030
1031
	/**
1032
	 * Forces the record to think that all its data has changed.
1033
	 * Doesn't write to the database. Only sets fields as changed
1034
	 * if they are not already marked as changed.
1035
	 *
1036
	 * @return $this
1037
	 */
1038
	public function forceChange() {
1039
		// Ensure lazy fields loaded
1040
		$this->loadLazyFields();
1041
1042
		// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1043
		$fieldNames = array_unique(array_merge(
1044
			array_keys($this->record),
1045
			array_keys($this->inheritedDatabaseFields())
1046
		));
1047
1048
		foreach($fieldNames as $fieldName) {
1049
			if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
1050
			// Populate the null values in record so that they actually get written
1051
			if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
1052
		}
1053
1054
		// @todo Find better way to allow versioned to write a new version after forceChange
1055
		if($this->isChanged('Version')) unset($this->changed['Version']);
1056
		return $this;
1057
	}
1058
1059
	/**
1060
	 * Validate the current object.
1061
	 *
1062
	 * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1063
	 * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1064
	 *
1065
	 * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1066
	 * and onAfterWrite() won't get called either.
1067
	 *
1068
	 * It is expected that you call validate() in your own application to test that an object is valid before
1069
	 * attempting a write, and respond appropriately if it isn't.
1070
	 *
1071
	 * @see {@link ValidationResult}
1072
	 * @return ValidationResult
1073
	 */
1074
	protected function validate() {
1075
		$result = ValidationResult::create();
1076
		$this->extend('validate', $result);
1077
		return $result;
1078
	}
1079
1080
	/**
1081
	 * Public accessor for {@see DataObject::validate()}
1082
	 *
1083
	 * @return ValidationResult
1084
	 */
1085
	public function doValidate() {
1086
		// validate will be public in 4.0
1087
		return $this->validate();
1088
	}
1089
1090
	/**
1091
	 * Event handler called before writing to the database.
1092
	 * You can overload this to clean up or otherwise process data before writing it to the
1093
	 * database.  Don't forget to call parent::onBeforeWrite(), though!
1094
	 *
1095
	 * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1096
	 *
1097
	 * @uses DataExtension->onBeforeWrite()
1098
	 */
1099
	protected function onBeforeWrite() {
1100
		$this->brokenOnWrite = false;
1101
1102
		$dummy = null;
1103
		$this->extend('onBeforeWrite', $dummy);
1104
	}
1105
1106
	/**
1107
	 * Event handler called after writing to the database.
1108
	 * You can overload this to act upon changes made to the data after it is written.
1109
	 * $this->changed will have a record
1110
	 * database.  Don't forget to call parent::onAfterWrite(), though!
1111
	 *
1112
	 * @uses DataExtension->onAfterWrite()
1113
	 */
1114
	protected function onAfterWrite() {
1115
		$dummy = null;
1116
		$this->extend('onAfterWrite', $dummy);
1117
	}
1118
1119
	/**
1120
	 * Event handler called before deleting from the database.
1121
	 * You can overload this to clean up or otherwise process data before delete this
1122
	 * record.  Don't forget to call parent::onBeforeDelete(), though!
1123
	 *
1124
	 * @uses DataExtension->onBeforeDelete()
1125
	 */
1126
	protected function onBeforeDelete() {
1127
		$this->brokenOnDelete = false;
1128
1129
		$dummy = null;
1130
		$this->extend('onBeforeDelete', $dummy);
1131
	}
1132
1133
	protected function onAfterDelete() {
1134
		$this->extend('onAfterDelete');
1135
	}
1136
1137
	/**
1138
	 * Load the default values in from the self::$defaults array.
1139
	 * Will traverse the defaults of the current class and all its parent classes.
1140
	 * Called by the constructor when creating new records.
1141
	 *
1142
	 * @uses DataExtension->populateDefaults()
1143
	 * @return DataObject $this
1144
	 */
1145
	public function populateDefaults() {
1146
		$classes = array_reverse(ClassInfo::ancestry($this));
1147
1148
		foreach($classes as $class) {
1149
			$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1150
1151
			if($defaults && !is_array($defaults)) {
1152
				user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
1153
					E_USER_WARNING);
1154
				$defaults = null;
1155
			}
1156
1157
			if($defaults) foreach($defaults as $fieldName => $fieldValue) {
0 ignored issues
show
Bug introduced by
The expression $defaults of type array|integer|double|string|boolean 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...
1158
				// SRM 2007-03-06: Stricter check
1159
				if(!isset($this->$fieldName) || $this->$fieldName === null) {
1160
					$this->$fieldName = $fieldValue;
1161
				}
1162
				// Set many-many defaults with an array of ids
1163
				if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
1164
					$manyManyJoin = $this->$fieldName();
1165
					$manyManyJoin->setByIdList($fieldValue);
1166
				}
1167
			}
1168
			if($class == 'DataObject') {
1169
				break;
1170
			}
1171
		}
1172
1173
		$this->extend('populateDefaults');
1174
		return $this;
1175
	}
1176
1177
	/**
1178
	 * Determine validation of this object prior to write
1179
	 *
1180
	 * @return ValidationException Exception generated by this write, or null if valid
1181
	 */
1182
	protected function validateWrite() {
1183
		if ($this->ObsoleteClassName) {
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1184
			return new ValidationException(
1185
				"Object is of class '{$this->ObsoleteClassName}' which doesn't exist - ".
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1186
				"you need to change the ClassName before you can write it",
1187
				E_USER_WARNING
1188
			);
1189
		}
1190
1191
		if(Config::inst()->get('DataObject', 'validation_enabled')) {
1192
			$result = $this->validate();
1193
			if (!$result->valid()) {
1194
				return new ValidationException(
1195
					$result,
1196
					$result->message(),
1197
					E_USER_WARNING
1198
				);
1199
			}
1200
		}
1201
	}
1202
1203
	/**
1204
	 * Prepare an object prior to write
1205
	 *
1206
	 * @throws ValidationException
1207
	 */
1208
	protected function preWrite() {
1209
		// Validate this object
1210
		if($writeException = $this->validateWrite()) {
1211
			// Used by DODs to clean up after themselves, eg, Versioned
1212
			$this->invokeWithExtensions('onAfterSkippedWrite');
1213
			throw $writeException;
1214
		}
1215
1216
		// Check onBeforeWrite
1217
		$this->brokenOnWrite = true;
1218
		$this->onBeforeWrite();
1219
		if($this->brokenOnWrite) {
1220
			user_error("$this->class has a broken onBeforeWrite() function."
1221
				. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1222
		}
1223
	}
1224
1225
	/**
1226
	 * Detects and updates all changes made to this object
1227
	 *
1228
	 * @param bool $forceChanges If set to true, force all fields to be treated as changed
1229
	 * @return bool True if any changes are detected
1230
	 */
1231
	protected function updateChanges($forceChanges = false)
1232
	{
1233
		if($forceChanges) {
1234
			// Force changes, but only for loaded fields
1235
			foreach($this->record as $field => $value) {
1236
				$this->changed[$field] = static::CHANGE_VALUE;
1237
			}
1238
			return true;
1239
		}
1240
		return $this->isChanged();
1241
	}
1242
1243
	/**
1244
	 * Writes a subset of changes for a specific table to the given manipulation
1245
	 *
1246
	 * @param string $baseTable Base table
1247
	 * @param string $now Timestamp to use for the current time
1248
	 * @param bool $isNewRecord Whether this should be treated as a new record write
1249
	 * @param array $manipulation Manipulation to write to
1250
	 * @param string $class Table and Class to select and write to
1251
	 */
1252
	protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
1253
		$manipulation[$class] = array();
1254
1255
		// Extract records for this table
1256
		foreach($this->record as $fieldName => $fieldValue) {
1257
1258
			// Check if this record pertains to this table, and
1259
			// we're not attempting to reset the BaseTable->ID
1260
			if(	empty($this->changed[$fieldName])
1261
				|| ($class === $baseTable && $fieldName === 'ID')
1262
				|| (!self::has_own_table_database_field($class, $fieldName)
1263
					&& !self::is_composite_field($class, $fieldName, false))
1264
			) {
1265
				continue;
1266
			}
1267
1268
1269
			// if database column doesn't correlate to a DBField instance...
1270
			$fieldObj = $this->dbObject($fieldName);
1271
			if(!$fieldObj) {
1272
				$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1273
			}
1274
1275
			// Ensure DBField is repopulated and written to the manipulation
1276
			$fieldObj->setValue($fieldValue, $this->record);
1277
			$fieldObj->writeToManipulation($manipulation[$class]);
1278
		}
1279
1280
		// Ensure update of Created and LastEdited columns
1281
		if($baseTable === $class) {
1282
			$manipulation[$class]['fields']['LastEdited'] = $now;
1283
			if($isNewRecord) {
1284
				$manipulation[$class]['fields']['Created']
1285
					= empty($this->record['Created'])
1286
						? $now
1287
						: $this->record['Created'];
1288
				$manipulation[$class]['fields']['ClassName'] = $this->class;
1289
			}
1290
		}
1291
1292
		// Inserts done one the base table are performed in another step, so the manipulation should instead
1293
		// attempt an update, as though it were a normal update.
1294
		$manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update';
1295
		$manipulation[$class]['id'] = $this->record['ID'];
1296
	}
1297
1298
	/**
1299
	 * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1300
	 *
1301
	 * Does nothing if an ID is already assigned for this record
1302
	 *
1303
	 * @param string $baseTable Base table
1304
	 * @param string $now Timestamp to use for the current time
1305
	 */
1306
	protected function writeBaseRecord($baseTable, $now) {
1307
		// Generate new ID if not specified
1308
		if($this->isInDB()) return;
1309
1310
		// Perform an insert on the base table
1311
		$insert = new SQLInsert('"'.$baseTable.'"');
1312
		$insert
1313
			->assign('"Created"', $now)
1314
			->execute();
1315
		$this->changed['ID'] = self::CHANGE_VALUE;
1316
		$this->record['ID'] = DB::get_generated_id($baseTable);
1317
	}
1318
1319
	/**
1320
	 * Generate and write the database manipulation for all changed fields
1321
	 *
1322
	 * @param string $baseTable Base table
1323
	 * @param string $now Timestamp to use for the current time
1324
	 * @param bool $isNewRecord If this is a new record
1325
	 */
1326
	protected function writeManipulation($baseTable, $now, $isNewRecord) {
1327
		// Generate database manipulations for each class
1328
		$manipulation = array();
1329
		foreach($this->getClassAncestry() as $class) {
1330
			if(self::has_own_table($class)) {
1331
				$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1332
			}
1333
		}
1334
1335
		// Allow extensions to extend this manipulation
1336
		$this->extend('augmentWrite', $manipulation);
1337
1338
		// New records have their insert into the base data table done first, so that they can pass the
1339
		// generated ID on to the rest of the manipulation
1340
		if($isNewRecord) {
1341
			$manipulation[$baseTable]['command'] = 'update';
1342
		}
1343
1344
		// Perform the manipulation
1345
		DB::manipulate($manipulation);
1346
	}
1347
1348
	/**
1349
	 * Writes all changes to this object to the database.
1350
	 *  - It will insert a record whenever ID isn't set, otherwise update.
1351
	 *  - All relevant tables will be updated.
1352
	 *  - $this->onBeforeWrite() gets called beforehand.
1353
	 *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1354
	 *
1355
	 *  @uses DataExtension->augmentWrite()
1356
	 *
1357
	 * @param boolean $showDebug Show debugging information
1358
	 * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1359
	 * @param boolean $forceWrite Write to database even if there are no changes
1360
	 * @param boolean $writeComponents Call write() on all associated component instances which were previously
1361
	 *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1362
	 *                                 {@link getManyManyComponents()} (Default: false)
1363
	 * @return int The ID of the record
1364
	 * @throws ValidationException Exception that can be caught and handled by the calling function
1365
	 */
1366
	public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1367
		$now = SS_Datetime::now()->Rfc2822();
1368
1369
		// Execute pre-write tasks
1370
		$this->preWrite();
1371
1372
		// Check if we are doing an update or an insert
1373
		$isNewRecord = !$this->isInDB() || $forceInsert;
1374
1375
		// Check changes exist, abort if there are none
1376
		$hasChanges = $this->updateChanges($isNewRecord);
1377
		if($hasChanges || $forceWrite || $isNewRecord) {
1378
			// New records have their insert into the base data table done first, so that they can pass the
1379
			// generated primary key on to the rest of the manipulation
1380
			$baseTable = ClassInfo::baseDataClass($this->class);
1381
			$this->writeBaseRecord($baseTable, $now);
1382
1383
			// Write the DB manipulation for all changed fields
1384
			$this->writeManipulation($baseTable, $now, $isNewRecord);
1385
1386
			// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1387
			$this->writeRelations();
1388
			$this->onAfterWrite();
1389
			$this->changed = array();
1390
		} else {
1391
			if($showDebug) Debug::message("no changes for DataObject");
1392
1393
			// Used by DODs to clean up after themselves, eg, Versioned
1394
			$this->invokeWithExtensions('onAfterSkippedWrite');
1395
		}
1396
1397
		// Ensure Created and LastEdited are populated
1398
		if(!isset($this->record['Created'])) {
1399
			$this->record['Created'] = $now;
1400
		}
1401
		$this->record['LastEdited'] = $now;
1402
1403
		// Write relations as necessary
1404
		if($writeComponents) $this->writeComponents(true);
1405
1406
		// Clears the cache for this object so get_one returns the correct object.
1407
		$this->flushCache();
1408
1409
		return $this->record['ID'];
1410
	}
1411
1412
	/**
1413
	 * Writes cached relation lists to the database, if possible
1414
	 */
1415
	public function writeRelations() {
1416
		if(!$this->isInDB()) return;
1417
1418
		// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1419
		if($this->unsavedRelations) {
1420
			foreach($this->unsavedRelations as $name => $list) {
1421
				$list->changeToList($this->$name());
1422
			}
1423
			$this->unsavedRelations = array();
1424
		}
1425
	}
1426
1427
	/**
1428
	 * Write the cached components to the database. Cached components could refer to two different instances of the
1429
	 * same record.
1430
	 *
1431
	 * @param $recursive Recursively write components
1432
	 * @return DataObject $this
1433
	 */
1434
	public function writeComponents($recursive = false) {
1435
		if(!$this->components) return $this;
1436
1437
		foreach($this->components as $component) {
1438
			$component->write(false, false, false, $recursive);
1439
		}
1440
		return $this;
1441
	}
1442
1443
	/**
1444
	 * Delete this data object.
1445
	 * $this->onBeforeDelete() gets called.
1446
	 * Note that in Versioned objects, both Stage and Live will be deleted.
1447
	 *  @uses DataExtension->augmentSQL()
1448
	 */
1449
	public function delete() {
1450
		$this->brokenOnDelete = true;
1451
		$this->onBeforeDelete();
1452
		if($this->brokenOnDelete) {
1453
			user_error("$this->class has a broken onBeforeDelete() function."
1454
				. " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1455
		}
1456
1457
		// Deleting a record without an ID shouldn't do anything
1458
		if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1459
1460
		// TODO: This is quite ugly.  To improve:
1461
		//  - move the details of the delete code in the DataQuery system
1462
		//  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1463
		//    obviously, that means getting requireTable() to configure cascading deletes ;-)
1464
		$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1465
		foreach($srcQuery->queriedTables() as $table) {
1466
			$delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1467
			$delete->execute();
1468
		}
1469
		// Remove this item out of any caches
1470
		$this->flushCache();
1471
1472
		$this->onAfterDelete();
1473
1474
		$this->OldID = $this->ID;
0 ignored issues
show
Documentation introduced by
The property OldID does not exist on object<DataObject>. 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...
1475
		$this->ID = 0;
1476
	}
1477
1478
	/**
1479
	 * Delete the record with the given ID.
1480
	 *
1481
	 * @param string $className The class name of the record to be deleted
1482
	 * @param int $id ID of record to be deleted
1483
	 */
1484
	public static function delete_by_id($className, $id) {
1485
		$obj = DataObject::get_by_id($className, $id);
1486
		if($obj) {
1487
			$obj->delete();
1488
		} else {
1489
			user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1490
		}
1491
	}
1492
1493
	/**
1494
	 * Get the class ancestry, including the current class name.
1495
	 * The ancestry will be returned as an array of class names, where the 0th element
1496
	 * will be the class that inherits directly from DataObject, and the last element
1497
	 * will be the current class.
1498
	 *
1499
	 * @return array Class ancestry
1500
	 */
1501
	public function getClassAncestry() {
1502
		if(!isset(DataObject::$_cache_get_class_ancestry[$this->class])) {
1503
			DataObject::$_cache_get_class_ancestry[$this->class] = array($this->class);
1504
			while(($class=get_parent_class(DataObject::$_cache_get_class_ancestry[$this->class][0])) != "DataObject") {
1505
				array_unshift(DataObject::$_cache_get_class_ancestry[$this->class], $class);
1506
			}
1507
		}
1508
		return DataObject::$_cache_get_class_ancestry[$this->class];
1509
	}
1510
1511
	/**
1512
	 * Return a component object from a one to one relationship, as a DataObject.
1513
	 * If no component is available, an 'empty component' will be returned for
1514
	 * non-polymorphic relations, or for polymorphic relations with a class set.
1515
	 *
1516
	 * @param string $componentName Name of the component
1517
	 *
1518
	 * @return DataObject The component object. It's exact type will be that of the component.
1519
	 */
1520
	public function getComponent($componentName) {
1521
		if(isset($this->components[$componentName])) {
1522
			return $this->components[$componentName];
1523
		}
1524
1525
		if($class = $this->hasOneComponent($componentName)) {
1526
			$joinField = $componentName . 'ID';
1527
			$joinID    = $this->getField($joinField);
1528
1529
			// Extract class name for polymorphic relations
1530
			if($class === 'DataObject') {
1531
				$class = $this->getField($componentName . 'Class');
1532
				if(empty($class)) return null;
1533
			}
1534
1535
			if($joinID) {
1536
				$component = DataObject::get_by_id($class, $joinID);
1537
			}
1538
1539
			if(empty($component)) {
1540
				$component = $this->model->$class->newObject();
1541
			}
1542
		} elseif($class = $this->belongsToComponent($componentName)) {
1543
1544
			$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
1545
			$joinID    = $this->ID;
1546
1547
			if($joinID) {
1548
1549
				$filter = $polymorphic
1550
					? array(
1551
						"{$joinField}ID" => $joinID,
1552
						"{$joinField}Class" => $this->class
1553
					)
1554
					: array(
1555
						$joinField => $joinID
1556
					);
1557
				$component = DataObject::get($class)->filter($filter)->first();
1558
			}
1559
1560
			if(empty($component)) {
1561
				$component = $this->model->$class->newObject();
1562
				if($polymorphic) {
1563
					$component->{$joinField.'ID'} = $this->ID;
1564
					$component->{$joinField.'Class'} = $this->class;
1565
				} else {
1566
					$component->$joinField = $this->ID;
1567
				}
1568
			}
1569
		} else {
1570
			throw new Exception("DataObject->getComponent(): Could not find component '$componentName'.");
1571
		}
1572
1573
		$this->components[$componentName] = $component;
1574
		return $component;
1575
	}
1576
1577
	/**
1578
	 * Returns a one-to-many relation as a HasManyList
1579
	 *
1580
	 * @param string $componentName Name of the component
1581
	 * @param string|null $filter Deprecated. A filter to be inserted into the WHERE clause
1582
	 * @param string|null|array $sort Deprecated. A sort expression to be inserted into the ORDER BY clause. If omitted,
1583
	 *                                the static field $default_sort on the component class will be used.
1584
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1585
	 * @param string|null|array $limit Deprecated. A limit expression to be inserted into the LIMIT clause
1586
	 *
1587
	 * @return HasManyList The components of the one-to-many relationship.
1588
	 */
1589
	public function getComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
1590
		$result = null;
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1591
1592
		if(!$componentClass = $this->hasManyComponent($componentName)) {
1593
			user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'"
1594
				. " on class '$this->class'", E_USER_ERROR);
1595
		}
1596
1597
		if($join) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $join of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1598
			throw new \InvalidArgumentException(
1599
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
1600
			);
1601
		}
1602
1603 View Code Duplication
		if($filter !== null || $sort !== null || $limit !== null) {
0 ignored issues
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...
1604
			Deprecation::notice('4.0', 'The $filter, $sort and $limit parameters for DataObject::getComponents()
1605
				have been deprecated. Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
1606
		}
1607
1608
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1609 View Code Duplication
		if(!$this->ID) {
0 ignored issues
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...
1610
			if(!isset($this->unsavedRelations[$componentName])) {
1611
				$this->unsavedRelations[$componentName] =
1612
					new UnsavedRelationList($this->class, $componentName, $componentClass);
0 ignored issues
show
Documentation introduced by
$this->class is of type string, but the function expects a array.

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...
Security Bug introduced by
It seems like $componentClass defined by $this->hasManyComponent($componentName) on line 1592 can also be of type false; however, UnsavedRelationList::__construct() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1613
			}
1614
			return $this->unsavedRelations[$componentName];
1615
		}
1616
1617
		// Determine type and nature of foreign relation
1618
		$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
1619
		if($polymorphic) {
1620
			$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1621
		} else {
1622
			$result = HasManyList::create($componentClass, $joinField);
1623
		}
1624
1625
		if($this->model) $result->setDataModel($this->model);
1626
1627
		return $result
1628
			->forForeignID($this->ID)
1629
			->where($filter)
1630
			->limit($limit)
0 ignored issues
show
Documentation introduced by
$limit is of type string|null|array, but the function expects a integer.

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...
1631
			->sort($sort);
1632
	}
1633
1634
	/**
1635
	 * @deprecated
1636
	 */
1637
	public function getComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
1638
		Deprecation::notice('4.0', "Use getComponents to get a filtered DataList for an object's relation");
1639
		return $this->getComponents($componentName, $filter, $sort, $join, $limit);
1640
	}
1641
1642
	/**
1643
	 * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1644
	 *
1645
	 * @param $relationName Relation name.
1646
	 * @return string Class name, or null if not found.
1647
	 */
1648
	public function getRelationClass($relationName) {
1649
		// Go through all relationship configuration fields.
1650
		$candidates = array_merge(
1651
			($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1652
			($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1653
			($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
1654
			($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
1655
			($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1656
		);
1657
1658
		if (isset($candidates[$relationName])) {
1659
			$remoteClass = $candidates[$relationName];
1660
1661
			// If dot notation is present, extract just the first part that contains the class.
1662 View Code Duplication
			if(($fieldPos = strpos($remoteClass, '.'))!==false) {
0 ignored issues
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...
1663
				return substr($remoteClass, 0, $fieldPos);
1664
			}
1665
1666
			// Otherwise just return the class
1667
			return $remoteClass;
1668
		}
1669
1670
		return null;
1671
	}
1672
1673
	/**
1674
	 * Tries to find the database key on another object that is used to store a
1675
	 * relationship to this class. If no join field can be found it defaults to 'ParentID'.
1676
	 *
1677
	 * If the remote field is polymorphic then $polymorphic is set to true, and the return value
1678
	 * is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
1679
	 *
1680
	 * @param string $component Name of the relation on the current object pointing to the
1681
	 * remote object.
1682
	 * @param string $type the join type - either 'has_many' or 'belongs_to'
1683
	 * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
1684
	 * @return string
1685
	 */
1686
	public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
1687
		// Extract relation from current object
1688
		if($type === 'has_many') {
1689
			$remoteClass = $this->hasManyComponent($component, false);
1690
		} else {
1691
			$remoteClass = $this->belongsToComponent($component, false);
1692
		}
1693
1694
		if(empty($remoteClass)) {
1695
			throw new Exception("Unknown $type component '$component' on class '$this->class'");
1696
		}
1697
		if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
1698
			throw new Exception(
1699
				"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
1700
			);
1701
		}
1702
1703
		// If presented with an explicit field name (using dot notation) then extract field name
1704
		$remoteField = null;
1705
		if(strpos($remoteClass, '.') !== false) {
1706
			list($remoteClass, $remoteField) = explode('.', $remoteClass);
1707
		}
1708
1709
		// Reference remote has_one to check against
1710
		$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
1711
1712
		// Without an explicit field name, attempt to match the first remote field
1713
		// with the same type as the current class
1714
		if(empty($remoteField)) {
1715
			// look for remote has_one joins on this class or any parent classes
1716
			$remoteRelationsMap = array_flip($remoteRelations);
1717
			foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1718
				if(array_key_exists($class, $remoteRelationsMap)) {
1719
					$remoteField = $remoteRelationsMap[$class];
1720
					break;
1721
				}
1722
			}
1723
		}
1724
1725
		// In case of an indeterminate remote field show an error
1726
		if(empty($remoteField)) {
1727
			$polymorphic = false;
1728
			$message = "No has_one found on class '$remoteClass'";
1729
			if($type == 'has_many') {
1730
				// include a hint for has_many that is missing a has_one
1731
				$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
1732
				$message .= " requires a has_one on '$remoteClass'";
1733
			}
1734
			throw new Exception($message);
1735
		}
1736
1737
		// If given an explicit field name ensure the related class specifies this
1738
		if(empty($remoteRelations[$remoteField])) {
1739
			throw new Exception("Missing expected has_one named '$remoteField'
1740
				on class '$remoteClass' referenced by $type named '$component'
1741
				on class {$this->class}"
1742
			);
1743
		}
1744
1745
		// Inspect resulting found relation
1746
		if($remoteRelations[$remoteField] === 'DataObject') {
1747
			$polymorphic = true;
1748
			return $remoteField; // Composite polymorphic field does not include 'ID' suffix
1749
		} else {
1750
			$polymorphic = false;
1751
			return $remoteField . 'ID';
1752
		}
1753
	}
1754
1755
	/**
1756
	 * Returns a many-to-many component, as a ManyManyList.
1757
	 * @param string $componentName Name of the many-many component
1758
	 * @return ManyManyList The set of components
1759
	 *
1760
	 * @todo Implement query-params
1761
	 */
1762
	public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
1763
		list($parentClass, $componentClass, $parentField, $componentField, $table)
1764
			= $this->manyManyComponent($componentName);
1765
1766 View Code Duplication
		if($filter !== null || $sort !== null || $join !== null || $limit !== null) {
0 ignored issues
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...
1767
			Deprecation::notice('4.0', 'The $filter, $sort, $join and $limit parameters for
1768
				DataObject::getManyManyComponents() have been deprecated.
1769
				Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
1770
		}
1771
1772
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1773 View Code Duplication
		if(!$this->ID) {
0 ignored issues
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...
1774
			if(!isset($this->unsavedRelations[$componentName])) {
1775
				$this->unsavedRelations[$componentName] =
1776
					new UnsavedRelationList($parentClass, $componentName, $componentClass);
1777
			}
1778
			return $this->unsavedRelations[$componentName];
1779
		}
1780
1781
		$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
1782
		$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1783
1784
1785
		// Store component data in query meta-data
1786
		$result = $result->alterDataQuery(function($query) use ($extraFields) {
1787
			$query->setQueryParam('Component.ExtraFields', $extraFields);
1788
		});
1789
		
1790
		if($this->model) $result->setDataModel($this->model);
1791
1792
		$this->extend('updateManyManyComponents', $result);
1793
1794
		// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1795
		// foreignID set elsewhere.
1796
		return $result
1797
			->forForeignID($this->ID)
1798
			->where($filter)
1799
			->sort($sort)
1800
			->limit($limit);
1801
	}
1802
1803
	/**
1804
	 * @deprecated 4.0 Method has been replaced by hasOne() and hasOneComponent()
1805
	 * @param string $component
1806
	 * @return array|null
1807
	 */
1808
	public function has_one($component = null) {
1809
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1810
			Deprecation::notice('4.0', 'Please use hasOneComponent() instead');
1811
			return $this->hasOneComponent($component);
1812
		}
1813
1814
		Deprecation::notice('4.0', 'Please use hasOne() instead');
1815
		return $this->hasOne();
1816
	}
1817
1818
	/**
1819
	 * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1820
	 * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1821
	 *
1822
	 * @param string $component Deprecated - Name of component
1823
	 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1824
	 * 							their classes.
1825
	 */
1826 View Code Duplication
	public function hasOne($component = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1827
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1828
			Deprecation::notice(
1829
				'4.0',
1830
				'Please use DataObject::hasOneComponent() instead of passing a component name to hasOne()',
1831
				Deprecation::SCOPE_GLOBAL
1832
			);
1833
			return $this->hasOneComponent($component);
1834
		}
1835
1836
		return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1837
	}
1838
1839
	/**
1840
	 * Return data for a specific has_one component.
1841
	 * @param string $component
1842
	 * @return string|null
1843
	 */
1844
	public function hasOneComponent($component) {
1845
		$hasOnes = (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1846
1847
		if(isset($hasOnes[$component])) {
1848
			return $hasOnes[$component];
1849
		}
1850
	}
1851
1852
	/**
1853
	 * @deprecated 4.0 Method has been replaced by belongsTo() and belongsToComponent()
1854
	 * @param string $component
1855
	 * @param bool $classOnly
1856
	 * @return array|null
1857
	 */
1858 View Code Duplication
	public function belongs_to($component = null, $classOnly = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1859
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1860
			Deprecation::notice('4.0', 'Please use belongsToComponent() instead');
1861
			return $this->belongsToComponent($component, $classOnly);
1862
		}
1863
1864
		Deprecation::notice('4.0', 'Please use belongsTo() instead');
1865
		return $this->belongsTo(null, $classOnly);
1866
	}
1867
1868
	/**
1869
	 * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1870
	 * their class name will be returned.
1871
	 *
1872
	 * @param string $component - Name of component
1873
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1874
	 *        the field data stripped off. It defaults to TRUE.
1875
	 * @return string|array
1876
	 */
1877 View Code Duplication
	public function belongsTo($component = null, $classOnly = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1878
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1879
			Deprecation::notice(
1880
				'4.0',
1881
				'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
1882
				Deprecation::SCOPE_GLOBAL
1883
			);
1884
			return $this->belongsToComponent($component, $classOnly);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->belongsToComponen...component, $classOnly); of type string|false adds false to the return on line 1884 which is incompatible with the return type documented by DataObject::belongsTo of type string|array. It seems like you forgot to handle an error condition.
Loading history...
1885
		}
1886
1887
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1888
		if($belongsTo && $classOnly) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $belongsTo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1889
			return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1890
		} else {
1891
			return $belongsTo ? $belongsTo : array();
1892
		}
1893
	}
1894
1895
	/**
1896
	 * Return data for a specific belongs_to component.
1897
	 * @param string $component
1898
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1899
	 *        the field data stripped off. It defaults to TRUE.
1900
	 * @return string|false
1901
	 */
1902 View Code Duplication
	public function belongsToComponent($component, $classOnly = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1903
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1904
1905
		if($belongsTo && array_key_exists($component, $belongsTo)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $belongsTo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1906
			$belongsTo = $belongsTo[$component];
1907
		} else {
1908
			return false;
1909
		}
1910
1911
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
1912
	}
1913
1914
	/**
1915
	 * Return all of the database fields defined in self::$db and all the parent classes.
1916
	 * Doesn't include any fields specified by self::$has_one.  Use $this->hasOne() to get these fields
1917
	 *
1918
	 * @param string $fieldName Limit the output to a specific field name
1919
	 * @return array The database fields
1920
	 */
1921
	public function db($fieldName = null) {
1922
		$classes = ClassInfo::ancestry($this, true);
1923
1924
		// If we're looking for a specific field, we want to hit subclasses first as they may override field types
1925
		if($fieldName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1926
			$classes = array_reverse($classes);
1927
		}
1928
1929
		$items = array();
1930
		foreach($classes as $class) {
1931
			if(isset(self::$_cache_db[$class])) {
1932
				$dbItems = self::$_cache_db[$class];
1933
			} else {
1934
				$dbItems = (array) Config::inst()->get($class, 'db', Config::UNINHERITED);
1935
				self::$_cache_db[$class] = $dbItems;
1936
			}
1937
1938
			if($fieldName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1939
				if(isset($dbItems[$fieldName])) {
1940
					return $dbItems[$fieldName];
1941
				}
1942
			} else {
1943
				$items = isset($items) ? array_merge((array) $items, $dbItems) : $dbItems;
1944
			}
1945
		}
1946
1947
		return $items;
1948
	}
1949
1950
	/**
1951
	 * @deprecated 4.0 Method has been replaced by hasMany() and hasManyComponent()
1952
	 * @param string $component
1953
	 * @param bool $classOnly
1954
	 * @return array|null
1955
	 */
1956 View Code Duplication
	public function has_many($component = null, $classOnly = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1957
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1958
			Deprecation::notice('4.0', 'Please use hasManyComponent() instead');
1959
			return $this->hasManyComponent($component, $classOnly);
1960
		}
1961
1962
		Deprecation::notice('4.0', 'Please use hasMany() instead');
1963
		return $this->hasMany(null, $classOnly);
1964
	}
1965
1966
	/**
1967
	 * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1968
	 * relationships and their classes will be returned.
1969
	 *
1970
	 * @param string $component Deprecated - Name of component
1971
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1972
	 *        the field data stripped off. It defaults to TRUE.
1973
	 * @return string|array|false
1974
	 */
1975 View Code Duplication
	public function hasMany($component = null, $classOnly = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1976
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1977
			Deprecation::notice(
1978
				'4.0',
1979
				'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
1980
				Deprecation::SCOPE_GLOBAL
1981
			);
1982
			return $this->hasManyComponent($component, $classOnly);
1983
		}
1984
1985
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
1986
		if($hasMany && $classOnly) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasMany of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1987
			return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1988
		} else {
1989
			return $hasMany ? $hasMany : array();
1990
		}
1991
	}
1992
1993
	/**
1994
	 * Return data for a specific has_many component.
1995
	 * @param string $component
1996
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1997
	 *        the field data stripped off. It defaults to TRUE.
1998
	 * @return string|false
1999
	 */
2000 View Code Duplication
	public function hasManyComponent($component, $classOnly = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2001
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2002
2003
		if($hasMany && array_key_exists($component, $hasMany)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasMany of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2004
			$hasMany = $hasMany[$component];
2005
		} else {
2006
			return false;
2007
		}
2008
2009
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
2010
	}
2011
2012
	/**
2013
	 * @deprecated 4.0 Method has been replaced by manyManyExtraFields() and
2014
	 *                 manyManyExtraFieldsForComponent()
2015
	 * @param string $component
2016
	 * @return array
2017
	 */
2018
	public function many_many_extraFields($component = null) {
2019
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2020
			Deprecation::notice('4.0', 'Please use manyManyExtraFieldsForComponent() instead');
2021
			return $this->manyManyExtraFieldsForComponent($component);
2022
		}
2023
2024
		Deprecation::notice('4.0', 'Please use manyManyExtraFields() instead');
2025
		return $this->manyManyExtraFields();
2026
	}
2027
2028
	/**
2029
	 * Return the many-to-many extra fields specification.
2030
	 *
2031
	 * If you don't specify a component name, it returns all
2032
	 * extra fields for all components available.
2033
	 *
2034
	 * @param string $component Deprecated - Name of component
2035
	 * @return array|null
2036
	 */
2037 View Code Duplication
	public function manyManyExtraFields($component = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2038
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2039
			Deprecation::notice(
2040
				'4.0',
2041
				'Please use DataObject::manyManyExtraFieldsForComponent() instead of passing a component name
2042
					to manyManyExtraFields()',
2043
				Deprecation::SCOPE_GLOBAL
2044
			);
2045
			return $this->manyManyExtraFieldsForComponent($component);
2046
		}
2047
2048
		return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2049
	}
2050
2051
	/**
2052
	 * Return the many-to-many extra fields specification for a specific component.
2053
	 * @param string $component
2054
	 * @return array|null
2055
	 */
2056
	public function manyManyExtraFieldsForComponent($component) {
2057
		// Get all many_many_extraFields defined in this class or parent classes
2058
		$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2059
		// Extra fields are immediately available
2060
		if(isset($extraFields[$component])) {
2061
			return $extraFields[$component];
2062
		}
2063
2064
		// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
2065
		$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2066
		$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2067
		if($candidate) {
2068
			$relationName = null;
2069
			// Extract class and relation name from dot-notation
2070 View Code Duplication
			if(strpos($candidate, '.') !== false) {
0 ignored issues
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...
2071
				list($candidate, $relationName) = explode('.', $candidate, 2);
2072
			}
2073
2074
			// If we've not already found the relation name from dot notation, we need to find a relation that points
2075
			// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
2076
			// so it's safe to assume that it's the correct one
2077
			if(!$relationName) {
2078
				$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2079
2080
				foreach($candidateManyManys as $relation => $relatedClass) {
2081
					if (is_a($this, $relatedClass)) {
2082
						$relationName = $relation;
2083
					}
2084
				}
2085
			}
2086
2087
			// If we've found a matching relation on the target class, see if we can find extra fields for it
2088
			$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
2089
			if(isset($extraFields[$relationName])) {
2090
				return $extraFields[$relationName];
2091
			}
2092
		}
2093
2094
		return isset($items) ? $items : null;
0 ignored issues
show
Bug introduced by
The variable $items seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
2095
	}
2096
2097
	/**
2098
	 * @deprecated 4.0 Method has been renamed to manyMany()
2099
	 * @param string $component
2100
	 * @return array|null
2101
	 */
2102
	public function many_many($component = null) {
2103
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2104
			Deprecation::notice('4.0', 'Please use manyManyComponent() instead');
2105
			return $this->manyManyComponent($component);
2106
		}
2107
2108
		Deprecation::notice('4.0', 'Please use manyMany() instead');
2109
		return $this->manyMany();
2110
	}
2111
2112
	/**
2113
	 * Return information about a many-to-many component.
2114
	 * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2115
	 * components are returned.
2116
	 *
2117
	 * @see DataObject::manyManyComponent()
2118
	 * @param string $component Deprecated - Name of component
2119
	 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2120
	 */
2121
	public function manyMany($component = null) {
2122
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2123
			Deprecation::notice(
2124
				'4.0',
2125
				'Please use DataObject::manyManyComponent() instead of passing a component name to manyMany()',
2126
				Deprecation::SCOPE_GLOBAL
2127
			);
2128
			return $this->manyManyComponent($component);
2129
		}
2130
2131
		$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
2132
		$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2133
2134
		$items = array_merge($manyManys, $belongsManyManys);
2135
		return $items;
2136
	}
2137
2138
	/**
2139
	 * Return information about a specific many_many component. Returns a numeric array of:
2140
	 * array(
2141
	 * 	<classname>,		The class that relation is defined in e.g. "Product"
2142
	 * 	<candidateName>,	The target class of the relation e.g. "Category"
2143
	 * 	<parentField>,		The field name pointing to <classname>'s table e.g. "ProductID"
2144
	 * 	<childField>,		The field name pointing to <candidatename>'s table e.g. "CategoryID"
2145
	 * 	<joinTable>			The join table between the two classes e.g. "Product_Categories"
2146
	 * )
2147
	 * @param string $component The component name
2148
	 * @return array|null
2149
	 */
2150
	public function manyManyComponent($component) {
2151
		$classes = $this->getClassAncestry();
2152
		foreach($classes as $class) {
2153
			$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
2154
			// Check if the component is defined in many_many on this class
2155
			$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2156
			if($candidate) {
2157
				$parentField = $class . "ID";
2158
				$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
2159
				return array($class, $candidate, $parentField, $childField, "{$class}_$component");
2160
			}
2161
2162
			// Check if the component is defined in belongs_many_many on this class
2163
			$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
2164
			$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
2165
			if($candidate) {
2166
				// Extract class and relation name from dot-notation
2167 View Code Duplication
				if(strpos($candidate, '.') !== false) {
0 ignored issues
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...
2168
					list($candidate, $relationName) = explode('.', $candidate, 2);
2169
				}
2170
2171
				$childField = $candidate . "ID";
2172
2173
				// We need to find the inverse component name
2174
				$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2175
				if(!$otherManyMany) {
2176
					throw new LogicException("Inverse component of $candidate not found ({$this->class})");
2177
				}
2178
2179
				// If we've got a relation name (extracted from dot-notation), we can already work out
2180
				// the join table and candidate class name...
2181
				if(isset($relationName) && isset($otherManyMany[$relationName])) {
2182
					$candidateClass = $otherManyMany[$relationName];
2183
					$joinTable = "{$candidate}_{$relationName}";
2184
				} else {
2185
					// ... otherwise, we need to loop over the many_manys and find a relation that
2186
					// matches up to this class
2187
					foreach($otherManyMany as $inverseComponentName => $candidateClass) {
0 ignored issues
show
Bug introduced by
The expression $otherManyMany of type array|integer|double|string|boolean 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...
2188
						if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
2189
							$joinTable = "{$candidate}_{$inverseComponentName}";
2190
							break;
2191
						}
2192
					}
2193
				}
2194
2195
				// If we could work out the join table, we've got all the info we need
2196
				if(isset($joinTable)) {
2197
					$parentField = ($class == $candidate) ? "ChildID" : $candidateClass . "ID";
0 ignored issues
show
Bug introduced by
The variable $candidateClass does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2198
					return array($class, $candidate, $parentField, $childField, $joinTable);
2199
				}
2200
2201
				throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
2202
			}
2203
		}
2204
	}
2205
2206
	/**
2207
	 * This returns an array (if it exists) describing the database extensions that are required, or false if none
2208
	 *
2209
	 * This is experimental, and is currently only a Postgres-specific enhancement.
2210
	 *
2211
	 * @return array or false
2212
	 */
2213
	public function database_extensions($class){
2214
		$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2215
2216
		if($extensions)
2217
			return $extensions;
2218
		else
2219
			return false;
2220
	}
2221
2222
	/**
2223
	 * Generates a SearchContext to be used for building and processing
2224
	 * a generic search form for properties on this object.
2225
	 *
2226
	 * @return SearchContext
2227
	 */
2228
	public function getDefaultSearchContext() {
2229
		return new SearchContext(
2230
			$this->class,
2231
			$this->scaffoldSearchFields(),
2232
			$this->defaultSearchFilters()
2233
		);
2234
	}
2235
2236
	/**
2237
	 * Determine which properties on the DataObject are
2238
	 * searchable, and map them to their default {@link FormField}
2239
	 * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2240
	 *
2241
	 * Some additional logic is included for switching field labels, based on
2242
	 * how generic or specific the field type is.
2243
	 *
2244
	 * Used by {@link SearchContext}.
2245
	 *
2246
	 * @param array $_params
2247
	 *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2248
	 *   'restrictFields': Numeric array of a field name whitelist
2249
	 * @return FieldList
2250
	 */
2251
	public function scaffoldSearchFields($_params = null) {
2252
		$params = array_merge(
2253
			array(
2254
				'fieldClasses' => false,
2255
				'restrictFields' => false
2256
			),
2257
			(array)$_params
2258
		);
2259
		$fields = new FieldList();
2260
		foreach($this->searchableFields() as $fieldName => $spec) {
2261
			if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
2262
2263
			// If a custom fieldclass is provided as a string, use it
2264
			if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2265
				$fieldClass = $params['fieldClasses'][$fieldName];
2266
				$field = new $fieldClass($fieldName);
2267
			// If we explicitly set a field, then construct that
2268
			} else if(isset($spec['field'])) {
2269
				// If it's a string, use it as a class name and construct
2270
				if(is_string($spec['field'])) {
2271
					$fieldClass = $spec['field'];
2272
					$field = new $fieldClass($fieldName);
2273
2274
				// If it's a FormField object, then just use that object directly.
2275
				} else if($spec['field'] instanceof FormField) {
2276
					$field = $spec['field'];
2277
2278
				// Otherwise we have a bug
2279
				} else {
2280
					user_error("Bad value for searchable_fields, 'field' value: "
2281
						. var_export($spec['field'], true), E_USER_WARNING);
2282
				}
2283
2284
			// Otherwise, use the database field's scaffolder
2285
			} else {
2286
				$field = $this->relObject($fieldName)->scaffoldSearchField();
2287
			}
2288
2289
			if (strstr($fieldName, '.')) {
2290
				$field->setName(str_replace('.', '__', $fieldName));
0 ignored issues
show
Bug introduced by
The variable $field does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2291
			}
2292
			$field->setTitle($spec['title']);
2293
2294
			$fields->push($field);
2295
		}
2296
		return $fields;
2297
	}
2298
2299
	/**
2300
	 * Scaffold a simple edit form for all properties on this dataobject,
2301
	 * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2302
	 * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2303
	 *
2304
	 * @uses FormScaffolder
2305
	 *
2306
	 * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2307
	 * @return FieldList
2308
	 */
2309
	public function scaffoldFormFields($_params = null) {
2310
		$params = array_merge(
2311
			array(
2312
				'tabbed' => false,
2313
				'includeRelations' => false,
2314
				'restrictFields' => false,
2315
				'fieldClasses' => false,
2316
				'ajaxSafe' => false
2317
			),
2318
			(array)$_params
2319
		);
2320
2321
		$fs = new FormScaffolder($this);
2322
		$fs->tabbed = $params['tabbed'];
2323
		$fs->includeRelations = $params['includeRelations'];
2324
		$fs->restrictFields = $params['restrictFields'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $params['restrictFields'] of type false is incompatible with the declared type array of property $restrictFields.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2325
		$fs->fieldClasses = $params['fieldClasses'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $params['fieldClasses'] of type false is incompatible with the declared type array of property $fieldClasses.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2326
		$fs->ajaxSafe = $params['ajaxSafe'];
2327
2328
		return $fs->getFieldList();
2329
	}
2330
2331
	/**
2332
	 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2333
	 * being called on extensions
2334
	 *
2335
	 * @param callable $callback The callback to execute
2336
	 */
2337
	protected function beforeUpdateCMSFields($callback) {
2338
		$this->beforeExtending('updateCMSFields', $callback);
2339
	}
2340
2341
	/**
2342
	 * Centerpiece of every data administration interface in Silverstripe,
2343
	 * which returns a {@link FieldList} suitable for a {@link Form} object.
2344
	 * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2345
	 * generate this set. To customize, overload this method in a subclass
2346
	 * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2347
	 *
2348
	 * <code>
2349
	 * class MyCustomClass extends DataObject {
2350
	 *  static $db = array('CustomProperty'=>'Boolean');
2351
	 *
2352
	 *  function getCMSFields() {
2353
	 *    $fields = parent::getCMSFields();
2354
	 *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2355
	 *    return $fields;
2356
	 *  }
2357
	 * }
2358
	 * </code>
2359
	 *
2360
	 * @see Good example of complex FormField building: SiteTree::getCMSFields()
2361
	 *
2362
	 * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2363
	 */
2364
	public function getCMSFields() {
2365
		$tabbedFields = $this->scaffoldFormFields(array(
2366
			// Don't allow has_many/many_many relationship editing before the record is first saved
2367
			'includeRelations' => ($this->ID > 0),
2368
			'tabbed' => true,
2369
			'ajaxSafe' => true
2370
		));
2371
2372
		$this->extend('updateCMSFields', $tabbedFields);
2373
2374
		return $tabbedFields;
2375
	}
2376
2377
	/**
2378
	 * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2379
	 * including that dataobject's extensions customised actions could be added to the EditForm.
2380
	 *
2381
	 * @return an Empty FieldList(); need to be overload by solid subclass
2382
	 */
2383
	public function getCMSActions() {
2384
		$actions = new FieldList();
2385
		$this->extend('updateCMSActions', $actions);
2386
		return $actions;
2387
	}
2388
2389
2390
	/**
2391
	 * Used for simple frontend forms without relation editing
2392
	 * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2393
	 * by default. To customize, either overload this method in your
2394
	 * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2395
	 *
2396
	 * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2397
	 *
2398
	 * @param array $params See {@link scaffoldFormFields()}
2399
	 * @return FieldList Always returns a simple field collection without TabSet.
2400
	 */
2401
	public function getFrontEndFields($params = null) {
2402
		$untabbedFields = $this->scaffoldFormFields($params);
2403
		$this->extend('updateFrontEndFields', $untabbedFields);
2404
2405
		return $untabbedFields;
2406
	}
2407
2408
	/**
2409
	 * Gets the value of a field.
2410
	 * Called by {@link __get()} and any getFieldName() methods you might create.
2411
	 *
2412
	 * @param string $field The name of the field
2413
	 *
2414
	 * @return mixed The field value
2415
	 */
2416
	public function getField($field) {
2417
		// If we already have an object in $this->record, then we should just return that
2418
		if(isset($this->record[$field]) && is_object($this->record[$field]))  return $this->record[$field];
2419
2420
		// Do we have a field that needs to be lazy loaded?
2421 View Code Duplication
		if(isset($this->record[$field.'_Lazy'])) {
0 ignored issues
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...
2422
			$tableClass = $this->record[$field.'_Lazy'];
2423
			$this->loadLazyFields($tableClass);
2424
		}
2425
2426
		// Otherwise, we need to determine if this is a complex field
2427
		if(self::is_composite_field($this->class, $field)) {
2428
			$helper = $this->castingHelper($field);
2429
			$fieldObj = Object::create_from_string($helper, $field);
2430
2431
			$compositeFields = $fieldObj->compositeDatabaseFields();
2432
			foreach ($compositeFields as $compositeName => $compositeType) {
2433 View Code Duplication
				if(isset($this->record[$field.$compositeName.'_Lazy'])) {
0 ignored issues
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...
2434
					$tableClass = $this->record[$field.$compositeName.'_Lazy'];
2435
					$this->loadLazyFields($tableClass);
2436
				}
2437
			}
2438
2439
			// write value only if either the field value exists,
2440
			// or a valid record has been loaded from the database
2441
			$value = (isset($this->record[$field])) ? $this->record[$field] : null;
2442
			if($value || $this->exists()) $fieldObj->setValue($value, $this->record, false);
2443
2444
			$this->record[$field] = $fieldObj;
2445
2446
			return $this->record[$field];
2447
		}
2448
2449
		return isset($this->record[$field]) ? $this->record[$field] : null;
2450
	}
2451
2452
	/**
2453
	 * Loads all the stub fields that an initial lazy load didn't load fully.
2454
	 *
2455
	 * @param string $tableClass Base table to load the values from. Others are joined as required.
2456
	 * Not specifying a tableClass will load all lazy fields from all tables.
2457
	 * @return bool Flag if lazy loading succeeded
2458
	 */
2459
	protected function loadLazyFields($tableClass = null) {
2460
		if(!$this->isInDB() || !is_numeric($this->ID)) {
2461
			return false;
2462
		}
2463
2464
		if (!$tableClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tableClass of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2465
			$loaded = array();
2466
2467
			foreach ($this->record as $key => $value) {
2468
				if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2469
					$this->loadLazyFields($value);
2470
					$loaded[$value] = $value;
2471
				}
2472
			}
2473
2474
			return false;
2475
		}
2476
2477
		$dataQuery = new DataQuery($tableClass);
2478
2479
		// Reset query parameter context to that of this DataObject
2480
		if($params = $this->getSourceQueryParams()) {
2481
			foreach($params as $key => $value) $dataQuery->setQueryParam($key, $value);
2482
		}
2483
2484
		// Limit query to the current record, unless it has the Versioned extension,
2485
		// in which case it requires special handling through augmentLoadLazyFields()
2486
		if(!$this->hasExtension('Versioned')) {
2487
			$dataQuery->where("\"$tableClass\".\"ID\" = {$this->record['ID']}")->limit(1);
2488
		}
2489
2490
		$columns = array();
2491
2492
		// Add SQL for fields, both simple & multi-value
2493
		// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2494
		$databaseFields = self::database_fields($tableClass, false);
2495
		if($databaseFields) foreach($databaseFields as $k => $v) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $databaseFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2496
			if(!isset($this->record[$k]) || $this->record[$k] === null) {
2497
				$columns[] = $k;
2498
			}
2499
		}
2500
2501
		if ($columns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $columns of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2502
			$query = $dataQuery->query();
2503
			$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2504
			$this->extend('augmentSQL', $query, $dataQuery);
2505
2506
			$dataQuery->setQueriedColumns($columns);
2507
			$newData = $dataQuery->execute()->record();
2508
2509
			// Load the data into record
2510
			if($newData) {
2511
				foreach($newData as $k => $v) {
2512
					if (in_array($k, $columns)) {
2513
						$this->record[$k] = $v;
2514
						$this->original[$k] = $v;
2515
						unset($this->record[$k . '_Lazy']);
2516
					}
2517
				}
2518
2519
			// No data means that the query returned nothing; assign 'null' to all the requested fields
2520
			} else {
2521
				foreach($columns as $k) {
2522
					$this->record[$k] = null;
2523
					$this->original[$k] = null;
2524
					unset($this->record[$k . '_Lazy']);
2525
				}
2526
			}
2527
		}
2528
		return true;
2529
	}
2530
2531
	/**
2532
	 * Return the fields that have changed.
2533
	 *
2534
	 * The change level affects what the functions defines as "changed":
2535
	 * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2536
	 * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2537
	 *   for example a change from 0 to null would not be included.
2538
	 *
2539
	 * Example return:
2540
	 * <code>
2541
	 * array(
2542
	 *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2543
	 * )
2544
	 * </code>
2545
	 *
2546
	 * @param boolean $databaseFieldsOnly Get only database fields that have changed
2547
	 * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2548
	 * @return array
2549
	 */
2550
	public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
2551
		$changedFields = array();
2552
2553
		// Update the changed array with references to changed obj-fields
2554
		foreach($this->record as $k => $v) {
2555
			if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2556
				$this->changed[$k] = self::CHANGE_VALUE;
2557
			}
2558
		}
2559
2560
		if($databaseFieldsOnly) {
2561
			// Merge all DB fields together
2562
			$inheritedFields = $this->inheritedDatabaseFields();
2563
			$compositeFields = static::composite_fields(get_class($this));
2564
			$fixedFields = $this->config()->fixed_fields;
0 ignored issues
show
Documentation introduced by
The property fixed_fields does not exist on object<Config_ForClass>. 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...
2565
			$databaseFields = array_merge(
2566
				$inheritedFields,
2567
				$fixedFields,
2568
				$compositeFields
2569
			);
2570
			$fields = array_intersect_key((array)$this->changed, $databaseFields);
2571
		} else {
2572
			$fields = $this->changed;
2573
		}
2574
2575
		// Filter the list to those of a certain change level
2576
		if($changeLevel > self::CHANGE_STRICT) {
2577
			if($fields) foreach($fields as $name => $level) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2578
				if($level < $changeLevel) {
2579
					unset($fields[$name]);
2580
				}
2581
			}
2582
		}
2583
2584
		if($fields) foreach($fields as $name => $level) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2585
			$changedFields[$name] = array(
2586
				'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2587
				'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2588
				'level' => $level
2589
			);
2590
		}
2591
2592
		return $changedFields;
2593
	}
2594
2595
	/**
2596
	 * Uses {@link getChangedFields()} to determine if fields have been changed
2597
	 * since loading them from the database.
2598
	 *
2599
	 * @param string $fieldName Name of the database field to check, will check for any if not given
2600
	 * @param int $changeLevel See {@link getChangedFields()}
2601
	 * @return boolean
2602
	 */
2603
	public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
2604
		if (!$fieldName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldName of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2605
			// Limit "any changes" to db fields only
2606
			$changed = $this->getChangedFields(true, $changeLevel);
2607
			return !empty($changed);
2608
		} else {
2609
			// Given a field name, check all fields
2610
			$changed = $this->getChangedFields(false, $changeLevel);
2611
			return array_key_exists($fieldName, $changed);
2612
		}
2613
	}
2614
2615
	/**
2616
	 * Set the value of the field
2617
	 * Called by {@link __set()} and any setFieldName() methods you might create.
2618
	 *
2619
	 * @param string $fieldName Name of the field
2620
	 * @param mixed $val New field value
2621
	 * @return DataObject $this
2622
	 */
2623
	public function setField($fieldName, $val) {
2624
		//if it's a has_one component, destroy the cache
2625
		if (substr($fieldName, -2) == 'ID') {
2626
			unset($this->components[substr($fieldName, 0, -2)]);
2627
		}
2628
		// Situation 1: Passing an DBField
2629
		if($val instanceof DBField) {
2630
			$val->Name = $fieldName;
0 ignored issues
show
Documentation introduced by
The property Name does not exist on object<DBField>. 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...
2631
2632
			// If we've just lazy-loaded the column, then we need to populate the $original array by
2633
			// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
2634
			// on a call to getChanged()?
2635
			$this->getField($fieldName);
2636
2637
			$this->record[$fieldName] = $val;
2638
		// Situation 2: Passing a literal or non-DBField object
2639
		} else {
2640
			// If this is a proper database field, we shouldn't be getting non-DBField objects
2641
			if(is_object($val) && $this->db($fieldName)) {
2642
				user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2643
			}
2644
2645
			// if a field is not existing or has strictly changed
2646
			if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2647
				// TODO Add check for php-level defaults which are not set in the db
2648
				// TODO Add check for hidden input-fields (readonly) which are not set in the db
2649
				// At the very least, the type has changed
2650
				$this->changed[$fieldName] = self::CHANGE_STRICT;
2651
2652
				if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
2653
						&& $this->record[$fieldName] != $val)) {
2654
2655
					// Value has changed as well, not just the type
2656
					$this->changed[$fieldName] = self::CHANGE_VALUE;
2657
				}
2658
2659
				// If we've just lazy-loaded the column, then we need to populate the $original array by
2660
				// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
2661
				// on a call to getChanged()?
2662
				$this->getField($fieldName);
2663
2664
				// Value is always saved back when strict check succeeds.
2665
				$this->record[$fieldName] = $val;
2666
			}
2667
		}
2668
		return $this;
2669
	}
2670
2671
	/**
2672
	 * Set the value of the field, using a casting object.
2673
	 * This is useful when you aren't sure that a date is in SQL format, for example.
2674
	 * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2675
	 * can be saved into the Image table.
2676
	 *
2677
	 * @param string $fieldName Name of the field
2678
	 * @param mixed $value New field value
0 ignored issues
show
Bug introduced by
There is no parameter named $value. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
2679
	 * @return DataObject $this
2680
	 */
2681
	public function setCastedField($fieldName, $val) {
2682
		if(!$fieldName) {
2683
			user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2684
		}
2685
		$castingHelper = $this->castingHelper($fieldName);
2686
		if($castingHelper) {
2687
			$fieldObj = Object::create_from_string($castingHelper, $fieldName);
2688
			$fieldObj->setValue($val);
2689
			$fieldObj->saveInto($this);
2690
		} else {
2691
			$this->$fieldName = $val;
2692
		}
2693
		return $this;
2694
	}
2695
2696
	/**
2697
	 * {@inheritdoc}
2698
	 */
2699
	public function castingHelper($field) {
2700
		if ($fieldSpec = $this->db($field)) {
2701
			return $fieldSpec;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $fieldSpec; (array) is incompatible with the return type of the parent method ViewableData::castingHelper of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2702
		}
2703
2704
		// many_many_extraFields aren't presented by db(), so we check if the source query params
2705
		// provide us with meta-data for a many_many relation we can inspect for extra fields.
2706
		$queryParams = $this->getSourceQueryParams();
2707
		if (!empty($queryParams['Component.ExtraFields'])) {
2708
			$extraFields = $queryParams['Component.ExtraFields'];
2709
2710
			if (isset($extraFields[$field])) {
2711
				return $extraFields[$field];
2712
			}
2713
		}
2714
2715
		return parent::castingHelper($field);
2716
	}
2717
2718
	/**
2719
	 * Returns true if the given field exists in a database column on any of
2720
	 * the objects tables and optionally look up a dynamic getter with
2721
	 * get<fieldName>().
2722
	 *
2723
	 * @param string $field Name of the field
2724
	 * @return boolean True if the given field exists
2725
	 */
2726
	public function hasField($field) {
2727
		return (
2728
			array_key_exists($field, $this->record)
2729
			|| $this->db($field)
2730
			|| (substr($field,-2) == 'ID') && $this->hasOneComponent(substr($field,0, -2))
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->hasOneComponent(substr($field, 0, -2)) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2731
			|| $this->hasMethod("get{$field}")
2732
		);
2733
	}
2734
2735
	/**
2736
	 * Returns true if the given field exists as a database column
2737
	 *
2738
	 * @param string $field Name of the field
2739
	 *
2740
	 * @return boolean
2741
	 */
2742
	public function hasDatabaseField($field) {
2743
		if(isset(self::$fixed_fields[$field])) return true;
2744
2745
		return array_key_exists($field, $this->inheritedDatabaseFields());
2746
	}
2747
2748
	/**
2749
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2750
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2751
	 *
2752
	 * @param string $field Name of the field
2753
	 * @return string The field type of the given field
2754
	 */
2755
	public function hasOwnTableDatabaseField($field) {
2756
		return self::has_own_table_database_field($this->class, $field);
2757
	}
2758
2759
	/**
2760
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2761
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2762
	 *
2763
	 * @param string $class Class name to check
2764
	 * @param string $field Name of the field
2765
	 * @return string The field type of the given field
2766
	 */
2767
	public static function has_own_table_database_field($class, $field) {
2768
		// Since database_fields omits 'ID'
2769
		if($field == "ID") return "Int";
2770
2771
		$fieldMap = self::database_fields($class, false);
2772
2773
		// Remove string-based "constructor-arguments" from the DBField definition
2774
		if(isset($fieldMap[$field])) {
2775
			$spec = $fieldMap[$field];
2776
			if(is_string($spec)) return strtok($spec,'(');
2777
			else return $spec['type'];
2778
		}
2779
	}
2780
2781
	/**
2782
	 * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2783
	 * actually looking in the database.
2784
	 *
2785
	 * @param string $dataClass
2786
	 * @return bool
2787
	 */
2788
	public static function has_own_table($dataClass) {
2789
		if(!is_subclass_of($dataClass,'DataObject')) return false;
2790
2791
		$dataClass = ClassInfo::class_name($dataClass);
2792
		if(!isset(DataObject::$cache_has_own_table[$dataClass])) {
2793
			if(get_parent_class($dataClass) == 'DataObject') {
2794
				DataObject::$cache_has_own_table[$dataClass] = true;
2795
			} else {
2796
				DataObject::$cache_has_own_table[$dataClass]
2797
					= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
2798
					|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
2799
			}
2800
		}
2801
		return DataObject::$cache_has_own_table[$dataClass];
2802
	}
2803
2804
	/**
2805
	 * Returns true if the member is allowed to do the given action.
2806
	 * See {@link extendedCan()} for a more versatile tri-state permission control.
2807
	 *
2808
	 * @param string $perm The permission to be checked, such as 'View'.
2809
	 * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2810
	 * in user.
2811
	 *
2812
	 * @return boolean True if the the member is allowed to do the given action
2813
	 */
2814
	public function can($perm, $member = null) {
2815
		if(!isset($member)) {
2816
			$member = Member::currentUser();
2817
		}
2818
		if(Permission::checkMember($member, "ADMIN")) return true;
2819
2820
		if($this->manyManyComponent('Can' . $perm)) {
2821
			if($this->ParentID && $this->SecurityType == 'Inherit') {
2822
				if(!($p = $this->Parent)) {
0 ignored issues
show
Documentation introduced by
The property Parent does not exist on object<DataObject>. 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...
2823
					return false;
2824
				}
2825
				return $this->Parent->can($perm, $member);
2826
2827
			} else {
2828
				$permissionCache = $this->uninherited('permissionCache');
2829
				$memberID = $member ? $member->ID : 'none';
2830
2831
				if(!isset($permissionCache[$memberID][$perm])) {
2832
					if($member->ID) {
2833
						$groups = $member->Groups();
2834
					}
2835
2836
					$groupList = implode(', ', $groups->column("ID"));
0 ignored issues
show
Bug introduced by
The variable $groups does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2837
2838
					// TODO Fix relation table hardcoding
2839
					$query = new SQLQuery(
0 ignored issues
show
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
2840
						"\"Page_Can$perm\".PageID",
2841
					array("\"Page_Can$perm\""),
2842
						"GroupID IN ($groupList)");
2843
2844
					$permissionCache[$memberID][$perm] = $query->execute()->column();
2845
2846
					if($perm == "View") {
2847
						// TODO Fix relation table hardcoding
2848
						$query = new SQLQuery("\"SiteTree\".\"ID\"", array(
0 ignored issues
show
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
2849
							"\"SiteTree\"",
2850
							"LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2851
							), "\"Page_CanView\".\"PageID\" IS NULL");
0 ignored issues
show
Documentation introduced by
'"Page_CanView"."PageID" IS NULL' is of type string, but the function expects a array.

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...
2852
2853
							$unsecuredPages = $query->execute()->column();
2854
							if($permissionCache[$memberID][$perm]) {
2855
								$permissionCache[$memberID][$perm]
2856
									= array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2857
							} else {
2858
								$permissionCache[$memberID][$perm] = $unsecuredPages;
2859
							}
2860
					}
2861
2862
					Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2863
				}
2864
2865
				if($permissionCache[$memberID][$perm]) {
2866
					return in_array($this->ID, $permissionCache[$memberID][$perm]);
2867
				}
2868
			}
2869
		} else {
2870
			return parent::can($perm, $member);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ViewableData as the method can() does only exist in the following sub-classes of ViewableData: AdminRootController, AggregateTest_Bar, AggregateTest_Baz, AggregateTest_Fab, AggregateTest_Fac, AggregateTest_Foo, BasicAuthTest_ControllerSecuredWithPermission, BasicAuthTest_ControllerSecuredWithoutPermission, BulkLoaderTestPlayer, CMSFormTest_Controller, CMSMenuTest_LeftAndMainController, CMSProfileController, CMSSecurity, CheckboxFieldTest_Article, CheckboxSetFieldTest_Article, CheckboxSetFieldTest_Tag, ClassInfoTest_BaseClass, ClassInfoTest_BaseDataClass, ClassInfoTest_ChildClass, ClassInfoTest_GrandChildClass, ClassInfoTest_HasFields, ClassInfoTest_NoFields, ClassInfoTest_WithRelation, CliController, ComponentSetTest_Player, ComponentSetTest_Team, CompositeDBFieldTest_DataObject, Controller, ControllerTest_AccessBaseController, ControllerTest_AccessSecuredController, ControllerTest_AccessWildcardSecuredController, ControllerTest_ContainerController, ControllerTest_Controller, ControllerTest_HasAction, ControllerTest_HasAction_Unsecured, ControllerTest_IndexSecuredController, ControllerTest_SubController, ControllerTest_UnsecuredController, CsvBulkLoaderTest_Player, CsvBulkLoaderTest_PlayerContract, CsvBulkLoaderTest_Team, DailyTask, DataDifferencerTest_HasOneRelationObject, DataDifferencerTest_MockImage, DataDifferencerTest_Object, DataExtensionTest_CMSFieldsBase, DataExtensionTest_CMSFieldsChild, DataExtensionTest_CMSFieldsGrandchild, DataExtensionTest_Member, DataExtensionTest_MyObject, DataExtensionTest_Player, DataExtensionTest_RelatedObject, DataObject, DataObjectDuplicateTestClass1, DataObjectDuplicateTestClass2, DataObjectDuplicateTestClass3, DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO, DataObjectTest\NamespacedClass, DataObjectTest\RelationClass, DataObjectTest_Bogey, DataObjectTest_CEO, DataObjectTest_Company, DataObjectTest_EquipmentCompany, DataObjectTest_ExtendedTeamComment, DataObjectTest_Fan, DataObjectTest_FieldlessSubTable, DataObjectTest_FieldlessTable, DataObjectTest_Fixture, DataObjectTest_Play, DataObjectTest_Player, DataObjectTest_Ploy, DataObjectTest_Staff, DataObjectTest_SubEquipmentCompany, DataObjectTest_SubTeam, DataObjectTest_Team, DataObjectTest_TeamComment, DataObjectTest_ValidatedObject, DataQueryTest_A, DataQueryTest_B, DataQueryTest_C, DataQueryTest_D, DataQueryTest_E, DataQueryTest_F, DataQueryTest_G, DatabaseAdmin, DatabaseTest_MyObject, DatetimeFieldTest_Model, DbDateTimeTest_Team, DecimalTest_DataObject, DevAdminControllerTest_Controller1, DevBuildController, DevelopmentAdmin, DirectorTestRequest_Controller, EmailFieldTest_Controller, FakeController, File, FileTest_MyCustomFile, FixtureBlueprintTest_Article, FixtureBlueprintTest_Page, FixtureBlueprintTest_SiteTree, FixtureFactoryTest_DataObject, FixtureFactoryTest_DataObjectRelation, Folder, FormScaffolderTest_Article, FormScaffolderTest_Author, FormScaffolderTest_Tag, FormTest_Controller, FormTest_ControllerWithSecurityToken, FormTest_ControllerWithStrictPostCheck, FormTest_Player, FormTest_Team, FulltextFilterTest_DataObject, GridFieldAction_Delete_Team, GridFieldAction_Edit_Team, GridFieldAddExistingAutocompleterTest_Controller, GridFieldDetailFormTest_Category, GridFieldDetailFormTest_CategoryController, GridFieldDetailFormTest_Controller, GridFieldDetailFormTest_GroupController, GridFieldDetailFormTest_PeopleGroup, GridFieldDetailFormTest_Person, GridFieldExportButtonTest_NoView, GridFieldExportButtonTest_Team, GridFieldPrintButtonTest_DO, GridFieldSortableHeaderTest_Cheerleader, GridFieldSortableHeaderTest_CheerleaderHat, GridFieldSortableHeaderTest_Team, GridFieldTest_Cheerleader, GridFieldTest_Permissions, GridFieldTest_Player, GridFieldTest_Team, GridField_URLHandlerTest_Controller, Group, GroupTest_Member, HierarchyHideTest_Object, HierarchyHideTest_SubObject, HierarchyTest_Object, HourlyTask, HtmlEditorFieldTest_Object, Image, Image_Cached, InstallerTest, JSTestRunner, LeftAndMain, LeftAndMainTest_Controller, LeftAndMainTest_Object, ListboxFieldTest_Article, ListboxFieldTest_DataObject, ListboxFieldTest_Tag, LoginAttempt, ManyManyListTest_Category, ManyManyListTest_ExtraFields, ManyManyListTest_IndirectPrimary, ManyManyListTest_Product, ManyManyListTest_Secondary, ManyManyListTest_SecondarySub, Member, MemberDatetimeOptionsetFieldTest_Controller, MemberPassword, ModelAdmin, ModelAdminTest_Admin, ModelAdminTest_Contact, ModelAdminTest_Player, ModelAdminTest_PlayerAdmin, MoneyFieldTest_CustomSetter_Object, MoneyFieldTest_Object, MoneyTest_DataObject, MoneyTest_SubClass, MonthlyTask, MySQLDatabaseTest_Data, NumericFieldTest_Object, OtherSubclassWithSameField, Permission, PermissionRole, PermissionRoleCode, QuarterHourlyTask, RequestHandlingFieldTest_Controller, RequestHandlingTest_AllowedController, RequestHandlingTest_Controller, RequestHandlingTest_Cont...rFormWithAllowedActions, RequestHandlingTest_FormActionController, RestfulServiceTest_Controller, SQLInsertTestBase, SQLQueryTestBase, SQLQueryTestChild, SQLQueryTest_DO, SQLUpdateChild, SQLUpdateTestBase, SSViewerCacheBlockTest_Model, SSViewerCacheBlockTest_VersionedModel, SSViewerTest_Controller, SSViewerTest_Object, SapphireInfo, SapphireREPL, ScheduledTask, SearchContextTest_Action, SearchContextTest_AllFilterTypes, SearchContextTest_Book, SearchContextTest_Company, SearchContextTest_Deadline, SearchContextTest_Person, SearchContextTest_Project, SearchFilterApplyRelationTest_DO, SearchFilterApplyRelationTest_HasManyChild, SearchFilterApplyRelationTest_HasManyGrantChild, SearchFilterApplyRelationTest_HasManyParent, SearchFilterApplyRelationTest_HasOneChild, SearchFilterApplyRelationTest_HasOneGrantChild, SearchFilterApplyRelationTest_HasOneParent, SearchFilterApplyRelationTest_ManyManyChild, SearchFilterApplyRelationTest_ManyManyGrantChild, SearchFilterApplyRelationTest_ManyManyParent, Security, SecurityAdmin, SecurityTest_NullController, SecurityTest_SecuredController, SilverStripe\Framework\Tests\ClassI, SubclassedDBFieldObject, TaskRunner, TestRunner, TransactionTest_Object, UnsavedRelationListTest_DataObject, Upload, UploadFieldTest_Controller, UploadFieldTest_ExtendedFile, UploadFieldTest_Record, VersionableExtensionsTest_DataObject, VersionedLazySub_DataObject, VersionedLazy_DataObject, VersionedTest_AnotherSubclass, VersionedTest_DataObject, VersionedTest_PublicStage, VersionedTest_PublicViaExtension, VersionedTest_RelatedWithoutVersion, VersionedTest_SingleStage, VersionedTest_Subclass, VersionedTest_UnversionedWithField, VersionedTest_WithIndexes, WeeklyTask, XMLDataFormatterTest_DataObject, YamlFixtureTest_DataObject, YamlFixtureTest_DataObjectRelation, YearlyTask, i18nTestModule, i18nTest_DataObject, i18nTextCollectorTestMyObject, i18nTextCollectorTestMySubObject. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
2871
		}
2872
	}
2873
2874
	/**
2875
	 * Process tri-state responses from permission-alterting extensions.  The extensions are
2876
	 * expected to return one of three values:
2877
	 *
2878
	 *  - false: Disallow this permission, regardless of what other extensions say
2879
	 *  - true: Allow this permission, as long as no other extensions return false
2880
	 *  - NULL: Don't affect the outcome
2881
	 *
2882
	 * This method itself returns a tri-state value, and is designed to be used like this:
2883
	 *
2884
	 * <code>
2885
	 * $extended = $this->extendedCan('canDoSomething', $member);
2886
	 * if($extended !== null) return $extended;
2887
	 * else return $normalValue;
2888
	 * </code>
2889
	 *
2890
	 * @param String $methodName Method on the same object, e.g. {@link canEdit()}
2891
	 * @param Member|int $member
2892
	 * @return boolean|null
2893
	 */
2894
	public function extendedCan($methodName, $member) {
2895
		$results = $this->extend($methodName, $member);
2896
		if($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2897
			// Remove NULLs
2898
			$results = array_filter($results, function($v) {return !is_null($v);});
2899
			// If there are any non-NULL responses, then return the lowest one of them.
2900
			// If any explicitly deny the permission, then we don't get access
2901
			if($results) return min($results);
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2902
		}
2903
		return null;
2904
	}
2905
2906
	/**
2907
	 * @param Member $member
2908
	 * @return boolean
2909
	 */
2910 View Code Duplication
	public function canView($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2911
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2910 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2912
		if($extended !== null) {
2913
			return $extended;
2914
		}
2915
		return Permission::check('ADMIN', 'any', $member);
2916
	}
2917
2918
	/**
2919
	 * @param Member $member
2920
	 * @return boolean
2921
	 */
2922 View Code Duplication
	public function canEdit($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2923
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2922 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2924
		if($extended !== null) {
2925
			return $extended;
2926
		}
2927
		return Permission::check('ADMIN', 'any', $member);
2928
	}
2929
2930
	/**
2931
	 * @param Member $member
2932
	 * @return boolean
2933
	 */
2934 View Code Duplication
	public function canDelete($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2935
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2934 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2936
		if($extended !== null) {
2937
			return $extended;
2938
		}
2939
		return Permission::check('ADMIN', 'any', $member);
2940
	}
2941
2942
	/**
2943
	 * @todo Should canCreate be a static method?
2944
	 *
2945
	 * @param Member $member
2946
	 * @return boolean
2947
	 */
2948 View Code Duplication
	public function canCreate($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2949
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2948 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2950
		if($extended !== null) {
2951
			return $extended;
2952
		}
2953
		return Permission::check('ADMIN', 'any', $member);
2954
	}
2955
2956
	/**
2957
	 * Debugging used by Debug::show()
2958
	 *
2959
	 * @return string HTML data representing this object
2960
	 */
2961
	public function debug() {
2962
		$val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2963
		if($this->record) foreach($this->record as $fieldName => $fieldVal) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2964
			$val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2965
		}
2966
		$val .= "</ul>\n";
2967
		return $val;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $val; (string) is incompatible with the return type of the parent method ViewableData::Debug of type ViewableData_Debugger.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2968
	}
2969
2970
	/**
2971
	 * Return the DBField object that represents the given field.
2972
	 * This works similarly to obj() with 2 key differences:
2973
	 *   - it still returns an object even when the field has no value.
2974
	 *   - it only matches fields and not methods
2975
	 *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2976
	 *
2977
	 * @param string $fieldName Name of the field
2978
	 * @return DBField The field as a DBField object
2979
	 */
2980
	public function dbObject($fieldName) {
2981
		// If we have a CompositeDBField object in $this->record, then return that
2982
		if(isset($this->record[$fieldName]) && is_object($this->record[$fieldName])) {
2983
			return $this->record[$fieldName];
2984
2985
		// Special case for ID field
2986
		} else if($fieldName == 'ID') {
2987
			return new PrimaryKey($fieldName, $this);
0 ignored issues
show
Documentation introduced by
$this is of type this<DataObject>, but the function expects a object<DataOject>.

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...
2988
2989
		// Special case for ClassName
2990
		} else if($fieldName == 'ClassName') {
2991
			$val = get_class($this);
2992
			return DBField::create_field('Varchar', $val, $fieldName);
2993
2994
		} else if(array_key_exists($fieldName, self::$fixed_fields)) {
2995
			return DBField::create_field(self::$fixed_fields[$fieldName], $this->$fieldName, $fieldName);
2996
2997
		// General casting information for items in $db
2998
		} else if($helper = $this->db($fieldName)) {
2999
			$obj = Object::create_from_string($helper, $fieldName);
3000
			$obj->setValue($this->$fieldName, $this->record, false);
3001
			return $obj;
3002
3003
		// Special case for has_one relationships
3004
		} else if(preg_match('/ID$/', $fieldName) && $this->hasOneComponent(substr($fieldName,0,-2))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->hasOneComponent(substr($fieldName, 0, -2)) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3005
			$val = $this->$fieldName;
3006
			return DBField::create_field('ForeignKey', $val, $fieldName, $this);
3007
3008
		// has_one for polymorphic relations do not end in ID
3009
		} else if(($type = $this->hasOneComponent($fieldName)) && ($type === 'DataObject')) {
3010
			$val = $this->$fieldName();
3011
			return DBField::create_field('PolymorphicForeignKey', $val, $fieldName, $this);
3012
3013
		}
3014
	}
3015
3016
	/**
3017
	 * Traverses to a DBField referenced by relationships between data objects.
3018
	 *
3019
	 * The path to the related field is specified with dot separated syntax
3020
	 * (eg: Parent.Child.Child.FieldName).
3021
	 *
3022
	 * @param string $fieldPath
3023
	 *
3024
	 * @return mixed DBField of the field on the object or a DataList instance.
3025
	 */
3026
	public function relObject($fieldPath) {
3027
		$object = null;
0 ignored issues
show
Unused Code introduced by
$object is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
3028
3029
		if(strpos($fieldPath, '.') !== false) {
3030
			$parts = explode('.', $fieldPath);
3031
			$fieldName = array_pop($parts);
3032
3033
			// Traverse dot syntax
3034
			$component = $this;
3035
3036 View Code Duplication
			foreach($parts as $relation) {
0 ignored issues
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...
3037
				if($component instanceof SS_List) {
3038
					if(method_exists($component,$relation)) {
3039
						$component = $component->$relation();
3040
					} else {
3041
						$component = $component->relation($relation);
3042
					}
3043
				} else {
3044
					$component = $component->$relation();
3045
				}
3046
			}
3047
3048
			$object = $component->dbObject($fieldName);
3049
3050
		} else {
3051
			$object = $this->dbObject($fieldPath);
3052
		}
3053
3054
		return $object;
3055
	}
3056
3057
	/**
3058
	 * Traverses to a field referenced by relationships between data objects, returning the value
3059
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3060
	 *
3061
	 * @param $fieldPath string
3062
	 * @return string | null - will return null on a missing value
3063
	 */
3064
	public function relField($fieldName) {
3065
		$component = $this;
3066
3067
		// We're dealing with relations here so we traverse the dot syntax
3068
		if(strpos($fieldName, '.') !== false) {
3069
			$relations = explode('.', $fieldName);
3070
			$fieldName = array_pop($relations);
3071
			foreach($relations as $relation) {
3072
				// Inspect $component for element $relation
3073
				if($component->hasMethod($relation)) {
3074
					// Check nested method
3075
					$component = $component->$relation();
3076
				} elseif($component instanceof SS_List) {
3077
					// Select adjacent relation from DataList
3078
					$component = $component->relation($relation);
3079
				} elseif($component instanceof DataObject
3080
					&& ($dbObject = $component->dbObject($relation))
3081
				) {
3082
					// Select db object
3083
					$component = $dbObject;
3084
				} else {
3085
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
3086
				}
3087
			}
3088
		}
3089
3090
		// Bail if the component is null
3091
		if(!$component) {
3092
			return null;
3093
		}
3094
		if($component->hasMethod($fieldName)) {
3095
			return $component->$fieldName();
3096
		}
3097
		return $component->$fieldName;
3098
	}
3099
3100
	/**
3101
	 * Temporary hack to return an association name, based on class, to get around the mangle
3102
	 * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3103
	 *
3104
	 * @return String
3105
	 */
3106
	public function getReverseAssociation($className) {
3107
		if (is_array($this->manyMany())) {
3108
			$many_many = array_flip($this->manyMany());
3109
			if (array_key_exists($className, $many_many)) return $many_many[$className];
3110
		}
3111
		if (is_array($this->hasMany())) {
3112
			$has_many = array_flip($this->hasMany());
3113
			if (array_key_exists($className, $has_many)) return $has_many[$className];
3114
		}
3115
		if (is_array($this->hasOne())) {
3116
			$has_one = array_flip($this->hasOne());
3117
			if (array_key_exists($className, $has_one)) return $has_one[$className];
3118
		}
3119
3120
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by DataObject::getReverseAssociation of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
3121
	}
3122
3123
	/**
3124
	 * Return all objects matching the filter
3125
	 * sub-classes are automatically selected and included
3126
	 *
3127
	 * @param string $callerClass The class of objects to be returned
3128
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3129
	 * Supports parameterised queries. See SQLQuery::addWhere() for syntax examples.
3130
	 * @param string|array $sort A sort expression to be inserted into the ORDER
3131
	 * BY clause.  If omitted, self::$default_sort will be used.
3132
	 * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3133
	 * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3134
	 * @param string $containerClass The container class to return the results in.
3135
	 *
3136
	 * @todo $containerClass is Ignored, why?
3137
	 *
3138
	 * @return DataList The objects matching the filter, in the class specified by $containerClass
3139
	 */
3140
	public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
3141
			$containerClass = 'DataList') {
3142
3143
		if($callerClass == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $callerClass of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
3144
			$callerClass = get_called_class();
3145
			if($callerClass == 'DataObject') {
3146
				throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3147
			}
3148
3149
			if($filter || $sort || $join || $limit || ($containerClass != 'DataList')) {
3150
				throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3151
					. ' arguments');
3152
			}
3153
3154
			$result = DataList::create(get_called_class());
3155
			$result->setDataModel(DataModel::inst());
3156
			return $result;
3157
		}
3158
3159
		if($join) {
3160
			throw new \InvalidArgumentException(
3161
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3162
			);
3163
		}
3164
3165
		$result = DataList::create($callerClass)->where($filter)->sort($sort);
3166
3167
		if($limit && strpos($limit, ',') !== false) {
3168
			$limitArguments = explode(',', $limit);
3169
			$result = $result->limit($limitArguments[1],$limitArguments[0]);
3170
		} elseif($limit) {
3171
			$result = $result->limit($limit);
0 ignored issues
show
Documentation introduced by
$limit is of type string|array, but the function expects a integer.

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...
3172
		}
3173
3174
		$result->setDataModel(DataModel::inst());
3175
		return $result;
3176
	}
3177
3178
3179
	/**
3180
	 * @deprecated
3181
	 */
3182
	public function Aggregate($class = null) {
3183
		Deprecation::notice('4.0', 'Call aggregate methods on a DataList directly instead. In templates'
3184
			. ' an example of the new syntax is &lt% cached List(Member).max(LastEdited) %&gt instead'
3185
			. ' (check partial-caching.md documentation for more details.)');
3186
3187
		if($class) {
3188
			$list = new DataList($class);
3189
			$list->setDataModel(DataModel::inst());
3190
		} else if(isset($this)) {
3191
			$list = new DataList(get_class($this));
3192
			$list->setDataModel($this->model);
3193
		} else {
3194
			throw new \InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed"
3195
				. " a classname");
3196
		}
3197
		return $list;
3198
	}
3199
3200
	/**
3201
	 * @deprecated
3202
	 */
3203
	public function RelationshipAggregate($relationship) {
3204
		Deprecation::notice('4.0', 'Call aggregate methods on a relationship directly instead.');
3205
3206
		return $this->$relationship();
3207
	}
3208
3209
	/**
3210
	 * Return the first item matching the given query.
3211
	 * All calls to get_one() are cached.
3212
	 *
3213
	 * @param string $callerClass The class of objects to be returned
3214
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3215
	 * Supports parameterised queries. See SQLQuery::addWhere() for syntax examples.
3216
	 * @param boolean $cache Use caching
3217
	 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3218
	 *
3219
	 * @return DataObject The first item matching the query
3220
	 */
3221
	public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
3222
		$SNG = singleton($callerClass);
3223
3224
		$cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3225
		$cacheKey = md5(var_export($cacheComponents, true));
3226
3227
		// Flush destroyed items out of the cache
3228
		if($cache && isset(DataObject::$_cache_get_one[$callerClass][$cacheKey])
3229
				&& DataObject::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
3230
				&& DataObject::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
3231
3232
			DataObject::$_cache_get_one[$callerClass][$cacheKey] = false;
3233
		}
3234
		if(!$cache || !isset(DataObject::$_cache_get_one[$callerClass][$cacheKey])) {
3235
			$dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
3236
			$item = $dl->First();
3237
3238
			if($cache) {
3239
				DataObject::$_cache_get_one[$callerClass][$cacheKey] = $item;
3240
				if(!DataObject::$_cache_get_one[$callerClass][$cacheKey]) {
3241
					DataObject::$_cache_get_one[$callerClass][$cacheKey] = false;
3242
				}
3243
			}
3244
		}
3245
		return $cache ? DataObject::$_cache_get_one[$callerClass][$cacheKey] : $item;
0 ignored issues
show
Bug introduced by
The variable $item does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3246
	}
3247
3248
	/**
3249
	 * Flush the cached results for all relations (has_one, has_many, many_many)
3250
	 * Also clears any cached aggregate data.
3251
	 *
3252
	 * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3253
	 *                            When false will just clear session-local cached data
3254
	 * @return DataObject $this
3255
	 */
3256
	public function flushCache($persistent = true) {
3257
		if($persistent) Aggregate::flushCache($this->class);
3258
3259
		if($this->class == 'DataObject') {
3260
			DataObject::$_cache_get_one = array();
3261
			return $this;
3262
		}
3263
3264
		$classes = ClassInfo::ancestry($this->class);
3265
		foreach($classes as $class) {
3266
			if(isset(DataObject::$_cache_get_one[$class])) unset(DataObject::$_cache_get_one[$class]);
3267
		}
3268
3269
		$this->extend('flushCache');
3270
3271
		$this->components = array();
3272
		return $this;
3273
	}
3274
3275
	/**
3276
	 * Flush the get_one global cache and destroy associated objects.
3277
	 */
3278
	public static function flush_and_destroy_cache() {
3279
		if(DataObject::$_cache_get_one) foreach(DataObject::$_cache_get_one as $class => $items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \DataObject::$_cache_get_one of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3280
			if(is_array($items)) foreach($items as $item) {
3281
				if($item) $item->destroy();
3282
			}
3283
		}
3284
		DataObject::$_cache_get_one = array();
3285
	}
3286
3287
	/**
3288
	 * Reset all global caches associated with DataObject.
3289
	 */
3290
	public static function reset() {
3291
		self::clear_classname_spec_cache();
3292
		DataObject::$cache_has_own_table = array();
3293
		DataObject::$_cache_db = array();
3294
		DataObject::$_cache_get_one = array();
3295
		DataObject::$_cache_composite_fields = array();
3296
		DataObject::$_cache_is_composite_field = array();
3297
		DataObject::$_cache_custom_database_fields = array();
3298
		DataObject::$_cache_get_class_ancestry = array();
3299
		DataObject::$_cache_field_labels = array();
3300
	}
3301
3302
	/**
3303
	 * Return the given element, searching by ID
3304
	 *
3305
	 * @param string $callerClass The class of the object to be returned
3306
	 * @param int $id The id of the element
3307
	 * @param boolean $cache See {@link get_one()}
3308
	 *
3309
	 * @return DataObject The element
3310
	 */
3311
	public static function get_by_id($callerClass, $id, $cache = true) {
3312
		if(!is_numeric($id)) {
3313
			user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3314
		}
3315
3316
		// Check filter column
3317
		if(is_subclass_of($callerClass, 'DataObject')) {
3318
			$baseClass = ClassInfo::baseDataClass($callerClass);
3319
			$column = "\"$baseClass\".\"ID\"";
3320
		} else{
3321
			// This simpler code will be used by non-DataObject classes that implement DataObjectInterface
3322
			$column = '"ID"';
3323
		}
3324
3325
		// Relegate to get_one
3326
		return DataObject::get_one($callerClass, array($column => $id), $cache);
3327
	}
3328
3329
	/**
3330
	 * Get the name of the base table for this object
3331
	 */
3332
	public function baseTable() {
3333
		$tableClasses = ClassInfo::dataClassesFor($this->class);
3334
		return array_shift($tableClasses);
3335
	}
3336
3337
	/**
3338
	 * @var Array Parameters used in the query that built this object.
3339
	 * This can be used by decorators (e.g. lazy loading) to
3340
	 * run additional queries using the same context.
3341
	 */
3342
	protected $sourceQueryParams;
3343
3344
	/**
3345
	 * @see $sourceQueryParams
3346
	 * @return array
3347
	 */
3348
	public function getSourceQueryParams() {
3349
		return $this->sourceQueryParams;
3350
	}
3351
3352
	/**
3353
	 * @see $sourceQueryParams
3354
	 * @param array
3355
	 */
3356
	public function setSourceQueryParams($array) {
3357
		$this->sourceQueryParams = $array;
3358
	}
3359
3360
	/**
3361
	 * @see $sourceQueryParams
3362
	 * @param array
3363
	 */
3364
	public function setSourceQueryParam($key, $value) {
3365
		$this->sourceQueryParams[$key] = $value;
3366
	}
3367
3368
	/**
3369
	 * @see $sourceQueryParams
3370
	 * @return Mixed
3371
	 */
3372
	public function getSourceQueryParam($key) {
3373
		if(isset($this->sourceQueryParams[$key])) return $this->sourceQueryParams[$key];
3374
		else return null;
3375
	}
3376
3377
	//-------------------------------------------------------------------------------------------//
3378
3379
	/**
3380
	 * Return the database indexes on this table.
3381
	 * This array is indexed by the name of the field with the index, and
3382
	 * the value is the type of index.
3383
	 */
3384
	public function databaseIndexes() {
3385
		$has_one = $this->uninherited('has_one',true);
0 ignored issues
show
Unused Code introduced by
The call to DataObject::uninherited() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
3386
		$classIndexes = $this->uninherited('indexes',true);
0 ignored issues
show
Unused Code introduced by
The call to DataObject::uninherited() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
3387
		//$fileIndexes = $this->uninherited('fileIndexes', true);
3388
3389
		$indexes = array();
3390
3391
		if($has_one) {
3392
			foreach($has_one as $relationshipName => $fieldType) {
0 ignored issues
show
Bug introduced by
The expression $has_one of type array|integer|double|string|boolean 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...
3393
				$indexes[$relationshipName . 'ID'] = true;
3394
			}
3395
		}
3396
3397
		if($classIndexes) {
3398
			foreach($classIndexes as $indexName => $indexType) {
0 ignored issues
show
Bug introduced by
The expression $classIndexes of type array|integer|double|string|boolean 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...
3399
				$indexes[$indexName] = $indexType;
3400
			}
3401
		}
3402
3403
		if(get_parent_class($this) == "DataObject") {
3404
			$indexes['ClassName'] = true;
3405
		}
3406
3407
		return $indexes;
3408
	}
3409
3410
	/**
3411
	 * Check the database schema and update it as necessary.
3412
	 *
3413
	 * @uses DataExtension->augmentDatabase()
3414
	 */
3415
	public function requireTable() {
3416
		// Only build the table if we've actually got fields
3417
		$fields = self::database_fields($this->class);
3418
		$extensions = self::database_extensions($this->class);
3419
3420
		$indexes = $this->databaseIndexes();
3421
3422
		// Validate relationship configuration
3423
		$this->validateModelDefinitions();
3424
3425
		if($fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3426
			$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
3427
			DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
0 ignored issues
show
Documentation introduced by
$fields is of type array, but the function expects a string|null.

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...
Documentation introduced by
$indexes is of type array, but the function expects a string|null.

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...
3428
				$extensions);
3429
		} else {
3430
			DB::dont_require_table($this->class);
3431
		}
3432
3433
		// Build any child tables for many_many items
3434
		if($manyMany = $this->uninherited('many_many', true)) {
0 ignored issues
show
Unused Code introduced by
The call to DataObject::uninherited() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
3435
			$extras = $this->uninherited('many_many_extraFields', true);
0 ignored issues
show
Unused Code introduced by
The call to DataObject::uninherited() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
3436
			foreach($manyMany as $relationship => $childClass) {
0 ignored issues
show
Bug introduced by
The expression $manyMany of type array|integer|double|string|boolean 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...
3437
				// Build field list
3438
				$manymanyFields = array(
3439
					"{$this->class}ID" => "Int",
3440
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => "Int",
3441
				);
3442
				if(isset($extras[$relationship])) {
3443
					$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
3444
				}
3445
3446
				// Build index list
3447
				$manymanyIndexes = array(
3448
					"{$this->class}ID" => true,
3449
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true,
3450
				);
3451
3452
				DB::require_table("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null,
0 ignored issues
show
Documentation introduced by
$manymanyFields is of type array<?,string>, but the function expects a string|null.

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...
Documentation introduced by
$manymanyIndexes is of type array<?,boolean>, but the function expects a string|null.

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...
3453
					$extensions);
3454
			}
3455
		}
3456
3457
		// Let any extentions make their own database fields
3458
		$this->extend('augmentDatabase', $dummy);
3459
	}
3460
3461
	/**
3462
	 * Validate that the configured relations for this class use the correct syntaxes
3463
	 * @throws LogicException
3464
	 */
3465
	protected function validateModelDefinitions() {
3466
		$modelDefinitions = array(
3467
			'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
3468
			'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
3469
			'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
3470
			'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
3471
			'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
3472
			'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
3473
			'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
3474
		);
3475
3476
		foreach($modelDefinitions as $defType => $relations) {
3477
			if( ! $relations) continue;
3478
3479
			foreach($relations as $k => $v) {
0 ignored issues
show
Bug introduced by
The expression $relations of type array|integer|double|string|boolean 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...
3480
				if($defType === 'many_many_extraFields') {
3481
					if(!is_array($v)) {
3482
						throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
3483
							. var_export($k, true) . " => " . var_export($v, true)
3484
							. ". Each many_many_extraFields entry should map to a field specification array.");
3485
					}
3486
				} else {
3487
					if(!is_string($k) || is_numeric($k) || !is_string($v)) {
3488
						throw new LogicException("$this->class::$defType has a bad entry: "
3489
							. var_export($k, true). " => " . var_export($v, true) . ".  Each map key should be a
3490
							 relationship name, and the map value should be the data class to join to.");
3491
					}
3492
				}
3493
			}
3494
		}
3495
	}
3496
3497
	/**
3498
	 * Add default records to database. This function is called whenever the
3499
	 * database is built, after the database tables have all been created. Overload
3500
	 * this to add default records when the database is built, but make sure you
3501
	 * call parent::requireDefaultRecords().
3502
	 *
3503
	 * @uses DataExtension->requireDefaultRecords()
3504
	 */
3505
	public function requireDefaultRecords() {
3506
		$defaultRecords = $this->stat('default_records');
3507
3508
		if(!empty($defaultRecords)) {
3509
			$hasData = DataObject::get_one($this->class);
3510
			if(!$hasData) {
3511
				$className = $this->class;
3512
				foreach($defaultRecords as $record) {
0 ignored issues
show
Bug introduced by
The expression $defaultRecords of type array|integer|double|string|boolean 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...
3513
					$obj = $this->model->$className->newObject($record);
3514
					$obj->write();
3515
				}
3516
				DB::alteration_message("Added default records to $className table","created");
3517
			}
3518
		}
3519
3520
		// Let any extentions make their own database default data
3521
		$this->extend('requireDefaultRecords', $dummy);
3522
	}
3523
3524
	/**
3525
	 * Returns fields bu traversing the class heirachy in a bottom-up direction.
3526
	 *
3527
	 * Needed to avoid getCMSFields being empty when customDatabaseFields overlooks
3528
	 * the inheritance chain of the $db array, where a child data object has no $db array,
3529
	 * but still needs to know the properties of its parent. This should be merged into databaseFields or
3530
	 * customDatabaseFields.
3531
	 *
3532
	 * @todo review whether this is still needed after recent API changes
3533
	 */
3534
	public function inheritedDatabaseFields() {
3535
		$fields     = array();
3536
		$currentObj = $this->class;
3537
3538
		while($currentObj != 'DataObject') {
3539
			$fields     = array_merge($fields, self::custom_database_fields($currentObj));
3540
			$currentObj = get_parent_class($currentObj);
3541
		}
3542
3543
		return (array) $fields;
3544
	}
3545
3546
	/**
3547
	 * Get the default searchable fields for this object, as defined in the
3548
	 * $searchable_fields list. If searchable fields are not defined on the
3549
	 * data object, uses a default selection of summary fields.
3550
	 *
3551
	 * @return array
3552
	 */
3553
	public function searchableFields() {
3554
		// can have mixed format, need to make consistent in most verbose form
3555
		$fields = $this->stat('searchable_fields');
3556
		$labels = $this->fieldLabels();
3557
3558
		// fallback to summary fields (unless empty array is explicitly specified)
3559
		if( ! $fields && ! is_array($fields)) {
3560
			$summaryFields = array_keys($this->summaryFields());
3561
			$fields = array();
3562
3563
			// remove the custom getters as the search should not include them
3564
			if($summaryFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $summaryFields of type array<integer|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3565
				foreach($summaryFields as $key => $name) {
3566
					$spec = $name;
3567
3568
					// Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3569 View Code Duplication
					if(($fieldPos = strpos($name, '.')) !== false) {
0 ignored issues
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...
3570
						$name = substr($name, 0, $fieldPos);
3571
					}
3572
3573
					if($this->hasDatabaseField($name)) {
3574
						$fields[] = $name;
3575
					} elseif($this->relObject($spec)) {
3576
						$fields[] = $spec;
3577
					}
3578
				}
3579
			}
3580
		}
3581
3582
		// we need to make sure the format is unified before
3583
		// augmenting fields, so extensions can apply consistent checks
3584
		// but also after augmenting fields, because the extension
3585
		// might use the shorthand notation as well
3586
3587
		// rewrite array, if it is using shorthand syntax
3588
		$rewrite = array();
3589
		foreach($fields as $name => $specOrName) {
0 ignored issues
show
Bug introduced by
The expression $fields of type array|integer|double|string|boolean 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...
3590
			$identifer = (is_int($name)) ? $specOrName : $name;
3591
3592
			if(is_int($name)) {
3593
				// Format: array('MyFieldName')
3594
				$rewrite[$identifer] = array();
3595
			} elseif(is_array($specOrName)) {
3596
				// Format: array('MyFieldName' => array(
3597
				//   'filter => 'ExactMatchFilter',
3598
				//   'field' => 'NumericField', // optional
3599
				//   'title' => 'My Title', // optional
3600
				// ))
3601
				$rewrite[$identifer] = array_merge(
3602
					array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3603
					(array)$specOrName
3604
				);
3605
			} else {
3606
				// Format: array('MyFieldName' => 'ExactMatchFilter')
3607
				$rewrite[$identifer] = array(
3608
					'filter' => $specOrName,
3609
				);
3610
			}
3611
			if(!isset($rewrite[$identifer]['title'])) {
3612
				$rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3613
					? $labels[$identifer] : FormField::name_to_label($identifer);
3614
			}
3615
			if(!isset($rewrite[$identifer]['filter'])) {
3616
				$rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3617
			}
3618
		}
3619
3620
		$fields = $rewrite;
3621
3622
		// apply DataExtensions if present
3623
		$this->extend('updateSearchableFields', $fields);
3624
3625
		return $fields;
3626
	}
3627
3628
	/**
3629
	 * Get any user defined searchable fields labels that
3630
	 * exist. Allows overriding of default field names in the form
3631
	 * interface actually presented to the user.
3632
	 *
3633
	 * The reason for keeping this separate from searchable_fields,
3634
	 * which would be a logical place for this functionality, is to
3635
	 * avoid bloating and complicating the configuration array. Currently
3636
	 * much of this system is based on sensible defaults, and this property
3637
	 * would generally only be set in the case of more complex relationships
3638
	 * between data object being required in the search interface.
3639
	 *
3640
	 * Generates labels based on name of the field itself, if no static property
3641
	 * {@link self::field_labels} exists.
3642
	 *
3643
	 * @uses $field_labels
3644
	 * @uses FormField::name_to_label()
3645
	 *
3646
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3647
	 *
3648
	 * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3649
	 */
3650
	public function fieldLabels($includerelations = true) {
3651
		$cacheKey = $this->class . '_' . $includerelations;
3652
3653
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
3654
			$customLabels = $this->stat('field_labels');
3655
			$autoLabels = array();
3656
3657
			// get all translated static properties as defined in i18nCollectStatics()
3658
			$ancestry = ClassInfo::ancestry($this->class);
3659
			$ancestry = array_reverse($ancestry);
3660
			if($ancestry) foreach($ancestry as $ancestorClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ancestry of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3661
				if($ancestorClass == 'ViewableData') break;
3662
				$types = array(
3663
					'db'        => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3664
				);
3665
				if($includerelations){
3666
					$types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3667
					$types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3668
					$types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3669
					$types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3670
				}
3671
				foreach($types as $type => $attrs) {
3672
					foreach($attrs as $name => $spec) {
3673
						$autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}",FormField::name_to_label($name));
3674
					}
3675
				}
3676
			}
3677
3678
			$labels = array_merge((array)$autoLabels, (array)$customLabels);
3679
			$this->extend('updateFieldLabels', $labels);
3680
			self::$_cache_field_labels[$cacheKey] = $labels;
3681
		}
3682
3683
		return self::$_cache_field_labels[$cacheKey];
3684
	}
3685
3686
	/**
3687
	 * Get a human-readable label for a single field,
3688
	 * see {@link fieldLabels()} for more details.
3689
	 *
3690
	 * @uses fieldLabels()
3691
	 * @uses FormField::name_to_label()
3692
	 *
3693
	 * @param string $name Name of the field
3694
	 * @return string Label of the field
3695
	 */
3696
	public function fieldLabel($name) {
3697
		$labels = $this->fieldLabels();
3698
		return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3699
	}
3700
3701
	/**
3702
	 * Get the default summary fields for this object.
3703
	 *
3704
	 * @todo use the translation apparatus to return a default field selection for the language
3705
	 *
3706
	 * @return array
3707
	 */
3708
	public function summaryFields() {
3709
		$fields = $this->stat('summary_fields');
3710
3711
		// if fields were passed in numeric array,
3712
		// convert to an associative array
3713
		if($fields && array_key_exists(0, $fields)) {
3714
			$fields = array_combine(array_values($fields), array_values($fields));
3715
		}
3716
3717
		if (!$fields) {
3718
			$fields = array();
3719
			// try to scaffold a couple of usual suspects
3720
			if ($this->hasField('Name')) $fields['Name'] = 'Name';
3721
			if ($this->hasDataBaseField('Title')) $fields['Title'] = 'Title';
3722
			if ($this->hasField('Description')) $fields['Description'] = 'Description';
3723
			if ($this->hasField('FirstName')) $fields['FirstName'] = 'First Name';
3724
		}
3725
		$this->extend("updateSummaryFields", $fields);
3726
3727
		// Final fail-over, just list ID field
3728
		if(!$fields) $fields['ID'] = 'ID';
3729
3730
		// Localize fields (if possible)
3731
		foreach($this->fieldLabels(false) as $name => $label) {
0 ignored issues
show
Bug introduced by
The expression $this->fieldLabels(false) of type array|string 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...
3732
			// only attempt to localize if the label definition is the same as the field name.
3733
			// this will preserve any custom labels set in the summary_fields configuration
3734
			if(isset($fields[$name]) && $name === $fields[$name]) {
3735
				$fields[$name] = $label;
3736
			}
3737
		}
3738
3739
		return $fields;
3740
	}
3741
3742
	/**
3743
	 * Defines a default list of filters for the search context.
3744
	 *
3745
	 * If a filter class mapping is defined on the data object,
3746
	 * it is constructed here. Otherwise, the default filter specified in
3747
	 * {@link DBField} is used.
3748
	 *
3749
	 * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3750
	 *
3751
	 * @return array
3752
	 */
3753
	public function defaultSearchFilters() {
3754
		$filters = array();
3755
3756
		foreach($this->searchableFields() as $name => $spec) {
3757
			$filterClass = $spec['filter'];
0 ignored issues
show
Unused Code introduced by
$filterClass is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
3758
3759
			if($spec['filter'] instanceof SearchFilter) {
3760
				$filters[$name] = $spec['filter'];
3761
			} else {
3762
				$class = $spec['filter'];
3763
3764
				if(!is_subclass_of($spec['filter'], 'SearchFilter')) {
3765
					$class = 'PartialMatchFilter';
3766
				}
3767
3768
				$filters[$name] = new $class($name);
3769
			}
3770
		}
3771
3772
		return $filters;
3773
	}
3774
3775
	/**
3776
	 * @return boolean True if the object is in the database
3777
	 */
3778
	public function isInDB() {
3779
		return is_numeric( $this->ID ) && $this->ID > 0;
3780
	}
3781
3782
	/*
3783
	 * @ignore
3784
	 */
3785
	private static $subclass_access = true;
3786
3787
	/**
3788
	 * Temporarily disable subclass access in data object qeur
3789
	 */
3790
	public static function disable_subclass_access() {
3791
		self::$subclass_access = false;
3792
	}
3793
	public static function enable_subclass_access() {
3794
		self::$subclass_access = true;
3795
	}
3796
3797
	//-------------------------------------------------------------------------------------------//
3798
3799
	/**
3800
	 * Database field definitions.
3801
	 * This is a map from field names to field type. The field
3802
	 * type should be a class that extends .
3803
	 * @var array
3804
	 * @config
3805
	 */
3806
	private static $db = null;
3807
3808
	/**
3809
	 * Use a casting object for a field. This is a map from
3810
	 * field name to class name of the casting object.
3811
	 * @var array
3812
	 */
3813
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
3814
		"ID" => 'Int',
3815
		"ClassName" => 'Varchar',
3816
		"LastEdited" => "SS_Datetime",
3817
		"Created" => "SS_Datetime",
3818
		"Title" => 'Text',
3819
	);
3820
3821
	/**
3822
	 * Specify custom options for a CREATE TABLE call.
3823
	 * Can be used to specify a custom storage engine for specific database table.
3824
	 * All options have to be keyed for a specific database implementation,
3825
	 * identified by their class name (extending from {@link SS_Database}).
3826
	 *
3827
	 * <code>
3828
	 * array(
3829
	 *  'MySQLDatabase' => 'ENGINE=MyISAM'
3830
	 * )
3831
	 * </code>
3832
	 *
3833
	 * Caution: This API is experimental, and might not be
3834
	 * included in the next major release. Please use with care.
3835
	 *
3836
	 * @var array
3837
	 * @config
3838
	 */
3839
	private static $create_table_options = array(
3840
		'MySQLDatabase' => 'ENGINE=InnoDB'
3841
	);
3842
3843
	/**
3844
	 * If a field is in this array, then create a database index
3845
	 * on that field. This is a map from fieldname to index type.
3846
	 * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3847
	 *
3848
	 * @var array
3849
	 * @config
3850
	 */
3851
	private static $indexes = null;
3852
3853
	/**
3854
	 * Inserts standard column-values when a DataObject
3855
	 * is instanciated. Does not insert default records {@see $default_records}.
3856
	 * This is a map from fieldname to default value.
3857
	 *
3858
	 *  - If you would like to change a default value in a sub-class, just specify it.
3859
	 *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3860
	 *    or false in your subclass.  Setting it to null won't work.
3861
	 *
3862
	 * @var array
3863
	 * @config
3864
	 */
3865
	private static $defaults = null;
3866
3867
	/**
3868
	 * Multidimensional array which inserts default data into the database
3869
	 * on a db/build-call as long as the database-table is empty. Please use this only
3870
	 * for simple constructs, not for SiteTree-Objects etc. which need special
3871
	 * behaviour such as publishing and ParentNodes.
3872
	 *
3873
	 * Example:
3874
	 * array(
3875
	 *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3876
	 *  array('Title' => "DefaultPage2")
3877
	 * ).
3878
	 *
3879
	 * @var array
3880
	 * @config
3881
	 */
3882
	private static $default_records = null;
3883
3884
	/**
3885
	 * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3886
	 * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3887
	 *
3888
	 * Note that you cannot have a has_one and belongs_to relationship with the same name.
3889
	 *
3890
	 *	@var array
3891
	 * @config
3892
	 */
3893
	private static $has_one = null;
3894
3895
	/**
3896
	 * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3897
	 *
3898
	 * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3899
	 * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3900
	 * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3901
	 *
3902
	 * Note that you cannot have a has_one and belongs_to relationship with the same name.
3903
	 *
3904
	 * @var array
3905
	 * @config
3906
	 */
3907
	private static $belongs_to;
3908
3909
	/**
3910
	 * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3911
	 *
3912
	 * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3913
	 * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3914
	 * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3915
	 * which foreign key to use.
3916
	 *
3917
	 * @var array
3918
	 * @config
3919
	 */
3920
	private static $has_many = null;
3921
3922
	/**
3923
	 * many-many relationship definitions.
3924
	 * This is a map from component name to data type.
3925
	 * @var array
3926
	 * @config
3927
	 */
3928
	private static $many_many = null;
3929
3930
	/**
3931
	 * Extra fields to include on the connecting many-many table.
3932
	 * This is a map from field name to field type.
3933
	 *
3934
	 * Example code:
3935
	 * <code>
3936
	 * public static $many_many_extraFields = array(
3937
	 *  'Members' => array(
3938
	 *			'Role' => 'Varchar(100)'
3939
	 *		)
3940
	 * );
3941
	 * </code>
3942
	 *
3943
	 * @var array
3944
	 * @config
3945
	 */
3946
	private static $many_many_extraFields = null;
3947
3948
	/**
3949
	 * The inverse side of a many-many relationship.
3950
	 * This is a map from component name to data type.
3951
	 * @var array
3952
	 * @config
3953
	 */
3954
	private static $belongs_many_many = null;
3955
3956
	/**
3957
	 * The default sort expression. This will be inserted in the ORDER BY
3958
	 * clause of a SQL query if no other sort expression is provided.
3959
	 * @var string
3960
	 * @config
3961
	 */
3962
	private static $default_sort = null;
3963
3964
	/**
3965
	 * Default list of fields that can be scaffolded by the ModelAdmin
3966
	 * search interface.
3967
	 *
3968
	 * Overriding the default filter, with a custom defined filter:
3969
	 * <code>
3970
	 *  static $searchable_fields = array(
3971
	 *     "Name" => "PartialMatchFilter"
3972
	 *  );
3973
	 * </code>
3974
	 *
3975
	 * Overriding the default form fields, with a custom defined field.
3976
	 * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3977
	 * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3978
	 * <code>
3979
	 *  static $searchable_fields = array(
3980
	 *    "Name" => array(
3981
	 *      "field" => "TextField"
3982
	 *    )
3983
	 *  );
3984
	 * </code>
3985
	 *
3986
	 * Overriding the default form field, filter and title:
3987
	 * <code>
3988
	 *  static $searchable_fields = array(
3989
	 *    "Organisation.ZipCode" => array(
3990
	 *      "field" => "TextField",
3991
	 *      "filter" => "PartialMatchFilter",
3992
	 *      "title" => 'Organisation ZIP'
3993
	 *    )
3994
	 *  );
3995
	 * </code>
3996
	 * @config
3997
	 */
3998
	private static $searchable_fields = null;
3999
4000
	/**
4001
	 * User defined labels for searchable_fields, used to override
4002
	 * default display in the search form.
4003
	 * @config
4004
	 */
4005
	private static $field_labels = null;
4006
4007
	/**
4008
	 * Provides a default list of fields to be used by a 'summary'
4009
	 * view of this object.
4010
	 * @config
4011
	 */
4012
	private static $summary_fields = null;
4013
4014
	/**
4015
	 * Provides a list of allowed methods that can be called via RESTful api.
4016
	 */
4017
	public static $allowed_actions = null;
4018
4019
	/**
4020
	 * Collect all static properties on the object
4021
	 * which contain natural language, and need to be translated.
4022
	 * The full entity name is composed from the class name and a custom identifier.
4023
	 *
4024
	 * @return array A numerical array which contains one or more entities in array-form.
4025
	 * Each numeric entity array contains the "arguments" for a _t() call as array values:
4026
	 * $entity, $string, $priority, $context.
4027
	 */
4028
	public function provideI18nEntities() {
4029
		$entities = array();
4030
4031
		$entities["{$this->class}.SINGULARNAME"] = array(
4032
			$this->singular_name(),
4033
4034
			'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
4035
		);
4036
4037
		$entities["{$this->class}.PLURALNAME"] = array(
4038
			$this->plural_name(),
4039
4040
			'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the'
4041
			. ' interface'
4042
		);
4043
4044
		return $entities;
4045
	}
4046
4047
	/**
4048
	 * Returns true if the given method/parameter has a value
4049
	 * (Uses the DBField::hasValue if the parameter is a database field)
4050
	 *
4051
	 * @param string $field The field name
4052
	 * @param array $arguments
4053
	 * @param bool $cache
4054
	 * @return boolean
4055
	 */
4056
	public function hasValue($field, $arguments = null, $cache = true) {
4057
		// has_one fields should not use dbObject to check if a value is given
4058
		if(!$this->hasOneComponent($field) && ($obj = $this->dbObject($field))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->hasOneComponent($field) of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
4059
			return $obj->exists();
4060
		} else {
4061
			return parent::hasValue($field, $arguments, $cache);
4062
		}
4063
	}
4064
4065
}
4066