Completed
Push — master ( 9e3f76...51d53f )
by Hamish
10:45
created

DataObject::loadLazyFields()   C

Complexity

Conditions 19
Paths 16

Size

Total Lines 74
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 19
eloc 41
c 2
b 1
f 0
nc 16
nop 1
dl 0
loc 74
rs 5.3296

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
use SilverStripe\Model\FieldType\DBField;
4
use SilverStripe\Model\FieldType\DBDatetime;
5
use SilverStripe\Model\FieldType\DBComposite;
6
use SilverStripe\Model\FieldType\DBClassName;
7
8
/**
9
 * A single database record & abstract class for the data-access-model.
10
 *
11
 * <h2>Extensions</h2>
12
 *
13
 * See {@link Extension} and {@link DataExtension}.
14
 *
15
 * <h2>Permission Control</h2>
16
 *
17
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
18
 * strings which can be selected on a group-by-group basis.
19
 *
20
 * <code>
21
 * class Article extends DataObject implements PermissionProvider {
22
 *  static $api_access = true;
23
 *
24
 *  function canView($member = false) {
25
 *    return Permission::check('ARTICLE_VIEW');
26
 *  }
27
 *  function canEdit($member = false) {
28
 *    return Permission::check('ARTICLE_EDIT');
29
 *  }
30
 *  function canDelete() {
31
 *    return Permission::check('ARTICLE_DELETE');
32
 *  }
33
 *  function canCreate() {
34
 *    return Permission::check('ARTICLE_CREATE');
35
 *  }
36
 *  function providePermissions() {
37
 *    return array(
38
 *      'ARTICLE_VIEW' => 'Read an article object',
39
 *      'ARTICLE_EDIT' => 'Edit an article object',
40
 *      'ARTICLE_DELETE' => 'Delete an article object',
41
 *      'ARTICLE_CREATE' => 'Create an article object',
42
 *    );
43
 *  }
44
 * }
45
 * </code>
46
 *
47
 * Object-level access control by {@link Group} membership:
48
 * <code>
49
 * class Article extends DataObject {
50
 *   static $api_access = true;
51
 *
52
 *   function canView($member = false) {
53
 *     if(!$member) $member = Member::currentUser();
54
 *     return $member->inGroup('Subscribers');
55
 *   }
56
 *   function canEdit($member = false) {
57
 *     if(!$member) $member = Member::currentUser();
58
 *     return $member->inGroup('Editors');
59
 *   }
60
 *
61
 *   // ...
62
 * }
63
 * </code>
64
 *
65
 * If any public method on this class is prefixed with an underscore,
66
 * the results are cached in memory through {@link cachedCall()}.
67
 *
68
 *
69
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
70
 *  and defineMethods()
71
 *
72
 * @package framework
73
 * @subpackage model
74
 *
75
 * @property integer ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
76
 * @property string ClassName Class name of the DataObject
77
 * @property string LastEdited Date and time of DataObject's last modification.
78
 * @property string Created Date and time of DataObject creation.
79
 */
80
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider {
81
82
	/**
83
	 * Human-readable singular name.
84
	 * @var string
85
	 * @config
86
	 */
87
	private static $singular_name = null;
88
89
	/**
90
	 * Human-readable plural name
91
	 * @var string
92
	 * @config
93
	 */
94
	private static $plural_name = null;
95
96
	/**
97
	 * Allow API access to this object?
98
	 * @todo Define the options that can be set here
99
	 * @config
100
	 */
101
	private static $api_access = false;
102
103
	/**
104
	 * Allows specification of a default value for the ClassName field.
105
	 * Configure this value only in subclasses of DataObject.
106
	 *
107
	 * @config
108
	 * @var string
109
	 */
110
	private static $default_classname = null;
111
112
	/**
113
	 * True if this DataObject has been destroyed.
114
	 * @var boolean
115
	 */
116
	public $destroyed = false;
117
118
	/**
119
	 * The DataModel from this this object comes
120
	 */
121
	protected $model;
122
123
	/**
124
	 * Data stored in this objects database record. An array indexed by fieldname.
125
	 *
126
	 * Use {@link toMap()} if you want an array representation
127
	 * of this object, as the $record array might contain lazy loaded field aliases.
128
	 *
129
	 * @var array
130
	 */
131
	protected $record;
132
133
	/**
134
	 * Represents a field that hasn't changed (before === after, thus before == after)
135
	 */
136
	const CHANGE_NONE = 0;
137
138
	/**
139
	 * Represents a field that has changed type, although not the loosely defined value.
140
	 * (before !== after && before == after)
141
	 * E.g. change 1 to true or "true" to true, but not true to 0.
142
	 * Value changes are by nature also considered strict changes.
143
	 */
144
	const CHANGE_STRICT = 1;
145
146
	/**
147
	 * Represents a field that has changed the loosely defined value
148
	 * (before != after, thus, before !== after))
149
	 * E.g. change false to true, but not false to 0
150
	 */
151
	const CHANGE_VALUE = 2;
152
153
	/**
154
	 * An array indexed by fieldname, true if the field has been changed.
155
	 * Use {@link getChangedFields()} and {@link isChanged()} to inspect
156
	 * the changed state.
157
	 *
158
	 * @var array
159
	 */
160
	private $changed;
161
162
	/**
163
	 * The database record (in the same format as $record), before
164
	 * any changes.
165
	 * @var array
166
	 */
167
	protected $original;
168
169
	/**
170
	 * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
171
	 * @var boolean
172
	 */
173
	protected $brokenOnDelete = false;
174
175
	/**
176
	 * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
177
	 * @var boolean
178
	 */
179
	protected $brokenOnWrite = false;
180
181
	/**
182
	 * @config
183
	 * @var boolean Should dataobjects be validated before they are written?
184
	 * Caution: Validation can contain safeguards against invalid/malicious data,
185
	 * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
186
	 * to only disable validation for very specific use cases.
187
	 */
188
	private static $validation_enabled = true;
189
190
	/**
191
	 * Static caches used by relevant functions.
192
	 */
193
	protected static $_cache_has_own_table = array();
194
	protected static $_cache_get_one;
195
	protected static $_cache_get_class_ancestry;
196
	protected static $_cache_field_labels = array();
197
198
	/**
199
	 * Base fields which are not defined in static $db
200
	 *
201
	 * @config
202
	 * @var array
203
	 */
204
	private static $fixed_fields = array(
205
		'ID' => 'PrimaryKey',
206
		'ClassName' => 'DBClassName',
207
		'LastEdited' => 'SS_Datetime',
208
		'Created' => 'SS_Datetime',
209
	);
210
211
	/**
212
	 * Core dataobject extensions
213
	 *
214
	 * @config
215
	 * @var array
216
	 */
217
	private static $extensions = array(
218
		'AssetControl' => '\\SilverStripe\\Filesystem\\AssetControlExtension'
219
	);
220
221
	/**
222
	 * Override table name for this class. If ignored will default to FQN of class.
223
	 * This option is not inheritable, and must be set on each class.
224
	 * If left blank naming will default to the legacy (3.x) behaviour.
225
	 *
226
	 * @var string
227
	 */
228
	private static $table_name = null;
229
230
	/**
231
	 * Non-static relationship cache, indexed by component name.
232
	 */
233
	protected $components;
234
235
	/**
236
	 * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
237
	 */
238
	protected $unsavedRelations;
239
240
	/**
241
	 * Get schema object
242
	 *
243
	 * @return DataObjectSchema
244
	 */
245
	public static function getSchema() {
246
		return Injector::inst()->get('DataObjectSchema');
247
	}
248
249
	/**
250
	 * Return the complete map of fields to specification on this object, including fixed_fields.
251
	 * "ID" will be included on every table.
252
	 *
253
	 * Composite DB field specifications are returned by reference if necessary, but not in the return
254
	 * array.
255
	 *
256
	 * Can be called directly on an object. E.g. Member::database_fields()
257
	 *
258
	 * @param string $class Class name to query from
259
	 * @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
260
	 */
261
	public static function database_fields($class = null) {
262
		if(empty($class)) {
263
			$class = get_called_class();
264
		}
265
		return static::getSchema()->databaseFields($class);
266
	}
267
268
	/**
269
	 * Get all database columns explicitly defined on a class in {@link DataObject::$db}
270
	 * and {@link DataObject::$has_one}. Resolves instances of {@link DBComposite}
271
	 * into the actual database fields, rather than the name of the field which
272
	 * might not equate a database column.
273
	 *
274
	 * Does not include "base fields" like "ID", "ClassName", "Created", "LastEdited",
275
	 * see {@link database_fields()}.
276
	 *
277
	 * Can be called directly on an object. E.g. Member::custom_database_fields()
278
	 *
279
	 * @uses DBComposite->compositeDatabaseFields()
280
	 *
281
	 * @param string $class Class name to query from
282
	 * @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
283
	 */
284
	public static function custom_database_fields($class = null) {
285
		if(empty($class)) {
286
			$class = get_called_class();
287
		}
288
289
		// Remove fixed fields. This assumes that NO fixed_fields are composite
290
		$fields = static::getSchema()->databaseFields($class);
291
		$fields = array_diff_key($fields, self::config()->fixed_fields);
292
		return $fields;
293
	}
294
295
	/**
296
	 * Returns the field class if the given db field on the class is a composite field.
297
	 * Will check all applicable ancestor classes and aggregate results.
298
	 *
299
	 * @param string $class Class to check
300
	 * @param string $name Field to check
301
	 * @param boolean $aggregated True if parent classes should be checked, or false to limit to this class
302
	 * @return string|false Class spec name of composite field if it exists, or false if not
303
	 */
304
	public static function is_composite_field($class, $name, $aggregated = true) {
305
		$fields = self::composite_fields($class, $aggregated);
306
		return isset($fields[$name]) ? $fields[$name] : false;
307
	}
308
309
	/**
310
	 * Returns a list of all the composite if the given db field on the class is a composite field.
311
	 * Will check all applicable ancestor classes and aggregate results.
312
	 *
313
	 * Can be called directly on an object. E.g. Member::composite_fields(), or Member::composite_fields(null, true)
314
	 * to aggregate.
315
	 *
316
	 * Includes composite has_one (Polymorphic) fields
317
	 *
318
	 * @param string $class Name of class to check
319
	 * @param bool $aggregated Include fields in entire hierarchy, rather than just on this table
320
	 * @return array List of composite fields and their class spec
321
	 */
322
	public static function composite_fields($class = null, $aggregated = true) {
323
		// Check $class
324
		if(empty($class)) {
325
			$class = get_called_class();
326
		}
327
		return static::getSchema()->compositeFields($class, $aggregated);
328
	}
329
330
	/**
331
	 * Construct a new DataObject.
332
	 *
333
	 * @param array|null $record This will be null for a new database record.  Alternatively, you can pass an array of
334
	 * field values.  Normally this contructor is only used by the internal systems that get objects from the database.
335
	 * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
336
	 *                             Singletons don't have their defaults set.
337
	 * @param DataModel $model
338
	 * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
339
	 */
340
	public function __construct($record = null, $isSingleton = false, $model = null, $queryParams = array()) {
341
		parent::__construct();
342
343
		// Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
344
		$this->setSourceQueryParams($queryParams);
345
346
		// Set the fields data.
347
		if(!$record) {
348
			$record = array(
349
				'ID' => 0,
350
				'ClassName' => get_class($this),
351
				'RecordClassName' => get_class($this)
352
			);
353
		}
354
355
		if(!is_array($record) && !is_a($record, "stdClass")) {
356
			if(is_object($record)) $passed = "an object of type '$record->class'";
357
			else $passed = "The value '$record'";
358
359
			user_error("DataObject::__construct passed $passed.  It's supposed to be passed an array,"
360
				. " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
361
				E_USER_WARNING);
362
			$record = null;
363
		}
364
365
		if(is_a($record, "stdClass")) {
366
			$record = (array)$record;
367
		}
368
369
		// Set $this->record to $record, but ignore NULLs
370
		$this->record = array();
371
		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...
372
			// Ensure that ID is stored as a number and not a string
373
			// To do: this kind of clean-up should be done on all numeric fields, in some relatively
374
			// performant manner
375
			if($v !== null) {
376
				if($k == 'ID' && is_numeric($v)) $this->record[$k] = (int)$v;
377
				else $this->record[$k] = $v;
378
			}
379
		}
380
381
		// Identify fields that should be lazy loaded, but only on existing records
382
		if(!empty($record['ID'])) {
383
			$currentObj = get_class($this);
384
			while($currentObj != 'DataObject') {
385
				$fields = self::custom_database_fields($currentObj);
386
				foreach($fields as $field => $type) {
387
					if(!array_key_exists($field, $record)) $this->record[$field.'_Lazy'] = $currentObj;
388
				}
389
				$currentObj = get_parent_class($currentObj);
390
			}
391
		}
392
393
		$this->original = $this->record;
394
395
		// Keep track of the modification date of all the data sourced to make this page
396
		// From this we create a Last-Modified HTTP header
397
		if(isset($record['LastEdited'])) {
398
			HTTP::register_modification_date($record['LastEdited']);
399
		}
400
401
		// this must be called before populateDefaults(), as field getters on a DataObject
402
		// may call getComponent() and others, which rely on $this->model being set.
403
		$this->model = $model ? $model : DataModel::inst();
404
405
		// Must be called after parent constructor
406
		if(!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
407
			$this->populateDefaults();
408
		}
409
410
		// prevent populateDefaults() and setField() from marking overwritten defaults as changed
411
		$this->changed = array();
412
	}
413
414
	/**
415
	 * Set the DataModel
416
	 * @param DataModel $model
417
	 * @return DataObject $this
418
	 */
419
	public function setDataModel(DataModel $model) {
420
		$this->model = $model;
421
		return $this;
422
	}
423
424
	/**
425
	 * Destroy all of this objects dependant objects and local caches.
426
	 * You'll need to call this to get the memory of an object that has components or extensions freed.
427
	 */
428
	public function destroy() {
429
		//$this->destroyed = true;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
430
		gc_collect_cycles();
431
		$this->flushCache(false);
432
	}
433
434
	/**
435
	 * Create a duplicate of this node.
436
	 * Note: now also duplicates relations.
437
	 *
438
	 * @param bool $doWrite Perform a write() operation before returning the object.
439
	 * If this is true, it will create the duplicate in the database.
440
	 * @return DataObject A duplicate of this node. The exact type will be the type of this node.
441
	 */
442
	public function duplicate($doWrite = true) {
443
		$className = $this->class;
444
		$clone = new $className( $this->toMap(), false, $this->model );
445
		$clone->ID = 0;
446
447
		$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
448
		if($doWrite) {
449
			$clone->write();
450
			$this->duplicateManyManyRelations($this, $clone);
451
		}
452
		$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
453
454
		return $clone;
455
	}
456
457
	/**
458
	 * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object
459
	 * The destinationObject must be written to the database already and have an ID. Writing is performed
460
	 * automatically when adding the new relations.
461
	 *
462
	 * @param DataObject $sourceObject the source object to duplicate from
463
	 * @param DataObject $destinationObject the destination object to populate with the duplicated relations
464
	 * @return DataObject with the new many_many relations copied in
465
	 */
466
	protected function duplicateManyManyRelations($sourceObject, $destinationObject) {
467
		if (!$destinationObject || $destinationObject->ID < 1) {
468
			user_error("Can't duplicate relations for an object that has not been written to the database",
469
				E_USER_ERROR);
470
		}
471
472
		//duplicate complex relations
473
		// DO NOT copy has_many relations, because copying the relation would result in us changing the has_one
474
		// relation on the other side of this relation to point at the copy and no longer the original (being a
475
		// has_one, it can only point at one thing at a time). So, all relations except has_many can and are copied
476
		if ($sourceObject->hasOne()) foreach($sourceObject->hasOne() as $name => $type) {
477
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
478
		}
479
		if ($sourceObject->manyMany()) foreach($sourceObject->manyMany() as $name => $type) {
480
			//many_many include belongs_many_many
481
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
482
		}
483
484
		return $destinationObject;
485
	}
486
487
	/**
488
	 * Helper function to duplicate relations from one object to another
489
	 * @param DataObject $sourceObject the source object to duplicate from
490
	 * @param DataObject $destinationObject the destination object to populate with the duplicated relations
491
	 * @param string $name the name of the relation to duplicate (e.g. members)
492
	 */
493
	private function duplicateRelations($sourceObject, $destinationObject, $name) {
494
		$relations = $sourceObject->$name();
495
		if ($relations) {
496
			if ($relations instanceOf RelationList) {   //many-to-something relation
497
				if ($relations->count() > 0) {  //with more than one thing it is related to
498
					foreach($relations as $relation) {
499
						$destinationObject->$name()->add($relation);
500
					}
501
				}
502
			} else {    //one-to-one relation
503
				$destinationObject->{"{$name}ID"} = $relations->ID;
504
			}
505
		}
506
	}
