Completed
Push — master ( e0a51b...59bba5 )
by Daniel
28s
created

DataObject::dbObject()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
506
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
507
		}
508
		if ($sourceObject->manyMany()) foreach($sourceObject->manyMany() as $name => $type) {
509
			//many_many include belongs_many_many
510
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
511
		}
512
513
		return $destinationObject;
514
	}
515
516
	/**
517
	 * Helper function to duplicate relations from one object to another
518
	 * @param DataObject $sourceObject the source object to duplicate from
519
	 * @param DataObject $destinationObject the destination object to populate with the duplicated relations
520
	 * @param string $name the name of the relation to duplicate (e.g. members)
521
	 */
522
	private function duplicateRelations($sourceObject, $destinationObject, $name) {
523
		$relations = $sourceObject->$name();
524
		if ($relations) {
525
			if ($relations instanceOf RelationList) {   //many-to-something relation
526
				if ($relations->count() > 0) {  //with more than one thing it is related to
527
					foreach($relations as $relation) {
528
						$destinationObject->$name()->add($relation);
529
					}
530
				}
531
			} else {    //one-to-one relation
532
				$destinationObject->{"{$name}ID"} = $relations->ID;
533
			}
534
		}
535
	}
536
537
	public function getObsoleteClassName() {
538
		$className = $this->getField("ClassName");
539
		if (!ClassInfo::exists($className)) return $className;
540
	}
541
542
	public function getClassName() {
543
		$className = $this->getField("ClassName");
544
		if (!ClassInfo::exists($className)) return get_class($this);
545
		return $className;
546
	}
547
548
	/**
549
	 * Set the ClassName attribute. {@link $class} is also updated.
550
	 * Warning: This will produce an inconsistent record, as the object
551
	 * instance will not automatically switch to the new subclass.
552
	 * Please use {@link newClassInstance()} for this purpose,
553
	 * or destroy and reinstanciate the record.
554
	 *
555
	 * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
556
	 * @return DataObject $this
557
	 */
558
	public function setClassName($className) {
559
		$className = trim($className);
560
		if(!$className || !is_subclass_of($className, 'SilverStripe\ORM\DataObject')) return;
561
562
		$this->class = $className;
563
		$this->setField("ClassName", $className);
564
		return $this;
565
	}
566
567
	/**
568
	 * Create a new instance of a different class from this object's record.
569
	 * This is useful when dynamically changing the type of an instance. Specifically,
570
	 * it ensures that the instance of the class is a match for the className of the
571
	 * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
572
	 * property manually before calling this method, as it will confuse change detection.
573
	 *
574
	 * If the new class is different to the original class, defaults are populated again
575
	 * because this will only occur automatically on instantiation of a DataObject if
576
	 * there is no record, or the record has no ID. In this case, we do have an ID but
577
	 * we still need to repopulate the defaults.
578
	 *
579
	 * @param string $newClassName The name of the new class
580
	 *
581
	 * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
582
	 */
583
	public function newClassInstance($newClassName) {
584
		$originalClass = $this->ClassName;
585
		$newInstance = new $newClassName(array_merge(
586
			$this->record,
587
			array(
588
				'ClassName' => $originalClass,
589
				'RecordClassName' => $originalClass,
590
			)
591
		), false, $this->model);
592
593
		if($newClassName != $originalClass) {
594
			$newInstance->setClassName($newClassName);
595
			$newInstance->populateDefaults();
596
			$newInstance->forceChange();
597
		}
598
599
		return $newInstance;
600
	}
601
602
	/**
603
	 * Adds methods from the extensions.
604
	 * Called by Object::__construct() once per class.
605
	 */
606
	public function defineMethods() {
607
		parent::defineMethods();
608
609
		// Define the extra db fields - this is only necessary for extensions added in the
610
		// class definition.  Object::add_extension() will call this at definition time for
611
		// those objects, which is a better mechanism.  Perhaps extensions defined inside the
612
		// class def can somehow be applied at definiton time also?
613
		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...
614
			if(!$instance->class) {
615
				$class = get_class($instance);
616
				user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls"
617
					. " parent::__construct()", E_USER_ERROR);
618
			}
619
		}
620
621
		if($this->class == 'SilverStripe\ORM\DataObject') return;
622
623
		// Set up accessors for joined items
624
		if($manyMany = $this->manyMany()) {
625
			foreach($manyMany as $relationship => $class) {
626
				$this->addWrapperMethod($relationship, 'getManyManyComponents');
627
			}
628
		}
629
		if($hasMany = $this->hasMany()) {
630
631
			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...
632
				$this->addWrapperMethod($relationship, 'getComponents');
633
			}
634
635
		}
636
		if($hasOne = $this->hasOne()) {
637
			foreach($hasOne as $relationship => $class) {
0 ignored issues
show
Bug introduced by
The expression $hasOne of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
638
				$this->addWrapperMethod($relationship, 'getComponent');
639
			}
640
		}
641
		if($belongsTo = $this->belongsTo()) foreach(array_keys($belongsTo) as $relationship) {
642
			$this->addWrapperMethod($relationship, 'getComponent');
643
		}
644
	}
645
646
	/**
647
	 * Returns true if this object "exists", i.e., has a sensible value.
648
	 * The default behaviour for a DataObject is to return true if
649
	 * the object exists in the database, you can override this in subclasses.
650
	 *
651
	 * @return boolean true if this object exists
652
	 */
653
	public function exists() {
654
		return (isset($this->record['ID']) && $this->record['ID'] > 0);
655
	}
656
657
	/**
658
	 * Returns TRUE if all values (other than "ID") are
659
	 * considered empty (by weak boolean comparison).
660
	 *
661
	 * @return boolean
662
	 */
663
	public function isEmpty() {
664
		$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...
665
		foreach($this->toMap() as $field => $value){
666
			// only look at custom fields
667
			if(isset($fixed[$field])) {
668
				continue;
669
			}
670
671
			$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 SilverStripe\ORM\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...
672
			if(!$dbObject) {
673
				continue;
674
			}
675
			if($dbObject->exists()) {
676
				return false;
677
			}
678
		}
679
		return true;
680
	}
681
682
	/**
683
	 * Pluralise this item given a specific count.
684
	 *
685
	 * E.g. "0 Pages", "1 File", "3 Images"
686
	 *
687
	 * @param string $count
688
	 * @param bool $prependNumber Include number in result. Defaults to true.
689
	 * @return string
690
	 */
691
	public function i18n_pluralise($count, $prependNumber = true) {
692
		return i18n::pluralise(
693
			$this->i18n_singular_name(),
694
			$this->i18n_plural_name(),
695
			$count,
696
			$prependNumber
697
		);
698
	}
699
700
	/**
701
	 * Get the user friendly singular name of this DataObject.
702
	 * If the name is not defined (by redefining $singular_name in the subclass),
703
	 * this returns the class name.
704
	 *
705
	 * @return string User friendly singular name of this DataObject
706
	 */
707
	public function singular_name() {
708
		if(!$name = $this->stat('singular_name')) {
709
			$reflection = new \ReflectionClass($this);
710
			$name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $reflection->getShortName()))));
711
		}
712
713
		return $name;
714
	}
715
716
	/**
717
	 * Get the translated user friendly singular name of this DataObject
718
	 * same as singular_name() but runs it through the translating function
719
	 *
720
	 * Translating string is in the form:
721
	 *     $this->class.SINGULARNAME
722
	 * Example:
723
	 *     Page.SINGULARNAME
724
	 *
725
	 * @return string User friendly translated singular name of this DataObject
726
	 */
727
	public function i18n_singular_name() {
728
		return _t($this->class.'.SINGULARNAME', $this->singular_name());
729
	}
730
731
	/**
732
	 * Get the user friendly plural name of this DataObject
733
	 * If the name is not defined (by renaming $plural_name in the subclass),
734
	 * this returns a pluralised version of the class name.
735
	 *
736
	 * @return string User friendly plural name of this DataObject
737
	 */
738
	public function plural_name() {
739
		if($name = $this->stat('plural_name')) {
740
			return $name;
741
		} else {
742
			$name = $this->singular_name();
743
			//if the penultimate character is not a vowel, replace "y" with "ies"
744
			if (preg_match('/[^aeiou]y$/i', $name)) {
745
				$name = substr($name,0,-1) . 'ie';
746
			}
747
			return ucfirst($name . 's');
748
		}
749
	}
750
751
	/**
752
	 * Get the translated user friendly plural name of this DataObject
753
	 * Same as plural_name but runs it through the translation function
754
	 * Translation string is in the form:
755
	 *      $this->class.PLURALNAME
756
	 * Example:
757
	 *      Page.PLURALNAME
758
	 *
759
	 * @return string User friendly translated plural name of this DataObject
760
	 */
761
	public function i18n_plural_name()
762
	{
763
		$name = $this->plural_name();
764
		return _t($this->class.'.PLURALNAME', $name);
765
	}
766
767
	/**
768
	 * Standard implementation of a title/label for a specific
769
	 * record. Tries to find properties 'Title' or 'Name',
770
	 * and falls back to the 'ID'. Useful to provide
771
	 * user-friendly identification of a record, e.g. in errormessages
772
	 * or UI-selections.
773
	 *
774
	 * Overload this method to have a more specialized implementation,
775
	 * e.g. for an Address record this could be:
776
	 * <code>
777
	 * function getTitle() {
778
	 *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
779
	 * }
780
	 * </code>
781
	 *
782
	 * @return string
783
	 */
784
	public function getTitle() {
785
		if($this->hasDatabaseField('Title')) return $this->getField('Title');
786
		if($this->hasDatabaseField('Name')) return $this->getField('Name');
787
788
		return "#{$this->ID}";
789
	}
790
791
	/**
792
	 * Returns the associated database record - in this case, the object itself.
793
	 * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
794
	 *
795
	 * @return DataObject Associated database record
796
	 */
797
	public function data() {
798
		return $this;
799
	}
800
801
	/**
802
	 * Convert this object to a map.
803
	 *
804
	 * @return array The data as a map.
805
	 */
806
	public function toMap() {
807
		$this->loadLazyFields();
808
		return $this->record;
809
	}
810
811
	/**
812
	 * Return all currently fetched database fields.
813
	 *
814
	 * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
815
	 * Obviously, this makes it a lot faster.
816
	 *
817
	 * @return array The data as a map.
818
	 */
819
	public function getQueriedDatabaseFields() {
820
		return $this->record;
821
	}
822
823
	/**
824
	 * Update a number of fields on this object, given a map of the desired changes.
825
	 *
826
	 * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
827
	 * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
828
	 *
829
	 * update() doesn't write the main object, but if you use the dot syntax, it will write()
830
	 * the related objects that it alters.
831
	 *
832
	 * @param array $data A map of field name to data values to update.
833
	 * @return DataObject $this
834
	 */
835
	public function update($data) {
836
		foreach($data as $k => $v) {
837
			// Implement dot syntax for updates
838
			if(strpos($k,'.') !== false) {
839
				$relations = explode('.', $k);
840
				$fieldName = array_pop($relations);
841
				$relObj = $this;
842
				foreach($relations as $i=>$relation) {
843
					// no support for has_many or many_many relationships,
844
					// as the updater wouldn't know which object to write to (or create)
845
					if($relObj->$relation() instanceof DataObject) {
846
						$parentObj = $relObj;
847
						$relObj = $relObj->$relation();
848
						// If the intermediate relationship objects have been created, then write them
849
						if($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
850
							$relObj->write();
851
							$relatedFieldName = $relation."ID";
852
							$parentObj->$relatedFieldName = $relObj->ID;
853
							$parentObj->write();
854
						}
855
					} else {
856
						user_error(
857
							"DataObject::update(): Can't traverse relationship '$relation'," .
858
							"it has to be a has_one relationship or return a single DataObject",
859
							E_USER_NOTICE
860
						);
861
						// unset relation object so we don't write properties to the wrong object
862
						unset($relObj);
863
						break;
864
					}
865
				}
866
867
				if($relObj) {
868
					$relObj->$fieldName = $v;
869
					$relObj->write();
870
					$relatedFieldName = $relation."ID";
0 ignored issues
show
Bug introduced by
The variable $relation seems to be defined by a foreach iteration on line 842. 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...
871
					$this->$relatedFieldName = $relObj->ID;
872
					$relObj->flushCache();
873
				} else {
874
					user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
875
				}
876
			} else {
877
				$this->$k = $v;
878
			}
879
		}
880
		return $this;
881
	}
882
883
	/**
884
	 * Pass changes as a map, and try to
885
	 * get automatic casting for these fields.
886
	 * Doesn't write to the database. To write the data,
887
	 * use the write() method.
888
	 *
889
	 * @param array $data A map of field name to data values to update.
890
	 * @return DataObject $this
891
	 */
892
	public function castedUpdate($data) {
893
		foreach($data as $k => $v) {
894
			$this->setCastedField($k,$v);
895
		}
896
		return $this;
897
	}
898
899
	/**
900
	 * Merges data and relations from another object of same class,
901
	 * without conflict resolution. Allows to specify which
902
	 * dataset takes priority in case its not empty.
903
	 * has_one-relations are just transferred with priority 'right'.
904
	 * has_many and many_many-relations are added regardless of priority.
905
	 *
906
	 * Caution: has_many/many_many relations are moved rather than duplicated,
907
	 * meaning they are not connected to the merged object any longer.
908
	 * Caution: Just saves updated has_many/many_many relations to the database,
909
	 * doesn't write the updated object itself (just writes the object-properties).
910
	 * Caution: Does not delete the merged object.
911
	 * Caution: Does now overwrite Created date on the original object.
912
	 *
913
	 * @param DataObject $rightObj
914
	 * @param string $priority left|right Determines who wins in case of a conflict (optional)
915
	 * @param bool $includeRelations Merge any existing relations (optional)
916
	 * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
917
	 *                            Only applicable with $priority='right'. (optional)
918
	 * @return Boolean
919
	 */
920
	public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) {
921
		$leftObj = $this;
922
923
		if($leftObj->ClassName != $rightObj->ClassName) {
924
			// we can't merge similiar subclasses because they might have additional relations
925
			user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
926
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
927
			return false;
928
		}
929
930
		if(!$rightObj->ID) {
931
			user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
932
				to make sure all relations are transferred properly.').", E_USER_WARNING);
933
			return false;
934
		}
935
936
		// makes sure we don't merge data like ID or ClassName
937
		$leftData = $leftObj->db();
938
		$rightData = $rightObj->db();
939
940
		foreach($rightData as $key=>$rightSpec) {
0 ignored issues
show
Bug introduced by
The expression $rightData of type array|string|null 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...
941
			// Don't merge ID
942
			if($key === 'ID') {
943
				continue;
944
			}
945
946
			// Only merge relations if allowed
947
			if($rightSpec === 'ForeignKey' && !$includeRelations) {
948
				continue;
949
			}
950
951
			// don't merge conflicting values if priority is 'left'
952
			if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
953
				continue;
954
			}
955
956
			// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
957
			if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
958
				continue;
959
			}
960
961
			// TODO remove redundant merge of has_one fields
962
			$leftObj->{$key} = $rightObj->{$key};
963
		}
964
965
		// merge relations
966
		if($includeRelations) {
967
			if($manyMany = $this->manyMany()) {
968
				foreach($manyMany as $relationship => $class) {
969
					$leftComponents = $leftObj->getManyManyComponents($relationship);
970
					$rightComponents = $rightObj->getManyManyComponents($relationship);
971
					if($rightComponents && $rightComponents->exists()) {
972
						$leftComponents->addMany($rightComponents->column('ID'));
973
					}
974
					$leftComponents->write();
975
				}
976
			}
977
978
			if($hasMany = $this->hasMany()) {
979
				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...
980
					$leftComponents = $leftObj->getComponents($relationship);
981
					$rightComponents = $rightObj->getComponents($relationship);
982
					if($rightComponents && $rightComponents->exists()) {
983
						$leftComponents->addMany($rightComponents->column('ID'));
984
					}
985
					$leftComponents->write();
986
				}
987
988
			}
989
		}
990
991
		return true;
992
	}
993
994
	/**
995
	 * Forces the record to think that all its data has changed.
996
	 * Doesn't write to the database. Only sets fields as changed
997
	 * if they are not already marked as changed.
998
	 *
999
	 * @return $this
1000
	 */
1001
	public function forceChange() {
1002
		// Ensure lazy fields loaded
1003
		$this->loadLazyFields();
1004
1005
		// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1006
		$fieldNames = array_unique(array_merge(
1007
			array_keys($this->record),
1008
			array_keys($this->db())
1009
		));
1010
1011
		foreach($fieldNames as $fieldName) {
1012
			if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
1013
			// Populate the null values in record so that they actually get written
1014
			if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
1015
		}
1016
1017
		// @todo Find better way to allow versioned to write a new version after forceChange
1018
		if($this->isChanged('Version')) unset($this->changed['Version']);
1019
		return $this;
1020
	}
1021
1022
	/**
1023
	 * Validate the current object.
1024
	 *
1025
	 * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1026
	 * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1027
	 *
1028
	 * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1029
	 * and onAfterWrite() won't get called either.
1030
	 *
1031
	 * It is expected that you call validate() in your own application to test that an object is valid before
1032
	 * attempting a write, and respond appropriately if it isn't.
1033
	 *
1034
	 * @see {@link ValidationResult}
1035
	 * @return ValidationResult
1036
	 */
1037
	public function validate() {
1038
		$result = ValidationResult::create();
1039
		$this->extend('validate', $result);
1040
		return $result;
1041
	}
1042
1043
	/**
1044
	 * Public accessor for {@see DataObject::validate()}
1045
	 *
1046
	 * @return ValidationResult
1047
	 */
1048
	public function doValidate() {
1049
		Deprecation::notice('5.0', 'Use validate');
1050
		return $this->validate();
1051
	}
1052
1053
	/**
1054
	 * Event handler called before writing to the database.
1055
	 * You can overload this to clean up or otherwise process data before writing it to the
1056
	 * database.  Don't forget to call parent::onBeforeWrite(), though!
1057
	 *
1058
	 * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1059
	 *
1060
	 * @uses DataExtension->onBeforeWrite()
1061
	 */
1062
	protected function onBeforeWrite() {
1063
		$this->brokenOnWrite = false;
1064
1065
		$dummy = null;
1066
		$this->extend('onBeforeWrite', $dummy);
1067
	}
1068
1069
	/**
1070
	 * Event handler called after writing to the database.
1071
	 * You can overload this to act upon changes made to the data after it is written.
1072
	 * $this->changed will have a record
1073
	 * database.  Don't forget to call parent::onAfterWrite(), though!
1074
	 *
1075
	 * @uses DataExtension->onAfterWrite()
1076
	 */
1077
	protected function onAfterWrite() {
1078
		$dummy = null;
1079
		$this->extend('onAfterWrite', $dummy);
1080
	}
1081
1082
	/**
1083
	 * Event handler called before deleting from the database.
1084
	 * You can overload this to clean up or otherwise process data before delete this
1085
	 * record.  Don't forget to call parent::onBeforeDelete(), though!
1086
	 *
1087
	 * @uses DataExtension->onBeforeDelete()
1088
	 */
1089
	protected function onBeforeDelete() {
1090
		$this->brokenOnDelete = false;
1091
1092
		$dummy = null;
1093
		$this->extend('onBeforeDelete', $dummy);
1094
	}
1095
1096
	protected function onAfterDelete() {
1097
		$this->extend('onAfterDelete');
1098
	}
1099
1100
	/**
1101
	 * Load the default values in from the self::$defaults array.
1102
	 * Will traverse the defaults of the current class and all its parent classes.
1103
	 * Called by the constructor when creating new records.
1104
	 *
1105
	 * @uses DataExtension->populateDefaults()
1106
	 * @return DataObject $this
1107
	 */
1108
	public function populateDefaults() {
1109
		$classes = array_reverse(ClassInfo::ancestry($this));
1110
1111
		foreach($classes as $class) {
1112
			$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1113
1114
			if($defaults && !is_array($defaults)) {
1115
				user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
1116
					E_USER_WARNING);
1117
				$defaults = null;
1118
			}
1119
1120
			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...
1121
				// SRM 2007-03-06: Stricter check
1122
				if(!isset($this->$fieldName) || $this->$fieldName === null) {
1123
					$this->$fieldName = $fieldValue;
1124
				}
1125
				// Set many-many defaults with an array of ids
1126
				if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
1127
					$manyManyJoin = $this->$fieldName();
1128
					$manyManyJoin->setByIdList($fieldValue);
1129
				}
1130
			}
1131
			if($class == 'SilverStripe\ORM\DataObject') {
1132
				break;
1133
			}
1134
		}
1135
1136
		$this->extend('populateDefaults');
1137
		return $this;
1138
	}
1139
1140
	/**
1141
	 * Determine validation of this object prior to write
1142
	 *
1143
	 * @return ValidationException Exception generated by this write, or null if valid
1144
	 */
1145
	protected function validateWrite() {
1146
		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...
1147
			return new ValidationException(
1148
				"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...
1149
				"you need to change the ClassName before you can write it",
1150
				E_USER_WARNING
1151
			);
1152
		}
1153
1154
		if(Config::inst()->get('SilverStripe\ORM\DataObject', 'validation_enabled')) {
1155
			$result = $this->validate();
1156
			if (!$result->valid()) {
1157
				return new ValidationException(
1158
					$result,
1159
					$result->message(),
1160
					E_USER_WARNING
1161
				);
1162
			}
1163
		}
1164
	}
1165
1166
	/**
1167
	 * Prepare an object prior to write
1168
	 *
1169
	 * @throws ValidationException
1170
	 */
1171
	protected function preWrite() {
1172
		// Validate this object
1173
		if($writeException = $this->validateWrite()) {
1174
			// Used by DODs to clean up after themselves, eg, Versioned
1175
			$this->invokeWithExtensions('onAfterSkippedWrite');
1176
			throw $writeException;
1177
		}
1178
1179
		// Check onBeforeWrite
1180
		$this->brokenOnWrite = true;
1181
		$this->onBeforeWrite();
1182
		if($this->brokenOnWrite) {
1183
			user_error("$this->class has a broken onBeforeWrite() function."
1184
				. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1185
		}
1186
	}
1187
1188
	/**
1189
	 * Detects and updates all changes made to this object
1190
	 *
1191
	 * @param bool $forceChanges If set to true, force all fields to be treated as changed
1192
	 * @return bool True if any changes are detected
1193
	 */
1194
	protected function updateChanges($forceChanges = false)
1195
	{
1196
		if($forceChanges) {
1197
			// Force changes, but only for loaded fields
1198
			foreach($this->record as $field => $value) {
1199
				$this->changed[$field] = static::CHANGE_VALUE;
1200
			}
1201
			return true;
1202
		}
1203
		return $this->isChanged();
1204
	}
1205
1206
	/**
1207
	 * Writes a subset of changes for a specific table to the given manipulation
1208
	 *
1209
	 * @param string $baseTable Base table
1210
	 * @param string $now Timestamp to use for the current time
1211
	 * @param bool $isNewRecord Whether this should be treated as a new record write
1212
	 * @param array $manipulation Manipulation to write to
1213
	 * @param string $class Class of table to manipulate
1214
	 */
1215
	protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
1216
		$table = $this->getSchema()->tableName($class);
1217
		$manipulation[$table] = array();
1218
1219
		// Extract records for this table
1220
		foreach($this->record as $fieldName => $fieldValue) {
1221
1222
			// Check if this record pertains to this table, and
1223
			// we're not attempting to reset the BaseTable->ID
1224
			if(	empty($this->changed[$fieldName])
1225
				|| ($table === $baseTable && $fieldName === 'ID')
1226
				|| (!self::has_own_table_database_field($class, $fieldName)
1227
					&& !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...
1228
			) {
1229
				continue;
1230
			}
1231
1232
1233
			// if database column doesn't correlate to a DBField instance...
1234
			$fieldObj = $this->dbObject($fieldName);
1235
			if(!$fieldObj) {
1236
				$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1237
			}
1238
1239
			// Write to manipulation
1240
			$fieldObj->writeToManipulation($manipulation[$table]);
1241
		}
1242
1243
		// Ensure update of Created and LastEdited columns
1244
		if($baseTable === $table) {
1245
			$manipulation[$table]['fields']['LastEdited'] = $now;
1246
			if($isNewRecord) {
1247
				$manipulation[$table]['fields']['Created']
1248
					= empty($this->record['Created'])
1249
						? $now
1250
						: $this->record['Created'];
1251
				$manipulation[$table]['fields']['ClassName'] = $this->class;
1252
			}
1253
		}
1254
1255
		// Inserts done one the base table are performed in another step, so the manipulation should instead
1256
		// attempt an update, as though it were a normal update.
1257
		$manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1258
		$manipulation[$table]['id'] = $this->record['ID'];
1259
		$manipulation[$table]['class'] = $class;
1260
	}
1261
1262
	/**
1263
	 * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1264
	 *
1265
	 * Does nothing if an ID is already assigned for this record
1266
	 *
1267
	 * @param string $baseTable Base table
1268
	 * @param string $now Timestamp to use for the current time
1269
	 */
1270
	protected function writeBaseRecord($baseTable, $now) {
1271
		// Generate new ID if not specified
1272
		if($this->isInDB()) return;
1273
1274
		// Perform an insert on the base table
1275
		$insert = new SQLInsert('"'.$baseTable.'"');
1276
		$insert
1277
			->assign('"Created"', $now)
1278
			->execute();
1279
		$this->changed['ID'] = self::CHANGE_VALUE;
1280
		$this->record['ID'] = DB::get_generated_id($baseTable);
1281
	}
1282
1283
	/**
1284
	 * Generate and write the database manipulation for all changed fields
1285
	 *
1286
	 * @param string $baseTable Base table
1287
	 * @param string $now Timestamp to use for the current time
1288
	 * @param bool $isNewRecord If this is a new record
1289
	 */
1290
	protected function writeManipulation($baseTable, $now, $isNewRecord) {
1291
		// Generate database manipulations for each class
1292
		$manipulation = array();
1293
		foreach($this->getClassAncestry() as $class) {
1294
			if(self::has_own_table($class)) {
1295
				$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1296
			}
1297
		}
1298
1299
		// Allow extensions to extend this manipulation
1300
		$this->extend('augmentWrite', $manipulation);
1301
1302
		// New records have their insert into the base data table done first, so that they can pass the
1303
		// generated ID on to the rest of the manipulation
1304
		if($isNewRecord) {
1305
			$manipulation[$baseTable]['command'] = 'update';
1306
		}
1307
1308
		// Perform the manipulation
1309
		DB::manipulate($manipulation);
1310
	}
1311
1312
	/**
1313
	 * Writes all changes to this object to the database.
1314
	 *  - It will insert a record whenever ID isn't set, otherwise update.
1315
	 *  - All relevant tables will be updated.
1316
	 *  - $this->onBeforeWrite() gets called beforehand.
1317
	 *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1318
	 *
1319
	 *  @uses DataExtension->augmentWrite()
1320
	 *
1321
	 * @param boolean $showDebug Show debugging information
1322
	 * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1323
	 * @param boolean $forceWrite Write to database even if there are no changes
1324
	 * @param boolean $writeComponents Call write() on all associated component instances which were previously
1325
	 *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1326
	 *                                 {@link getManyManyComponents()} (Default: false)
1327
	 * @return int The ID of the record
1328
	 * @throws ValidationException Exception that can be caught and handled by the calling function
1329
	 */
1330
	public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1331
		$now = DBDatetime::now()->Rfc2822();
1332
1333
		// Execute pre-write tasks
1334
		$this->preWrite();
1335
1336
		// Check if we are doing an update or an insert
1337
		$isNewRecord = !$this->isInDB() || $forceInsert;
1338
1339
		// Check changes exist, abort if there are none
1340
		$hasChanges = $this->updateChanges($isNewRecord);
1341
		if($hasChanges || $forceWrite || $isNewRecord) {
1342
			// New records have their insert into the base data table done first, so that they can pass the
1343
			// generated primary key on to the rest of the manipulation
1344
			$baseTable = $this->baseTable();
1345
			$this->writeBaseRecord($baseTable, $now);
1346
1347
			// Write the DB manipulation for all changed fields
1348
			$this->writeManipulation($baseTable, $now, $isNewRecord);
1349
1350
			// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1351
			$this->writeRelations();
1352
			$this->onAfterWrite();
1353
			$this->changed = array();
1354
		} else {
1355
			if($showDebug) Debug::message("no changes for DataObject");
1356
1357
			// Used by DODs to clean up after themselves, eg, Versioned
1358
			$this->invokeWithExtensions('onAfterSkippedWrite');
1359
		}
1360
1361
		// Ensure Created and LastEdited are populated
1362
		if(!isset($this->record['Created'])) {
1363
			$this->record['Created'] = $now;
1364
		}
1365
		$this->record['LastEdited'] = $now;
1366
1367
		// Write relations as necessary
1368
		if($writeComponents) $this->writeComponents(true);
1369
1370
		// Clears the cache for this object so get_one returns the correct object.
1371
		$this->flushCache();
1372
1373
		return $this->record['ID'];
1374
	}