507
508
	public function getObsoleteClassName() {
509
		$className = $this->getField("ClassName");
510
		if (!ClassInfo::exists($className)) return $className;
511
	}
512
513
	public function getClassName() {
514
		$className = $this->getField("ClassName");
515
		if (!ClassInfo::exists($className)) return get_class($this);
516
		return $className;
517
	}
518
519
	/**
520
	 * Set the ClassName attribute. {@link $class} is also updated.
521
	 * Warning: This will produce an inconsistent record, as the object
522
	 * instance will not automatically switch to the new subclass.
523
	 * Please use {@link newClassInstance()} for this purpose,
524
	 * or destroy and reinstanciate the record.
525
	 *
526
	 * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
527
	 * @return DataObject $this
528
	 */
529
	public function setClassName($className) {
530
		$className = trim($className);
531
		if(!$className || !is_subclass_of($className, 'DataObject')) return;
532
533
		$this->class = $className;
534
		$this->setField("ClassName", $className);
535
		return $this;
536
	}
537
538
	/**
539
	 * Create a new instance of a different class from this object's record.
540
	 * This is useful when dynamically changing the type of an instance. Specifically,
541
	 * it ensures that the instance of the class is a match for the className of the
542
	 * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
543
	 * property manually before calling this method, as it will confuse change detection.
544
	 *
545
	 * If the new class is different to the original class, defaults are populated again
546
	 * because this will only occur automatically on instantiation of a DataObject if
547
	 * there is no record, or the record has no ID. In this case, we do have an ID but
548
	 * we still need to repopulate the defaults.
549
	 *
550
	 * @param string $newClassName The name of the new class
551
	 *
552
	 * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
553
	 */
554
	public function newClassInstance($newClassName) {
555
		$originalClass = $this->ClassName;
556
		$newInstance = new $newClassName(array_merge(
557
			$this->record,
558
			array(
559
				'ClassName' => $originalClass,
560
				'RecordClassName' => $originalClass,
561
			)
562
		), false, $this->model);
563
564
		if($newClassName != $originalClass) {
565
			$newInstance->setClassName($newClassName);
566
			$newInstance->populateDefaults();
567
			$newInstance->forceChange();
568
		}
569
570
		return $newInstance;
571
	}
572
573
	/**
574
	 * Adds methods from the extensions.
575
	 * Called by Object::__construct() once per class.
576
	 */
577
	public function defineMethods() {
578
		parent::defineMethods();
579
580
		// Define the extra db fields - this is only necessary for extensions added in the
581
		// class definition.  Object::add_extension() will call this at definition time for
582
		// those objects, which is a better mechanism.  Perhaps extensions defined inside the
583
		// class def can somehow be applied at definiton time also?
584
		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 Extension[] 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...
585
			if(!$instance->class) {
586
				$class = get_class($instance);
587
				user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls"
588
					. " parent::__construct()", E_USER_ERROR);
589
			}
590
		}
591
592
		if($this->class == 'DataObject') return;
593
594
		// Set up accessors for joined items
595
		if($manyMany = $this->manyMany()) {
596
			foreach($manyMany as $relationship => $class) {
597
				$this->addWrapperMethod($relationship, 'getManyManyComponents');
598
			}
599
		}
600
		if($hasMany = $this->hasMany()) {
601
602
			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...
603
				$this->addWrapperMethod($relationship, 'getComponents');
604
			}
605
606
		}
607
		if($hasOne = $this->hasOne()) {
608
			foreach($hasOne as $relationship => $class) {
609
				$this->addWrapperMethod($relationship, 'getComponent');
610
			}
611
		}
612
		if($belongsTo = $this->belongsTo()) foreach(array_keys($belongsTo) as $relationship) {
613
			$this->addWrapperMethod($relationship, 'getComponent');
614
		}
615
	}
616
617
	/**
618
	 * Returns true if this object "exists", i.e., has a sensible value.
619
	 * The default behaviour for a DataObject is to return true if
620
	 * the object exists in the database, you can override this in subclasses.
621
	 *
622
	 * @return boolean true if this object exists
623
	 */
624
	public function exists() {
625
		return (isset($this->record['ID']) && $this->record['ID'] > 0);
626
	}
627
628
	/**
629
	 * Returns TRUE if all values (other than "ID") are
630
	 * considered empty (by weak boolean comparison).
631
	 *
632
	 * @return boolean
633
	 */
634
	public function isEmpty() {
635
		$fixed = $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...
636
		foreach($this->toMap() as $field => $value){
637
			// only look at custom fields
638
			if(isset($fixed[$field])) {
639
				continue;
640
			}
641
642
			$dbObject = $this->dbObject($field);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $dbObject is correct as $this->dbObject($field) (which targets DataObject::dbObject()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
643
			if(!$dbObject) {
644
				continue;
645
			}
646
			if($dbObject->exists()) {
647
				return false;
648
			}
649
		}
650
		return true;
651
	}
652
653
	/**
654
	 * Pluralise this item given a specific count.
655
	 *
656
	 * E.g. "0 Pages", "1 File", "3 Images"
657
	 *
658
	 * @param string $count
659
	 * @param bool $prependNumber Include number in result. Defaults to true.
660
	 * @return string
661
	 */
662
	public function i18n_pluralise($count, $prependNumber = true) {
663
		return i18n::pluralise(
664
			$this->i18n_singular_name(),
665
			$this->i18n_plural_name(),
666
			$count,
667
			$prependNumber
668
		);
669
	}
670
671
	/**
672
	 * Get the user friendly singular name of this DataObject.
673
	 * If the name is not defined (by redefining $singular_name in the subclass),
674
	 * this returns the class name.
675
	 *
676
	 * @return string User friendly singular name of this DataObject
677
	 */
678
	public function singular_name() {
679
		if(!$name = $this->stat('singular_name')) {
680
			$name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $this->class))));
681
		}
682
683
		return $name;
684
	}
685
686
	/**
687
	 * Get the translated user friendly singular name of this DataObject
688
	 * same as singular_name() but runs it through the translating function
689
	 *
690
	 * Translating string is in the form:
691
	 *     $this->class.SINGULARNAME
692
	 * Example:
693
	 *     Page.SINGULARNAME
694
	 *
695
	 * @return string User friendly translated singular name of this DataObject
696
	 */
697
	public function i18n_singular_name() {
698
		return _t($this->class.'.SINGULARNAME', $this->singular_name());
699
	}
700
701
	/**
702
	 * Get the user friendly plural name of this DataObject
703
	 * If the name is not defined (by renaming $plural_name in the subclass),
704
	 * this returns a pluralised version of the class name.
705
	 *
706
	 * @return string User friendly plural name of this DataObject
707
	 */
708
	public function plural_name() {
709
		if($name = $this->stat('plural_name')) {
710
			return $name;
711
		} else {
712
			$name = $this->singular_name();
713
			//if the penultimate character is not a vowel, replace "y" with "ies"
714
			if (preg_match('/[^aeiou]y$/i', $name)) {
715
				$name = substr($name,0,-1) . 'ie';
716
			}
717
			return ucfirst($name . 's');
718
		}
719
	}
720
721
	/**
722
	 * Get the translated user friendly plural name of this DataObject
723
	 * Same as plural_name but runs it through the translation function
724
	 * Translation string is in the form:
725
	 *      $this->class.PLURALNAME
726
	 * Example:
727
	 *      Page.PLURALNAME
728
	 *
729
	 * @return string User friendly translated plural name of this DataObject
730
	 */
731
	public function i18n_plural_name()
732
	{
733
		$name = $this->plural_name();
734
		return _t($this->class.'.PLURALNAME', $name);
735
	}
736
737
	/**
738
	 * Standard implementation of a title/label for a specific
739
	 * record. Tries to find properties 'Title' or 'Name',
740
	 * and falls back to the 'ID'. Useful to provide
741
	 * user-friendly identification of a record, e.g. in errormessages
742
	 * or UI-selections.
743
	 *
744
	 * Overload this method to have a more specialized implementation,
745
	 * e.g. for an Address record this could be:
746
	 * <code>
747
	 * function getTitle() {
748
	 *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
749
	 * }
750
	 * </code>
751
	 *
752
	 * @return string
753
	 */
754
	public function getTitle() {
755
		if($this->hasDatabaseField('Title')) return $this->getField('Title');
756
		if($this->hasDatabaseField('Name')) return $this->getField('Name');
757
758
		return "#{$this->ID}";
759
	}
760
761
	/**
762
	 * Returns the associated database record - in this case, the object itself.
763
	 * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
764
	 *
765
	 * @return DataObject Associated database record
766
	 */
767
	public function data() {
768
		return $this;
769
	}
770
771
	/**
772
	 * Convert this object to a map.
773
	 *
774
	 * @return array The data as a map.
775
	 */
776
	public function toMap() {
777
		$this->loadLazyFields();
778
		return $this->record;
779
	}
780
781
	/**
782
	 * Return all currently fetched database fields.
783
	 *
784
	 * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
785
	 * Obviously, this makes it a lot faster.
786
	 *
787
	 * @return array The data as a map.
788
	 */
789
	public function getQueriedDatabaseFields() {
790
		return $this->record;
791
	}
792
793
	/**
794
	 * Update a number of fields on this object, given a map of the desired changes.
795
	 *
796
	 * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
797
	 * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
798
	 *
799
	 * update() doesn't write the main object, but if you use the dot syntax, it will write()
800
	 * the related objects that it alters.
801
	 *
802
	 * @param array $data A map of field name to data values to update.
803
	 * @return DataObject $this
804
	 */
805
	public function update($data) {
806
		foreach($data as $k => $v) {
807
			// Implement dot syntax for updates
808
			if(strpos($k,'.') !== false) {
809
				$relations = explode('.', $k);
810
				$fieldName = array_pop($relations);
811
				$relObj = $this;
812
				foreach($relations as $i=>$relation) {
813
					// no support for has_many or many_many relationships,
814
					// as the updater wouldn't know which object to write to (or create)
815
					if($relObj->$relation() instanceof DataObject) {
816
						$parentObj = $relObj;
817
						$relObj = $relObj->$relation();
818
						// If the intermediate relationship objects have been created, then write them
819
						if($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
820
							$relObj->write();
821
							$relatedFieldName = $relation."ID";
822
							$parentObj->$relatedFieldName = $relObj->ID;
823
							$parentObj->write();
824
						}
825
					} else {
826
						user_error(
827
							"DataObject::update(): Can't traverse relationship '$relation'," .
828
							"it has to be a has_one relationship or return a single DataObject",
829
							E_USER_NOTICE
830
						);
831
						// unset relation object so we don't write properties to the wrong object
832
						unset($relObj);
833
						break;
834
					}
835
				}
836
837
				if($relObj) {
838
					$relObj->$fieldName = $v;
839
					$relObj->write();
840
					$relatedFieldName = $relation."ID";
0 ignored issues
show
Bug introduced by
The variable $relation seems to be defined by a foreach iteration on line 812. 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...
841
					$this->$relatedFieldName = $relObj->ID;
842
					$relObj->flushCache();
843
				} else {
844
					user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
845
				}
846
			} else {
847
				$this->$k = $v;
848
			}
849
		}
850
		return $this;
851
	}
852
853
	/**
854
	 * Pass changes as a map, and try to
855
	 * get automatic casting for these fields.
856
	 * Doesn't write to the database. To write the data,
857
	 * use the write() method.
858
	 *
859
	 * @param array $data A map of field name to data values to update.
860
	 * @return DataObject $this
861
	 */
862
	public function castedUpdate($data) {
863
		foreach($data as $k => $v) {
864
			$this->setCastedField($k,$v);
865
		}
866
		return $this;
867
	}
868
869
	/**
870
	 * Merges data and relations from another object of same class,
871
	 * without conflict resolution. Allows to specify which
872
	 * dataset takes priority in case its not empty.
873
	 * has_one-relations are just transferred with priority 'right'.
874
	 * has_many and many_many-relations are added regardless of priority.
875
	 *
876
	 * Caution: has_many/many_many relations are moved rather than duplicated,
877
	 * meaning they are not connected to the merged object any longer.
878
	 * Caution: Just saves updated has_many/many_many relations to the database,
879
	 * doesn't write the updated object itself (just writes the object-properties).
880
	 * Caution: Does not delete the merged object.
881
	 * Caution: Does now overwrite Created date on the original object.
882
	 *
883
	 * @param $obj DataObject
884
	 * @param $priority String left|right Determines who wins in case of a conflict (optional)
885
	 * @param $includeRelations Boolean Merge any existing relations (optional)
886
	 * @param $overwriteWithEmpty Boolean Overwrite existing left values with empty right values.
887
	 *                            Only applicable with $priority='right'. (optional)
888
	 * @return Boolean
889
	 */
890
	public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) {
891
		$leftObj = $this;
892
893
		if($leftObj->ClassName != $rightObj->ClassName) {
894
			// we can't merge similiar subclasses because they might have additional relations
895
			user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
896
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
897
			return false;
898
		}
899
900
		if(!$rightObj->ID) {
901
			user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
902
				to make sure all relations are transferred properly.').", E_USER_WARNING);
903
			return false;
904
		}
905
906
		// makes sure we don't merge data like ID or ClassName
907
		$leftData = $leftObj->db();
908
		$rightData = $rightObj->db();
909
910
		foreach($rightData as $key=>$rightSpec) {
911
			// Don't merge ID
912
			if($key === 'ID') {
913
				continue;
914
			}
915
916
			// Only merge relations if allowed
917
			if($rightSpec === 'ForeignKey' && !$includeRelations) {
918
				continue;
919
			}
920
921
			// don't merge conflicting values if priority is 'left'
922
			if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
923
				continue;
924
			}
925
926
			// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
927
			if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
928
				continue;
929
			}
930
931
			// TODO remove redundant merge of has_one fields
932
			$leftObj->{$key} = $rightObj->{$key};
933
		}
934
935
		// merge relations
936
		if($includeRelations) {
937
			if($manyMany = $this->manyMany()) {
938
				foreach($manyMany as $relationship => $class) {
939
					$leftComponents = $leftObj->getManyManyComponents($relationship);
940
					$rightComponents = $rightObj->getManyManyComponents($relationship);
941
					if($rightComponents && $rightComponents->exists()) {
942
						$leftComponents->addMany($rightComponents->column('ID'));
943
					}
944
					$leftComponents->write();
945
				}
946
			}
947
948
			if($hasMany = $this->hasMany()) {
949
				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...
950
					$leftComponents = $leftObj->getComponents($relationship);
951
					$rightComponents = $rightObj->getComponents($relationship);
952
					if($rightComponents && $rightComponents->exists()) {
953
						$leftComponents->addMany($rightComponents->column('ID'));
954
					}
955
					$leftComponents->write();
956
				}
957
958
			}
959
		}
960
961
		return true;
962
	}
963
964
	/**
965
	 * Forces the record to think that all its data has changed.
966
	 * Doesn't write to the database. Only sets fields as changed
967
	 * if they are not already marked as changed.
968
	 *
969
	 * @return $this
970
	 */
971
	public function forceChange() {
972
		// Ensure lazy fields loaded
973
		$this->loadLazyFields();
974
975
		// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
976
		$fieldNames = array_unique(array_merge(
977
			array_keys($this->record),
978
			array_keys($this->db())
979
		));
980
981
		foreach($fieldNames as $fieldName) {
982
			if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
983
			// Populate the null values in record so that they actually get written
984
			if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
985
		}
986
987
		// @todo Find better way to allow versioned to write a new version after forceChange
988
		if($this->isChanged('Version')) unset($this->changed['Version']);
989
		return $this;
990
	}
991
992
	/**
993
	 * Validate the current object.
994
	 *
995
	 * By default, there is no validation - objects are always valid!  However, you can overload this method in your
996
	 * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
997
	 *
998
	 * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
999
	 * and onAfterWrite() won't get called either.
1000
	 *
1001
	 * It is expected that you call validate() in your own application to test that an object is valid before
1002
	 * attempting a write, and respond appropriately if it isn't.
1003
	 *
1004
	 * @see {@link ValidationResult}
1005
	 * @return ValidationResult
1006
	 */
1007
	public function validate() {
1008
		$result = ValidationResult::create();
1009
		$this->extend('validate', $result);
1010
		return $result;
1011
	}
1012
1013
	/**
1014
	 * Public accessor for {@see DataObject::validate()}
1015
	 *
1016
	 * @return ValidationResult
1017
	 */
1018
	public function doValidate() {
1019
		Deprecation::notice('5.0', 'Use validate');
1020
		return $this->validate();
1021
	}
1022
1023
	/**
1024
	 * Event handler called before writing to the database.
1025
	 * You can overload this to clean up or otherwise process data before writing it to the
1026
	 * database.  Don't forget to call parent::onBeforeWrite(), though!
1027
	 *
1028
	 * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1029
	 *
1030
	 * @uses DataExtension->onBeforeWrite()
1031
	 */