1375
1376
	/**
1377
	 * Writes cached relation lists to the database, if possible
1378
	 */
1379
	public function writeRelations() {
1380
		if(!$this->isInDB()) return;
1381
1382
		// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1383
		if($this->unsavedRelations) {
1384
			foreach($this->unsavedRelations as $name => $list) {
1385
				$list->changeToList($this->$name());
1386
			}
1387
			$this->unsavedRelations = array();
1388
		}
1389
	}
1390
1391
	/**
1392
	 * Write the cached components to the database. Cached components could refer to two different instances of the
1393
	 * same record.
1394
	 *
1395
	 * @param bool $recursive Recursively write components
1396
	 * @return DataObject $this
1397
	 */
1398
	public function writeComponents($recursive = false) {
1399
		if(!$this->components) return $this;
1400
1401
		foreach($this->components as $component) {
1402
			$component->write(false, false, false, $recursive);
1403
		}
1404
		return $this;
1405
	}
1406
1407
	/**
1408
	 * Delete this data object.
1409
	 * $this->onBeforeDelete() gets called.
1410
	 * Note that in Versioned objects, both Stage and Live will be deleted.
1411
	 *  @uses DataExtension->augmentSQL()
1412
	 */
1413
	public function delete() {
1414
		$this->brokenOnDelete = true;
1415
		$this->onBeforeDelete();
1416
		if($this->brokenOnDelete) {
1417
			user_error("$this->class has a broken onBeforeDelete() function."
1418
				. " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1419
		}
1420
1421
		// Deleting a record without an ID shouldn't do anything
1422
		if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1423
1424
		// TODO: This is quite ugly.  To improve:
1425
		//  - move the details of the delete code in the DataQuery system
1426
		//  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1427
		//    obviously, that means getting requireTable() to configure cascading deletes ;-)
1428
		$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1429
		foreach($srcQuery->queriedTables() as $table) {
1430
			$delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1431
			$delete->execute();
1432
		}
1433
		// Remove this item out of any caches
1434
		$this->flushCache();
1435
1436
		$this->onAfterDelete();
1437
1438
		$this->OldID = $this->ID;
0 ignored issues
show
Documentation introduced by
The property OldID does not exist on object<SilverStripe\ORM\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...
1439
		$this->ID = 0;
1440
	}
1441
1442
	/**
1443
	 * Delete the record with the given ID.
1444
	 *
1445
	 * @param string $className The class name of the record to be deleted
1446
	 * @param int $id ID of record to be deleted
1447
	 */
1448
	public static function delete_by_id($className, $id) {
1449
		$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...
1450
		if($obj) {
1451
			$obj->delete();
1452
		} else {
1453
			user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1454
		}
1455
	}
1456
1457
	/**
1458
	 * Get the class ancestry, including the current class name.
1459
	 * The ancestry will be returned as an array of class names, where the 0th element
1460
	 * will be the class that inherits directly from DataObject, and the last element
1461
	 * will be the current class.
1462
	 *
1463
	 * @return array Class ancestry
1464
	 */
1465
	public function getClassAncestry() {
1466
		return ClassInfo::ancestry(get_class($this));
1467
	}
1468
1469
	/**
1470
	 * Return a component object from a one to one relationship, as a DataObject.
1471
	 * If no component is available, an 'empty component' will be returned for
1472
	 * non-polymorphic relations, or for polymorphic relations with a class set.
1473
	 *
1474
	 * @param string $componentName Name of the component
1475
	 * @return DataObject The component object. It's exact type will be that of the component.
1476
	 * @throws Exception
1477
	 */
1478
	public function getComponent($componentName) {
1479
		if(isset($this->components[$componentName])) {
1480
			return $this->components[$componentName];
1481
		}
1482
1483
		if($class = $this->hasOneComponent($componentName)) {
1484
			$joinField = $componentName . 'ID';
1485
			$joinID    = $this->getField($joinField);
1486
1487
			// Extract class name for polymorphic relations
1488
			if($class === 'SilverStripe\ORM\DataObject') {
1489
				$class = $this->getField($componentName . 'Class');
1490
				if(empty($class)) return null;
1491
			}
1492
1493
			if($joinID) {
1494
				// Ensure that the selected object originates from the same stage, subsite, etc
1495
				$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...
1496
					->filter('ID', $joinID)
1497
					->setDataQueryParam($this->getInheritableQueryParams())
1498
					->first();
1499
			}
1500
1501
			if(empty($component)) {
1502
				$component = $this->model->$class->newObject();
1503
			}
1504
		} elseif($class = $this->belongsToComponent($componentName)) {
1505
			$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
1506
			$joinID = $this->ID;
1507
1508
			if($joinID) {
1509
				// Prepare filter for appropriate join type
1510
				if($polymorphic) {
1511
					$filter = array(
1512
						"{$joinField}ID" => $joinID,
1513
						"{$joinField}Class" => $this->class
1514
					);
1515
				} else {
1516
					$filter = array(
1517
						$joinField => $joinID
1518
					);
1519
				}
1520
1521
				// Ensure that the selected object originates from the same stage, subsite, etc
1522
				$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...
1523
					->filter($filter)
1524
					->setDataQueryParam($this->getInheritableQueryParams())
1525
					->first();
1526
			}
1527
1528
			if(empty($component)) {
1529
				$component = $this->model->$class->newObject();
1530
				if($polymorphic) {
1531
					$component->{$joinField.'ID'} = $this->ID;
1532
					$component->{$joinField.'Class'} = $this->class;
1533
				} else {
1534
					$component->$joinField = $this->ID;
1535
				}
1536
			}
1537
		} else {
1538
			throw new InvalidArgumentException(
1539
				"DataObject->getComponent(): Could not find component '$componentName'."
1540
			);
1541
		}
1542
1543
		$this->components[$componentName] = $component;
1544
		return $component;
1545
	}
1546
1547
	/**
1548
	 * Returns a one-to-many relation as a HasManyList
1549
	 *
1550
	 * @param string $componentName Name of the component
1551
	 * @return HasManyList The components of the one-to-many relationship.
1552
	 */
1553
	public function getComponents($componentName) {
1554
		$result = null;
1555
1556
		$componentClass = $this->hasManyComponent($componentName);
1557
		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...
1558
			throw new InvalidArgumentException(sprintf(
1559
				"DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1560
				$componentName,
1561
				$this->class
1562
			));
1563
		}
1564
1565
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1566
		if(!$this->ID) {
1567
			if(!isset($this->unsavedRelations[$componentName])) {
1568
				$this->unsavedRelations[$componentName] =
1569
					new UnsavedRelationList($this->class, $componentName, $componentClass);
1570
			}
1571
			return $this->unsavedRelations[$componentName];
1572
		}
1573
1574
		// Determine type and nature of foreign relation
1575
		$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
1576
		/** @var HasManyList $result */
1577
		if($polymorphic) {
1578
			$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1579
		} else {
1580
			$result = HasManyList::create($componentClass, $joinField);
1581
		}
1582
1583
		if($this->model) {
1584
			$result->setDataModel($this->model);
1585
		}
1586
1587
		return $result
1588
			->setDataQueryParam($this->getInheritableQueryParams())
1589
			->forForeignID($this->ID);
1590
	}
1591
1592
	/**
1593
	 * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1594
	 *
1595
	 * @param string $relationName Relation name.
1596
	 * @return string Class name, or null if not found.
1597
	 */
1598
	public function getRelationClass($relationName) {
1599
		// Go through all relationship configuration fields.
1600
		$candidates = array_merge(
1601
			($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1602
			($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1603
			($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
1604
			($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
1605
			($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1606
		);
1607
1608
		if (isset($candidates[$relationName])) {
1609
			$remoteClass = $candidates[$relationName];
1610
1611
			// If dot notation is present, extract just the first part that contains the class.
1612
			if(($fieldPos = strpos($remoteClass, '.'))!==false) {
1613
				return substr($remoteClass, 0, $fieldPos);
1614
			}
1615
1616
			// Otherwise just return the class
1617
			return $remoteClass;
1618
		}
1619
1620
		return null;
1621
	}
1622
1623
	/**
1624
	 * Given a relation name, determine the relation type
1625
	 *
1626
	 * @param string $component Name of component
1627
	 * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1628
	 */
1629
	public function getRelationType($component) {
1630
		$types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1631
		foreach($types as $type) {
1632
			$relations = Config::inst()->get($this->class, $type);
1633
			if($relations && isset($relations[$component])) {
1634
				return $type;
1635
			}
1636
		}
1637
		return null;
1638
	}
1639
1640
	/**
1641
	 * Given a relation declared on a remote class, generate a substitute component for the opposite
1642
	 * side of the relation.
1643
	 *
1644
	 * Notes on behaviour:
1645
	 *  - This can still be used on components that are defined on both sides, but do not need to be.
1646
	 *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1647
	 *  - Cannot be used on polymorphic relationships
1648
	 *  - Cannot be used on unsaved objects.
1649
	 *
1650
	 * @param string $remoteClass
1651
	 * @param string $remoteRelation
1652
	 * @return DataList|DataObject The component, either as a list or single object
1653
	 * @throws BadMethodCallException
1654
	 * @throws InvalidArgumentException
1655
	 */
1656
	public function inferReciprocalComponent($remoteClass, $remoteRelation) {
1657
		/** @var DataObject $remote */
1658
		$remote = $remoteClass::singleton();
1659
		$class = $remote->getRelationClass($remoteRelation);
1660
1661
		// Validate arguments
1662
		if(!$this->isInDB()) {
1663
			throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1664
		}
1665
		if(empty($class)) {
1666
			throw new InvalidArgumentException(sprintf(
1667
				"%s invoked with invalid relation %s.%s",
1668
				__METHOD__,
1669
				$remoteClass,
1670
				$remoteRelation
1671
			));
1672
		}
1673
		if($class === 'SilverStripe\ORM\DataObject') {
1674
			throw new InvalidArgumentException(sprintf(
1675
				"%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1676
				"This method does not support polymorphic relationships",
1677
				__METHOD__,
1678
				$remoteClass,
1679
				$remoteRelation
1680
			));
1681
		}
1682
		if(!is_a($this, $class, true)) {
1683
			throw new InvalidArgumentException(sprintf(
1684
				"Relation %s on %s does not refer to objects of type %s",
1685
				$remoteRelation, $remoteClass, get_class($this)
1686
			));
1687
		}
1688
1689
		// Check the relation type to mock
1690
		$relationType = $remote->getRelationType($remoteRelation);
1691
		switch($relationType) {
1692
			case 'has_one': {
0 ignored issues
show
Coding Style introduced by
case statements should 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.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

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

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

Loading history...
1693
				// Mock has_many
1694
				$joinField = "{$remoteRelation}ID";
1695
				$componentClass = static::getSchema()->classForField($remoteClass, $joinField);
1696
				$result = HasManyList::create($componentClass, $joinField);
1697
				if ($this->model) {
1698
					$result->setDataModel($this->model);
1699
				}
1700
				return $result
1701
					->setDataQueryParam($this->getInheritableQueryParams())
1702
					->forForeignID($this->ID);
1703
			}
1704
			case 'belongs_to':
1705
			case 'has_many': {
0 ignored issues
show
Coding Style introduced by
case statements should 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.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

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

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

Loading history...
1706
				// These relations must have a has_one on the other end, so find it
1707
				$joinField = $remote->getRemoteJoinField($remoteRelation, $relationType, $polymorphic);
1708
				if ($polymorphic) {
1709
					throw new InvalidArgumentException(sprintf(
1710
						"%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1711
						"to be a has_one polymorphic. This method does not support polymorphic relationships",
1712
						__METHOD__,
1713
						$remoteClass,
1714
						$remoteRelation
1715
					));
1716
				}
1717
				$joinID = $this->getField($joinField);
1718
				if (empty($joinID)) {
1719
					return null;
1720
				}
1721
				// Get object by joined ID
1722
				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...
1723
					->filter('ID', $joinID)
1724
					->setDataQueryParam($this->getInheritableQueryParams())
1725
					->first();
1726
			}
1727
			case 'many_many':
1728
			case 'belongs_many_many': {
0 ignored issues
show
Coding Style introduced by
case statements should 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.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

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

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

Loading history...
1729
				// Get components and extra fields from parent
1730
				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...
1731
					= $remote->manyManyComponent($remoteRelation);
1732
				$extraFields = $remote->manyManyExtraFieldsForComponent($remoteRelation) ?: array();
1733
1734
				// Reverse parent and component fields and create an inverse ManyManyList
1735
				/** @var ManyManyList $result */
1736
				$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1737
				if($this->model) {
1738
					$result->setDataModel($this->model);
1739
				}
1740
				$this->extend('updateManyManyComponents', $result);
1741
1742
				// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1743
				// foreignID set elsewhere.
1744
				return $result
1745
					->setDataQueryParam($this->getInheritableQueryParams())
1746
					->forForeignID($this->ID);
1747
			}
1748
			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...
1749
				return null;
1750
			}
1751
		}
1752
	}
1753
1754
	/**
1755
	 * Tries to find the database key on another object that is used to store a
1756
	 * relationship to this class. If no join field can be found it defaults to 'ParentID'.
1757
	 *
1758
	 * If the remote field is polymorphic then $polymorphic is set to true, and the return value
1759
	 * is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
1760
	 *
1761
	 * @param string $component Name of the relation on the current object pointing to the
1762
	 * remote object.
1763
	 * @param string $type the join type - either 'has_many' or 'belongs_to'
1764
	 * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
1765
	 * @return string
1766
	 * @throws Exception
1767
	 */
1768
	public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
1769
		// Extract relation from current object
1770
		if($type === 'has_many') {
1771
			$remoteClass = $this->hasManyComponent($component, false);
1772
		} else {
1773
			$remoteClass = $this->belongsToComponent($component, false);
1774
		}
1775
1776
		if(empty($remoteClass)) {
1777
			throw new Exception("Unknown $type component '$component' on class '$this->class'");
1778
		}
1779
		if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
1780
			throw new Exception(
1781
				"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
1782
			);
1783
		}
1784
1785
		// If presented with an explicit field name (using dot notation) then extract field name
1786
		$remoteField = null;
1787
		if(strpos($remoteClass, '.') !== false) {
1788
			list($remoteClass, $remoteField) = explode('.', $remoteClass);
1789
		}
1790
1791
		// Reference remote has_one to check against
1792
		$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
1793
1794
		// Without an explicit field name, attempt to match the first remote field
1795
		// with the same type as the current class
1796
		if(empty($remoteField)) {
1797
			// look for remote has_one joins on this class or any parent classes
1798
			$remoteRelationsMap = array_flip($remoteRelations);
1799
			foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1800
				if(array_key_exists($class, $remoteRelationsMap)) {
1801
					$remoteField = $remoteRelationsMap[$class];
1802
					break;
1803
				}
1804
			}
1805
		}
1806
1807
		// In case of an indeterminate remote field show an error
1808
		if(empty($remoteField)) {
1809
			$polymorphic = false;
1810
			$message = "No has_one found on class '$remoteClass'";
1811
			if($type == 'has_many') {
1812
				// include a hint for has_many that is missing a has_one
1813
				$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
1814
				$message .= " requires a has_one on '$remoteClass'";
1815
			}
1816
			throw new Exception($message);
1817
		}
1818
1819
		// If given an explicit field name ensure the related class specifies this
1820
		if(empty($remoteRelations[$remoteField])) {
1821
			throw new Exception("Missing expected has_one named '$remoteField'
1822
				on class '$remoteClass' referenced by $type named '$component'
1823
				on class {$this->class}"
1824
			);
1825
		}
1826
1827
		// Inspect resulting found relation
1828
		if($remoteRelations[$remoteField] === 'SilverStripe\ORM\DataObject') {
1829
			$polymorphic = true;
1830
			return $remoteField; // Composite polymorphic field does not include 'ID' suffix
1831
		} else {
1832
			$polymorphic = false;
1833
			return $remoteField . 'ID';
1834
		}
1835
	}
1836
1837
	/**
1838
	 * Returns a many-to-many component, as a ManyManyList.
1839
	 * @param string $componentName Name of the many-many component
1840
	 * @return ManyManyList The set of components
1841
	 */
1842
	public function getManyManyComponents($componentName) {
1843
		$manyManyComponent = $this->manyManyComponent($componentName);
1844
		if(!$manyManyComponent) {
1845
			throw new InvalidArgumentException(sprintf(
1846
				"DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1847
				$componentName,
1848
				$this->class
1849
			));
1850
		}
1851
1852
		list($parentClass, $componentClass, $parentField, $componentField, $table) = $manyManyComponent;
1853
1854
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1855
		if(!$this->ID) {
1856
			if(!isset($this->unsavedRelations[$componentName])) {
1857
				$this->unsavedRelations[$componentName] =
1858
					new UnsavedRelationList($parentClass, $componentName, $componentClass);
1859
			}
1860
			return $this->unsavedRelations[$componentName];
1861
		}
1862
1863
		$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
1864
		/** @var ManyManyList $result */
1865
		$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1866
1867
1868
		// Store component data in query meta-data
1869
		$result = $result->alterDataQuery(function($query) use ($extraFields) {
1870
			$query->setQueryParam('Component.ExtraFields', $extraFields);
1871
		});
1872
1873
		if($this->model) {
1874
			$result->setDataModel($this->model);
1875
		}
1876
1877
		$this->extend('updateManyManyComponents', $result);
1878
1879
		// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1880
		// foreignID set elsewhere.
1881
		return $result
1882
			->setDataQueryParam($this->getInheritableQueryParams())
1883
			->forForeignID($this->ID);
1884
	}
1885
1886
	/**
1887
	 * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1888
	 * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1889
	 *
1890
	 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1891
	 * 							their classes.
1892
	 */
1893
	public function hasOne() {
1894
		return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1895
	}
1896
1897
	/**
1898
	 * Return data for a specific has_one component.
1899
	 * @param string $component
1900
	 * @return string|null
1901
	 */
1902
	public function hasOneComponent($component) {
1903
		$classes = ClassInfo::ancestry($this, true);
1904
1905
		foreach(array_reverse($classes) as $class) {
1906
			$hasOnes = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
1907
			if(isset($hasOnes[$component])) {
1908
				return $hasOnes[$component];
1909
			}
1910
		}
1911
	}
1912
1913
	/**
1914
	 * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1915
	 * their class name will be returned.
1916
	 *
1917
	 * @param string $component - Name of component
1918
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1919
	 *        the field data stripped off. It defaults to TRUE.
1920
	 * @return string|array
1921
	 */
1922
	public function belongsTo($component = null, $classOnly = true) {
1923
		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...
1924
			Deprecation::notice(
1925
				'4.0',
1926
				'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
1927
				Deprecation::SCOPE_GLOBAL
1928
			);
1929
			return $this->belongsToComponent($component, $classOnly);
1930
		}
1931
1932
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1933
		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...
1934
			return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1935
		} else {
1936
			return $belongsTo ? $belongsTo : array();
1937
		}
1938
	}
1939
1940
	/**
1941
	 * Return data for a specific belongs_to component.
1942
	 * @param string $component
1943
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1944
	 *        the field data stripped off. It defaults to TRUE.
1945
	 * @return string|null
1946
	 */
1947
	public function belongsToComponent($component, $classOnly = true) {
1948
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1949
1950
		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...
1951
			$belongsTo = $belongsTo[$component];
1952
		} else {
1953
			return null;
1954
		}
1955
1956
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
1957
	}
1958
1959
	/**
1960
	 * Return all of the database fields in this object
1961
	 *
1962
	 * @param string $fieldName Limit the output to a specific field name
1963
	 * @param bool $includeClass If returning a single column, prefix the column with the class name
1964
	 * in Table.Column(spec) format
1965
	 * @return array|string|null The database fields, or if searching a single field,
1966
	 * just this one field if found. Field will be a string in FieldClass(args)
1967
	 * format, or RecordClass.FieldClass(args) format if $includeClass is true
1968
	 */
1969
	public function db($fieldName = null, $includeClass = false) {
1970
		$classes = ClassInfo::ancestry($this, true);
1971
1972
		// If we're looking for a specific field, we want to hit subclasses first as they may override field types
1973
		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...
1974
			$classes = array_reverse($classes);
1975
		}
1976
1977
		$db = array();
1978
		foreach($classes as $class) {
1979
			// Merge fields with new fields and composite fields
1980
			$fields = self::database_fields($class);
1981
			$compositeFields = self::composite_fields($class, false);
1982
			$db = array_merge($db, $fields, $compositeFields);
1983
1984
			// Check for search field
1985
			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...
1986
				// Return found field
1987
				if(!$includeClass) {
1988
					return $db[$fieldName];
1989
				}
1990
				return $class . "." . $db[$fieldName];
1991
			}
1992
		}
1993
1994
		// At end of search complete
1995
		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...
1996
			return null;
1997
		} else {
1998
			return $db;
1999
		}
2000
	}
2001
2002
	/**
2003
	 * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2004
	 * relationships and their classes will be returned.
2005
	 *
2006
	 * @param string $component Deprecated - Name of component
2007
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2008
	 *        the field data stripped off. It defaults to TRUE.
2009
	 * @return string|array|false
2010
	 */
2011
	public function hasMany($component = null, $classOnly = true) {
2012
		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...
2013
			Deprecation::notice(
2014
				'4.0',
2015
				'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
2016
				Deprecation::SCOPE_GLOBAL
2017
			);
2018
			return $this->hasManyComponent($component, $classOnly);
2019
		}
2020
2021
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2022
		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...
2023
			return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2024
		} else {
2025
			return $hasMany ? $hasMany : array();
2026
		}
2027
	}
2028
2029
	/**
2030
	 * Return data for a specific has_many component.
2031
	 * @param string $component
2032
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2033
	 *        the field data stripped off. It defaults to TRUE.
2034
	 * @return string|null
2035
	 */
2036
	public function hasManyComponent($component, $classOnly = true) {
2037
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2038
2039
		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...
2040
			$hasMany = $hasMany[$component];
2041
		} else {
2042
			return null;
2043
		}
2044
2045
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
2046
	}
2047
2048
	/**
2049
	 * Return the many-to-many extra fields specification.
2050
	 *
2051
	 * If you don't specify a component name, it returns all
2052
	 * extra fields for all components available.
2053
	 *
2054
	 * @return array|null
2055
	 */
2056
	public function manyManyExtraFields() {
2057
		return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2058
	}
2059
2060
	/**
2061
	 * Return the many-to-many extra fields specification for a specific component.
2062
	 * @param string $component
2063
	 * @return array|null
2064
	 */
2065
	public function manyManyExtraFieldsForComponent($component) {
2066
		// Get all many_many_extraFields defined in this class or parent classes
2067
		$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2068
		// Extra fields are immediately available
2069
		if(isset($extraFields[$component])) {
2070
			return $extraFields[$component];
2071
		}
2072
2073
		// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
2074
		$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2075
		$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2076
		if($candidate) {
2077
			$relationName = null;
2078
			// Extract class and relation name from dot-notation
2079
			if(strpos($candidate, '.') !== false) {
2080
				list($candidate, $relationName) = explode('.', $candidate, 2);
2081
			}
2082
2083
			// If we've not already found the relation name from dot notation, we need to find a relation that points
2084
			// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
2085
			// so it's safe to assume that it's the correct one
2086
			if(!$relationName) {
2087
				$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2088
2089
				foreach($candidateManyManys as $relation => $relatedClass) {
2090
					if (is_a($this, $relatedClass)) {
2091
						$relationName = $relation;
2092
					}
2093
				}
2094
			}
2095
2096
			// If we've found a matching relation on the target class, see if we can find extra fields for it
2097
			$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
2098
			if(isset($extraFields[$relationName])) {
2099
				return $extraFields[$relationName];
2100
			}
2101
		}
2102
2103
		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...
2104
	}
2105
2106
	/**
2107
	 * Return information about a many-to-many component.
2108
	 * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2109
	 * components are returned.
2110
	 *
2111
	 * @see DataObject::manyManyComponent()
2112
	 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2113
	 */
2114
	public function manyMany() {
2115
		$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
2116
		$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2117
		$items = array_merge($manyManys, $belongsManyManys);
2118
		return $items;
2119
	}
2120
2121
	/**
2122
	 * Return information about a specific many_many component. Returns a numeric array of:
2123
	 * array(
2124
	 * 	<classname>,		The class that relation is defined in e.g. "Product"
2125
	 * 	<candidateName>,	The target class of the relation e.g. "Category"
2126
	 * 	<parentField>,		The field name pointing to <classname>'s table e.g. "ProductID"
2127
	 * 	<childField>,		The field name pointing to <candidatename>'s table e.g. "CategoryID"
2128
	 * 	<joinTable>			The join table between the two classes e.g. "Product_Categories"
2129
	 * )
2130
	 * @param string $component The component name
2131
	 * @return array|null
2132
	 */
2133
	public function manyManyComponent($component) {
2134
		$classes = $this->getClassAncestry();
2135
		foreach($classes as $class) {
2136
			$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
2137
			// Check if the component is defined in many_many on this class
2138
			if(isset($manyMany[$component])) {
2139
				$candidate = $manyMany[$component];
2140
				$classTable = static::getSchema()->tableName($class);
2141
				$candidateTable = static::getSchema()->tableName($candidate);
2142
				$parentField = "{$classTable}ID";
2143
				$childField = $class === $candidate ? "ChildID" : "{$candidateTable}ID";
2144
				$joinTable = "{$classTable}_{$component}";
2145
				return array($class, $candidate, $parentField, $childField, $joinTable);
2146
			}
2147
2148
			// Check if the component is defined in belongs_many_many on this class
2149
			$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
2150
			if(!isset($belongsManyMany[$component])) {
2151
				continue;
2152
			}
2153
2154
			// Extract class and relation name from dot-notation
2155
			$candidate = $belongsManyMany[$component];
2156
			$relationName = null;
2157
			if(strpos($candidate, '.') !== false) {
2158
				list($candidate, $relationName) = explode('.', $candidate, 2);
2159
			}
2160
			$candidateTable = static::getSchema()->tableName($candidate);
2161
			$childField = $candidateTable . "ID";
2162
2163
			// We need to find the inverse component name, if not explicitly given
2164
			$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2165
			if(!$relationName && $otherManyMany) {
2166
				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...
2167
					if($childClass === $class || is_subclass_of($class, $childClass)) {
2168
						$relationName = $inverseComponentName;
2169
						break;
2170
					}
2171
				}
2172
			}
2173
2174
			// Check valid relation found
2175
			if(!$relationName || !$otherManyMany || !isset($otherManyMany[$relationName])) {
2176
				throw new LogicException("Inverse component of $candidate not found ({$this->class})");
2177
			}
2178
2179
			// If we've got a relation name (extracted from dot-notation), we can already work out
2180
			// the join table and candidate class name...
2181
			$childClass = $otherManyMany[$relationName];
2182
			$joinTable = "{$candidateTable}_{$relationName}";
2183
2184
			// If we could work out the join table, we've got all the info we need
2185
			if ($childClass === $candidate) {
2186
				$parentField = "ChildID";
2187
			} else {
2188
				$childTable = static::getSchema()->tableName($childClass);
2189
				$parentField = "{$childTable}ID";
2190
			}
2191
			return array($class, $candidate, $parentField, $childField, $joinTable);
2192
		}
2193
	}
2194
2195
	/**
2196
	 * This returns an array (if it exists) describing the database extensions that are required, or false if none
2197
	 *
2198
	 * This is experimental, and is currently only a Postgres-specific enhancement.
2199
	 *
2200
	 * @param $class
2201
	 * @return array or false
2202
	 */
2203
	public function database_extensions($class){
2204
		$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2205
2206
		if($extensions) {
2207
			return $extensions;
2208
		} else {
2209
			return false;
2210
		}
2211
	}
2212
2213
	/**
2214
	 * Generates a SearchContext to be used for building and processing
2215
	 * a generic search form for properties on this object.
2216
	 *
2217
	 * @return SearchContext
2218
	 */
2219
	public function getDefaultSearchContext() {
2220
		return new SearchContext(
2221
			$this->class,
2222
			$this->scaffoldSearchFields(),
2223
			$this->defaultSearchFilters()
2224
		);
2225
	}
2226
2227
	/**
2228
	 * Determine which properties on the DataObject are
2229
	 * searchable, and map them to their default {@link FormField}
2230
	 * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2231
	 *
2232
	 * Some additional logic is included for switching field labels, based on
2233
	 * how generic or specific the field type is.
2234
	 *
2235
	 * Used by {@link SearchContext}.
2236
	 *
2237
	 * @param array $_params
2238
	 *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2239
	 *   'restrictFields': Numeric array of a field name whitelist
2240
	 * @return FieldList
2241
	 */