1032
	protected function onBeforeWrite() {
1033
		$this->brokenOnWrite = false;
1034
1035
		$dummy = null;
1036
		$this->extend('onBeforeWrite', $dummy);
1037
	}
1038
1039
	/**
1040
	 * Event handler called after writing to the database.
1041
	 * You can overload this to act upon changes made to the data after it is written.
1042
	 * $this->changed will have a record
1043
	 * database.  Don't forget to call parent::onAfterWrite(), though!
1044
	 *
1045
	 * @uses DataExtension->onAfterWrite()
1046
	 */
1047
	protected function onAfterWrite() {
1048
		$dummy = null;
1049
		$this->extend('onAfterWrite', $dummy);
1050
	}
1051
1052
	/**
1053
	 * Event handler called before deleting from the database.
1054
	 * You can overload this to clean up or otherwise process data before delete this
1055
	 * record.  Don't forget to call parent::onBeforeDelete(), though!
1056
	 *
1057
	 * @uses DataExtension->onBeforeDelete()
1058
	 */
1059
	protected function onBeforeDelete() {
1060
		$this->brokenOnDelete = false;
1061
1062
		$dummy = null;
1063
		$this->extend('onBeforeDelete', $dummy);
1064
	}
1065
1066
	protected function onAfterDelete() {
1067
		$this->extend('onAfterDelete');
1068
	}
1069
1070
	/**
1071
	 * Load the default values in from the self::$defaults array.
1072
	 * Will traverse the defaults of the current class and all its parent classes.
1073
	 * Called by the constructor when creating new records.
1074
	 *
1075
	 * @uses DataExtension->populateDefaults()
1076
	 * @return DataObject $this
1077
	 */
1078
	public function populateDefaults() {
1079
		$classes = array_reverse(ClassInfo::ancestry($this));
1080
1081
		foreach($classes as $class) {
1082
			$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1083
1084
			if($defaults && !is_array($defaults)) {
1085
				user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
1086
					E_USER_WARNING);
1087
				$defaults = null;
1088
			}
1089
1090
			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...
1091
				// SRM 2007-03-06: Stricter check
1092
				if(!isset($this->$fieldName) || $this->$fieldName === null) {
1093
					$this->$fieldName = $fieldValue;
1094
				}
1095
				// Set many-many defaults with an array of ids
1096
				if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
1097
					$manyManyJoin = $this->$fieldName();
1098
					$manyManyJoin->setByIdList($fieldValue);
1099
				}
1100
			}
1101
			if($class == 'DataObject') {
1102
				break;
1103
			}
1104
		}
1105
1106
		$this->extend('populateDefaults');
1107
		return $this;
1108
	}
1109
1110
	/**
1111
	 * Determine validation of this object prior to write
1112
	 *
1113
	 * @return ValidationException Exception generated by this write, or null if valid
1114
	 */
1115
	protected function validateWrite() {
1116
		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...
1117
			return new ValidationException(
1118
				"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...
1119
				"you need to change the ClassName before you can write it",
1120
				E_USER_WARNING
1121
			);
1122
		}
1123
1124
		if(Config::inst()->get('DataObject', 'validation_enabled')) {
1125
			$result = $this->validate();
1126
			if (!$result->valid()) {
1127
				return new ValidationException(
1128
					$result,
1129
					$result->message(),
1130
					E_USER_WARNING
1131
				);
1132
			}
1133
		}
1134
	}
1135
1136
	/**
1137
	 * Prepare an object prior to write
1138
	 *
1139
	 * @throws ValidationException
1140
	 */
1141
	protected function preWrite() {
1142
		// Validate this object
1143
		if($writeException = $this->validateWrite()) {
1144
			// Used by DODs to clean up after themselves, eg, Versioned
1145
			$this->invokeWithExtensions('onAfterSkippedWrite');
1146
			throw $writeException;
1147
		}
1148
1149
		// Check onBeforeWrite
1150
		$this->brokenOnWrite = true;
1151
		$this->onBeforeWrite();
1152
		if($this->brokenOnWrite) {
1153
			user_error("$this->class has a broken onBeforeWrite() function."
1154
				. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1155
		}
1156
	}
1157
1158
	/**
1159
	 * Detects and updates all changes made to this object
1160
	 *
1161
	 * @param bool $forceChanges If set to true, force all fields to be treated as changed
1162
	 * @return bool True if any changes are detected
1163
	 */
1164
	protected function updateChanges($forceChanges = false)
1165
	{
1166
		if($forceChanges) {
1167
			// Force changes, but only for loaded fields
1168
			foreach($this->record as $field => $value) {
1169
				$this->changed[$field] = static::CHANGE_VALUE;
1170
			}
1171
			return true;
1172
		}
1173
		return $this->isChanged();
1174
	}
1175
1176
	/**
1177
	 * Writes a subset of changes for a specific table to the given manipulation
1178
	 *
1179
	 * @param string $baseTable Base table
1180
	 * @param string $now Timestamp to use for the current time
1181
	 * @param bool $isNewRecord Whether this should be treated as a new record write
1182
	 * @param array $manipulation Manipulation to write to
1183
	 * @param string $class Class of table to manipulate
1184
	 */
1185
	protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
1186
		$table = $this->getSchema()->tableName($class);
1187
		$manipulation[$table] = array();
1188
1189
		// Extract records for this table
1190
		foreach($this->record as $fieldName => $fieldValue) {
1191
1192
			// Check if this record pertains to this table, and
1193
			// we're not attempting to reset the BaseTable->ID
1194
			if(	empty($this->changed[$fieldName])
1195
				|| ($table === $baseTable && $fieldName === 'ID')
1196
				|| (!self::has_own_table_database_field($class, $fieldName)
1197
					&& !self::is_composite_field($class, $fieldName, false))
0 ignored issues
show
Bug Best Practice introduced by
The expression self::is_composite_field...ass, $fieldName, false) of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false 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...
1198
			) {
1199
				continue;
1200
			}
1201
1202
1203
			// if database column doesn't correlate to a DBField instance...
1204
			$fieldObj = $this->dbObject($fieldName);
1205
			if(!$fieldObj) {
1206
				$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1207
			}
1208
1209
			// Write to manipulation
1210
			$fieldObj->writeToManipulation($manipulation[$table]);
1211
		}
1212
1213
		// Ensure update of Created and LastEdited columns
1214
		if($baseTable === $table) {
1215
			$manipulation[$table]['fields']['LastEdited'] = $now;
1216
			if($isNewRecord) {
1217
				$manipulation[$table]['fields']['Created']
1218
					= empty($this->record['Created'])
1219
						? $now
1220
						: $this->record['Created'];
1221
				$manipulation[$table]['fields']['ClassName'] = $this->class;
1222
			}
1223
		}
1224
1225
		// Inserts done one the base table are performed in another step, so the manipulation should instead
1226
		// attempt an update, as though it were a normal update.
1227
		$manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1228
		$manipulation[$table]['id'] = $this->record['ID'];
1229
		$manipulation[$table]['class'] = $class;
1230
	}
1231
1232
	/**
1233
	 * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1234
	 *
1235
	 * Does nothing if an ID is already assigned for this record
1236
	 *
1237
	 * @param string $baseTable Base table
1238
	 * @param string $now Timestamp to use for the current time
1239
	 */
1240
	protected function writeBaseRecord($baseTable, $now) {
1241
		// Generate new ID if not specified
1242
		if($this->isInDB()) return;
1243
1244
		// Perform an insert on the base table
1245
		$insert = new SQLInsert('"'.$baseTable.'"');
1246
		$insert
1247
			->assign('"Created"', $now)
1248
			->execute();
1249
		$this->changed['ID'] = self::CHANGE_VALUE;
1250
		$this->record['ID'] = DB::get_generated_id($baseTable);
1251
	}
1252
1253
	/**
1254
	 * Generate and write the database manipulation for all changed fields
1255
	 *
1256
	 * @param string $baseTable Base table
1257
	 * @param string $now Timestamp to use for the current time
1258
	 * @param bool $isNewRecord If this is a new record
1259
	 */
1260
	protected function writeManipulation($baseTable, $now, $isNewRecord) {
1261
		// Generate database manipulations for each class
1262
		$manipulation = array();
1263
		foreach($this->getClassAncestry() as $class) {
1264
			if(self::has_own_table($class)) {
1265
				$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1266
			}
1267
		}
1268
1269
		// Allow extensions to extend this manipulation
1270
		$this->extend('augmentWrite', $manipulation);
1271
1272
		// New records have their insert into the base data table done first, so that they can pass the
1273
		// generated ID on to the rest of the manipulation
1274
		if($isNewRecord) {
1275
			$manipulation[$baseTable]['command'] = 'update';
1276
		}
1277
1278
		// Perform the manipulation
1279
		DB::manipulate($manipulation);
1280
	}
1281
1282
	/**
1283
	 * Writes all changes to this object to the database.
1284
	 *  - It will insert a record whenever ID isn't set, otherwise update.
1285
	 *  - All relevant tables will be updated.
1286
	 *  - $this->onBeforeWrite() gets called beforehand.
1287
	 *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1288
	 *
1289
	 *  @uses DataExtension->augmentWrite()
1290
	 *
1291
	 * @param boolean $showDebug Show debugging information
1292
	 * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1293
	 * @param boolean $forceWrite Write to database even if there are no changes
1294
	 * @param boolean $writeComponents Call write() on all associated component instances which were previously
1295
	 *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1296
	 *                                 {@link getManyManyComponents()} (Default: false)
1297
	 * @return int The ID of the record
1298
	 * @throws ValidationException Exception that can be caught and handled by the calling function
1299
	 */
1300
	public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1301
		$now = DBDatetime::now()->Rfc2822();
1302
1303
		// Execute pre-write tasks
1304
		$this->preWrite();
1305
1306
		// Check if we are doing an update or an insert
1307
		$isNewRecord = !$this->isInDB() || $forceInsert;
1308
1309
		// Check changes exist, abort if there are none
1310
		$hasChanges = $this->updateChanges($isNewRecord);
1311
		if($hasChanges || $forceWrite || $isNewRecord) {
1312
			// New records have their insert into the base data table done first, so that they can pass the
1313
			// generated primary key on to the rest of the manipulation
1314
			$baseTable = $this->baseTable();
1315
			$this->writeBaseRecord($baseTable, $now);
1316
1317
			// Write the DB manipulation for all changed fields
1318
			$this->writeManipulation($baseTable, $now, $isNewRecord);
1319
1320
			// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1321
			$this->writeRelations();
1322
			$this->onAfterWrite();
1323
			$this->changed = array();
1324
		} else {
1325
			if($showDebug) Debug::message("no changes for DataObject");
1326
1327
			// Used by DODs to clean up after themselves, eg, Versioned
1328
			$this->invokeWithExtensions('onAfterSkippedWrite');
1329
		}
1330
1331
		// Ensure Created and LastEdited are populated
1332
		if(!isset($this->record['Created'])) {
1333
			$this->record['Created'] = $now;
1334
		}
1335
		$this->record['LastEdited'] = $now;
1336
1337
		// Write relations as necessary
1338
		if($writeComponents) $this->writeComponents(true);
1339
1340
		// Clears the cache for this object so get_one returns the correct object.
1341
		$this->flushCache();
1342
1343
		return $this->record['ID'];
1344
	}
1345
1346
	/**
1347
	 * Writes cached relation lists to the database, if possible
1348
	 */
1349
	public function writeRelations() {
1350
		if(!$this->isInDB()) return;
1351
1352
		// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1353
		if($this->unsavedRelations) {
1354
			foreach($this->unsavedRelations as $name => $list) {
1355
				$list->changeToList($this->$name());
1356
			}
1357
			$this->unsavedRelations = array();
1358
		}
1359
	}
1360
1361
	/**
1362
	 * Write the cached components to the database. Cached components could refer to two different instances of the
1363
	 * same record.
1364
	 *
1365
	 * @param bool $recursive Recursively write components
1366
	 * @return DataObject $this
1367
	 */
1368
	public function writeComponents($recursive = false) {
1369
		if(!$this->components) return $this;
1370
1371
		foreach($this->components as $component) {
1372
			$component->write(false, false, false, $recursive);
1373
		}
1374
		return $this;
1375
	}
1376
1377
	/**
1378
	 * Delete this data object.
1379
	 * $this->onBeforeDelete() gets called.
1380
	 * Note that in Versioned objects, both Stage and Live will be deleted.
1381
	 *  @uses DataExtension->augmentSQL()
1382
	 */