2242
	public function scaffoldSearchFields($_params = null) {
2243
		$params = array_merge(
2244
			array(
2245
				'fieldClasses' => false,
2246
				'restrictFields' => false
2247
			),
2248
			(array)$_params
2249
		);
2250
		$fields = new FieldList();
2251
		foreach($this->searchableFields() as $fieldName => $spec) {
2252
			if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
2253
2254
			// If a custom fieldclass is provided as a string, use it
2255
			if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2256
				$fieldClass = $params['fieldClasses'][$fieldName];
2257
				$field = new $fieldClass($fieldName);
2258
			// If we explicitly set a field, then construct that
2259
			} else if(isset($spec['field'])) {
2260
				// If it's a string, use it as a class name and construct
2261
				if(is_string($spec['field'])) {
2262
					$fieldClass = $spec['field'];
2263
					$field = new $fieldClass($fieldName);
2264
2265
				// If it's a FormField object, then just use that object directly.
2266
				} else if($spec['field'] instanceof FormField) {
2267
					$field = $spec['field'];
2268
2269
				// Otherwise we have a bug
2270
				} else {
2271
					user_error("Bad value for searchable_fields, 'field' value: "
2272
						. var_export($spec['field'], true), E_USER_WARNING);
2273
				}
2274
2275
			// Otherwise, use the database field's scaffolder
2276
			} else {
2277
				$field = $this->relObject($fieldName)->scaffoldSearchField();
2278
			}
2279
2280
			// Allow fields to opt out of search
2281
			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...
2282
				continue;
2283
			}
2284
2285
			if (strstr($fieldName, '.')) {
2286
				$field->setName(str_replace('.', '__', $fieldName));
2287
			}
2288
			$field->setTitle($spec['title']);
2289
2290
			$fields->push($field);
2291
		}
2292
		return $fields;
2293
	}
2294
2295
	/**
2296
	 * Scaffold a simple edit form for all properties on this dataobject,
2297
	 * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2298
	 * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2299
	 *
2300
	 * @uses FormScaffolder
2301
	 *
2302
	 * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2303
	 * @return FieldList
2304
	 */
2305
	public function scaffoldFormFields($_params = null) {
2306
		$params = array_merge(
2307
			array(
2308
				'tabbed' => false,
2309
				'includeRelations' => false,
2310
				'restrictFields' => false,
2311
				'fieldClasses' => false,
2312
				'ajaxSafe' => false
2313
			),
2314
			(array)$_params
2315
		);
2316
2317
		$fs = new FormScaffolder($this);
2318
		$fs->tabbed = $params['tabbed'];
2319
		$fs->includeRelations = $params['includeRelations'];
2320
		$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...
2321
		$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...
2322
		$fs->ajaxSafe = $params['ajaxSafe'];
2323
2324
		return $fs->getFieldList();
2325
	}
2326
2327
	/**
2328
	 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2329
	 * being called on extensions
2330
	 *
2331
	 * @param callable $callback The callback to execute
2332
	 */
2333
	protected function beforeUpdateCMSFields($callback) {
2334
		$this->beforeExtending('updateCMSFields', $callback);
2335
	}
2336
2337
	/**
2338
	 * Centerpiece of every data administration interface in Silverstripe,
2339
	 * which returns a {@link FieldList} suitable for a {@link Form} object.
2340
	 * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2341
	 * generate this set. To customize, overload this method in a subclass
2342
	 * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2343
	 *
2344
	 * <code>
2345
	 * class MyCustomClass extends DataObject {
2346
	 *  static $db = array('CustomProperty'=>'Boolean');
2347
	 *
2348
	 *  function getCMSFields() {
2349
	 *    $fields = parent::getCMSFields();
2350
	 *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2351
	 *    return $fields;
2352
	 *  }
2353
	 * }
2354
	 * </code>
2355
	 *
2356
	 * @see Good example of complex FormField building: SiteTree::getCMSFields()
2357
	 *
2358
	 * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2359
	 */
2360
	public function getCMSFields() {
2361
		$tabbedFields = $this->scaffoldFormFields(array(
2362
			// Don't allow has_many/many_many relationship editing before the record is first saved
2363
			'includeRelations' => ($this->ID > 0),
2364
			'tabbed' => true,
2365
			'ajaxSafe' => true
2366
		));
2367
2368
		$this->extend('updateCMSFields', $tabbedFields);
2369
2370
		return $tabbedFields;
2371
	}
2372
2373
	/**
2374
	 * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2375
	 * including that dataobject's extensions customised actions could be added to the EditForm.
2376
	 *
2377
	 * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2378
	 */
2379
	public function getCMSActions() {
2380
		$actions = new FieldList();
2381
		$this->extend('updateCMSActions', $actions);
2382
		return $actions;
2383
	}
2384
2385
2386
	/**
2387
	 * Used for simple frontend forms without relation editing
2388
	 * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2389
	 * by default. To customize, either overload this method in your
2390
	 * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2391
	 *
2392
	 * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2393
	 *
2394
	 * @param array $params See {@link scaffoldFormFields()}
2395
	 * @return FieldList Always returns a simple field collection without TabSet.
2396
	 */
2397
	public function getFrontEndFields($params = null) {
2398
		$untabbedFields = $this->scaffoldFormFields($params);
2399
		$this->extend('updateFrontEndFields', $untabbedFields);
2400
2401
		return $untabbedFields;
2402
	}
2403
2404
	/**
2405
	 * Gets the value of a field.
2406
	 * Called by {@link __get()} and any getFieldName() methods you might create.
2407
	 *
2408
	 * @param string $field The name of the field
2409
	 *
2410
	 * @return mixed The field value
2411
	 */
2412
	public function getField($field) {
2413
		// If we already have an object in $this->record, then we should just return that
2414
		if(isset($this->record[$field]) && is_object($this->record[$field])) {
2415
			return $this->record[$field];
2416
		}
2417
2418
		// Do we have a field that needs to be lazy loaded?
2419
		if(isset($this->record[$field.'_Lazy'])) {
2420
			$tableClass = $this->record[$field.'_Lazy'];
2421
			$this->loadLazyFields($tableClass);
2422
		}
2423
2424
		// In case of complex fields, return the DBField object
2425
		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...
2426
			$this->record[$field] = $this->dbObject($field);
2427
		}
2428
2429
		return isset($this->record[$field]) ? $this->record[$field] : null;
2430
	}
2431
2432
	/**
2433
	 * Loads all the stub fields that an initial lazy load didn't load fully.
2434
	 *
2435
	 * @param string $class Class to load the values from. Others are joined as required.
2436
	 * Not specifying a tableClass will load all lazy fields from all tables.
2437
	 * @return bool Flag if lazy loading succeeded
2438
	 */
2439
	protected function loadLazyFields($class = null) {
2440
		if(!$this->isInDB() || !is_numeric($this->ID)) {
2441
			return false;
2442
		}
2443
2444
		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...
2445
			$loaded = array();
2446
2447
			foreach ($this->record as $key => $value) {
2448
				if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2449
					$this->loadLazyFields($value);
2450
					$loaded[$value] = $value;
2451
				}
2452
			}
2453
2454
			return false;
2455
		}
2456
2457
		$dataQuery = new DataQuery($class);
2458
2459
		// Reset query parameter context to that of this DataObject
2460
		if($params = $this->getSourceQueryParams()) {
2461
			foreach($params as $key => $value) {
2462
				$dataQuery->setQueryParam($key, $value);
2463
			}
2464
		}
2465
2466
		// Limit query to the current record, unless it has the Versioned extension,
2467
		// in which case it requires special handling through augmentLoadLazyFields()
2468
		$baseIDColumn = static::getSchema()->sqlColumnForField($this, 'ID');
2469
		$dataQuery->where([
2470
			$baseIDColumn => $this->record['ID']
2471
		])->limit(1);
2472
2473
		$columns = array();
2474
2475
		// Add SQL for fields, both simple & multi-value
2476
		// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2477
		$databaseFields = self::database_fields($class);
2478
		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...
2479
			if(!isset($this->record[$k]) || $this->record[$k] === null) {
2480
				$columns[] = $k;
2481
			}
2482
		}
2483
2484
		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...
2485
			$query = $dataQuery->query();
2486
			$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2487
			$this->extend('augmentSQL', $query, $dataQuery);
2488
2489
			$dataQuery->setQueriedColumns($columns);
2490
			$newData = $dataQuery->execute()->record();
2491
2492
			// Load the data into record
2493
			if($newData) {
2494
				foreach($newData as $k => $v) {
2495
					if (in_array($k, $columns)) {
2496
						$this->record[$k] = $v;
2497
						$this->original[$k] = $v;
2498
						unset($this->record[$k . '_Lazy']);
2499
					}
2500
				}
2501
2502
			// No data means that the query returned nothing; assign 'null' to all the requested fields
2503
			} else {
2504
				foreach($columns as $k) {
2505
					$this->record[$k] = null;
2506
					$this->original[$k] = null;
2507
					unset($this->record[$k . '_Lazy']);
2508
				}
2509
			}
2510
		}
2511
		return true;
2512
	}
2513
2514
	/**
2515
	 * Return the fields that have changed.
2516
	 *
2517
	 * The change level affects what the functions defines as "changed":
2518
	 * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2519
	 * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2520
	 *   for example a change from 0 to null would not be included.
2521
	 *
2522
	 * Example return:
2523
	 * <code>
2524
	 * array(
2525
	 *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2526
	 * )
2527
	 * </code>
2528
	 *
2529
	 * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2530
	 * to return all database fields, or an array for an explicit filter. false returns all fields.
2531
	 * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2532
	 * @return array
2533
	 */
2534
	public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
2535
		$changedFields = array();
2536
2537
		// Update the changed array with references to changed obj-fields
2538
		foreach($this->record as $k => $v) {
2539
			// Prevents DBComposite infinite looping on isChanged
2540
			if(is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2541
				continue;
2542
			}
2543
			if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2544
				$this->changed[$k] = self::CHANGE_VALUE;
2545
			}
2546
		}
2547
2548
		if(is_array($databaseFieldsOnly)) {
2549
			$fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2550
		} elseif($databaseFieldsOnly) {
2551
			$fields = array_intersect_key((array)$this->changed, $this->db());
2552
		} else {
2553
			$fields = $this->changed;
2554
		}
2555
2556
		// Filter the list to those of a certain change level
2557
		if($changeLevel > self::CHANGE_STRICT) {
2558
			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...
2559
				if($level < $changeLevel) {
2560
					unset($fields[$name]);
2561
				}
2562
			}
2563
		}
2564
2565
		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...
2566
			$changedFields[$name] = array(
2567
				'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2568
				'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2569
				'level' => $level
2570
			);
2571
		}
2572
2573
		return $changedFields;
2574
	}
2575
2576
	/**
2577
	 * Uses {@link getChangedFields()} to determine if fields have been changed
2578
	 * since loading them from the database.
2579
	 *
2580
	 * @param string $fieldName Name of the database field to check, will check for any if not given
2581
	 * @param int $changeLevel See {@link getChangedFields()}
2582
	 * @return boolean
2583
	 */
2584
	public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
2585
		$fields = $fieldName ? array($fieldName) : true;
2586
		$changed = $this->getChangedFields($fields, $changeLevel);
2587
		if(!isset($fieldName)) {
2588
			return !empty($changed);
2589
		}
2590
		else {
2591
			return array_key_exists($fieldName, $changed);
2592
		}
2593
	}
2594
2595
	/**
2596
	 * Set the value of the field
2597
	 * Called by {@link __set()} and any setFieldName() methods you might create.
2598
	 *
2599
	 * @param string $fieldName Name of the field
2600
	 * @param mixed $val New field value
2601
	 * @return $this
2602
	 */
2603
	public function setField($fieldName, $val) {
2604
		$this->objCacheClear();
2605
		//if it's a has_one component, destroy the cache
2606
		if (substr($fieldName, -2) == 'ID') {
2607
			unset($this->components[substr($fieldName, 0, -2)]);
2608
		}
2609
2610
		// If we've just lazy-loaded the column, then we need to populate the $original array
2611
		if(isset($this->record[$fieldName.'_Lazy'])) {
2612
			$tableClass = $this->record[$fieldName.'_Lazy'];
2613
			$this->loadLazyFields($tableClass);
2614
		}
2615
2616
		// Situation 1: Passing an DBField
2617
		if($val instanceof DBField) {
2618
			$val->setName($fieldName);
2619
			$val->saveInto($this);
2620
2621
			// Situation 1a: Composite fields should remain bound in case they are
2622
			// later referenced to update the parent dataobject
2623
			if($val instanceof DBComposite) {
2624
				$val->bindTo($this);
2625
				$this->record[$fieldName] = $val;
2626
			}
2627
		// Situation 2: Passing a literal or non-DBField object
2628
		} else {
2629
			// If this is a proper database field, we shouldn't be getting non-DBField objects
2630
			if(is_object($val) && $this->db($fieldName)) {
2631
				user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2632
			}
2633
2634
			// if a field is not existing or has strictly changed
2635
			if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2636
				// TODO Add check for php-level defaults which are not set in the db
2637
				// TODO Add check for hidden input-fields (readonly) which are not set in the db
2638
				// At the very least, the type has changed
2639
				$this->changed[$fieldName] = self::CHANGE_STRICT;
2640
2641
				if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
2642
						&& $this->record[$fieldName] != $val)) {
2643
2644
					// Value has changed as well, not just the type
2645
					$this->changed[$fieldName] = self::CHANGE_VALUE;
2646
				}
2647
2648
				// Value is always saved back when strict check succeeds.
2649
				$this->record[$fieldName] = $val;
2650
			}
2651
		}
2652
		return $this;
2653
	}
2654
2655
	/**
2656
	 * Set the value of the field, using a casting object.
2657
	 * This is useful when you aren't sure that a date is in SQL format, for example.
2658
	 * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2659
	 * can be saved into the Image table.
2660
	 *
2661
	 * @param string $fieldName Name of the field
2662
	 * @param mixed $value New field value
2663
	 * @return $this
2664
	 */
2665
	public function setCastedField($fieldName, $value) {
2666
		if(!$fieldName) {
2667
			user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2668
		}
2669
		$fieldObj = $this->dbObject($fieldName);
2670
		if($fieldObj) {
2671
			$fieldObj->setValue($value);
2672
			$fieldObj->saveInto($this);
2673
		} else {
2674
			$this->$fieldName = $value;
2675
		}
2676
		return $this;
2677
	}
2678
2679
	/**
2680
	 * {@inheritdoc}
2681
	 */
2682
	public function castingHelper($field) {
2683
		if ($fieldSpec = $this->db($field)) {
2684
			return $fieldSpec;
2685
		}
2686
2687
		// many_many_extraFields aren't presented by db(), so we check if the source query params
2688
		// provide us with meta-data for a many_many relation we can inspect for extra fields.
2689
		$queryParams = $this->getSourceQueryParams();
2690
		if (!empty($queryParams['Component.ExtraFields'])) {
2691
			$extraFields = $queryParams['Component.ExtraFields'];
2692
2693
			if (isset($extraFields[$field])) {
2694
				return $extraFields[$field];
2695
			}
2696
		}
2697
2698
		return parent::castingHelper($field);
2699
	}
2700
2701
	/**
2702
	 * Returns true if the given field exists in a database column on any of
2703
	 * the objects tables and optionally look up a dynamic getter with
2704
	 * get<fieldName>().
2705
	 *
2706
	 * @param string $field Name of the field
2707
	 * @return boolean True if the given field exists
2708
	 */
2709
	public function hasField($field) {
2710
		return (
2711
			array_key_exists($field, $this->record)
2712
			|| $this->db($field)
2713
			|| (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...
2714
			|| $this->hasMethod("get{$field}")
2715
		);
2716
	}
2717
2718
	/**
2719
	 * Returns true if the given field exists as a database column
2720
	 *
2721
	 * @param string $field Name of the field
2722
	 *
2723
	 * @return boolean
2724
	 */
2725
	public function hasDatabaseField($field) {
2726
		return $this->db($field)
2727
 			&& ! 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...
2728
	}
2729
2730
	/**
2731
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2732
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2733
	 *
2734
	 * @param string $field Name of the field
2735
	 * @return string The field type of the given field
2736
	 */
2737
	public function hasOwnTableDatabaseField($field) {
2738
		return self::has_own_table_database_field($this->class, $field);
2739
	}
2740
2741
	/**
2742
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2743
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2744
	 *
2745
	 * @param string $class Class name to check
2746
	 * @param string $field Name of the field
2747
	 * @return string The field type of the given field
2748
	 */
2749
	public static function has_own_table_database_field($class, $field) {
2750
		$fieldMap = self::database_fields($class);
2751
2752
		// Remove string-based "constructor-arguments" from the DBField definition
2753
		if(isset($fieldMap[$field])) {
2754
			$spec = $fieldMap[$field];
2755
			if(is_string($spec)) {
2756
				return strtok($spec,'(');
2757
			} else {
2758
				return $spec['type'];
2759
			}
2760
		}
2761
	}
2762
2763
	/**
2764
	 * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2765
	 * actually looking in the database.
2766
	 *
2767
	 * @param string $dataClass
2768
	 * @return bool
2769
	 */
2770
	public static function has_own_table($dataClass) {
2771
		if(!is_subclass_of($dataClass, 'SilverStripe\ORM\DataObject')) {
2772
			return false;
2773
		}
2774
		$fields = static::database_fields($dataClass);
2775
		return !empty($fields);
2776
	}
2777
2778
	/**
2779
	 * Returns true if the member is allowed to do the given action.
2780
	 * See {@link extendedCan()} for a more versatile tri-state permission control.
2781
	 *
2782
	 * @param string $perm The permission to be checked, such as 'View'.
2783
	 * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2784
	 * in user.
2785
	 * @param array $context Additional $context to pass to extendedCan()
2786
	 *
2787
	 * @return boolean True if the the member is allowed to do the given action
2788
	 */
2789
	public function can($perm, $member = null, $context = array()) {
2790
		if(!isset($member)) {
2791
			$member = Member::currentUser();
2792
		}
2793
		if(Permission::checkMember($member, "ADMIN")) return true;
2794
2795
		if($this->manyManyComponent('Can' . $perm)) {
2796
			if($this->ParentID && $this->SecurityType == 'Inherit') {
2797
				if(!($p = $this->Parent)) {
0 ignored issues
show
Documentation introduced by
The property Parent does not exist on object<SilverStripe\ORM\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...
2798
					return false;
2799
				}
2800
				return $this->Parent->can($perm, $member);
2801
2802
			} else {
2803
				$permissionCache = $this->uninherited('permissionCache');
2804
				$memberID = $member ? $member->ID : 'none';
2805
2806
				if(!isset($permissionCache[$memberID][$perm])) {
2807
					if($member->ID) {
2808
						$groups = $member->Groups();
2809
					}
2810
2811
					$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...
2812
2813
					// TODO Fix relation table hardcoding
2814
					$query = new SQLSelect(
2815
						"\"Page_Can$perm\".PageID",
2816
					array("\"Page_Can$perm\""),
2817
						"GroupID IN ($groupList)");
2818
2819
					$permissionCache[$memberID][$perm] = $query->execute()->column();
2820
2821
					if($perm == "View") {
2822
						// TODO Fix relation table hardcoding
2823
						$query = new SQLSelect("\"SiteTree\".\"ID\"", array(
2824
							"\"SiteTree\"",
2825
							"LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2826
							), "\"Page_CanView\".\"PageID\" IS NULL");
2827
2828
							$unsecuredPages = $query->execute()->column();
2829
							if($permissionCache[$memberID][$perm]) {
2830
								$permissionCache[$memberID][$perm]
2831
									= array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2832
							} else {
2833
								$permissionCache[$memberID][$perm] = $unsecuredPages;
2834
							}
2835
					}
2836
2837
					Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2838
				}
2839
2840
				if($permissionCache[$memberID][$perm]) {
2841
					return in_array($this->ID, $permissionCache[$memberID][$perm]);
2842
				}
2843
			}
2844
		} else {
2845
			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, CampaignAdmin, 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, 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, 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, GroupTest_Member, HTMLEditorFieldTest_Object, HierarchyHideTest_Object, HierarchyHideTest_SubObject, HierarchyTest_Object, Image, InstallerTest, LeftAndMain, LeftAndMainTest_Controller, LeftAndMainTest_Object, ListboxFieldTest_Article, ListboxFieldTest_DataObject, ListboxFieldTest_Tag, ManyManyListTest_Category, ManyManyListTest_ExtraFields, ManyManyListTest_IndirectPrimary, ManyManyListTest_Product, ManyManyListTest_Secondary, ManyManyListTest_SecondarySub, MemberDatetimeOptionsetFieldTest_Controller, 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, 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, SecurityAdmin, SecurityTest_NullController, SecurityTest_SecuredController, SilverStripe\Filesystem\...ProtectedFileController, SilverStripe\Framework\Tests\ClassI, SilverStripe\ORM\DataObject, SilverStripe\ORM\DatabaseAdmin, SilverStripe\ORM\Versioning\ChangeSet, SilverStripe\ORM\Versioning\ChangeSetItem, SilverStripe\Security\CMSSecurity, SilverStripe\Security\Group, SilverStripe\Security\LoginAttempt, SilverStripe\Security\Member, SilverStripe\Security\MemberPassword, SilverStripe\Security\Permission, SilverStripe\Security\PermissionRole, SilverStripe\Security\PermissionRoleCode, SilverStripe\Security\RememberLoginHash, SilverStripe\Security\Security, SubclassedDBFieldObject, TaskRunner, TestNamespace\SSViewerTestModel, TestNamespace\SSViewerTestModel_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...
2846
		}
2847
	}
2848
2849
	/**
2850
	 * Process tri-state responses from permission-alterting extensions.  The extensions are
2851
	 * expected to return one of three values:
2852
	 *
2853
	 *  - false: Disallow this permission, regardless of what other extensions say
2854
	 *  - true: Allow this permission, as long as no other extensions return false
2855
	 *  - NULL: Don't affect the outcome
2856
	 *
2857
	 * This method itself returns a tri-state value, and is designed to be used like this:
2858
	 *
2859
	 * <code>
2860
	 * $extended = $this->extendedCan('canDoSomething', $member);
2861
	 * if($extended !== null) return $extended;
2862
	 * else return $normalValue;
2863
	 * </code>
2864
	 *
2865
	 * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2866
	 * @param Member|int $member
2867
	 * @param array $context Optional context
2868
	 * @return boolean|null
2869
	 */
2870
	public function extendedCan($methodName, $member, $context = array()) {
2871
		$results = $this->extend($methodName, $member, $context);
2872
		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...
2873
			// Remove NULLs
2874
			$results = array_filter($results, function($v) {return !is_null($v);});
2875
			// If there are any non-NULL responses, then return the lowest one of them.
2876
			// If any explicitly deny the permission, then we don't get access
2877
			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...
2878
		}
2879
		return null;
2880
	}
2881
2882
	/**
2883
	 * @param Member $member
2884
	 * @return boolean
2885
	 */
2886
	public function canView($member = null) {
2887
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2886 can be null; however, SilverStripe\ORM\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...
2888
		if($extended !== null) {
2889
			return $extended;
2890
		}
2891
		return Permission::check('ADMIN', 'any', $member);
2892
	}
2893
2894
	/**
2895
	 * @param Member $member
2896
	 * @return boolean
2897
	 */
2898
	public function canEdit($member = null) {
2899
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2898 can be null; however, SilverStripe\ORM\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...
2900
		if($extended !== null) {
2901
			return $extended;
2902
		}
2903
		return Permission::check('ADMIN', 'any', $member);
2904
	}
2905
2906
	/**
2907
	 * @param Member $member
2908
	 * @return boolean
2909
	 */
2910
	public function canDelete($member = null) {
2911
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2910 can be null; however, SilverStripe\ORM\DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2912
		if($extended !== null) {
2913
			return $extended;
2914
		}
2915
		return Permission::check('ADMIN', 'any', $member);
2916
	}
2917
2918
	/**
2919
	 * @param Member $member
2920
	 * @param array $context Additional context-specific data which might
2921
	 * affect whether (or where) this object could be created.
2922
	 * @return boolean
2923
	 */
2924
	public function canCreate($member = null, $context = array()) {
2925
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2924 can be null; however, SilverStripe\ORM\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...
2926
		if($extended !== null) {
2927
			return $extended;
2928
		}
2929
		return Permission::check('ADMIN', 'any', $member);
2930
	}
2931
2932
	/**
2933
	 * Debugging used by Debug::show()
2934
	 *
2935
	 * @return string HTML data representing this object
2936
	 */
2937
	public function debug() {
2938
		$val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2939
		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...
2940
			$val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2941
		}
2942
		$val .= "</ul>\n";
2943
		return $val;
2944
	}
2945
2946
	/**
2947
	 * Return the DBField object that represents the given field.
2948
	 * This works similarly to obj() with 2 key differences:
2949
	 *   - it still returns an object even when the field has no value.
2950
	 *   - it only matches fields and not methods
2951
	 *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2952
	 *
2953
	 * @param string $fieldName Name of the field
2954
	 * @return DBField The field as a DBField object
2955
	 */
2956
	public function dbObject($fieldName) {
2957
		// Check for field in DB
2958
		$helper = $this->db($fieldName, true);
2959
2960
		if(!$helper) {
2961
			return null;
2962
		}
2963
2964
		$value = isset($this->record[$fieldName])
2965
			? $this->record[$fieldName]
2966
			: null;
2967
2968
		// If we have a DBField object in $this->record, then return that
2969
		if($value instanceof DBField) {
2970
			return $value;
2971
		}
2972
2973
		list($table, $spec) = explode('.', $helper);
2974
		$obj = Object::create_from_string($spec, $fieldName);
2975
		$obj->setTable($table);
2976
		$obj->setValue($value, $this, false);
2977
		return $obj;
2978
	}
2979
2980
	/**
2981
	 * Traverses to a DBField referenced by relationships between data objects.
2982
	 *
2983
	 * The path to the related field is specified with dot separated syntax
2984
	 * (eg: Parent.Child.Child.FieldName).
2985
	 *
2986
	 * @param string $fieldPath
2987
	 *
2988
	 * @return mixed DBField of the field on the object or a DataList instance.
2989
	 */
2990
	public function relObject($fieldPath) {
2991
		$object = null;
2992
2993
		if(strpos($fieldPath, '.') !== false) {
2994
			$parts = explode('.', $fieldPath);
2995
			$fieldName = array_pop($parts);
2996
2997
			// Traverse dot syntax
2998
			$component = $this;
2999
3000
			foreach($parts as $relation) {
3001
				if($component instanceof SS_List) {
3002
					if(method_exists($component,$relation)) {
3003
						$component = $component->$relation();
3004
					} else {
3005
						$component = $component->relation($relation);
3006
					}
3007
				} else {
3008
					$component = $component->$relation();
3009
				}
3010
			}
3011
3012
			$object = $component->dbObject($fieldName);
3013
3014
		} else {
3015
			$object = $this->dbObject($fieldPath);
3016
		}
3017
3018
		return $object;
3019
	}
3020
3021
	/**
3022
	 * Traverses to a field referenced by relationships between data objects, returning the value
3023
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3024
	 *
3025
	 * @param $fieldName string
3026
	 * @return string | null - will return null on a missing value
3027
	 */
3028
	public function relField($fieldName) {
3029
		$component = $this;
3030
3031
		// We're dealing with relations here so we traverse the dot syntax
3032
		if(strpos($fieldName, '.') !== false) {
3033
			$relations = explode('.', $fieldName);
3034
			$fieldName = array_pop($relations);
3035
			foreach($relations as $relation) {
3036
				// Inspect $component for element $relation
3037
				if($component->hasMethod($relation)) {
3038
					// Check nested method
3039
					$component = $component->$relation();
3040
				} elseif($component instanceof SS_List) {
3041
					// Select adjacent relation from DataList
3042
					$component = $component->relation($relation);
3043
				} elseif($component instanceof DataObject
3044
					&& ($dbObject = $component->dbObject($relation))
3045
				) {
3046
					// Select db object
3047
					$component = $dbObject;
3048
				} else {
3049
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
3050
				}
3051
			}
3052
		}
3053
3054
		// Bail if the component is null
3055
		if(!$component) {
3056
			return null;
3057
		}
3058
		if($component->hasMethod($fieldName)) {
3059
			return $component->$fieldName();
3060
		}
3061
		return $component->$fieldName;
3062
	}
3063
3064
	/**
3065
	 * Temporary hack to return an association name, based on class, to get around the mangle
3066
	 * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3067
	 *
3068
	 * @param string $className
3069
	 * @return string
3070
	 */