1383
	public function delete() {
1384
		$this->brokenOnDelete = true;
1385
		$this->onBeforeDelete();
1386
		if($this->brokenOnDelete) {
1387
			user_error("$this->class has a broken onBeforeDelete() function."
1388
				. " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1389
		}
1390
1391
		// Deleting a record without an ID shouldn't do anything
1392
		if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1393
1394
		// TODO: This is quite ugly.  To improve:
1395
		//  - move the details of the delete code in the DataQuery system
1396
		//  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1397
		//    obviously, that means getting requireTable() to configure cascading deletes ;-)
1398
		$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1399
		foreach($srcQuery->queriedTables() as $table) {
1400
			$delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1401
			$delete->execute();
1402
		}
1403
		// Remove this item out of any caches
1404
		$this->flushCache();
1405
1406
		$this->onAfterDelete();
1407
1408
		$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...
1409
		$this->ID = 0;
1410
	}
1411
1412
	/**
1413
	 * Delete the record with the given ID.
1414
	 *
1415
	 * @param string $className The class name of the record to be deleted
1416
	 * @param int $id ID of record to be deleted
1417
	 */
1418
	public static function delete_by_id($className, $id) {
1419
		$obj = DataObject::get_by_id($className, $id);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1420
		if($obj) {
1421
			$obj->delete();
1422
		} else {
1423
			user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1424
		}
1425
	}
1426
1427
	/**
1428
	 * Get the class ancestry, including the current class name.
1429
	 * The ancestry will be returned as an array of class names, where the 0th element
1430
	 * will be the class that inherits directly from DataObject, and the last element
1431
	 * will be the current class.
1432
	 *
1433
	 * @return array Class ancestry
1434
	 */
1435
	public function getClassAncestry() {
1436
		return ClassInfo::ancestry(get_class($this));
1437
	}
1438
1439
	/**
1440
	 * Return a component object from a one to one relationship, as a DataObject.
1441
	 * If no component is available, an 'empty component' will be returned for
1442
	 * non-polymorphic relations, or for polymorphic relations with a class set.
1443
	 *
1444
	 * @param string $componentName Name of the component
1445
	 * @return DataObject The component object. It's exact type will be that of the component.
1446
	 * @throws Exception
1447
	 */
1448
	public function getComponent($componentName) {
1449
		if(isset($this->components[$componentName])) {
1450
			return $this->components[$componentName];
1451
		}
1452
1453
		if($class = $this->hasOneComponent($componentName)) {
1454
			$joinField = $componentName . 'ID';
1455
			$joinID    = $this->getField($joinField);
1456
1457
			// Extract class name for polymorphic relations
1458
			if($class === 'DataObject') {
1459
				$class = $this->getField($componentName . 'Class');
1460
				if(empty($class)) return null;
1461
			}
1462
1463
			if($joinID) {
1464
				// Ensure that the selected object originates from the same stage, subsite, etc
1465
				$component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1466
					->filter('ID', $joinID)
1467
					->setDataQueryParam($this->getInheritableQueryParams())
1468
					->first();
1469
			}
1470
1471
			if(empty($component)) {
1472
				$component = $this->model->$class->newObject();
1473
			}
1474
		} elseif($class = $this->belongsToComponent($componentName)) {
1475
			$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
1476
			$joinID = $this->ID;
1477
1478
			if($joinID) {
1479
				// Prepare filter for appropriate join type
1480
				if($polymorphic) {
1481
					$filter = array(
1482
						"{$joinField}ID" => $joinID,
1483
						"{$joinField}Class" => $this->class
1484
					);
1485
				} else {
1486
					$filter = array(
1487
						$joinField => $joinID
1488
					);
1489
				}
1490
1491
				// Ensure that the selected object originates from the same stage, subsite, etc
1492
				$component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1493
					->filter($filter)
1494
					->setDataQueryParam($this->getInheritableQueryParams())
1495
					->first();
1496
			}
1497
1498
			if(empty($component)) {
1499
				$component = $this->model->$class->newObject();
1500
				if($polymorphic) {
1501
					$component->{$joinField.'ID'} = $this->ID;
1502
					$component->{$joinField.'Class'} = $this->class;
1503
				} else {
1504
					$component->$joinField = $this->ID;
1505
				}
1506
			}
1507
		} else {
1508
			throw new InvalidArgumentException(
1509
				"DataObject->getComponent(): Could not find component '$componentName'."
1510
			);
1511
		}
1512
1513
		$this->components[$componentName] = $component;
1514
		return $component;
1515
	}
1516
1517
	/**
1518
	 * Returns a one-to-many relation as a HasManyList
1519
	 *
1520
	 * @param string $componentName Name of the component
1521
	 * @return HasManyList The components of the one-to-many relationship.
1522
	 */
1523
	public function getComponents($componentName) {
1524
		$result = null;
1525
1526
		$componentClass = $this->hasManyComponent($componentName);
1527
		if(!$componentClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $componentClass 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...
1528
			throw new InvalidArgumentException(sprintf(
1529
				"DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1530
				$componentName,
1531
				$this->class
1532
			));
1533
		}
1534
1535
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1536
		if(!$this->ID) {
1537
			if(!isset($this->unsavedRelations[$componentName])) {
1538
				$this->unsavedRelations[$componentName] =
1539
					new UnsavedRelationList($this->class, $componentName, $componentClass);
1540
			}
1541
			return $this->unsavedRelations[$componentName];
1542
		}
1543
1544
		// Determine type and nature of foreign relation
1545
		$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
1546
		/** @var HasManyList $result */
1547
		if($polymorphic) {
1548
			$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1549
		} else {
1550
			$result = HasManyList::create($componentClass, $joinField);
1551
		}
1552
1553
		if($this->model) {
1554
			$result->setDataModel($this->model);
1555
		}
1556
1557
		return $result
1558
			->setDataQueryParam($this->getInheritableQueryParams())
1559
			->forForeignID($this->ID);
1560
	}
1561
1562
	/**
1563
	 * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1564
	 *
1565
	 * @param string $relationName Relation name.
1566
	 * @return string Class name, or null if not found.
1567
	 */
1568
	public function getRelationClass($relationName) {
1569
		// Go through all relationship configuration fields.
1570
		$candidates = array_merge(
1571
			($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1572
			($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1573
			($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
1574
			($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
1575
			($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1576
		);
1577
1578
		if (isset($candidates[$relationName])) {
1579
			$remoteClass = $candidates[$relationName];
1580
1581
			// If dot notation is present, extract just the first part that contains the class.
1582
			if(($fieldPos = strpos($remoteClass, '.'))!==false) {
1583
				return substr($remoteClass, 0, $fieldPos);
1584
			}
1585
1586
			// Otherwise just return the class
1587
			return $remoteClass;
1588
		}
1589
1590
		return null;
1591
	}
1592
1593
	/**
1594
	 * Given a relation name, determine the relation type
1595
	 *
1596
	 * @param string $component Name of component
1597
	 * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1598
	 */
1599
	public function getRelationType($component) {
1600
		$types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1601
		foreach($types as $type) {
1602
			$relations = Config::inst()->get($this->class, $type);
1603
			if($relations && isset($relations[$component])) {
1604
				return $type;
1605
			}
1606
		}
1607
		return null;
1608
	}
1609
1610
	/**
1611
	 * Given a relation declared on a remote class, generate a substitute component for the opposite
1612
	 * side of the relation.
1613
	 *
1614
	 * Notes on behaviour:
1615
	 *  - This can still be used on components that are defined on both sides, but do not need to be.
1616
	 *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1617
	 *  - Cannot be used on polymorphic relationships
1618
	 *  - Cannot be used on unsaved objects.
1619
	 *
1620
	 * @param string $remoteClass
1621
	 * @param string $remoteRelation
1622
	 * @return DataList|DataObject The component, either as a list or single object
1623
	 * @throws BadMethodCallException
1624
	 * @throws InvalidArgumentException
1625
	 */
1626
	public function inferReciprocalComponent($remoteClass, $remoteRelation) {
1627
		/** @var DataObject $remote */
1628
		$remote = $remoteClass::singleton();
1629
		$class = $remote->getRelationClass($remoteRelation);
1630
1631
		// Validate arguments
1632
		if(!$this->isInDB()) {
1633
			throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1634
		}
1635
		if(empty($class)) {
1636
			throw new InvalidArgumentException(sprintf(
1637
				"%s invoked with invalid relation %s.%s",
1638
				__METHOD__,
1639
				$remoteClass,
1640
				$remoteRelation
1641
			));
1642
		}
1643
		if($class === 'DataObject') {
1644
			throw new InvalidArgumentException(sprintf(
1645
				"%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1646
				"This method does not support polymorphic relationships",
1647
				__METHOD__,
1648
				$remoteClass,
1649
				$remoteRelation
1650
			));
1651
		}
1652
		if(!is_a($this, $class, true)) {
1653
			throw new InvalidArgumentException(sprintf(
1654
				"Relation %s on %s does not refer to objects of type %s",
1655
				$remoteRelation, $remoteClass, get_class($this)
1656
			));
1657
		}
1658
1659
		// Check the relation type to mock
1660
		$relationType = $remote->getRelationType($remoteRelation);
1661
		switch($relationType) {
1662
			case 'has_one': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1663
				// Mock has_many
1664
				$joinField = "{$remoteRelation}ID";
1665
				$componentClass = static::getSchema()->classForField($remoteClass, $joinField);
1666
				$result = HasManyList::create($componentClass, $joinField);
1667
				if ($this->model) {
1668
					$result->setDataModel($this->model);
1669
				}
1670
				return $result
1671
					->setDataQueryParam($this->getInheritableQueryParams())
1672
					->forForeignID($this->ID);
1673
			}
1674
			case 'belongs_to':
1675
			case 'has_many': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1676
				// These relations must have a has_one on the other end, so find it
1677
				$joinField = $remote->getRemoteJoinField($remoteRelation, $relationType, $polymorphic);
1678
				if ($polymorphic) {
1679
					throw new InvalidArgumentException(sprintf(
1680
						"%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1681
						"to be a has_one polymorphic. This method does not support polymorphic relationships",
1682
						__METHOD__,
1683
						$remoteClass,
1684
						$remoteRelation
1685
					));
1686
				}
1687
				$joinID = $this->getField($joinField);
1688
				if (empty($joinID)) {
1689
					return null;
1690
				}
1691
				// Get object by joined ID
1692
				return DataObject::get($remoteClass)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1693
					->filter('ID', $joinID)
1694
					->setDataQueryParam($this->getInheritableQueryParams())
1695
					->first();
1696
			}
1697
			case 'many_many':
1698
			case 'belongs_many_many': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1699
				// Get components and extra fields from parent
1700
				list($componentClass, $parentClass, $componentField, $parentField, $table)
0 ignored issues
show
Unused Code introduced by
The assignment to $parentClass is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1701
					= $remote->manyManyComponent($remoteRelation);
1702
				$extraFields = $remote->manyManyExtraFieldsForComponent($remoteRelation) ?: array();
1703
1704
				// Reverse parent and component fields and create an inverse ManyManyList
1705
				/** @var ManyManyList $result */
1706
				$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1707
				if($this->model) {
1708
					$result->setDataModel($this->model);
1709
				}
1710
				$this->extend('updateManyManyComponents', $result);
1711
1712
				// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1713
				// foreignID set elsewhere.
1714
				return $result
1715
					->setDataQueryParam($this->getInheritableQueryParams())
1716
					->forForeignID($this->ID);
1717
			}
1718
			default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1719
				return null;
1720
			}
1721
		}
1722
	}
1723
1724
	/**
1725
	 * Tries to find the database key on another object that is used to store a
1726
	 * relationship to this class. If no join field can be found it defaults to 'ParentID'.
1727
	 *
1728
	 * If the remote field is polymorphic then $polymorphic is set to true, and the return value
1729
	 * is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
1730
	 *
1731
	 * @param string $component Name of the relation on the current object pointing to the
1732
	 * remote object.
1733
	 * @param string $type the join type - either 'has_many' or 'belongs_to'
1734
	 * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
1735
	 * @return string
1736
	 * @throws Exception
1737
	 */
1738
	public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
1739
		// Extract relation from current object
1740
		if($type === 'has_many') {
1741
			$remoteClass = $this->hasManyComponent($component, false);
1742
		} else {
1743
			$remoteClass = $this->belongsToComponent($component, false);
1744
		}
1745
1746
		if(empty($remoteClass)) {
1747
			throw new Exception("Unknown $type component '$component' on class '$this->class'");
1748
		}
1749
		if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
1750
			throw new Exception(
1751
				"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
1752
			);
1753
		}
1754
1755
		// If presented with an explicit field name (using dot notation) then extract field name
1756
		$remoteField = null;
1757
		if(strpos($remoteClass, '.') !== false) {
1758
			list($remoteClass, $remoteField) = explode('.', $remoteClass);
1759
		}
1760
1761
		// Reference remote has_one to check against
1762
		$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
1763
1764
		// Without an explicit field name, attempt to match the first remote field
1765
		// with the same type as the current class
1766
		if(empty($remoteField)) {
1767
			// look for remote has_one joins on this class or any parent classes
1768
			$remoteRelationsMap = array_flip($remoteRelations);
1769
			foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1770
				if(array_key_exists($class, $remoteRelationsMap)) {
1771
					$remoteField = $remoteRelationsMap[$class];
1772
					break;
1773
				}
1774
			}
1775
		}
1776
1777
		// In case of an indeterminate remote field show an error
1778
		if(empty($remoteField)) {
1779
			$polymorphic = false;
1780
			$message = "No has_one found on class '$remoteClass'";
1781
			if($type == 'has_many') {
1782
				// include a hint for has_many that is missing a has_one
1783
				$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
1784
				$message .= " requires a has_one on '$remoteClass'";
1785
			}
1786
			throw new Exception($message);
1787
		}
1788
1789
		// If given an explicit field name ensure the related class specifies this
1790
		if(empty($remoteRelations[$remoteField])) {
1791
			throw new Exception("Missing expected has_one named '$remoteField'
1792
				on class '$remoteClass' referenced by $type named '$component'
1793
				on class {$this->class}"
1794
			);
1795
		}
1796
1797
		// Inspect resulting found relation
1798
		if($remoteRelations[$remoteField] === 'DataObject') {
1799
			$polymorphic = true;
1800
			return $remoteField; // Composite polymorphic field does not include 'ID' suffix
1801
		} else {
1802
			$polymorphic = false;
1803
			return $remoteField . 'ID';
1804
		}
1805
	}
1806
1807
	/**
1808
	 * Returns a many-to-many component, as a ManyManyList.
1809
	 * @param string $componentName Name of the many-many component
1810
	 * @return ManyManyList The set of components
1811
	 */
1812
	public function getManyManyComponents($componentName) {
1813
		$manyManyComponent = $this->manyManyComponent($componentName);
1814
		if(!$manyManyComponent) {
1815
			throw new InvalidArgumentException(sprintf(
1816
				"DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1817
				$componentName,
1818
				$this->class
1819
			));
1820
		}
1821
1822
		list($parentClass, $componentClass, $parentField, $componentField, $table) = $manyManyComponent;
1823
1824
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1825
		if(!$this->ID) {
1826
			if(!isset($this->unsavedRelations[$componentName])) {
1827
				$this->unsavedRelations[$componentName] =
1828
					new UnsavedRelationList($parentClass, $componentName, $componentClass);
1829
			}
1830
			return $this->unsavedRelations[$componentName];
1831
		}
1832
1833
		$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
1834
		/** @var ManyManyList $result */
1835
		$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1836
1837
1838
		// Store component data in query meta-data
1839
		$result = $result->alterDataQuery(function($query) use ($extraFields) {
1840
			$query->setQueryParam('Component.ExtraFields', $extraFields);
1841
		});
1842
1843
		if($this->model) {
1844
			$result->setDataModel($this->model);
1845
		}
1846
1847
		$this->extend('updateManyManyComponents', $result);
1848
1849
		// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1850
		// foreignID set elsewhere.
1851
		return $result
1852
			->setDataQueryParam($this->getInheritableQueryParams())
1853
			->forForeignID($this->ID);
1854
	}
1855
1856
	/**
1857
	 * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1858
	 * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1859
	 *
1860
	 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1861
	 * 							their classes.
1862
	 */
1863
	public function hasOne() {
1864
		return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1865
	}
1866
1867
	/**
1868
	 * Return data for a specific has_one component.
1869
	 * @param string $component
1870
	 * @return string|null
1871
	 */
1872
	public function hasOneComponent($component) {
1873
		$classes = ClassInfo::ancestry($this, true);
1874
1875
		foreach(array_reverse($classes) as $class) {
1876
			$hasOnes = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
1877
			if(isset($hasOnes[$component])) {
1878
				return $hasOnes[$component];
1879
			}
1880
		}
1881
	}
1882
1883
	/**
1884
	 * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1885
	 * their class name will be returned.
1886
	 *
1887
	 * @param string $component - Name of component
1888
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1889
	 *        the field data stripped off. It defaults to TRUE.
1890
	 * @return string|array
1891
	 */
1892
	public function belongsTo($component = null, $classOnly = true) {
1893
		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...
1894
			Deprecation::notice(
1895
				'4.0',
1896
				'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
1897
				Deprecation::SCOPE_GLOBAL
1898
			);
1899
			return $this->belongsToComponent($component, $classOnly);
1900
		}
1901
1902
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1903
		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...
1904
			return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1905
		} else {
1906
			return $belongsTo ? $belongsTo : array();
1907
		}
1908
	}
1909
1910
	/**
1911
	 * Return data for a specific belongs_to component.
1912
	 * @param string $component
1913
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1914
	 *        the field data stripped off. It defaults to TRUE.
1915
	 * @return string|null
1916
	 */
1917
	public function belongsToComponent($component, $classOnly = true) {
1918
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1919
1920
		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...
1921
			$belongsTo = $belongsTo[$component];
1922
		} else {
1923
			return null;
1924
		}
1925
1926
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
1927
	}
1928
1929
	/**
1930
	 * Return all of the database fields in this object
1931
	 *
1932
	 * @param string $fieldName Limit the output to a specific field name
1933
	 * @param bool $includeClass If returning a single column, prefix the column with the class name
1934
	 * in Table.Column(spec) format
1935
	 * @return array|string|null The database fields, or if searching a single field,
1936
	 * just this one field if found. Field will be a string in FieldClass(args)
1937
	 * format, or RecordClass.FieldClass(args) format if $includeClass is true
1938
	 */
1939
	public function db($fieldName = null, $includeClass = false) {
1940
		$classes = ClassInfo::ancestry($this, true);
1941
1942
		// If we're looking for a specific field, we want to hit subclasses first as they may override field types
1943
		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...
1944
			$classes = array_reverse($classes);
1945
		}
1946
1947
		$db = array();
1948
		foreach($classes as $class) {
1949
			// Merge fields with new fields and composite fields
1950
			$fields = self::database_fields($class);
1951
			$compositeFields = self::composite_fields($class, false);
1952
			$db = array_merge($db, $fields, $compositeFields);
1953
1954
			// Check for search field
1955
			if($fieldName && isset($db[$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...
1956
				// Return found field
1957
				if(!$includeClass) {
1958
					return $db[$fieldName];
1959
				}
1960
				return $class . "." . $db[$fieldName];
1961
			}
1962
		}
1963
1964
		// At end of search complete
1965
		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...
1966
			return null;
1967
		} else {
1968
			return $db;
1969
		}
1970
	}
1971
1972
	/**
1973
	 * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1974
	 * relationships and their classes will be returned.
1975
	 *
1976
	 * @param string $component Deprecated - Name of component
1977
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1978
	 *        the field data stripped off. It defaults to TRUE.
1979
	 * @return string|array|false
1980
	 */
1981
	public function hasMany($component = null, $classOnly = true) {
1982
		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...
1983
			Deprecation::notice(
1984
				'4.0',
1985
				'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
1986
				Deprecation::SCOPE_GLOBAL
1987
			);
1988
			return $this->hasManyComponent($component, $classOnly);
1989
		}
1990
1991
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
1992
		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...
1993
			return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1994
		} else {
1995
			return $hasMany ? $hasMany : array();
1996
		}
1997
	}
1998
1999
	/**
2000
	 * Return data for a specific has_many component.
2001
	 * @param string $component
2002
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2003
	 *        the field data stripped off. It defaults to TRUE.
2004
	 * @return string|null
2005
	 */
2006
	public function hasManyComponent($component, $classOnly = true) {
2007
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2008
2009
		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...
2010
			$hasMany = $hasMany[$component];
2011
		} else {
2012
			return null;
2013
		}
2014
2015
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
2016
	}
2017
2018
	/**
2019
	 * Return the many-to-many extra fields specification.
2020
	 *
2021
	 * If you don't specify a component name, it returns all
2022
	 * extra fields for all components available.
2023
	 *
2024
	 * @return array|null
2025
	 */
2026
	public function manyManyExtraFields() {
2027
		return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2028
	}
2029
2030
	/**
2031
	 * Return the many-to-many extra fields specification for a specific component.
2032
	 * @param string $component
2033
	 * @return array|null
2034
	 */
2035
	public function manyManyExtraFieldsForComponent($component) {
2036
		// Get all many_many_extraFields defined in this class or parent classes
2037
		$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2038
		// Extra fields are immediately available
2039
		if(isset($extraFields[$component])) {
2040
			return $extraFields[$component];
2041
		}
2042
2043
		// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
2044
		$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2045
		$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2046
		if($candidate) {
2047
			$relationName = null;
2048
			// Extract class and relation name from dot-notation
2049
			if(strpos($candidate, '.') !== false) {
2050
				list($candidate, $relationName) = explode('.', $candidate, 2);
2051
			}
2052
2053
			// If we've not already found the relation name from dot notation, we need to find a relation that points
2054
			// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
2055
			// so it's safe to assume that it's the correct one
2056
			if(!$relationName) {
2057
				$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2058
2059
				foreach($candidateManyManys as $relation => $relatedClass) {
2060
					if (is_a($this, $relatedClass)) {
2061
						$relationName = $relation;
2062
					}
2063
				}
2064
			}
2065
2066
			// If we've found a matching relation on the target class, see if we can find extra fields for it
2067
			$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
2068
			if(isset($extraFields[$relationName])) {
2069
				return $extraFields[$relationName];
2070
			}
2071
		}
2072
2073
		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...
2074
	}
2075
2076
	/**
2077
	 * Return information about a many-to-many component.
2078
	 * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2079
	 * components are returned.
2080
	 *
2081
	 * @see DataObject::manyManyComponent()
2082
	 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2083
	 */
2084
	public function manyMany() {
2085
		$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
2086
		$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2087
		$items = array_merge($manyManys, $belongsManyManys);
2088
		return $items;
2089
	}
2090
2091
	/**
2092
	 * Return information about a specific many_many component. Returns a numeric array of:
2093
	 * array(
2094
	 * 	<classname>,		The class that relation is defined in e.g. "Product"
2095
	 * 	<candidateName>,	The target class of the relation e.g. "Category"
2096
	 * 	<parentField>,		The field name pointing to <classname>'s table e.g. "ProductID"
2097
	 * 	<childField>,		The field name pointing to <candidatename>'s table e.g. "CategoryID"
2098
	 * 	<joinTable>			The join table between the two classes e.g. "Product_Categories"
2099
	 * )
2100
	 * @param string $component The component name
2101
	 * @return array|null
2102
	 */
2103
	public function manyManyComponent($component) {
2104
		$classes = $this->getClassAncestry();
2105
		foreach($classes as $class) {
2106
			$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
2107
			// Check if the component is defined in many_many on this class
2108
			if(isset($manyMany[$component])) {
2109
				$candidate = $manyMany[$component];
2110
				$classTable = static::getSchema()->tableName($class);
2111
				$candidateTable = static::getSchema()->tableName($candidate);
2112
				$parentField = "{$classTable}ID";
2113
				$childField = $class === $candidate ? "ChildID" : "{$candidateTable}ID";
2114
				$joinTable = "{$classTable}_{$component}";
2115
				return array($class, $candidate, $parentField, $childField, $joinTable);
2116
			}
2117
2118
			// Check if the component is defined in belongs_many_many on this class
2119
			$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
2120
			if(!isset($belongsManyMany[$component])) {
2121
				continue;
2122
			}
2123
2124
			// Extract class and relation name from dot-notation
2125
			$candidate = $belongsManyMany[$component];
2126
			$relationName = null;
2127
			if(strpos($candidate, '.') !== false) {
2128
				list($candidate, $relationName) = explode('.', $candidate, 2);
2129
			}
2130
			$candidateTable = static::getSchema()->tableName($candidate);
2131
			$childField = $candidateTable . "ID";
2132
2133
			// We need to find the inverse component name, if not explicitly given
2134
			$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2135
			if(!$relationName && $otherManyMany) {
2136
				foreach($otherManyMany as $inverseComponentName => $childClass) {
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...
2137
					if($childClass === $class || is_subclass_of($class, $childClass)) {
2138
						$relationName = $inverseComponentName;
2139
						break;
2140
					}
2141
				}
2142
			}
2143
2144
			// Check valid relation found
2145
			if(!$relationName || !$otherManyMany || !isset($otherManyMany[$relationName])) {
2146
				throw new LogicException("Inverse component of $candidate not found ({$this->class})");
2147
			}
2148
2149
			// If we've got a relation name (extracted from dot-notation), we can already work out
2150
			// the join table and candidate class name...
2151
			$childClass = $otherManyMany[$relationName];
2152
			$joinTable = "{$candidateTable}_{$relationName}";
2153
2154
			// If we could work out the join table, we've got all the info we need
2155
			if ($childClass === $candidate) {
2156
				$parentField = "ChildID";
2157
			} else {
2158
				$childTable = static::getSchema()->tableName($childClass);
2159
				$parentField = "{$childTable}ID";
2160
			}
2161
			return array($class, $candidate, $parentField, $childField, $joinTable);
2162
		}
2163
	}
2164
2165
	/**
2166
	 * This returns an array (if it exists) describing the database extensions that are required, or false if none
2167
	 *
2168
	 * This is experimental, and is currently only a Postgres-specific enhancement.
2169
	 *
2170
	 * @param $class
2171
	 * @return array or false
2172
	 */
2173
	public function database_extensions($class){
2174
		$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2175
2176
		if($extensions) {
2177
			return $extensions;
2178
		} else {
2179
			return false;
2180
		}
2181
	}
2182
2183
	/**
2184
	 * Generates a SearchContext to be used for building and processing
2185
	 * a generic search form for properties on this object.
2186
	 *
2187
	 * @return SearchContext
2188
	 */
2189
	public function getDefaultSearchContext() {
2190
		return new SearchContext(
2191
			$this->class,
2192
			$this->scaffoldSearchFields(),
2193
			$this->defaultSearchFilters()
2194
		);
2195
	}
2196
2197
	/**
2198
	 * Determine which properties on the DataObject are
2199
	 * searchable, and map them to their default {@link FormField}
2200
	 * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2201
	 *
2202
	 * Some additional logic is included for switching field labels, based on
2203
	 * how generic or specific the field type is.
2204
	 *
2205
	 * Used by {@link SearchContext}.
2206
	 *
2207
	 * @param array $_params
2208
	 *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2209
	 *   'restrictFields': Numeric array of a field name whitelist
2210
	 * @return FieldList
2211
	 */
2212
	public function scaffoldSearchFields($_params = null) {
2213
		$params = array_merge(
2214
			array(
2215
				'fieldClasses' => false,
2216
				'restrictFields' => false
2217
			),
2218
			(array)$_params
2219
		);
2220
		$fields = new FieldList();
2221
		foreach($this->searchableFields() as $fieldName => $spec) {
2222
			if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
2223
2224
			// If a custom fieldclass is provided as a string, use it
2225
			if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2226
				$fieldClass = $params['fieldClasses'][$fieldName];
2227
				$field = new $fieldClass($fieldName);
2228
			// If we explicitly set a field, then construct that
2229
			} else if(isset($spec['field'])) {
2230
				// If it's a string, use it as a class name and construct
2231
				if(is_string($spec['field'])) {
2232
					$fieldClass = $spec['field'];
2233
					$field = new $fieldClass($fieldName);
2234
2235
				// If it's a FormField object, then just use that object directly.
2236
				} else if($spec['field'] instanceof FormField) {
2237
					$field = $spec['field'];
2238
2239
				// Otherwise we have a bug
2240
				} else {
2241
					user_error("Bad value for searchable_fields, 'field' value: "
2242
						. var_export($spec['field'], true), E_USER_WARNING);
2243
				}
2244
2245
			// Otherwise, use the database field's scaffolder
2246
			} else {
2247
				$field = $this->relObject($fieldName)->scaffoldSearchField();
2248
			}
2249
2250
			// Allow fields to opt out of search
2251
			if(!$field) {
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...
2252
				continue;
2253
			}
2254
2255
			if (strstr($fieldName, '.')) {
2256
				$field->setName(str_replace('.', '__', $fieldName));
2257
			}
2258
			$field->setTitle($spec['title']);
2259
2260
			$fields->push($field);
2261
		}
2262
		return $fields;
2263
	}
2264
2265
	/**
2266
	 * Scaffold a simple edit form for all properties on this dataobject,
2267
	 * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2268
	 * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2269
	 *
2270
	 * @uses FormScaffolder
2271
	 *
2272
	 * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2273
	 * @return FieldList
2274
	 */
2275
	public function scaffoldFormFields($_params = null) {
2276
		$params = array_merge(
2277
			array(
2278
				'tabbed' => false,
2279
				'includeRelations' => false,
2280
				'restrictFields' => false,
2281
				'fieldClasses' => false,
2282
				'ajaxSafe' => false
2283
			),
2284
			(array)$_params
2285
		);
2286
2287
		$fs = new FormScaffolder($this);
2288
		$fs->tabbed = $params['tabbed'];
2289
		$fs->includeRelations = $params['includeRelations'];
2290
		$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...
2291
		$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...
2292
		$fs->ajaxSafe = $params['ajaxSafe'];
2293
2294
		return $fs->getFieldList();
2295
	}
2296
2297
	/**
2298
	 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2299
	 * being called on extensions
2300
	 *
2301
	 * @param callable $callback The callback to execute
2302
	 */
2303
	protected function beforeUpdateCMSFields($callback) {
2304
		$this->beforeExtending('updateCMSFields', $callback);
2305
	}
2306
2307
	/**
2308
	 * Centerpiece of every data administration interface in Silverstripe,
2309
	 * which returns a {@link FieldList} suitable for a {@link Form} object.
2310
	 * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2311
	 * generate this set. To customize, overload this method in a subclass
2312
	 * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2313
	 *
2314
	 * <code>
2315
	 * class MyCustomClass extends DataObject {
2316
	 *  static $db = array('CustomProperty'=>'Boolean');
2317
	 *
2318
	 *  function getCMSFields() {
2319
	 *    $fields = parent::getCMSFields();
2320
	 *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2321
	 *    return $fields;
2322
	 *  }
2323
	 * }
2324
	 * </code>
2325
	 *
2326
	 * @see Good example of complex FormField building: SiteTree::getCMSFields()
2327
	 *
2328
	 * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2329
	 */
2330
	public function getCMSFields() {
2331
		$tabbedFields = $this->scaffoldFormFields(array(
2332
			// Don't allow has_many/many_many relationship editing before the record is first saved
2333
			'includeRelations' => ($this->ID > 0),
2334
			'tabbed' => true,
2335
			'ajaxSafe' => true
2336
		));
2337
2338
		$this->extend('updateCMSFields', $tabbedFields);
2339
2340
		return $tabbedFields;
2341
	}
2342
2343
	/**
2344
	 * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2345
	 * including that dataobject's extensions customised actions could be added to the EditForm.
2346
	 *
2347
	 * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2348
	 */
2349
	public function getCMSActions() {
2350
		$actions = new FieldList();
2351
		$this->extend('updateCMSActions', $actions);
2352
		return $actions;
2353
	}
2354
2355
2356
	/**
2357
	 * Used for simple frontend forms without relation editing
2358
	 * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2359
	 * by default. To customize, either overload this method in your
2360
	 * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2361
	 *
2362
	 * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2363
	 *
2364
	 * @param array $params See {@link scaffoldFormFields()}
2365
	 * @return FieldList Always returns a simple field collection without TabSet.
2366
	 */
2367
	public function getFrontEndFields($params = null) {
2368
		$untabbedFields = $this->scaffoldFormFields($params);
2369
		$this->extend('updateFrontEndFields', $untabbedFields);
2370
2371
		return $untabbedFields;
2372
	}
2373
2374
	/**
2375
	 * Gets the value of a field.
2376
	 * Called by {@link __get()} and any getFieldName() methods you might create.
2377
	 *
2378
	 * @param string $field The name of the field
2379
	 *
2380
	 * @return mixed The field value
2381
	 */
2382
	public function getField($field) {
2383
		// If we already have an object in $this->record, then we should just return that
2384
		if(isset($this->record[$field]) && is_object($this->record[$field])) {
2385
			return $this->record[$field];
2386
		}
2387
2388
		// Do we have a field that needs to be lazy loaded?
2389
		if(isset($this->record[$field.'_Lazy'])) {
2390
			$tableClass = $this->record[$field.'_Lazy'];
2391
			$this->loadLazyFields($tableClass);
2392
		}
2393
2394
		// In case of complex fields, return the DBField object
2395
		if(self::is_composite_field($this->class, $field)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::is_composite_field($this->class, $field) of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
2396
			$this->record[$field] = $this->dbObject($field);
2397
		}
2398
2399
		return isset($this->record[$field]) ? $this->record[$field] : null;
2400
	}
2401
2402
	/**
2403
	 * Loads all the stub fields that an initial lazy load didn't load fully.
2404
	 *
2405
	 * @param string $class Class to load the values from. Others are joined as required.
2406
	 * Not specifying a tableClass will load all lazy fields from all tables.
2407
	 * @return bool Flag if lazy loading succeeded
2408
	 */
2409
	protected function loadLazyFields($class = null) {
2410
		if(!$this->isInDB() || !is_numeric($this->ID)) {
2411
			return false;
2412
		}
2413
2414
		if (!$class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class 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...
2415
			$loaded = array();
2416
2417
			foreach ($this->record as $key => $value) {
2418
				if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2419
					$this->loadLazyFields($value);
2420
					$loaded[$value] = $value;
2421
				}
2422
			}
2423
2424
			return false;
2425
		}
2426
2427
		$dataQuery = new DataQuery($class);
2428
2429
		// Reset query parameter context to that of this DataObject
2430
		if($params = $this->getSourceQueryParams()) {
2431
			foreach($params as $key => $value) {
2432
				$dataQuery->setQueryParam($key, $value);
2433
			}
2434
		}
2435
2436
		// Limit query to the current record, unless it has the Versioned extension,
2437
		// in which case it requires special handling through augmentLoadLazyFields()
2438
		$baseIDColumn = static::getSchema()->sqlColumnForField($this, 'ID');
2439
		$dataQuery->where([
2440
			$baseIDColumn => $this->record['ID']
2441
		])->limit(1);
2442
2443
		$columns = array();
2444
2445
		// Add SQL for fields, both simple & multi-value
2446
		// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2447
		$databaseFields = self::database_fields($class);
2448
		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...
2449
			if(!isset($this->record[$k]) || $this->record[$k] === null) {
2450
				$columns[] = $k;
2451
			}
2452
		}
2453
2454
		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...
2455
			$query = $dataQuery->query();
2456
			$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2457
			$this->extend('augmentSQL', $query, $dataQuery);
2458
2459
			$dataQuery->setQueriedColumns($columns);
2460
			$newData = $dataQuery->execute()->record();
2461
2462
			// Load the data into record
2463
			if($newData) {
2464
				foreach($newData as $k => $v) {
2465
					if (in_array($k, $columns)) {
2466
						$this->record[$k] = $v;
2467
						$this->original[$k] = $v;
2468
						unset($this->record[$k . '_Lazy']);
2469
					}
2470
				}
2471
2472
			// No data means that the query returned nothing; assign 'null' to all the requested fields
2473
			} else {
2474
				foreach($columns as $k) {
2475
					$this->record[$k] = null;
2476
					$this->original[$k] = null;
2477
					unset($this->record[$k . '_Lazy']);
2478
				}
2479
			}
2480
		}
2481
		return true;
2482
	}
2483
2484
	/**
2485
	 * Return the fields that have changed.
2486
	 *
2487
	 * The change level affects what the functions defines as "changed":
2488
	 * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2489
	 * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2490
	 *   for example a change from 0 to null would not be included.
2491
	 *
2492
	 * Example return:
2493
	 * <code>
2494
	 * array(
2495
	 *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2496
	 * )
2497
	 * </code>
2498
	 *
2499
	 * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2500
	 * to return all database fields, or an array for an explicit filter. false returns all fields.
2501
	 * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2502
	 * @return array
2503
	 */
2504
	public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
2505
		$changedFields = array();
2506
2507
		// Update the changed array with references to changed obj-fields
2508
		foreach($this->record as $k => $v) {
2509
			// Prevents DBComposite infinite looping on isChanged
2510
			if(is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2511
				continue;
2512
			}
2513
			if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2514
				$this->changed[$k] = self::CHANGE_VALUE;
2515
			}
2516
		}
2517
2518
		if(is_array($databaseFieldsOnly)) {
2519
			$fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2520
		} elseif($databaseFieldsOnly) {
2521
			$fields = array_intersect_key((array)$this->changed, $this->db());
2522
		} else {
2523
			$fields = $this->changed;
2524
		}
2525
2526
		// Filter the list to those of a certain change level
2527
		if($changeLevel > self::CHANGE_STRICT) {
2528
			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...
2529
				if($level < $changeLevel) {
2530
					unset($fields[$name]);
2531
				}
2532
			}
2533
		}
2534
2535
		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...
2536
			$changedFields[$name] = array(
2537
				'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2538
				'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2539
				'level' => $level
2540
			);
2541
		}
2542
2543
		return $changedFields;
2544
	}