3071
	public function getReverseAssociation($className) {
3072
		if (is_array($this->manyMany())) {
3073
			$many_many = array_flip($this->manyMany());
3074
			if (array_key_exists($className, $many_many)) return $many_many[$className];
3075
		}
3076
		if (is_array($this->hasMany())) {
3077
			$has_many = array_flip($this->hasMany());
3078
			if (array_key_exists($className, $has_many)) return $has_many[$className];
3079
		}
3080
		if (is_array($this->hasOne())) {
3081
			$has_one = array_flip($this->hasOne());
3082
			if (array_key_exists($className, $has_one)) return $has_one[$className];
3083
		}
3084
3085
		return false;
3086
	}
3087
3088
	/**
3089
	 * Return all objects matching the filter
3090
	 * sub-classes are automatically selected and included
3091
	 *
3092
	 * @param string $callerClass The class of objects to be returned
3093
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3094
	 * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3095
	 * @param string|array $sort A sort expression to be inserted into the ORDER
3096
	 * BY clause.  If omitted, self::$default_sort will be used.
3097
	 * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3098
	 * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3099
	 * @param string $containerClass The container class to return the results in.
3100
	 *
3101
	 * @todo $containerClass is Ignored, why?
3102
	 *
3103
	 * @return DataList The objects matching the filter, in the class specified by $containerClass
3104
	 */
3105
	public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
3106
			$containerClass = 'SilverStripe\ORM\DataList') {
3107
3108
		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...
3109
			$callerClass = get_called_class();
3110
			if($callerClass == 'SilverStripe\ORM\DataObject') {
3111
				throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3112
			}
3113
3114
			if($filter || $sort || $join || $limit || ($containerClass != 'SilverStripe\ORM\DataList')) {
3115
				throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3116
					. ' arguments');
3117
			}
3118
3119
			$result = DataList::create(get_called_class());
3120
			$result->setDataModel(DataModel::inst());
3121
			return $result;
3122
		}
3123
3124
		if($join) {
3125
			throw new \InvalidArgumentException(
3126
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3127
			);
3128
		}
3129
3130
		$result = DataList::create($callerClass)->where($filter)->sort($sort);
3131
3132
		if($limit && strpos($limit, ',') !== false) {
3133
			$limitArguments = explode(',', $limit);
3134
			$result = $result->limit($limitArguments[1],$limitArguments[0]);
3135
		} elseif($limit) {
3136
			$result = $result->limit($limit);
3137
		}
3138
3139
		$result->setDataModel(DataModel::inst());
3140
		return $result;
3141
	}
3142
3143
3144
	/**
3145
	 * Return the first item matching the given query.
3146
	 * All calls to get_one() are cached.
3147
	 *
3148
	 * @param string $callerClass The class of objects to be returned
3149
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3150
	 * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3151
	 * @param boolean $cache Use caching
3152
	 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3153
	 *
3154
	 * @return DataObject The first item matching the query
3155
	 */
3156
	public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
3157
		$SNG = singleton($callerClass);
3158
3159
		$cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3160
		$cacheKey = md5(var_export($cacheComponents, true));
3161
3162
		// Flush destroyed items out of the cache
3163
		if($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
3164
				&& self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
3165
				&& self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
3166
3167
			self::$_cache_get_one[$callerClass][$cacheKey] = false;
3168
		}
3169
		if(!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3170
			$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...
3171
			$item = $dl->First();
3172
3173
			if($cache) {
3174
				self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3175
				if(!self::$_cache_get_one[$callerClass][$cacheKey]) {
3176
					self::$_cache_get_one[$callerClass][$cacheKey] = false;
3177
				}
3178
			}
3179
		}
3180
		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...
3181
	}
3182
3183
	/**
3184
	 * Flush the cached results for all relations (has_one, has_many, many_many)
3185
	 * Also clears any cached aggregate data.
3186
	 *
3187
	 * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3188
	 *                            When false will just clear session-local cached data
3189
	 * @return DataObject $this
3190
	 */
3191
	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...
3192
		if($this->class == 'SilverStripe\ORM\DataObject') {
3193
			self::$_cache_get_one = array();
3194
			return $this;
3195
		}
3196
3197
		$classes = ClassInfo::ancestry($this->class);
3198
		foreach($classes as $class) {
3199
			if(isset(self::$_cache_get_one[$class])) unset(self::$_cache_get_one[$class]);
3200
		}
3201
3202
		$this->extend('flushCache');
3203
3204
		$this->components = array();
3205
		return $this;
3206
	}
3207
3208
	/**
3209
	 * Flush the get_one global cache and destroy associated objects.
3210
	 */
3211
	public static function flush_and_destroy_cache() {
3212
		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...
3213
			if(is_array($items)) foreach($items as $item) {
3214
				if($item) $item->destroy();
3215
			}
3216
		}
3217
		self::$_cache_get_one = array();
3218
	}
3219
3220
	/**
3221
	 * Reset all global caches associated with DataObject.
3222
	 */
3223
	public static function reset() {
3224
		// @todo Decouple these
3225
		DBClassName::clear_classname_cache();
3226
		ClassInfo::reset_db_cache();
3227
		static::getSchema()->reset();
3228
		self::$_cache_has_own_table = array();
3229
		self::$_cache_get_one = array();
3230
		self::$_cache_field_labels = array();
3231
	}
3232
3233
	/**
3234
	 * Return the given element, searching by ID
3235
	 *
3236
	 * @param string $callerClass The class of the object to be returned
3237
	 * @param int $id The id of the element
3238
	 * @param boolean $cache See {@link get_one()}
3239
	 *
3240
	 * @return DataObject The element
3241
	 */
3242
	public static function get_by_id($callerClass, $id, $cache = true) {
3243
		if(!is_numeric($id)) {
3244
			user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3245
		}
3246
3247
		// Pass to get_one
3248
		$column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
3249
		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...
3250
	}
3251
3252
	/**
3253
	 * Get the name of the base table for this object
3254
	 *
3255
	 * @return string
3256
	 */
3257
	public function baseTable() {
3258
		return static::getSchema()->baseDataTable($this);
3259
	}
3260
3261
	/**
3262
	 * Get the base class for this object
3263
	 *
3264
	 * @return string
3265
	 */
3266
	public function baseClass() {
3267
		return static::getSchema()->baseDataClass($this);
3268
	}
3269
3270
	/**
3271
	 * @var array Parameters used in the query that built this object.
3272
	 * This can be used by decorators (e.g. lazy loading) to
3273
	 * run additional queries using the same context.
3274
	 */
3275
	protected $sourceQueryParams;
3276
3277
	/**
3278
	 * @see $sourceQueryParams
3279
	 * @return array
3280
	 */
3281
	public function getSourceQueryParams() {
3282
		return $this->sourceQueryParams;
3283
	}
3284
3285
	/**
3286
	 * Get list of parameters that should be inherited to relations on this object
3287
	 *
3288
	 * @return array
3289
	 */
3290
	public function getInheritableQueryParams() {
3291
		$params = $this->getSourceQueryParams();
3292
		$this->extend('updateInheritableQueryParams', $params);
3293
		return $params;
3294
	}
3295
3296
	/**
3297
	 * @see $sourceQueryParams
3298
	 * @param array
3299
	 */
3300
	public function setSourceQueryParams($array) {
3301
		$this->sourceQueryParams = $array;
3302
	}
3303
3304
	/**
3305
	 * @see $sourceQueryParams
3306
	 * @param string $key
3307
	 * @param string $value
3308
	 */
3309
	public function setSourceQueryParam($key, $value) {
3310
		$this->sourceQueryParams[$key] = $value;
3311
	}
3312
3313
	/**
3314
	 * @see $sourceQueryParams
3315
	 * @param string $key
3316
	 * @return string
3317
	 */
3318
	public function getSourceQueryParam($key) {
3319
		if(isset($this->sourceQueryParams[$key])) {
3320
			return $this->sourceQueryParams[$key];
3321
		}
3322
		return null;
3323
	}
3324
3325
	//-------------------------------------------------------------------------------------------//
3326
3327
	/**
3328
	 * Return the database indexes on this table.
3329
	 * This array is indexed by the name of the field with the index, and
3330
	 * the value is the type of index.
3331
	 */
3332
	public function databaseIndexes() {
3333
		$has_one = $this->uninherited('has_one');
3334
		$classIndexes = $this->uninherited('indexes');
3335
		//$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...
3336
3337
		$indexes = array();
3338
3339
		if($has_one) {
3340
			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...
3341
				$indexes[$relationshipName . 'ID'] = true;
3342
			}
3343
		}
3344
3345
		if($classIndexes) {
3346
			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...
3347
				$indexes[$indexName] = $indexType;
3348
			}
3349
		}
3350
3351
		if(get_parent_class($this) == 'SilverStripe\ORM\DataObject') {
3352
			$indexes['ClassName'] = true;
3353
		}
3354
3355
		return $indexes;
3356
	}
3357
3358
	/**
3359
	 * Check the database schema and update it as necessary.
3360
	 *
3361
	 * @uses DataExtension->augmentDatabase()
3362
	 */
3363
	public function requireTable() {
3364
		// Only build the table if we've actually got fields
3365
		$fields = self::database_fields($this->class);
3366
		$table = static::getSchema()->tableName($this->class);
3367
		$extensions = self::database_extensions($this->class);
3368
3369
		$indexes = $this->databaseIndexes();
3370
3371
		// Validate relationship configuration
3372
		$this->validateModelDefinitions();
3373
		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...
3374
			$hasAutoIncPK = get_parent_class($this) === 'SilverStripe\ORM\DataObject';
3375
			DB::require_table(
3376
				$table, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'), $extensions
3377
			);
3378
		} else {
3379
			DB::dont_require_table($table);
3380
		}
3381
3382
		// Build any child tables for many_many items
3383
		if($manyMany = $this->uninherited('many_many')) {
3384
			$extras = $this->uninherited('many_many_extraFields');
3385
			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...
3386
				// Build field list
3387
				if($this->class === $childClass) {
3388
					$childField = "ChildID";
3389
				} else {
3390
					$childTable = $this->getSchema()->tableName($childClass);
3391
					$childField = "{$childTable}ID";
3392
				}
3393
				$manymanyFields = array(
3394
					"{$table}ID" => "Int",
3395
					$childField => "Int",
3396
				);
3397
				if(isset($extras[$relationship])) {
3398
					$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
3399
				}
3400
3401
				// Build index list
3402
				$manymanyIndexes = array(
3403
					"{$table}ID" => true,
3404
					$childField => true,
3405
				);
3406
				$manyManyTable = "{$table}_$relationship";
3407
				DB::require_table($manyManyTable, $manymanyFields, $manymanyIndexes, true, null, $extensions);
3408
			}
3409
		}
3410
3411
		// Let any extentions make their own database fields
3412
		$this->extend('augmentDatabase', $dummy);
3413
	}
3414
3415
	/**
3416
	 * Validate that the configured relations for this class use the correct syntaxes
3417
	 * @throws LogicException
3418
	 */
3419
	protected function validateModelDefinitions() {
3420
		$modelDefinitions = array(
3421
			'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
3422
			'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
3423
			'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
3424
			'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
3425
			'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
3426
			'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
3427
			'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
3428
		);
3429
3430
		foreach($modelDefinitions as $defType => $relations) {
3431
			if( ! $relations) continue;
3432
3433
			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...
3434
				if($defType === 'many_many_extraFields') {
3435
					if(!is_array($v)) {
3436
						throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
3437
							. var_export($k, true) . " => " . var_export($v, true)
3438
							. ". Each many_many_extraFields entry should map to a field specification array.");
3439
					}
3440
				} else {
3441
					if(!is_string($k) || is_numeric($k) || !is_string($v)) {
3442
						throw new LogicException("$this->class::$defType has a bad entry: "
3443
							. var_export($k, true). " => " . var_export($v, true) . ".  Each map key should be a
3444
							 relationship name, and the map value should be the data class to join to.");
3445
					}
3446
				}
3447
			}
3448
		}
3449
	}
3450
3451
	/**
3452
	 * Add default records to database. This function is called whenever the
3453
	 * database is built, after the database tables have all been created. Overload
3454
	 * this to add default records when the database is built, but make sure you
3455
	 * call parent::requireDefaultRecords().
3456
	 *
3457
	 * @uses DataExtension->requireDefaultRecords()
3458
	 */
3459
	public function requireDefaultRecords() {
3460
		$defaultRecords = $this->stat('default_records');
3461
3462
		if(!empty($defaultRecords)) {
3463
			$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...
3464
			if(!$hasData) {
3465
				$className = $this->class;
3466
				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...
3467
					$obj = $this->model->$className->newObject($record);
3468
					$obj->write();
3469
				}
3470
				DB::alteration_message("Added default records to $className table","created");
3471
			}
3472
		}
3473
3474
		// Let any extentions make their own database default data
3475
		$this->extend('requireDefaultRecords', $dummy);
3476
	}
3477
3478
	/**
3479
	 * Get the default searchable fields for this object, as defined in the
3480
	 * $searchable_fields list. If searchable fields are not defined on the
3481
	 * data object, uses a default selection of summary fields.
3482
	 *
3483
	 * @return array
3484
	 */
3485
	public function searchableFields() {
3486
		// can have mixed format, need to make consistent in most verbose form
3487
		$fields = $this->stat('searchable_fields');
3488
		$labels = $this->fieldLabels();
3489
3490
		// fallback to summary fields (unless empty array is explicitly specified)
3491
		if( ! $fields && ! is_array($fields)) {
3492
			$summaryFields = array_keys($this->summaryFields());
3493
			$fields = array();
3494
3495
			// remove the custom getters as the search should not include them
3496
			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...
3497
				foreach($summaryFields as $key => $name) {
3498
					$spec = $name;
3499
3500
					// Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3501
					if(($fieldPos = strpos($name, '.')) !== false) {
3502
						$name = substr($name, 0, $fieldPos);
3503
					}
3504
3505
					if($this->hasDatabaseField($name)) {
3506
						$fields[] = $name;
3507
					} elseif($this->relObject($spec)) {
3508
						$fields[] = $spec;
3509
					}
3510
				}
3511
			}
3512
		}
3513
3514
		// we need to make sure the format is unified before
3515
		// augmenting fields, so extensions can apply consistent checks
3516
		// but also after augmenting fields, because the extension
3517
		// might use the shorthand notation as well
3518
3519
		// rewrite array, if it is using shorthand syntax
3520
		$rewrite = array();
3521
		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...
3522
			$identifer = (is_int($name)) ? $specOrName : $name;