2545
2546
	/**
2547
	 * Uses {@link getChangedFields()} to determine if fields have been changed
2548
	 * since loading them from the database.
2549
	 *
2550
	 * @param string $fieldName Name of the database field to check, will check for any if not given
2551
	 * @param int $changeLevel See {@link getChangedFields()}
2552
	 * @return boolean
2553
	 */
2554
	public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
2555
		$fields = $fieldName ? array($fieldName) : true;
2556
		$changed = $this->getChangedFields($fields, $changeLevel);
2557
		if(!isset($fieldName)) {
2558
			return !empty($changed);
2559
		}
2560
		else {
2561
			return array_key_exists($fieldName, $changed);
2562
		}
2563
	}
2564
2565
	/**
2566
	 * Set the value of the field
2567
	 * Called by {@link __set()} and any setFieldName() methods you might create.
2568
	 *
2569
	 * @param string $fieldName Name of the field
2570
	 * @param mixed $val New field value
2571
	 * @return DataObject $this
2572
	 */
2573
	public function setField($fieldName, $val) {
2574
		//if it's a has_one component, destroy the cache
2575
		if (substr($fieldName, -2) == 'ID') {
2576
			unset($this->components[substr($fieldName, 0, -2)]);
2577
		}
2578
2579
		// If we've just lazy-loaded the column, then we need to populate the $original array
2580
		if(isset($this->record[$fieldName.'_Lazy'])) {
2581
			$tableClass = $this->record[$fieldName.'_Lazy'];
2582
			$this->loadLazyFields($tableClass);
2583
		}
2584
2585
		// Situation 1: Passing an DBField
2586
		if($val instanceof DBField) {
2587
			$val->setName($fieldName);
2588
			$val->saveInto($this);
2589
2590
			// Situation 1a: Composite fields should remain bound in case they are
2591
			// later referenced to update the parent dataobject
2592
			if($val instanceof DBComposite) {
2593
				$val->bindTo($this);
2594
				$this->record[$fieldName] = $val;
2595
			}
2596
		// Situation 2: Passing a literal or non-DBField object
2597
		} else {
2598
			// If this is a proper database field, we shouldn't be getting non-DBField objects
2599
			if(is_object($val) && $this->db($fieldName)) {
2600
				user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2601
			}
2602
2603
			// if a field is not existing or has strictly changed
2604
			if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2605
				// TODO Add check for php-level defaults which are not set in the db
2606
				// TODO Add check for hidden input-fields (readonly) which are not set in the db
2607
				// At the very least, the type has changed
2608
				$this->changed[$fieldName] = self::CHANGE_STRICT;
2609
2610
				if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
2611
						&& $this->record[$fieldName] != $val)) {
2612
2613
					// Value has changed as well, not just the type
2614
					$this->changed[$fieldName] = self::CHANGE_VALUE;
2615
				}
2616
2617
				// Value is always saved back when strict check succeeds.
2618
				$this->record[$fieldName] = $val;
2619
			}
2620
		}
2621
		return $this;
2622
	}
2623
2624
	/**
2625
	 * Set the value of the field, using a casting object.
2626
	 * This is useful when you aren't sure that a date is in SQL format, for example.
2627
	 * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2628
	 * can be saved into the Image table.
2629
	 *
2630
	 * @param string $fieldName Name of the field
2631
	 * @param mixed $value New field value
2632
	 * @return $this
2633
	 */
2634
	public function setCastedField($fieldName, $value) {
2635
		if(!$fieldName) {
2636
			user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2637
		}
2638
		$fieldObj = $this->dbObject($fieldName);
2639
		if($fieldObj) {
2640
			$fieldObj->setValue($value);
2641
			$fieldObj->saveInto($this);
2642
		} else {
2643
			$this->$fieldName = $value;
2644
		}
2645
		return $this;
2646
	}
2647
2648
	/**
2649
	 * {@inheritdoc}
2650
	 */
2651
	public function castingHelper($field) {
2652
		if ($fieldSpec = $this->db($field)) {
2653
			return $fieldSpec;
2654
		}
2655
2656
		// many_many_extraFields aren't presented by db(), so we check if the source query params
2657
		// provide us with meta-data for a many_many relation we can inspect for extra fields.
2658
		$queryParams = $this->getSourceQueryParams();
2659
		if (!empty($queryParams['Component.ExtraFields'])) {
2660
			$extraFields = $queryParams['Component.ExtraFields'];
2661
2662
			if (isset($extraFields[$field])) {
2663
				return $extraFields[$field];
2664
			}
2665
		}
2666
2667
		return parent::castingHelper($field);
2668
	}
2669
2670
	/**
2671
	 * Returns true if the given field exists in a database column on any of
2672
	 * the objects tables and optionally look up a dynamic getter with
2673
	 * get<fieldName>().
2674
	 *
2675
	 * @param string $field Name of the field
2676
	 * @return boolean True if the given field exists
2677
	 */
2678
	public function hasField($field) {
2679
		return (
2680
			array_key_exists($field, $this->record)
2681
			|| $this->db($field)
2682
			|| (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...
2683
			|| $this->hasMethod("get{$field}")
2684
		);
2685
	}
2686
2687
	/**
2688
	 * Returns true if the given field exists as a database column
2689
	 *
2690
	 * @param string $field Name of the field
2691
	 *
2692
	 * @return boolean
2693
	 */
2694
	public function hasDatabaseField($field) {
2695
		return $this->db($field)
2696
 			&& ! self::is_composite_field(get_class($this), $field);
0 ignored issues
show
Bug Best Practice introduced by
The expression self::is_composite_field(get_class($this), $field) of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false 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...
2697
	}
2698
2699
	/**
2700
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2701
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2702
	 *
2703
	 * @param string $field Name of the field
2704
	 * @return string The field type of the given field
2705
	 */
2706
	public function hasOwnTableDatabaseField($field) {
2707
		return self::has_own_table_database_field($this->class, $field);
2708
	}
2709
2710
	/**
2711
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2712
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2713
	 *
2714
	 * @param string $class Class name to check
2715
	 * @param string $field Name of the field
2716
	 * @return string The field type of the given field
2717
	 */
2718
	public static function has_own_table_database_field($class, $field) {
2719
		$fieldMap = self::database_fields($class);
2720
2721
		// Remove string-based "constructor-arguments" from the DBField definition
2722
		if(isset($fieldMap[$field])) {
2723
			$spec = $fieldMap[$field];
2724
			if(is_string($spec)) {
2725
				return strtok($spec,'(');
2726
			} else {
2727
				return $spec['type'];
2728
			}
2729
		}
2730
	}
2731
2732
	/**
2733
	 * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2734
	 * actually looking in the database.
2735
	 *
2736
	 * @param string $dataClass
2737
	 * @return bool
2738
	 */
2739
	public static function has_own_table($dataClass) {
2740
		if(!is_subclass_of($dataClass, 'DataObject')) {
2741
			return false;
2742
		}
2743
		$fields = static::database_fields($dataClass);
2744
		return !empty($fields);
2745
	}
2746
2747
	/**
2748
	 * Returns true if the member is allowed to do the given action.
2749
	 * See {@link extendedCan()} for a more versatile tri-state permission control.
2750
	 *
2751
	 * @param string $perm The permission to be checked, such as 'View'.
2752
	 * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2753
	 * in user.
2754
	 * @param array $context Additional $context to pass to extendedCan()
2755
	 *
2756
	 * @return boolean True if the the member is allowed to do the given action
2757
	 */
2758
	public function can($perm, $member = null, $context = array()) {
2759
		if(!isset($member)) {
2760
			$member = Member::currentUser();
2761
		}
2762
		if(Permission::checkMember($member, "ADMIN")) return true;
2763
2764
		if($this->manyManyComponent('Can' . $perm)) {
2765
			if($this->ParentID && $this->SecurityType == 'Inherit') {
2766
				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...
2767
					return false;
2768
				}
2769
				return $this->Parent->can($perm, $member);
2770
2771
			} else {
2772
				$permissionCache = $this->uninherited('permissionCache');
2773
				$memberID = $member ? $member->ID : 'none';
2774
2775
				if(!isset($permissionCache[$memberID][$perm])) {
2776
					if($member->ID) {
2777
						$groups = $member->Groups();
2778
					}
2779
2780
					$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...
2781
2782
					// TODO Fix relation table hardcoding
2783
					$query = new SQLSelect(
2784
						"\"Page_Can$perm\".PageID",
2785
					array("\"Page_Can$perm\""),
2786
						"GroupID IN ($groupList)");
2787
2788
					$permissionCache[$memberID][$perm] = $query->execute()->column();
2789
2790
					if($perm == "View") {
2791
						// TODO Fix relation table hardcoding
2792
						$query = new SQLSelect("\"SiteTree\".\"ID\"", array(
2793
							"\"SiteTree\"",
2794
							"LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2795
							), "\"Page_CanView\".\"PageID\" IS NULL");
2796
2797
							$unsecuredPages = $query->execute()->column();
2798
							if($permissionCache[$memberID][$perm]) {
2799
								$permissionCache[$memberID][$perm]
2800
									= array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2801
							} else {
2802
								$permissionCache[$memberID][$perm] = $unsecuredPages;
2803
							}
2804
					}
2805
2806
					Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2807
				}
2808
2809
				if($permissionCache[$memberID][$perm]) {
2810
					return in_array($this->ID, $permissionCache[$memberID][$perm]);
2811
				}
2812
			}
2813
		} else {
2814
			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, AssetControlExtensionTest_ArchivedObject, AssetControlExtensionTest_Object, AssetControlExtensionTest_VersionedObject, AssetFieldTest_Controller, AssetFieldTest_Object, BasicAuthTest_ControllerSecuredWithPermission, BasicAuthTest_ControllerSecuredWithoutPermission, BulkLoaderTestPlayer, CMSMenuTest_CustomTitle, CMSMenuTest_LeftAndMainController, CMSProfileController, CMSSecurity, CampaignAdmin, ChangeSet, ChangeSetItem, ChangeSetItemTest_Versioned, ChangeSetTest_Base, ChangeSetTest_End, ChangeSetTest_EndChild, ChangeSetTest_Mid, CheckboxFieldTest_Article, CheckboxSetFieldTest_Article, CheckboxSetFieldTest_Tag, ClassInfoTest_BaseClass, ClassInfoTest_BaseDataClass, ClassInfoTest_ChildClass, ClassInfoTest_GrandChildClass, ClassInfoTest_HasFields, ClassInfoTest_NoFields, ClassInfoTest_WithCustomTable, ClassInfoTest_WithRelation, CliController, ComponentSetTest_Player, ComponentSetTest_Team, 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, DBClassNameTest_CustomDefault, DBClassNameTest_CustomDefaultSubclass, DBClassNameTest_Object, DBClassNameTest_ObjectSubClass, DBClassNameTest_ObjectSubSubClass, DBClassNameTest_OtherClass, DBCompositeTest_DataObject, DBFileTest_ImageOnly, DBFileTest_Object, DBFileTest_Subclass, DataDifferencerTest_HasOneRelationObject, DataDifferencerTest_Object, DataExtensionTest_CMSFieldsBase, DataExtensionTest_CMSFieldsChild, DataExtensionTest_CMSFieldsGrandchild, DataExtensionTest_Member, DataExtensionTest_MyObject, DataExtensionTest_Player, DataExtensionTest_RelatedObject, DataObject, DataObjectDuplicateTestClass1, DataObjectDuplicateTestClass2, DataObjectDuplicateTestClass3, DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO, DataObjectSchemaTest_BaseClass, DataObjectSchemaTest_BaseDataClass, DataObjectSchemaTest_ChildClass, DataObjectSchemaTest_GrandChildClass, DataObjectSchemaTest_HasFields, DataObjectSchemaTest_NoFields, DataObjectSchemaTest_WithCustomTable, DataObjectSchemaTest_WithRelation, 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, 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_Mom, GridFieldSortableHeaderTest_Team, GridFieldSortableHeaderTest_TeamGroup, GridFieldTest_Cheerleader, GridFieldTest_Permissions, GridFieldTest_Player, GridFieldTest_Team, GridField_URLHandlerTest_Controller, Group, GroupTest_Member, HTMLEditorFieldTest_Object, HierarchyHideTest_Object, HierarchyHideTest_SubObject, HierarchyTest_Object, Image, InstallerTest, 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, MySQLDatabaseTest_Data, Namespaced\DOST\MyObject, Namespaced\DOST\MyObject_CustomTable, Namespaced\DOST\MyObject_NamespacedTable, Namespaced\DOST\MyObject_Namespaced_Subclass, Namespaced\DOST\MyObject_NestedObject, Namespaced\DOST\MyObject_NoFields, NumericFieldTest_Object, OtherSubclassWithSameField, Permission, PermissionRole, PermissionRoleCode, RememberLoginHash, RequestHandlingFieldTest_Controller, RequestHandlingTest_AllowedController, RequestHandlingTest_Controller, RequestHandlingTest_Cont...rFormWithAllowedActions, RequestHandlingTest_FormActionController, SQLInsertTestBase, SQLSelectTestBase, SQLSelectTestChild, SQLSelectTest_DO, SQLUpdateChild, SQLUpdateTestBase, SSViewerCacheBlockTest_Model, SSViewerCacheBlockTest_VersionedModel, SSViewerTest_Object, SapphireInfo, SapphireREPL, 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\Filesystem\...ProtectedFileController, SilverStripe\Framework\Tests\ClassI, SubclassedDBFieldObject, TaskRunner, TestNamespace\SSViewerTest_Controller, TransactionTest_Object, UnsavedRelationListTest_DataObject, Upload, UploadFieldTest_Controller, UploadFieldTest_ExtendedFile, UploadFieldTest_Record, VersionableExtensionsTest_DataObject, VersionedLazySub_DataObject, VersionedLazy_DataObject, VersionedOwnershipTest_Attachment, VersionedOwnershipTest_Banner, VersionedOwnershipTest_CustomRelation, VersionedOwnershipTest_Image, VersionedOwnershipTest_Object, VersionedOwnershipTest_Page, VersionedOwnershipTest_Related, VersionedOwnershipTest_RelatedMany, VersionedOwnershipTest_Subclass, VersionedTest_AnotherSubclass, VersionedTest_DataObject, VersionedTest_PublicStage, VersionedTest_PublicViaExtension, VersionedTest_RelatedWithoutVersion, VersionedTest_SingleStage, VersionedTest_Subclass, VersionedTest_UnversionedWithField, VersionedTest_WithIndexes, XMLDataFormatterTest_DataObject, YamlFixtureTest_DataObject, YamlFixtureTest_DataObjectRelation, 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...
2815
		}
2816
	}
2817
2818
	/**
2819
	 * Process tri-state responses from permission-alterting extensions.  The extensions are
2820
	 * expected to return one of three values:
2821
	 *
2822
	 *  - false: Disallow this permission, regardless of what other extensions say
2823
	 *  - true: Allow this permission, as long as no other extensions return false
2824
	 *  - NULL: Don't affect the outcome
2825
	 *
2826
	 * This method itself returns a tri-state value, and is designed to be used like this:
2827
	 *
2828
	 * <code>
2829
	 * $extended = $this->extendedCan('canDoSomething', $member);
2830
	 * if($extended !== null) return $extended;
2831
	 * else return $normalValue;
2832
	 * </code>
2833
	 *
2834
	 * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2835
	 * @param Member|int $member
2836
	 * @param array $context Optional context
2837
	 * @return boolean|null
2838
	 */
2839
	public function extendedCan($methodName, $member, $context = array()) {
2840
		$results = $this->extend($methodName, $member, $context);
2841
		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...
2842
			// Remove NULLs
2843
			$results = array_filter($results, function($v) {return !is_null($v);});
2844
			// If there are any non-NULL responses, then return the lowest one of them.
2845
			// If any explicitly deny the permission, then we don't get access
2846
			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...
2847
		}
2848
		return null;
2849
	}
2850
2851
	/**
2852
	 * @param Member $member
2853
	 * @return boolean
2854
	 */
2855
	public function canView($member = null) {
2856
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2855 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...
2857
		if($extended !== null) {
2858
			return $extended;
2859
		}
2860
		return Permission::check('ADMIN', 'any', $member);
2861
	}
2862
2863
	/**
2864
	 * @param Member $member
2865
	 * @return boolean
2866
	 */
2867
	public function canEdit($member = null) {
2868
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2867 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...
2869
		if($extended !== null) {
2870
			return $extended;
2871
		}
2872
		return Permission::check('ADMIN', 'any', $member);
2873
	}
2874
2875
	/**
2876
	 * @param Member $member
2877
	 * @return boolean
2878
	 */
2879
	public function canDelete($member = null) {
2880
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2879 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...
2881
		if($extended !== null) {
2882
			return $extended;
2883
		}
2884
		return Permission::check('ADMIN', 'any', $member);
2885
	}
2886
2887
	/**
2888
	 * @param Member $member
2889
	 * @param array $context Additional context-specific data which might
2890
	 * affect whether (or where) this object could be created.
2891
	 * @return boolean
2892
	 */
2893
	public function canCreate($member = null, $context = array()) {
2894
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2893 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...
2895
		if($extended !== null) {
2896
			return $extended;
2897
		}
2898
		return Permission::check('ADMIN', 'any', $member);
2899
	}
2900
2901
	/**
2902
	 * Debugging used by Debug::show()
2903
	 *
2904
	 * @return string HTML data representing this object
2905
	 */
2906
	public function debug() {
2907
		$val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2908
		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...
2909
			$val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2910
		}
2911
		$val .= "</ul>\n";
2912
		return $val;
2913
	}
2914
2915
	/**
2916
	 * Return the DBField object that represents the given field.
2917
	 * This works similarly to obj() with 2 key differences:
2918
	 *   - it still returns an object even when the field has no value.
2919
	 *   - it only matches fields and not methods
2920
	 *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2921
	 *
2922
	 * @param string $fieldName Name of the field
2923
	 * @return DBField The field as a DBField object
2924
	 */
2925
	public function dbObject($fieldName) {
2926
		$value = isset($this->record[$fieldName])
2927
			? $this->record[$fieldName]
2928
			: null;
2929
2930
		// If we have a DBField object in $this->record, then return that
2931
		if(is_object($value)) {
2932
			return $value;
2933
		}
2934
2935
		// Build and populate new field otherwise
2936
		$helper = $this->db($fieldName, true);
2937
		if($helper) {
2938
			list($table, $spec) = explode('.', $helper);
2939
			$obj = Object::create_from_string($spec, $fieldName);
2940
			$obj->setTable($table);
2941
			$obj->setValue($value, $this, false);
2942
			return $obj;
2943
		}
2944
	}
2945
2946
	/**
2947
	 * Traverses to a DBField referenced by relationships between data objects.
2948
	 *
2949
	 * The path to the related field is specified with dot separated syntax
2950
	 * (eg: Parent.Child.Child.FieldName).
2951
	 *
2952
	 * @param string $fieldPath
2953
	 *
2954
	 * @return mixed DBField of the field on the object or a DataList instance.
2955
	 */
2956
	public function relObject($fieldPath) {
2957
		$object = null;
2958
2959
		if(strpos($fieldPath, '.') !== false) {
2960
			$parts = explode('.', $fieldPath);
2961
			$fieldName = array_pop($parts);
2962
2963
			// Traverse dot syntax
2964
			$component = $this;
2965
2966
			foreach($parts as $relation) {
2967
				if($component instanceof SS_List) {
2968
					if(method_exists($component,$relation)) {
2969
						$component = $component->$relation();
2970
					} else {
2971
						$component = $component->relation($relation);
2972
					}
2973
				} else {
2974
					$component = $component->$relation();
2975
				}
2976
			}
2977
2978
			$object = $component->dbObject($fieldName);
2979
2980
		} else {
2981
			$object = $this->dbObject($fieldPath);
2982
		}
2983
2984
		return $object;
2985
	}
2986
2987
	/**
2988
	 * Traverses to a field referenced by relationships between data objects, returning the value
2989
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2990
	 *
2991
	 * @param $fieldName string
2992
	 * @return string | null - will return null on a missing value
2993
	 */
2994
	public function relField($fieldName) {
2995
		$component = $this;
2996
2997
		// We're dealing with relations here so we traverse the dot syntax
2998
		if(strpos($fieldName, '.') !== false) {
2999
			$relations = explode('.', $fieldName);
3000
			$fieldName = array_pop($relations);
3001
			foreach($relations as $relation) {
3002
				// Inspect $component for element $relation
3003
				if($component->hasMethod($relation)) {
3004
					// Check nested method
3005
					$component = $component->$relation();
3006
				} elseif($component instanceof SS_List) {
3007
					// Select adjacent relation from DataList
3008
					$component = $component->relation($relation);
3009
				} elseif($component instanceof DataObject
3010
					&& ($dbObject = $component->dbObject($relation))
3011
				) {
3012
					// Select db object
3013
					$component = $dbObject;
3014
				} else {
3015
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
3016
				}
3017
			}
3018
		}
3019
3020
		// Bail if the component is null
3021
		if(!$component) {
3022
			return null;
3023
		}
3024
		if($component->hasMethod($fieldName)) {
3025
			return $component->$fieldName();
3026
		}
3027
		return $component->$fieldName;
3028
	}
3029
3030
	/**
3031
	 * Temporary hack to return an association name, based on class, to get around the mangle
3032
	 * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3033
	 *
3034
	 * @param string $className
3035
	 * @return string
3036
	 */
3037
	public function getReverseAssociation($className) {
3038
		if (is_array($this->manyMany())) {
3039
			$many_many = array_flip($this->manyMany());
3040
			if (array_key_exists($className, $many_many)) return $many_many[$className];
3041
		}
3042
		if (is_array($this->hasMany())) {
3043
			$has_many = array_flip($this->hasMany());
3044
			if (array_key_exists($className, $has_many)) return $has_many[$className];
3045
		}
3046
		if (is_array($this->hasOne())) {
3047
			$has_one = array_flip($this->hasOne());
3048
			if (array_key_exists($className, $has_one)) return $has_one[$className];
3049
		}
3050
3051
		return false;
3052
	}
3053
3054
	/**
3055
	 * Return all objects matching the filter
3056
	 * sub-classes are automatically selected and included
3057
	 *
3058
	 * @param string $callerClass The class of objects to be returned
3059
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3060
	 * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3061
	 * @param string|array $sort A sort expression to be inserted into the ORDER
3062
	 * BY clause.  If omitted, self::$default_sort will be used.
3063
	 * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3064
	 * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3065
	 * @param string $containerClass The container class to return the results in.
3066
	 *
3067
	 * @todo $containerClass is Ignored, why?
3068
	 *
3069
	 * @return DataList The objects matching the filter, in the class specified by $containerClass
3070
	 */
3071
	public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
3072
			$containerClass = 'DataList') {
3073
3074
		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...
3075
			$callerClass = get_called_class();
3076
			if($callerClass == 'DataObject') {
3077
				throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3078
			}
3079
3080
			if($filter || $sort || $join || $limit || ($containerClass != 'DataList')) {
3081
				throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3082
					. ' arguments');
3083
			}
3084
3085
			$result = DataList::create(get_called_class());
3086
			$result->setDataModel(DataModel::inst());
3087
			return $result;
3088
		}
3089
3090
		if($join) {
3091
			throw new \InvalidArgumentException(
3092
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3093
			);
3094
		}
3095
3096
		$result = DataList::create($callerClass)->where($filter)->sort($sort);
3097
3098
		if($limit && strpos($limit, ',') !== false) {
3099
			$limitArguments = explode(',', $limit);
3100
			$result = $result->limit($limitArguments[1],$limitArguments[0]);
3101
		} elseif($limit) {
3102
			$result = $result->limit($limit);
3103
		}
3104
3105
		$result->setDataModel(DataModel::inst());
3106
		return $result;
3107
	}
3108
3109
3110
	/**
3111
	 * Return the first item matching the given query.
3112
	 * All calls to get_one() are cached.
3113
	 *
3114
	 * @param string $callerClass The class of objects to be returned
3115
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3116
	 * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3117
	 * @param boolean $cache Use caching
3118
	 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3119
	 *
3120
	 * @return DataObject The first item matching the query
3121
	 */
3122
	public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
3123
		$SNG = singleton($callerClass);
3124
3125
		$cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3126
		$cacheKey = md5(var_export($cacheComponents, true));
3127
3128
		// Flush destroyed items out of the cache
3129
		if($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
3130
				&& self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
3131
				&& self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
3132
3133
			self::$_cache_get_one[$callerClass][$cacheKey] = false;
3134
		}
3135
		if(!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3136
			$dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3137
			$item = $dl->First();
3138
3139
			if($cache) {
3140
				self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3141
				if(!self::$_cache_get_one[$callerClass][$cacheKey]) {
3142
					self::$_cache_get_one[$callerClass][$cacheKey] = false;
3143
				}
3144
			}
3145
		}
3146
		return $cache ? self::$_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...
3147
	}
3148
3149
	/**
3150
	 * Flush the cached results for all relations (has_one, has_many, many_many)
3151
	 * Also clears any cached aggregate data.
3152
	 *
3153
	 * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3154
	 *                            When false will just clear session-local cached data
3155
	 * @return DataObject $this
3156
	 */
3157
	public function flushCache($persistent = true) {
0 ignored issues
show
Unused Code introduced by
The parameter $persistent is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
3158
		if($this->class == 'DataObject') {
3159
			self::$_cache_get_one = array();
3160
			return $this;
3161
		}
3162
3163
		$classes = ClassInfo::ancestry($this->class);
3164
		foreach($classes as $class) {
3165
			if(isset(self::$_cache_get_one[$class])) unset(self::$_cache_get_one[$class]);
3166
		}
3167
3168
		$this->extend('flushCache');
3169
3170
		$this->components = array();
3171
		return $this;
3172
	}
3173
3174
	/**
3175
	 * Flush the get_one global cache and destroy associated objects.
3176
	 */
3177
	public static function flush_and_destroy_cache() {
3178
		if(self::$_cache_get_one) foreach(self::$_cache_get_one as $class => $items) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$_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...
3179
			if(is_array($items)) foreach($items as $item) {
3180
				if($item) $item->destroy();
3181
			}
3182
		}
3183
		self::$_cache_get_one = array();
3184
	}
3185
3186
	/**
3187
	 * Reset all global caches associated with DataObject.
3188
	 */
3189
	public static function reset() {
3190
		// @todo Decouple these
3191
		DBClassName::clear_classname_cache();
3192
		ClassInfo::reset_db_cache();
3193
		static::getSchema()->reset();
3194
		self::$_cache_has_own_table = array();
3195
		self::$_cache_get_one = array();
3196
		self::$_cache_field_labels = array();
3197
	}
3198
3199
	/**
3200
	 * Return the given element, searching by ID
3201
	 *
3202
	 * @param string $callerClass The class of the object to be returned
3203
	 * @param int $id The id of the element
3204
	 * @param boolean $cache See {@link get_one()}
3205
	 *
3206
	 * @return DataObject The element
3207
	 */
3208
	public static function get_by_id($callerClass, $id, $cache = true) {
3209
		if(!is_numeric($id)) {
3210
			user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3211
		}
3212
3213
		// Pass to get_one
3214
		$column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
3215
		return DataObject::get_one($callerClass, array($column => $id), $cache);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3216
	}
3217
3218
	/**
3219
	 * Get the name of the base table for this object
3220
	 *
3221
	 * @return string
3222
	 */
3223
	public function baseTable() {
3224
		return static::getSchema()->baseDataTable($this);
3225
	}
3226
3227
	/**
3228
	 * Get the base class for this object
3229
	 *
3230
	 * @return string
3231
	 */
3232
	public function baseClass() {
3233
		return static::getSchema()->baseDataClass($this);
3234
	}
3235
3236
	/**
3237
	 * @var array Parameters used in the query that built this object.
3238
	 * This can be used by decorators (e.g. lazy loading) to
3239
	 * run additional queries using the same context.
3240
	 */
3241
	protected $sourceQueryParams;
3242
3243
	/**
3244
	 * @see $sourceQueryParams
3245
	 * @return array
3246
	 */
3247
	public function getSourceQueryParams() {
3248
		return $this->sourceQueryParams;
3249
	}
3250
3251
	/**
3252
	 * Get list of parameters that should be inherited to relations on this object
3253
	 *
3254
	 * @return array
3255
	 */
3256
	public function getInheritableQueryParams() {
3257
		$params = $this->getSourceQueryParams();
3258
		$this->extend('updateInheritableQueryParams', $params);
3259
		return $params;
3260
	}
3261
3262
	/**
3263
	 * @see $sourceQueryParams
3264
	 * @param array
3265
	 */
3266
	public function setSourceQueryParams($array) {
3267
		$this->sourceQueryParams = $array;
3268
	}
3269
3270
	/**
3271
	 * @see $sourceQueryParams
3272
	 * @param string $key
3273
	 * @param string $value
3274
	 */
3275
	public function setSourceQueryParam($key, $value) {
3276
		$this->sourceQueryParams[$key] = $value;
3277
	}
3278
3279
	/**
3280
	 * @see $sourceQueryParams
3281
	 * @param string $key
3282
	 * @return string
3283
	 */
3284
	public function getSourceQueryParam($key) {
3285
		if(isset($this->sourceQueryParams[$key])) {
3286
			return $this->sourceQueryParams[$key];
3287
		}
3288
		return null;
3289
	}
3290
3291
	//-------------------------------------------------------------------------------------------//
3292
3293
	/**
3294
	 * Return the database indexes on this table.
3295
	 * This array is indexed by the name of the field with the index, and
3296
	 * the value is the type of index.
3297
	 */
3298
	public function databaseIndexes() {
3299
		$has_one = $this->uninherited('has_one');
3300
		$classIndexes = $this->uninherited('indexes');
3301
		//$fileIndexes = $this->uninherited('fileIndexes', true);
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3302
3303
		$indexes = array();
3304
3305
		if($has_one) {
3306
			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...
3307
				$indexes[$relationshipName . 'ID'] = true;
3308
			}
3309
		}
3310
3311
		if($classIndexes) {
3312
			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...
3313
				$indexes[$indexName] = $indexType;
3314
			}
3315
		}
3316
3317
		if(get_parent_class($this) == "DataObject") {
3318
			$indexes['ClassName'] = true;
3319
		}
3320
3321
		return $indexes;
3322
	}
3323
3324
	/**
3325
	 * Check the database schema and update it as necessary.
3326
	 *
3327
	 * @uses DataExtension->augmentDatabase()
3328
	 */
3329
	public function requireTable() {
3330
		// Only build the table if we've actually got fields
3331
		$fields = self::database_fields($this->class);
3332
		$table = static::getSchema()->tableName($this->class);
3333
		$extensions = self::database_extensions($this->class);
3334
3335
		$indexes = $this->databaseIndexes();
3336
3337
		// Validate relationship configuration
3338
		$this->validateModelDefinitions();
3339
		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...
3340
			$hasAutoIncPK = get_parent_class($this) === 'DataObject';
3341
			DB::require_table(
3342
				$table, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'), $extensions
3343
			);
3344
		} else {
3345
			DB::dont_require_table($table);
3346
		}
3347
3348
		// Build any child tables for many_many items
3349
		if($manyMany = $this->uninherited('many_many')) {
3350
			$extras = $this->uninherited('many_many_extraFields');
3351
			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...
3352
				// Build field list
3353
				if($this->class === $childClass) {
3354
					$childField = "ChildID";
3355
				} else {
3356
					$childTable = $this->getSchema()->tableName($childClass);
3357
					$childField = "{$childTable}ID";
3358
				}
3359
				$manymanyFields = array(
3360
					"{$table}ID" => "Int",
3361
					$childField => "Int",
3362
				);
3363
				if(isset($extras[$relationship])) {
3364
					$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
3365
				}
3366
3367
				// Build index list
3368
				$manymanyIndexes = array(
3369
					"{$table}ID" => true,
3370
					$childField => true,
3371
				);
3372
				$manyManyTable = "{$table}_$relationship";
3373
				DB::require_table($manyManyTable, $manymanyFields, $manymanyIndexes, true, null, $extensions);
3374
			}
3375
		}
3376
3377
		// Let any extentions make their own database fields
3378
		$this->extend('augmentDatabase', $dummy);
3379
	}
3380
3381
	/**
3382
	 * Validate that the configured relations for this class use the correct syntaxes
3383
	 * @throws LogicException
3384
	 */
3385
	protected function validateModelDefinitions() {
3386
		$modelDefinitions = array(
3387
			'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
3388
			'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
3389
			'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
3390
			'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
3391
			'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
3392
			'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
3393
			'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
3394
		);
3395
3396
		foreach($modelDefinitions as $defType => $relations) {
3397
			if( ! $relations) continue;
3398
3399
			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...
3400
				if($defType === 'many_many_extraFields') {
3401
					if(!is_array($v)) {
3402
						throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
3403
							. var_export($k, true) . " => " . var_export($v, true)
3404
							. ". Each many_many_extraFields entry should map to a field specification array.");
3405
					}
3406
				} else {
3407
					if(!is_string($k) || is_numeric($k) || !is_string($v)) {
3408
						throw new LogicException("$this->class::$defType has a bad entry: "
3409
							. var_export($k, true). " => " . var_export($v, true) . ".  Each map key should be a
3410
							 relationship name, and the map value should be the data class to join to.");
3411
					}
3412
				}
3413
			}
3414
		}
3415
	}
3416
3417
	/**
3418
	 * Add default records to database. This function is called whenever the
3419
	 * database is built, after the database tables have all been created. Overload
3420
	 * this to add default records when the database is built, but make sure you
3421
	 * call parent::requireDefaultRecords().
3422
	 *
3423
	 * @uses DataExtension->requireDefaultRecords()
3424
	 */