3523
3524
			if(is_int($name)) {
3525
				// 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...
3526
				$rewrite[$identifer] = array();
3527
			} elseif(is_array($specOrName)) {
3528
				// 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...
3529
				//   'filter => 'ExactMatchFilter',
3530
				//   '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...
3531
				//   '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...
3532
				// ))
3533
				$rewrite[$identifer] = array_merge(
3534
					array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3535
					(array)$specOrName
3536
				);
3537
			} else {
3538
				// 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...
3539
				$rewrite[$identifer] = array(
3540
					'filter' => $specOrName,
3541
				);
3542
			}
3543
			if(!isset($rewrite[$identifer]['title'])) {
3544
				$rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3545
					? $labels[$identifer] : FormField::name_to_label($identifer);
3546
			}
3547
			if(!isset($rewrite[$identifer]['filter'])) {
3548
				$rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3549
			}
3550
		}
3551
3552
		$fields = $rewrite;
3553
3554
		// apply DataExtensions if present
3555
		$this->extend('updateSearchableFields', $fields);
3556
3557
		return $fields;
3558
	}
3559
3560
	/**
3561
	 * Get any user defined searchable fields labels that
3562
	 * exist. Allows overriding of default field names in the form
3563
	 * interface actually presented to the user.
3564
	 *
3565
	 * The reason for keeping this separate from searchable_fields,
3566
	 * which would be a logical place for this functionality, is to
3567
	 * avoid bloating and complicating the configuration array. Currently
3568
	 * much of this system is based on sensible defaults, and this property
3569
	 * would generally only be set in the case of more complex relationships
3570
	 * between data object being required in the search interface.
3571
	 *
3572
	 * Generates labels based on name of the field itself, if no static property
3573
	 * {@link self::field_labels} exists.
3574
	 *
3575
	 * @uses $field_labels
3576
	 * @uses FormField::name_to_label()
3577
	 *
3578
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3579
	 *
3580
	 * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3581
	 */
3582
	public function fieldLabels($includerelations = true) {
3583
		$cacheKey = $this->class . '_' . $includerelations;
3584
3585
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
3586
			$customLabels = $this->stat('field_labels');
3587
			$autoLabels = array();
3588
3589
			// get all translated static properties as defined in i18nCollectStatics()
3590
			$ancestry = ClassInfo::ancestry($this->class);
3591
			$ancestry = array_reverse($ancestry);
3592
			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...
3593
				if($ancestorClass == 'ViewableData') break;
3594
				$types = array(
3595
					'db'        => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3596
				);
3597
				if($includerelations){
3598
					$types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3599
					$types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3600
					$types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3601
					$types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3602
				}
3603
				foreach($types as $type => $attrs) {
3604
					foreach($attrs as $name => $spec) {
3605
						$autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}",FormField::name_to_label($name));
3606
					}
3607
				}
3608
			}
3609
3610
			$labels = array_merge((array)$autoLabels, (array)$customLabels);
3611
			$this->extend('updateFieldLabels', $labels);
3612
			self::$_cache_field_labels[$cacheKey] = $labels;
3613
		}
3614
3615
		return self::$_cache_field_labels[$cacheKey];
3616
	}
3617
3618
	/**
3619
	 * Get a human-readable label for a single field,
3620
	 * see {@link fieldLabels()} for more details.
3621
	 *
3622
	 * @uses fieldLabels()
3623
	 * @uses FormField::name_to_label()
3624
	 *
3625
	 * @param string $name Name of the field
3626
	 * @return string Label of the field
3627
	 */
3628
	public function fieldLabel($name) {
3629
		$labels = $this->fieldLabels();
3630
		return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3631
	}
3632
3633
	/**
3634
	 * Get the default summary fields for this object.
3635
	 *
3636
	 * @todo use the translation apparatus to return a default field selection for the language
3637
	 *
3638
	 * @return array
3639
	 */
3640
	public function summaryFields() {
3641
		$fields = $this->stat('summary_fields');
3642
3643
		// if fields were passed in numeric array,
3644
		// convert to an associative array
3645
		if($fields && array_key_exists(0, $fields)) {
3646
			$fields = array_combine(array_values($fields), array_values($fields));
3647
		}
3648
3649
		if (!$fields) {
3650
			$fields = array();
3651
			// try to scaffold a couple of usual suspects
3652
			if ($this->hasField('Name')) $fields['Name'] = 'Name';
3653
			if ($this->hasDatabaseField('Title')) $fields['Title'] = 'Title';
3654
			if ($this->hasField('Description')) $fields['Description'] = 'Description';
3655
			if ($this->hasField('FirstName')) $fields['FirstName'] = 'First Name';
3656
		}
3657
		$this->extend("updateSummaryFields", $fields);
3658
3659
		// Final fail-over, just list ID field
3660
		if(!$fields) $fields['ID'] = 'ID';
3661
3662
		// Localize fields (if possible)
3663
		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...
3664
			// only attempt to localize if the label definition is the same as the field name.
3665
			// this will preserve any custom labels set in the summary_fields configuration
3666
			if(isset($fields[$name]) && $name === $fields[$name]) {
3667
				$fields[$name] = $label;
3668
			}
3669
		}
3670
3671
		return $fields;
3672
	}
3673
3674
	/**
3675
	 * Defines a default list of filters for the search context.
3676
	 *
3677
	 * If a filter class mapping is defined on the data object,
3678
	 * it is constructed here. Otherwise, the default filter specified in
3679
	 * {@link DBField} is used.
3680
	 *
3681
	 * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3682
	 *
3683
	 * @return array
3684
	 */
3685
	public function defaultSearchFilters() {
3686
		$filters = array();
3687
3688
		foreach($this->searchableFields() as $name => $spec) {
3689
			if($spec['filter'] instanceof SearchFilter) {
3690
				$filters[$name] = $spec['filter'];
3691
			} else {
3692
				$class = $spec['filter'];
3693
3694
				if(!is_subclass_of($spec['filter'], 'SearchFilter')) {
3695
					$class = 'PartialMatchFilter';
3696
				}
3697
3698
				$filters[$name] = new $class($name);
3699
			}
3700
		}
3701
3702
		return $filters;
3703
	}
3704
3705
	/**
3706
	 * @return boolean True if the object is in the database
3707
	 */
3708
	public function isInDB() {
3709
		return is_numeric( $this->ID ) && $this->ID > 0;
3710
	}
3711
3712
	/*
3713
	 * @ignore
3714
	 */
3715
	private static $subclass_access = true;
3716
3717
	/**
3718
	 * Temporarily disable subclass access in data object qeur
3719
	 */
3720
	public static function disable_subclass_access() {
3721
		self::$subclass_access = false;
3722
	}
3723
	public static function enable_subclass_access() {
3724
		self::$subclass_access = true;
3725
	}
3726
3727
	//-------------------------------------------------------------------------------------------//
3728
3729
	/**
3730
	 * Database field definitions.
3731
	 * This is a map from field names to field type. The field
3732
	 * type should be a class that extends .
3733
	 * @var array
3734
	 * @config
3735
	 */
3736
	private static $db = null;
3737
3738
	/**
3739
	 * Use a casting object for a field. This is a map from
3740
	 * field name to class name of the casting object.
3741
	 *
3742
	 * @var array
3743
	 */
3744
	private static $casting = array(
3745
		"Title" => 'Text',
3746
	);
3747
3748
	/**
3749
	 * Specify custom options for a CREATE TABLE call.
3750
	 * Can be used to specify a custom storage engine for specific database table.
3751
	 * All options have to be keyed for a specific database implementation,
3752
	 * identified by their class name (extending from {@link SS_Database}).
3753
	 *
3754
	 * <code>
3755
	 * array(
3756
	 *  'MySQLDatabase' => 'ENGINE=MyISAM'
3757
	 * )
3758
	 * </code>
3759
	 *
3760
	 * Caution: This API is experimental, and might not be
3761
	 * included in the next major release. Please use with care.
3762
	 *
3763
	 * @var array
3764
	 * @config
3765
	 */
3766
	private static $create_table_options = array(
3767
		'SilverStripe\ORM\Connect\MySQLDatabase' => 'ENGINE=InnoDB'
3768
	);
3769
3770
	/**
3771
	 * If a field is in this array, then create a database index
3772
	 * on that field. This is a map from fieldname to index type.
3773
	 * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3774
	 *
3775
	 * @var array
3776
	 * @config
3777
	 */
3778
	private static $indexes = null;
3779
3780
	/**
3781
	 * Inserts standard column-values when a DataObject
3782
	 * is instanciated. Does not insert default records {@see $default_records}.
3783
	 * This is a map from fieldname to default value.
3784
	 *
3785
	 *  - If you would like to change a default value in a sub-class, just specify it.
3786
	 *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3787
	 *    or false in your subclass.  Setting it to null won't work.
3788
	 *
3789
	 * @var array
3790
	 * @config
3791
	 */
3792
	private static $defaults = null;
3793
3794
	/**
3795
	 * Multidimensional array which inserts default data into the database
3796
	 * on a db/build-call as long as the database-table is empty. Please use this only
3797
	 * for simple constructs, not for SiteTree-Objects etc. which need special
3798
	 * behaviour such as publishing and ParentNodes.
3799
	 *
3800
	 * Example:
3801
	 * array(
3802
	 *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3803
	 *  array('Title' => "DefaultPage2")
3804
	 * ).
3805
	 *
3806
	 * @var array
3807
	 * @config
3808
	 */
3809
	private static $default_records = null;
3810
3811
	/**
3812
	 * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3813
	 * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3814
	 *
3815
	 * Note that you cannot have a has_one and belongs_to relationship with the same name.
3816
	 *
3817
	 *	@var array
3818
	 * @config
3819
	 */
3820
	private static $has_one = null;
3821
3822
	/**
3823
	 * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3824
	 *
3825
	 * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3826
	 * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3827
	 * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3828
	 *
3829
	 * Note that you cannot have a has_one and belongs_to relationship with the same name.
3830
	 *
3831
	 * @var array
3832
	 * @config
3833
	 */
3834
	private static $belongs_to;
3835
3836
	/**
3837
	 * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3838
	 *
3839
	 * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3840
	 * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3841
	 * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3842
	 * which foreign key to use.
3843
	 *
3844
	 * @var array
3845
	 * @config
3846
	 */
3847
	private static $has_many = null;
3848
3849
	/**
3850
	 * many-many relationship definitions.
3851
	 * This is a map from component name to data type.
3852
	 * @var array
3853
	 * @config
3854
	 */
3855
	private static $many_many = null;
3856
3857
	/**
3858
	 * Extra fields to include on the connecting many-many table.
3859
	 * This is a map from field name to field type.
3860
	 *
3861
	 * Example code:
3862
	 * <code>
3863
	 * public static $many_many_extraFields = array(
3864
	 *  'Members' => array(
3865
	 *			'Role' => 'Varchar(100)'
3866
	 *		)
3867
	 * );
3868
	 * </code>
3869
	 *
3870
	 * @var array
3871
	 * @config
3872
	 */
3873
	private static $many_many_extraFields = null;
3874
3875
	/**
3876
	 * The inverse side of a many-many relationship.
3877
	 * This is a map from component name to data type.
3878
	 * @var array
3879
	 * @config
3880
	 */
3881
	private static $belongs_many_many = null;
3882
3883
	/**
3884
	 * The default sort expression. This will be inserted in the ORDER BY
3885
	 * clause of a SQL query if no other sort expression is provided.
3886
	 * @var string
3887
	 * @config
3888
	 */
3889
	private static $default_sort = null;
3890
3891
	/**
3892
	 * Default list of fields that can be scaffolded by the ModelAdmin
3893
	 * search interface.
3894
	 *
3895
	 * Overriding the default filter, with a custom defined filter:
3896
	 * <code>
3897
	 *  static $searchable_fields = array(
3898
	 *     "Name" => "PartialMatchFilter"
3899
	 *  );
3900
	 * </code>
3901
	 *
3902
	 * Overriding the default form fields, with a custom defined field.
3903
	 * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3904
	 * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3905
	 * <code>
3906
	 *  static $searchable_fields = array(
3907
	 *    "Name" => array(
3908
	 *      "field" => "TextField"
3909
	 *    )
3910
	 *  );
3911
	 * </code>
3912
	 *
3913
	 * Overriding the default form field, filter and title:
3914
	 * <code>
3915
	 *  static $searchable_fields = array(
3916
	 *    "Organisation.ZipCode" => array(
3917
	 *      "field" => "TextField",
3918
	 *      "filter" => "PartialMatchFilter",
3919
	 *      "title" => 'Organisation ZIP'
3920
	 *    )
3921
	 *  );
3922
	 * </code>
3923
	 * @config
3924
	 */
3925
	private static $searchable_fields = null;
3926
3927
	/**
3928
	 * User defined labels for searchable_fields, used to override
3929
	 * default display in the search form.
3930
	 * @config
3931
	 */
3932
	private static $field_labels = null;
3933
3934
	/**
3935
	 * Provides a default list of fields to be used by a 'summary'
3936
	 * view of this object.
3937
	 * @config
3938
	 */
3939
	private static $summary_fields = null;
3940
3941
	/**
3942
	 * Collect all static properties on the object
3943
	 * which contain natural language, and need to be translated.
3944
	 * The full entity name is composed from the class name and a custom identifier.
3945
	 *
3946
	 * @return array A numerical array which contains one or more entities in array-form.
3947
	 * Each numeric entity array contains the "arguments" for a _t() call as array values:
3948
	 * $entity, $string, $priority, $context.
3949
	 */
3950
	public function provideI18nEntities() {
3951
		$entities = array();
3952
3953
		$entities["{$this->class}.SINGULARNAME"] = array(
3954
			$this->singular_name(),
3955
3956
			'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
3957
		);
3958
3959
		$entities["{$this->class}.PLURALNAME"] = array(
3960
			$this->plural_name(),
3961
3962
			'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the'
3963
			. ' interface'
3964
		);
3965
3966
		return $entities;
3967
	}
3968
3969
	/**
3970
	 * Returns true if the given method/parameter has a value
3971
	 * (Uses the DBField::hasValue if the parameter is a database field)
3972
	 *
3973
	 * @param string $field The field name
3974
	 * @param array $arguments
3975
	 * @param bool $cache
3976
	 * @return boolean
3977
	 */
3978
	public function hasValue($field, $arguments = null, $cache = true) {
3979
		// has_one fields should not use dbObject to check if a value is given
3980
		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...
3981
			return $obj->exists();
3982
		} else {
3983
			return parent::hasValue($field, $arguments, $cache);
0 ignored issues
show
Bug introduced by
It seems like $arguments defined by parameter $arguments on line 3978 can also be of type null; however, ViewableData::hasValue() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
3984
		}
3985
	}
3986
3987
}
3988