3425
	public function requireDefaultRecords() {
3426
		$defaultRecords = $this->stat('default_records');
3427
3428
		if(!empty($defaultRecords)) {
3429
			$hasData = DataObject::get_one($this->class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3430
			if(!$hasData) {
3431
				$className = $this->class;
3432
				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...
3433
					$obj = $this->model->$className->newObject($record);
3434
					$obj->write();
3435
				}
3436
				DB::alteration_message("Added default records to $className table","created");
3437
			}
3438
		}
3439
3440
		// Let any extentions make their own database default data
3441
		$this->extend('requireDefaultRecords', $dummy);
3442
	}
3443
3444
	/**
3445
	 * Get the default searchable fields for this object, as defined in the
3446
	 * $searchable_fields list. If searchable fields are not defined on the
3447
	 * data object, uses a default selection of summary fields.
3448
	 *
3449
	 * @return array
3450
	 */
3451
	public function searchableFields() {
3452
		// can have mixed format, need to make consistent in most verbose form
3453
		$fields = $this->stat('searchable_fields');
3454
		$labels = $this->fieldLabels();
3455
3456
		// fallback to summary fields (unless empty array is explicitly specified)
3457
		if( ! $fields && ! is_array($fields)) {
3458
			$summaryFields = array_keys($this->summaryFields());
3459
			$fields = array();
3460
3461
			// remove the custom getters as the search should not include them
3462
			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...
3463
				foreach($summaryFields as $key => $name) {
3464
					$spec = $name;
3465
3466
					// Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3467
					if(($fieldPos = strpos($name, '.')) !== false) {
3468
						$name = substr($name, 0, $fieldPos);
3469
					}
3470
3471
					if($this->hasDatabaseField($name)) {
3472
						$fields[] = $name;
3473
					} elseif($this->relObject($spec)) {
3474
						$fields[] = $spec;
3475
					}
3476
				}
3477
			}
3478
		}
3479
3480
		// we need to make sure the format is unified before
3481
		// augmenting fields, so extensions can apply consistent checks
3482
		// but also after augmenting fields, because the extension
3483
		// might use the shorthand notation as well
3484
3485
		// rewrite array, if it is using shorthand syntax
3486
		$rewrite = array();
3487
		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...
3488
			$identifer = (is_int($name)) ? $specOrName : $name;
3489
3490
			if(is_int($name)) {
3491
				// Format: array('MyFieldName')
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3492
				$rewrite[$identifer] = array();
3493
			} elseif(is_array($specOrName)) {
3494
				// Format: array('MyFieldName' => array(
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3495
				//   'filter => 'ExactMatchFilter',
3496
				//   'field' => 'NumericField', // optional
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3497
				//   'title' => 'My Title', // optional
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3498
				// ))
3499
				$rewrite[$identifer] = array_merge(
3500
					array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3501
					(array)$specOrName
3502
				);
3503
			} else {
3504
				// Format: array('MyFieldName' => 'ExactMatchFilter')
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3505
				$rewrite[$identifer] = array(
3506
					'filter' => $specOrName,
3507
				);
3508
			}
3509
			if(!isset($rewrite[$identifer]['title'])) {
3510
				$rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3511
					? $labels[$identifer] : FormField::name_to_label($identifer);
3512
			}
3513
			if(!isset($rewrite[$identifer]['filter'])) {
3514
				$rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3515
			}
3516
		}
3517
3518
		$fields = $rewrite;
3519
3520
		// apply DataExtensions if present
3521
		$this->extend('updateSearchableFields', $fields);
3522
3523
		return $fields;
3524
	}
3525
3526
	/**
3527
	 * Get any user defined searchable fields labels that
3528
	 * exist. Allows overriding of default field names in the form
3529
	 * interface actually presented to the user.
3530
	 *
3531
	 * The reason for keeping this separate from searchable_fields,
3532
	 * which would be a logical place for this functionality, is to
3533
	 * avoid bloating and complicating the configuration array. Currently
3534
	 * much of this system is based on sensible defaults, and this property
3535
	 * would generally only be set in the case of more complex relationships
3536
	 * between data object being required in the search interface.
3537
	 *
3538
	 * Generates labels based on name of the field itself, if no static property
3539
	 * {@link self::field_labels} exists.
3540
	 *
3541
	 * @uses $field_labels
3542
	 * @uses FormField::name_to_label()
3543
	 *
3544
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3545
	 *
3546
	 * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3547
	 */
3548
	public function fieldLabels($includerelations = true) {
3549
		$cacheKey = $this->class . '_' . $includerelations;
3550
3551
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
3552
			$customLabels = $this->stat('field_labels');
3553
			$autoLabels = array();
3554
3555
			// get all translated static properties as defined in i18nCollectStatics()
3556
			$ancestry = ClassInfo::ancestry($this->class);
3557
			$ancestry = array_reverse($ancestry);
3558
			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...
3559
				if($ancestorClass == 'ViewableData') break;
3560
				$types = array(
3561
					'db'        => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3562
				);
3563
				if($includerelations){
3564
					$types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3565
					$types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3566
					$types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3567
					$types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3568
				}
3569
				foreach($types as $type => $attrs) {
3570
					foreach($attrs as $name => $spec) {
3571
						$autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}",FormField::name_to_label($name));
3572
					}
3573
				}
3574
			}
3575
3576
			$labels = array_merge((array)$autoLabels, (array)$customLabels);
3577
			$this->extend('updateFieldLabels', $labels);
3578
			self::$_cache_field_labels[$cacheKey] = $labels;
3579
		}
3580
3581
		return self::$_cache_field_labels[$cacheKey];
3582
	}
3583
3584
	/**
3585
	 * Get a human-readable label for a single field,
3586
	 * see {@link fieldLabels()} for more details.
3587
	 *
3588
	 * @uses fieldLabels()
3589
	 * @uses FormField::name_to_label()
3590
	 *
3591
	 * @param string $name Name of the field
3592
	 * @return string Label of the field
3593
	 */
3594
	public function fieldLabel($name) {
3595
		$labels = $this->fieldLabels();
3596
		return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3597
	}
3598
3599
	/**
3600
	 * Get the default summary fields for this object.
3601
	 *
3602
	 * @todo use the translation apparatus to return a default field selection for the language
3603
	 *
3604
	 * @return array
3605
	 */
3606
	public function summaryFields() {
3607
		$fields = $this->stat('summary_fields');
3608
3609
		// if fields were passed in numeric array,
3610
		// convert to an associative array
3611
		if($fields && array_key_exists(0, $fields)) {
3612
			$fields = array_combine(array_values($fields), array_values($fields));
3613
		}
3614
3615
		if (!$fields) {
3616
			$fields = array();
3617
			// try to scaffold a couple of usual suspects
3618
			if ($this->hasField('Name')) $fields['Name'] = 'Name';
3619
			if ($this->hasDatabaseField('Title')) $fields['Title'] = 'Title';
3620
			if ($this->hasField('Description')) $fields['Description'] = 'Description';
3621
			if ($this->hasField('FirstName')) $fields['FirstName'] = 'First Name';
3622
		}
3623
		$this->extend("updateSummaryFields", $fields);
3624
3625
		// Final fail-over, just list ID field
3626
		if(!$fields) $fields['ID'] = 'ID';
3627
3628
		// Localize fields (if possible)
3629
		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...
3630
			// only attempt to localize if the label definition is the same as the field name.
3631
			// this will preserve any custom labels set in the summary_fields configuration
3632
			if(isset($fields[$name]) && $name === $fields[$name]) {
3633
				$fields[$name] = $label;
3634
			}
3635
		}
3636
3637
		return $fields;
3638
	}
3639
3640
	/**
3641
	 * Defines a default list of filters for the search context.
3642
	 *
3643
	 * If a filter class mapping is defined on the data object,
3644
	 * it is constructed here. Otherwise, the default filter specified in
3645
	 * {@link DBField} is used.
3646
	 *
3647
	 * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3648
	 *
3649
	 * @return array
3650
	 */
3651
	public function defaultSearchFilters() {
3652
		$filters = array();
3653
3654
		foreach($this->searchableFields() as $name => $spec) {
3655
			if($spec['filter'] instanceof SearchFilter) {
3656
				$filters[$name] = $spec['filter'];
3657
			} else {
3658
				$class = $spec['filter'];
3659
3660
				if(!is_subclass_of($spec['filter'], 'SearchFilter')) {
3661
					$class = 'PartialMatchFilter';
3662
				}
3663
3664
				$filters[$name] = new $class($name);
3665
			}
3666
		}
3667
3668
		return $filters;
3669
	}
3670
3671
	/**
3672
	 * @return boolean True if the object is in the database
3673
	 */
3674
	public function isInDB() {
3675
		return is_numeric( $this->ID ) && $this->ID > 0;
3676
	}
3677
3678
	/*
3679
	 * @ignore
3680
	 */
3681
	private static $subclass_access = true;
3682
3683
	/**
3684
	 * Temporarily disable subclass access in data object qeur
3685
	 */
3686
	public static function disable_subclass_access() {
3687
		self::$subclass_access = false;
3688
	}
3689
	public static function enable_subclass_access() {
3690
		self::$subclass_access = true;
3691
	}
3692
3693
	//-------------------------------------------------------------------------------------------//
3694
3695
	/**
3696
	 * Database field definitions.
3697
	 * This is a map from field names to field type. The field
3698
	 * type should be a class that extends .
3699
	 * @var array
3700
	 * @config
3701
	 */
3702
	private static $db = null;
3703
3704
	/**
3705
	 * Use a casting object for a field. This is a map from
3706
	 * field name to class name of the casting object.
3707
	 *
3708
	 * @var array
3709
	 */
3710
	private static $casting = array(
3711
		"Title" => 'Text',
3712
	);
3713
3714
	/**
3715
	 * Specify custom options for a CREATE TABLE call.
3716
	 * Can be used to specify a custom storage engine for specific database table.
3717
	 * All options have to be keyed for a specific database implementation,
3718
	 * identified by their class name (extending from {@link SS_Database}).
3719
	 *
3720
	 * <code>
3721
	 * array(
3722
	 *  'MySQLDatabase' => 'ENGINE=MyISAM'
3723
	 * )
3724
	 * </code>
3725
	 *
3726
	 * Caution: This API is experimental, and might not be
3727
	 * included in the next major release. Please use with care.
3728
	 *
3729
	 * @var array
3730
	 * @config
3731
	 */
3732
	private static $create_table_options = array(
3733
		'MySQLDatabase' => 'ENGINE=InnoDB'
3734
	);
3735
3736
	/**
3737
	 * If a field is in this array, then create a database index
3738
	 * on that field. This is a map from fieldname to index type.
3739
	 * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3740
	 *
3741
	 * @var array
3742
	 * @config
3743
	 */
3744
	private static $indexes = null;
3745
3746
	/**
3747
	 * Inserts standard column-values when a DataObject
3748
	 * is instanciated. Does not insert default records {@see $default_records}.
3749
	 * This is a map from fieldname to default value.
3750
	 *
3751
	 *  - If you would like to change a default value in a sub-class, just specify it.
3752
	 *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3753
	 *    or false in your subclass.  Setting it to null won't work.
3754
	 *
3755
	 * @var array
3756
	 * @config
3757
	 */
3758
	private static $defaults = null;
3759
3760
	/**
3761
	 * Multidimensional array which inserts default data into the database
3762
	 * on a db/build-call as long as the database-table is empty. Please use this only
3763
	 * for simple constructs, not for SiteTree-Objects etc. which need special
3764
	 * behaviour such as publishing and ParentNodes.
3765
	 *
3766
	 * Example:
3767
	 * array(
3768
	 *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3769
	 *  array('Title' => "DefaultPage2")
3770
	 * ).
3771
	 *
3772
	 * @var array
3773
	 * @config
3774
	 */
3775
	private static $default_records = null;
3776
3777
	/**
3778
	 * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3779
	 * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3780
	 *
3781
	 * Note that you cannot have a has_one and belongs_to relationship with the same name.
3782
	 *
3783
	 *	@var array
3784
	 * @config
3785
	 */
3786
	private static $has_one = null;
3787
3788
	/**
3789
	 * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3790
	 *
3791
	 * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3792
	 * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3793
	 * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3794
	 *
3795
	 * Note that you cannot have a has_one and belongs_to relationship with the same name.
3796
	 *
3797
	 * @var array
3798
	 * @config
3799
	 */
3800
	private static $belongs_to;
3801
3802
	/**
3803
	 * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3804
	 *
3805
	 * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3806
	 * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3807
	 * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3808
	 * which foreign key to use.
3809
	 *
3810
	 * @var array
3811
	 * @config
3812
	 */
3813
	private static $has_many = null;
3814
3815
	/**
3816
	 * many-many relationship definitions.
3817
	 * This is a map from component name to data type.
3818
	 * @var array
3819
	 * @config
3820
	 */
3821
	private static $many_many = null;
3822
3823
	/**
3824
	 * Extra fields to include on the connecting many-many table.
3825
	 * This is a map from field name to field type.
3826
	 *
3827
	 * Example code:
3828
	 * <code>
3829
	 * public static $many_many_extraFields = array(
3830
	 *  'Members' => array(
3831
	 *			'Role' => 'Varchar(100)'
3832
	 *		)
3833
	 * );
3834
	 * </code>
3835
	 *
3836
	 * @var array
3837
	 * @config
3838
	 */
3839
	private static $many_many_extraFields = null;
3840
3841
	/**
3842
	 * The inverse side of a many-many relationship.
3843
	 * This is a map from component name to data type.
3844
	 * @var array
3845
	 * @config
3846
	 */
3847
	private static $belongs_many_many = null;
3848
3849
	/**
3850
	 * The default sort expression. This will be inserted in the ORDER BY
3851
	 * clause of a SQL query if no other sort expression is provided.
3852
	 * @var string
3853
	 * @config
3854
	 */
3855
	private static $default_sort = null;
3856
3857
	/**
3858
	 * Default list of fields that can be scaffolded by the ModelAdmin
3859
	 * search interface.
3860
	 *
3861
	 * Overriding the default filter, with a custom defined filter:
3862
	 * <code>
3863
	 *  static $searchable_fields = array(
3864
	 *     "Name" => "PartialMatchFilter"
3865
	 *  );
3866
	 * </code>
3867
	 *
3868
	 * Overriding the default form fields, with a custom defined field.
3869
	 * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3870
	 * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3871
	 * <code>
3872
	 *  static $searchable_fields = array(
3873
	 *    "Name" => array(
3874
	 *      "field" => "TextField"
3875
	 *    )
3876
	 *  );
3877
	 * </code>
3878
	 *
3879
	 * Overriding the default form field, filter and title:
3880
	 * <code>
3881
	 *  static $searchable_fields = array(
3882
	 *    "Organisation.ZipCode" => array(
3883
	 *      "field" => "TextField",
3884
	 *      "filter" => "PartialMatchFilter",
3885
	 *      "title" => 'Organisation ZIP'
3886
	 *    )
3887
	 *  );
3888
	 * </code>
3889
	 * @config
3890
	 */
3891
	private static $searchable_fields = null;
3892
3893
	/**
3894
	 * User defined labels for searchable_fields, used to override
3895
	 * default display in the search form.
3896
	 * @config
3897
	 */
3898
	private static $field_labels = null;
3899
3900
	/**
3901
	 * Provides a default list of fields to be used by a 'summary'
3902
	 * view of this object.
3903
	 * @config
3904
	 */
3905
	private static $summary_fields = null;
3906
3907
	/**
3908
	 * Collect all static properties on the object
3909
	 * which contain natural language, and need to be translated.
3910
	 * The full entity name is composed from the class name and a custom identifier.
3911
	 *
3912
	 * @return array A numerical array which contains one or more entities in array-form.
3913
	 * Each numeric entity array contains the "arguments" for a _t() call as array values:
3914
	 * $entity, $string, $priority, $context.
3915
	 */
3916
	public function provideI18nEntities() {
3917
		$entities = array();
3918
3919
		$entities["{$this->class}.SINGULARNAME"] = array(
3920
			$this->singular_name(),
3921
3922
			'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
3923
		);
3924
3925
		$entities["{$this->class}.PLURALNAME"] = array(
3926
			$this->plural_name(),
3927
3928
			'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the'
3929
			. ' interface'
3930
		);
3931
3932
		return $entities;
3933
	}
3934
3935
	/**
3936
	 * Returns true if the given method/parameter has a value
3937
	 * (Uses the DBField::hasValue if the parameter is a database field)
3938
	 *
3939
	 * @param string $field The field name
3940
	 * @param array $arguments
3941
	 * @param bool $cache
3942
	 * @return boolean
3943
	 */
3944
	public function hasValue($field, $arguments = null, $cache = true) {
3945
		// has_one fields should not use dbObject to check if a value is given
3946
		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...
3947
			return $obj->exists();
3948
		} else {
3949
			return parent::hasValue($field, $arguments, $cache);
3950
		}
3951
	}
3952
3953
}
3954