Completed
Push — namespace-template ( 7967f2...367a36 )
by Sam
10:48
created

DataObject::hasDatabaseField()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

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

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

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

}

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

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

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

Loading history...
715
			if(!$dbObject) {
716
				continue;
717
			}
718
			if($dbObject->exists()) {
719
				return false;
720
			}
721
		}
722
		return true;
723
	}
724
725
	/**
726
	 * Pluralise this item given a specific count.
727
	 *
728
	 * E.g. "0 Pages", "1 File", "3 Images"
729
	 *
730
	 * @param string $count
731
	 * @param bool $prependNumber Include number in result. Defaults to true.
732
	 * @return string
733
	 */
734
	public function i18n_pluralise($count, $prependNumber = true) {
735
		return i18n::pluralise(
736
			$this->i18n_singular_name(),
737
			$this->i18n_plural_name(),
738
			$count,
739
			$prependNumber
740
		);
741
	}
742
743
	/**
744
	 * Get the user friendly singular name of this DataObject.
745
	 * If the name is not defined (by redefining $singular_name in the subclass),
746
	 * this returns the class name.
747
	 *
748
	 * @return string User friendly singular name of this DataObject
749
	 */
750
	public function singular_name() {
751
		if(!$name = $this->stat('singular_name')) {
752
			$name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $this->class))));
753
		}
754
755
		return $name;
756
	}
757
758
	/**
759
	 * Get the translated user friendly singular name of this DataObject
760
	 * same as singular_name() but runs it through the translating function
761
	 *
762
	 * Translating string is in the form:
763
	 *     $this->class.SINGULARNAME
764
	 * Example:
765
	 *     Page.SINGULARNAME
766
	 *
767
	 * @return string User friendly translated singular name of this DataObject
768
	 */
769
	public function i18n_singular_name() {
770
		return _t($this->class.'.SINGULARNAME', $this->singular_name());
771
	}
772
773
	/**
774
	 * Get the user friendly plural name of this DataObject
775
	 * If the name is not defined (by renaming $plural_name in the subclass),
776
	 * this returns a pluralised version of the class name.
777
	 *
778
	 * @return string User friendly plural name of this DataObject
779
	 */
780
	public function plural_name() {
781
		if($name = $this->stat('plural_name')) {
782
			return $name;
783
		} else {
784
			$name = $this->singular_name();
785
			//if the penultimate character is not a vowel, replace "y" with "ies"
786
			if (preg_match('/[^aeiou]y$/i', $name)) {
787
				$name = substr($name,0,-1) . 'ie';
788
			}
789
			return ucfirst($name . 's');
790
		}
791
	}
792
793
	/**
794
	 * Get the translated user friendly plural name of this DataObject
795
	 * Same as plural_name but runs it through the translation function
796
	 * Translation string is in the form:
797
	 *      $this->class.PLURALNAME
798
	 * Example:
799
	 *      Page.PLURALNAME
800
	 *
801
	 * @return string User friendly translated plural name of this DataObject
802
	 */
803
	public function i18n_plural_name()
804
	{
805
		$name = $this->plural_name();
806
		return _t($this->class.'.PLURALNAME', $name);
807
	}
808
809
	/**
810
	 * Standard implementation of a title/label for a specific
811
	 * record. Tries to find properties 'Title' or 'Name',
812
	 * and falls back to the 'ID'. Useful to provide
813
	 * user-friendly identification of a record, e.g. in errormessages
814
	 * or UI-selections.
815
	 *
816
	 * Overload this method to have a more specialized implementation,
817
	 * e.g. for an Address record this could be:
818
	 * <code>
819
	 * function getTitle() {
820
	 *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
821
	 * }
822
	 * </code>
823
	 *
824
	 * @return string
825
	 */
826
	public function getTitle() {
827
		if($this->hasDatabaseField('Title')) return $this->getField('Title');
828
		if($this->hasDatabaseField('Name')) return $this->getField('Name');
829
830
		return "#{$this->ID}";
831
	}
832
833
	/**
834
	 * Returns the associated database record - in this case, the object itself.
835
	 * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
836
	 *
837
	 * @return DataObject Associated database record
838
	 */
839
	public function data() {
840
		return $this;
841
	}
842
843
	/**
844
	 * Convert this object to a map.
845
	 *
846
	 * @return array The data as a map.
847
	 */
848
	public function toMap() {
849
		$this->loadLazyFields();
850
		return $this->record;
851
	}
852
853
	/**
854
	 * Return all currently fetched database fields.
855
	 *
856
	 * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
857
	 * Obviously, this makes it a lot faster.
858
	 *
859
	 * @return array The data as a map.
860
	 */
861
	public function getQueriedDatabaseFields() {
862
		return $this->record;
863
	}
864
865
	/**
866
	 * Update a number of fields on this object, given a map of the desired changes.
867
	 *
868
	 * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
869
	 * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
870
	 *
871
	 * update() doesn't write the main object, but if you use the dot syntax, it will write()
872
	 * the related objects that it alters.
873
	 *
874
	 * @param array $data A map of field name to data values to update.
875
	 * @return DataObject $this
876
	 */
877
	public function update($data) {
878
		foreach($data as $k => $v) {
879
			// Implement dot syntax for updates
880
			if(strpos($k,'.') !== false) {
881
				$relations = explode('.', $k);
882
				$fieldName = array_pop($relations);
883
				$relObj = $this;
884
				foreach($relations as $i=>$relation) {
885
					// no support for has_many or many_many relationships,
886
					// as the updater wouldn't know which object to write to (or create)
887
					if($relObj->$relation() instanceof DataObject) {
888
						$parentObj = $relObj;
889
						$relObj = $relObj->$relation();
890
						// If the intermediate relationship objects have been created, then write them
891
						if($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
892
							$relObj->write();
893
							$relatedFieldName = $relation."ID";
894
							$parentObj->$relatedFieldName = $relObj->ID;
895
							$parentObj->write();
896
						}
897
					} else {
898
						user_error(
899
							"DataObject::update(): Can't traverse relationship '$relation'," .
900
							"it has to be a has_one relationship or return a single DataObject",
901
							E_USER_NOTICE
902
						);
903
						// unset relation object so we don't write properties to the wrong object
904
						unset($relObj);
905
						break;
906
					}
907
				}
908
909
				if($relObj) {
910
					$relObj->$fieldName = $v;
911
					$relObj->write();
912
					$relatedFieldName = $relation."ID";
0 ignored issues
show
Bug introduced by
The variable $relation seems to be defined by a foreach iteration on line 884. 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...
913
					$this->$relatedFieldName = $relObj->ID;
914
					$relObj->flushCache();
915
				} else {
916
					user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
917
				}
918
			} else {
919
				$this->$k = $v;
920
			}
921
		}
922
		return $this;
923
	}
924
925
	/**
926
	 * Pass changes as a map, and try to
927
	 * get automatic casting for these fields.
928
	 * Doesn't write to the database. To write the data,
929
	 * use the write() method.
930
	 *
931
	 * @param array $data A map of field name to data values to update.
932
	 * @return DataObject $this
933
	 */
934
	public function castedUpdate($data) {
935
		foreach($data as $k => $v) {
936
			$this->setCastedField($k,$v);
937
		}
938
		return $this;
939
	}
940
941
	/**
942
	 * Merges data and relations from another object of same class,
943
	 * without conflict resolution. Allows to specify which
944
	 * dataset takes priority in case its not empty.
945
	 * has_one-relations are just transferred with priority 'right'.
946
	 * has_many and many_many-relations are added regardless of priority.
947
	 *
948
	 * Caution: has_many/many_many relations are moved rather than duplicated,
949
	 * meaning they are not connected to the merged object any longer.
950
	 * Caution: Just saves updated has_many/many_many relations to the database,
951
	 * doesn't write the updated object itself (just writes the object-properties).
952
	 * Caution: Does not delete the merged object.
953
	 * Caution: Does now overwrite Created date on the original object.
954
	 *
955
	 * @param $obj DataObject
956
	 * @param $priority String left|right Determines who wins in case of a conflict (optional)
957
	 * @param $includeRelations Boolean Merge any existing relations (optional)
958
	 * @param $overwriteWithEmpty Boolean Overwrite existing left values with empty right values.
959
	 *                            Only applicable with $priority='right'. (optional)
960
	 * @return Boolean
961
	 */
962
	public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) {
963
		$leftObj = $this;
964
965
		if($leftObj->ClassName != $rightObj->ClassName) {
966
			// we can't merge similiar subclasses because they might have additional relations
967
			user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
968
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
969
			return false;
970
		}
971
972
		if(!$rightObj->ID) {
973
			user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
974
				to make sure all relations are transferred properly.').", E_USER_WARNING);
975
			return false;
976
		}
977
978
		// makes sure we don't merge data like ID or ClassName
979
		$leftData = $leftObj->db();
0 ignored issues
show
Unused Code introduced by
$leftData is not used, you could remove the assignment.

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

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

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

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

Loading history...
980
		$rightData = $rightObj->db();
981
982
		foreach($rightData as $key=>$rightSpec) {
983
			// Don't merge ID
984
			if($key === 'ID') {
985
				continue;
986
			}
987
988
			// Only merge relations if allowed
989
			if($rightSpec === 'ForeignKey' && !$includeRelations) {
990
				continue;
991
			}
992
993
			// don't merge conflicting values if priority is 'left'
994
			if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
995
				continue;
996
			}
997
998
			// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
999
			if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
1000
				continue;
1001
			}
1002
1003
			// TODO remove redundant merge of has_one fields
1004
			$leftObj->{$key} = $rightObj->{$key};
1005
		}
1006
1007
		// merge relations
1008
		if($includeRelations) {
1009 View Code Duplication
			if($manyMany = $this->manyMany()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1010
				foreach($manyMany as $relationship => $class) {
1011
					$leftComponents = $leftObj->getManyManyComponents($relationship);
1012
					$rightComponents = $rightObj->getManyManyComponents($relationship);
1013
					if($rightComponents && $rightComponents->exists()) {
1014
						$leftComponents->addMany($rightComponents->column('ID'));
1015
					}
1016
					$leftComponents->write();
1017
				}
1018
			}
1019
1020 View Code Duplication
			if($hasMany = $this->hasMany()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1021
				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...
1022
					$leftComponents = $leftObj->getComponents($relationship);
1023
					$rightComponents = $rightObj->getComponents($relationship);
1024
					if($rightComponents && $rightComponents->exists()) {
1025
						$leftComponents->addMany($rightComponents->column('ID'));
1026
					}
1027
					$leftComponents->write();
1028
				}
1029
1030
			}
1031
		}
1032
1033
		return true;
1034
	}
1035
1036
	/**
1037
	 * Forces the record to think that all its data has changed.
1038
	 * Doesn't write to the database. Only sets fields as changed
1039
	 * if they are not already marked as changed.
1040
	 *
1041
	 * @return $this
1042
	 */
1043
	public function forceChange() {
1044
		// Ensure lazy fields loaded
1045
		$this->loadLazyFields();
1046
1047
		// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1048
		$fieldNames = array_unique(array_merge(
1049
			array_keys($this->record),
1050
			array_keys($this->db())
1051
		));
1052
1053
		foreach($fieldNames as $fieldName) {
1054
			if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
1055
			// Populate the null values in record so that they actually get written
1056
			if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
1057
		}
1058
1059
		// @todo Find better way to allow versioned to write a new version after forceChange
1060
		if($this->isChanged('Version')) unset($this->changed['Version']);
1061
		return $this;
1062
	}
1063
1064
	/**
1065
	 * Validate the current object.
1066
	 *
1067
	 * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1068
	 * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1069
	 *
1070
	 * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1071
	 * and onAfterWrite() won't get called either.
1072
	 *
1073
	 * It is expected that you call validate() in your own application to test that an object is valid before
1074
	 * attempting a write, and respond appropriately if it isn't.
1075
	 *
1076
	 * @see {@link ValidationResult}
1077
	 * @return ValidationResult
1078
	 */
1079
	public function validate() {
1080
		$result = ValidationResult::create();
1081
		$this->extend('validate', $result);
1082
		return $result;
1083
	}
1084
1085
	/**
1086
	 * Public accessor for {@see DataObject::validate()}
1087
	 *
1088
	 * @return ValidationResult
1089
	 */
1090
	public function doValidate() {
1091
		Deprecation::notice('5.0', 'Use validate');
1092
		return $this->validate();
1093
	}
1094
1095
	/**
1096
	 * Event handler called before writing to the database.
1097
	 * You can overload this to clean up or otherwise process data before writing it to the
1098
	 * database.  Don't forget to call parent::onBeforeWrite(), though!
1099
	 *
1100
	 * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1101
	 *
1102
	 * @uses DataExtension->onBeforeWrite()
1103
	 */
1104
	protected function onBeforeWrite() {
1105
		$this->brokenOnWrite = false;
1106
1107
		$dummy = null;
1108
		$this->extend('onBeforeWrite', $dummy);
1109
	}
1110
1111
	/**
1112
	 * Event handler called after writing to the database.
1113
	 * You can overload this to act upon changes made to the data after it is written.
1114
	 * $this->changed will have a record
1115
	 * database.  Don't forget to call parent::onAfterWrite(), though!
1116
	 *
1117
	 * @uses DataExtension->onAfterWrite()
1118
	 */
1119
	protected function onAfterWrite() {
1120
		$dummy = null;
1121
		$this->extend('onAfterWrite', $dummy);
1122
	}
1123
1124
	/**
1125
	 * Event handler called before deleting from the database.
1126
	 * You can overload this to clean up or otherwise process data before delete this
1127
	 * record.  Don't forget to call parent::onBeforeDelete(), though!
1128
	 *
1129
	 * @uses DataExtension->onBeforeDelete()
1130
	 */
1131
	protected function onBeforeDelete() {
1132
		$this->brokenOnDelete = false;
1133
1134
		$dummy = null;
1135
		$this->extend('onBeforeDelete', $dummy);
1136
	}
1137
1138
	protected function onAfterDelete() {
1139
		$this->extend('onAfterDelete');
1140
	}
1141
1142
	/**
1143
	 * Load the default values in from the self::$defaults array.
1144
	 * Will traverse the defaults of the current class and all its parent classes.
1145
	 * Called by the constructor when creating new records.
1146
	 *
1147
	 * @uses DataExtension->populateDefaults()
1148
	 * @return DataObject $this
1149
	 */
1150
	public function populateDefaults() {
1151
		$classes = array_reverse(ClassInfo::ancestry($this));
1152
1153
		foreach($classes as $class) {
1154
			$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1155
1156
			if($defaults && !is_array($defaults)) {
1157
				user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
1158
					E_USER_WARNING);
1159
				$defaults = null;
1160
			}
1161
1162
			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...
1163
				// SRM 2007-03-06: Stricter check
1164
				if(!isset($this->$fieldName) || $this->$fieldName === null) {
1165
					$this->$fieldName = $fieldValue;
1166
				}
1167
				// Set many-many defaults with an array of ids
1168
				if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
1169
					$manyManyJoin = $this->$fieldName();
1170
					$manyManyJoin->setByIdList($fieldValue);
1171
				}
1172
			}
1173
			if($class == 'DataObject') {
1174
				break;
1175
			}
1176
		}
1177
1178
		$this->extend('populateDefaults');
1179
		return $this;
1180
	}
1181
1182
	/**
1183
	 * Determine validation of this object prior to write
1184
	 *
1185
	 * @return ValidationException Exception generated by this write, or null if valid
1186
	 */
1187
	protected function validateWrite() {
1188
		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...
1189
			return new ValidationException(
1190
				"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...
1191
				"you need to change the ClassName before you can write it",
1192
				E_USER_WARNING
1193
			);
1194
		}
1195
1196
		if(Config::inst()->get('DataObject', 'validation_enabled')) {
1197
			$result = $this->validate();
1198
			if (!$result->valid()) {
1199
				return new ValidationException(
1200
					$result,
1201
					$result->message(),
1202
					E_USER_WARNING
1203
				);
1204
			}
1205
		}
1206
	}
1207
1208
	/**
1209
	 * Prepare an object prior to write
1210
	 *
1211
	 * @throws ValidationException
1212
	 */
1213
	protected function preWrite() {
1214
		// Validate this object
1215
		if($writeException = $this->validateWrite()) {
1216
			// Used by DODs to clean up after themselves, eg, Versioned
1217
			$this->invokeWithExtensions('onAfterSkippedWrite');
1218
			throw $writeException;
1219
		}
1220
1221
		// Check onBeforeWrite
1222
		$this->brokenOnWrite = true;
1223
		$this->onBeforeWrite();
1224
		if($this->brokenOnWrite) {
1225
			user_error("$this->class has a broken onBeforeWrite() function."
1226
				. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1227
		}
1228
	}
1229
1230
	/**
1231
	 * Detects and updates all changes made to this object
1232
	 *
1233
	 * @param bool $forceChanges If set to true, force all fields to be treated as changed
1234
	 * @return bool True if any changes are detected
1235
	 */
1236
	protected function updateChanges($forceChanges = false)
1237
	{
1238
		if($forceChanges) {
1239
			// Force changes, but only for loaded fields
1240
			foreach($this->record as $field => $value) {
1241
				$this->changed[$field] = static::CHANGE_VALUE;
1242
			}
1243
			return true;
1244
		}
1245
		return $this->isChanged();
1246
	}
1247
1248
	/**
1249
	 * Writes a subset of changes for a specific table to the given manipulation
1250
	 *
1251
	 * @param string $baseTable Base table
1252
	 * @param string $now Timestamp to use for the current time
1253
	 * @param bool $isNewRecord Whether this should be treated as a new record write
1254
	 * @param array $manipulation Manipulation to write to
1255
	 * @param string $class Table and Class to select and write to
1256
	 */
1257
	protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
1258
		$manipulation[$class] = array();
1259
1260
		// Extract records for this table
1261
		foreach($this->record as $fieldName => $fieldValue) {
1262
1263
			// Check if this record pertains to this table, and
1264
			// we're not attempting to reset the BaseTable->ID
1265
			if(	empty($this->changed[$fieldName])
1266
				|| ($class === $baseTable && $fieldName === 'ID')
1267
				|| (!self::has_own_table_database_field($class, $fieldName)
1268
					&& !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...
1269
			) {
1270
				continue;
1271
			}
1272
1273
1274
			// if database column doesn't correlate to a DBField instance...
1275
			$fieldObj = $this->dbObject($fieldName);
1276
			if(!$fieldObj) {
1277
				$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1278
			}
1279
1280
			// Write to manipulation
1281
			$fieldObj->writeToManipulation($manipulation[$class]);
1282
		}
1283
1284
		// Ensure update of Created and LastEdited columns
1285
		if($baseTable === $class) {
1286
			$manipulation[$class]['fields']['LastEdited'] = $now;
1287
			if($isNewRecord) {
1288
				$manipulation[$class]['fields']['Created']
1289
					= empty($this->record['Created'])
1290
						? $now
1291
						: $this->record['Created'];
1292
				$manipulation[$class]['fields']['ClassName'] = $this->class;
1293
			}
1294
		}
1295
1296
		// Inserts done one the base table are performed in another step, so the manipulation should instead
1297
		// attempt an update, as though it were a normal update.
1298
		$manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update';
1299
		$manipulation[$class]['id'] = $this->record['ID'];
1300
	}
1301
1302
	/**
1303
	 * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1304
	 *
1305
	 * Does nothing if an ID is already assigned for this record
1306
	 *
1307
	 * @param string $baseTable Base table
1308
	 * @param string $now Timestamp to use for the current time
1309
	 */
1310
	protected function writeBaseRecord($baseTable, $now) {
1311
		// Generate new ID if not specified
1312
		if($this->isInDB()) return;
1313
1314
		// Perform an insert on the base table
1315
		$insert = new SQLInsert('"'.$baseTable.'"');
1316
		$insert
1317
			->assign('"Created"', $now)
1318
			->execute();
1319
		$this->changed['ID'] = self::CHANGE_VALUE;
1320
		$this->record['ID'] = DB::get_generated_id($baseTable);
1321
	}
1322
1323
	/**
1324
	 * Generate and write the database manipulation for all changed fields
1325
	 *
1326
	 * @param string $baseTable Base table
1327
	 * @param string $now Timestamp to use for the current time
1328
	 * @param bool $isNewRecord If this is a new record
1329
	 */
1330
	protected function writeManipulation($baseTable, $now, $isNewRecord) {
1331
		// Generate database manipulations for each class
1332
		$manipulation = array();
1333
		foreach($this->getClassAncestry() as $class) {
1334
			if(self::has_own_table($class)) {
1335
				$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1336
			}
1337
		}
1338
1339
		// Allow extensions to extend this manipulation
1340
		$this->extend('augmentWrite', $manipulation);
1341
1342
		// New records have their insert into the base data table done first, so that they can pass the
1343
		// generated ID on to the rest of the manipulation
1344
		if($isNewRecord) {
1345
			$manipulation[$baseTable]['command'] = 'update';
1346
		}
1347
1348
		// Perform the manipulation
1349
		DB::manipulate($manipulation);
1350
	}
1351
1352
	/**
1353
	 * Writes all changes to this object to the database.
1354
	 *  - It will insert a record whenever ID isn't set, otherwise update.
1355
	 *  - All relevant tables will be updated.
1356
	 *  - $this->onBeforeWrite() gets called beforehand.
1357
	 *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1358
	 *
1359
	 *  @uses DataExtension->augmentWrite()
1360
	 *
1361
	 * @param boolean $showDebug Show debugging information
1362
	 * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1363
	 * @param boolean $forceWrite Write to database even if there are no changes
1364
	 * @param boolean $writeComponents Call write() on all associated component instances which were previously
1365
	 *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1366
	 *                                 {@link getManyManyComponents()} (Default: false)
1367
	 * @return int The ID of the record
1368
	 * @throws ValidationException Exception that can be caught and handled by the calling function
1369
	 */
1370
	public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1371
		$now = DBDatetime::now()->Rfc2822();
1372
1373
		// Execute pre-write tasks
1374
		$this->preWrite();
1375
1376
		// Check if we are doing an update or an insert
1377
		$isNewRecord = !$this->isInDB() || $forceInsert;
1378
1379
		// Check changes exist, abort if there are none
1380
		$hasChanges = $this->updateChanges($isNewRecord);
1381
		if($hasChanges || $forceWrite || $isNewRecord) {
1382
			// New records have their insert into the base data table done first, so that they can pass the
1383
			// generated primary key on to the rest of the manipulation
1384
			$baseTable = ClassInfo::baseDataClass($this->class);
1385
			$this->writeBaseRecord($baseTable, $now);
1386
1387
			// Write the DB manipulation for all changed fields
1388
			$this->writeManipulation($baseTable, $now, $isNewRecord);
1389
1390
			// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1391
			$this->writeRelations();
1392
			$this->onAfterWrite();
1393
			$this->changed = array();
1394
		} else {
1395
			if($showDebug) Debug::message("no changes for DataObject");
1396
1397
			// Used by DODs to clean up after themselves, eg, Versioned
1398
			$this->invokeWithExtensions('onAfterSkippedWrite');
1399
		}
1400
1401
		// Ensure Created and LastEdited are populated
1402
		if(!isset($this->record['Created'])) {
1403
			$this->record['Created'] = $now;
1404
		}
1405
		$this->record['LastEdited'] = $now;
1406
1407
		// Write relations as necessary
1408
		if($writeComponents) $this->writeComponents(true);
1409
1410
		// Clears the cache for this object so get_one returns the correct object.
1411
		$this->flushCache();
1412
1413
		return $this->record['ID'];
1414
	}
1415
1416
	/**
1417
	 * Writes cached relation lists to the database, if possible
1418
	 */
1419
	public function writeRelations() {
1420
		if(!$this->isInDB()) return;
1421
1422
		// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1423
		if($this->unsavedRelations) {
1424
			foreach($this->unsavedRelations as $name => $list) {
1425
				$list->changeToList($this->$name());
1426
			}
1427
			$this->unsavedRelations = array();
1428
		}
1429
	}
1430
1431
	/**
1432
	 * Write the cached components to the database. Cached components could refer to two different instances of the
1433
	 * same record.
1434
	 *
1435
	 * @param $recursive Recursively write components
1436
	 * @return DataObject $this
1437
	 */
1438
	public function writeComponents($recursive = false) {
1439
		if(!$this->components) return $this;
1440
1441
		foreach($this->components as $component) {
1442
			$component->write(false, false, false, $recursive);
1443
		}
1444
		return $this;
1445
	}
1446
1447
	/**
1448
	 * Delete this data object.
1449
	 * $this->onBeforeDelete() gets called.
1450
	 * Note that in Versioned objects, both Stage and Live will be deleted.
1451
	 *  @uses DataExtension->augmentSQL()
1452
	 */
1453
	public function delete() {
1454
		$this->brokenOnDelete = true;
1455
		$this->onBeforeDelete();
1456
		if($this->brokenOnDelete) {
1457
			user_error("$this->class has a broken onBeforeDelete() function."
1458
				. " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1459
		}
1460
1461
		// Deleting a record without an ID shouldn't do anything
1462
		if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1463
1464
		// TODO: This is quite ugly.  To improve:
1465
		//  - move the details of the delete code in the DataQuery system
1466
		//  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1467
		//    obviously, that means getting requireTable() to configure cascading deletes ;-)
1468
		$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1469
		foreach($srcQuery->queriedTables() as $table) {
1470
			$delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1471
			$delete->execute();
1472
		}
1473
		// Remove this item out of any caches
1474
		$this->flushCache();
1475
1476
		$this->onAfterDelete();
1477
1478
		$this->OldID = $this->ID;
0 ignored issues
show
Documentation introduced by
The property OldID does not exist on object<DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1479
		$this->ID = 0;
1480
	}
1481
1482
	/**
1483
	 * Delete the record with the given ID.
1484
	 *
1485
	 * @param string $className The class name of the record to be deleted
1486
	 * @param int $id ID of record to be deleted
1487
	 */
1488
	public static function delete_by_id($className, $id) {
1489
		$obj = DataObject::get_by_id($className, $id);
1490
		if($obj) {
1491
			$obj->delete();
1492
		} else {
1493
			user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1494
		}
1495
	}
1496
1497
	/**
1498
	 * Get the class ancestry, including the current class name.
1499
	 * The ancestry will be returned as an array of class names, where the 0th element
1500
	 * will be the class that inherits directly from DataObject, and the last element
1501
	 * will be the current class.
1502
	 *
1503
	 * @return array Class ancestry
1504
	 */
1505
	public function getClassAncestry() {
1506
		if(!isset(self::$_cache_get_class_ancestry[$this->class])) {
1507
			self::$_cache_get_class_ancestry[$this->class] = array($this->class);
1508
			while(($class=get_parent_class(self::$_cache_get_class_ancestry[$this->class][0])) != "DataObject") {
1509
				array_unshift(self::$_cache_get_class_ancestry[$this->class], $class);
1510
			}
1511
		}
1512
		return self::$_cache_get_class_ancestry[$this->class];
1513
	}
1514
1515
	/**
1516
	 * Return a component object from a one to one relationship, as a DataObject.
1517
	 * If no component is available, an 'empty component' will be returned for
1518
	 * non-polymorphic relations, or for polymorphic relations with a class set.
1519
	 *
1520
	 * @param string $componentName Name of the component
1521
	 * @return DataObject The component object. It's exact type will be that of the component.
1522
	 * @throws Exception
1523
	 */
1524
	public function getComponent($componentName) {
1525
		if(isset($this->components[$componentName])) {
1526
			return $this->components[$componentName];
1527
		}
1528
1529
		if($class = $this->hasOneComponent($componentName)) {
1530
			$joinField = $componentName . 'ID';
1531
			$joinID    = $this->getField($joinField);
1532
1533
			// Extract class name for polymorphic relations
1534
			if($class === 'DataObject') {
1535
				$class = $this->getField($componentName . 'Class');
1536
				if(empty($class)) return null;
1537
			}
1538
1539
			if($joinID) {
1540
				// Ensure that the selected object originates from the same stage, subsite, etc
1541
				$component = DataObject::get($class)
1542
					->filter('ID', $joinID)
1543
					->setDataQueryParam($this->getInheritableQueryParams())
1544
					->first();
1545
			}
1546
1547
			if(empty($component)) {
1548
				$component = $this->model->$class->newObject();
1549
			}
1550
		} elseif($class = $this->belongsToComponent($componentName)) {
1551
			$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
1552
			$joinID = $this->ID;
1553
1554
			if($joinID) {
1555
				// Prepare filter for appropriate join type
1556
				if($polymorphic) {
1557
					$filter = array(
1558
						"{$joinField}ID" => $joinID,
1559
						"{$joinField}Class" => $this->class
1560
					);
1561
				} else {
1562
					$filter = array(
1563
						$joinField => $joinID
1564
					);
1565
				}
1566
1567
				// Ensure that the selected object originates from the same stage, subsite, etc
1568
				$component = DataObject::get($class)
1569
					->filter($filter)
1570
					->setDataQueryParam($this->getInheritableQueryParams())
1571
					->first();
1572
			}
1573
1574
			if(empty($component)) {
1575
				$component = $this->model->$class->newObject();
1576
				if($polymorphic) {
1577
					$component->{$joinField.'ID'} = $this->ID;
1578
					$component->{$joinField.'Class'} = $this->class;
1579
				} else {
1580
					$component->$joinField = $this->ID;
1581
				}
1582
			}
1583
		} else {
1584
			throw new InvalidArgumentException(
1585
				"DataObject->getComponent(): Could not find component '$componentName'."
1586
			);
1587
		}
1588
1589
		$this->components[$componentName] = $component;
1590
		return $component;
1591
	}
1592
1593
	/**
1594
	 * Returns a one-to-many relation as a HasManyList
1595
	 *
1596
	 * @param string $componentName Name of the component
1597
	 * @return HasManyList The components of the one-to-many relationship.
1598
	 */
1599
	public function getComponents($componentName) {
1600
		$result = null;
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

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

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

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

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

Loading history...
1601
1602
		$componentClass = $this->hasManyComponent($componentName);
1603
		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...
1604
			throw new InvalidArgumentException(sprintf(
1605
				"DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1606
				$componentName,
1607
				$this->class
1608
			));
1609
		}
1610
1611
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1612 View Code Duplication
		if(!$this->ID) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1613
			if(!isset($this->unsavedRelations[$componentName])) {
1614
				$this->unsavedRelations[$componentName] =
1615
					new UnsavedRelationList($this->class, $componentName, $componentClass);
0 ignored issues
show
Documentation introduced by
$this->class is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1616
			}
1617
			return $this->unsavedRelations[$componentName];
1618
		}
1619
1620
		// Determine type and nature of foreign relation
1621
		$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
1622
		/** @var HasManyList $result */
1623
		if($polymorphic) {
1624
			$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1625
		} else {
1626
			$result = HasManyList::create($componentClass, $joinField);
1627
		}
1628
1629
		if($this->model) {
1630
			$result->setDataModel($this->model);
1631
		}
1632
1633
		return $result
1634
			->setDataQueryParam($this->getInheritableQueryParams())
1635
			->forForeignID($this->ID);
1636
	}
1637
1638
	/**
1639
	 * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1640
	 *
1641
	 * @param string $relationName Relation name.
1642
	 * @return string Class name, or null if not found.
1643
	 */
1644
	public function getRelationClass($relationName) {
1645
		// Go through all relationship configuration fields.
1646
		$candidates = array_merge(
1647
			($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1648
			($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1649
			($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
1650
			($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
1651
			($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1652
		);
1653
1654
		if (isset($candidates[$relationName])) {
1655
			$remoteClass = $candidates[$relationName];
1656
1657
			// If dot notation is present, extract just the first part that contains the class.
1658 View Code Duplication
			if(($fieldPos = strpos($remoteClass, '.'))!==false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1659
				return substr($remoteClass, 0, $fieldPos);
1660
			}
1661
1662
			// Otherwise just return the class
1663
			return $remoteClass;
1664
		}
1665
1666
		return null;
1667
	}
1668
1669
	/**
1670
	 * Given a relation name, determine the relation type
1671
	 *
1672
	 * @param string $component Name of component
1673
	 * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1674
	 */
1675
	public function getRelationType($component) {
1676
		$types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1677
		foreach($types as $type) {
1678
			$relations = Config::inst()->get($this->class, $type);
1679
			if($relations && isset($relations[$component])) {
1680
				return $type;
1681
			}
1682
		}
1683
		return null;
1684
	}
1685
1686
	/**
1687
	 * Given a relation declared on a remote class, generate a substitute component for the opposite
1688
	 * side of the relation.
1689
	 *
1690
	 * Notes on behaviour:
1691
	 *  - This can still be used on components that are defined on both sides, but do not need to be.
1692
	 *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1693
	 *  - Cannot be used on polymorphic relationships
1694
	 *  - Cannot be used on unsaved objects.
1695
	 *
1696
	 * @param string $remoteClass
1697
	 * @param string $remoteRelation
1698
	 * @return DataList|DataObject The component, either as a list or single object
1699
	 * @throws BadMethodCallException
1700
	 * @throws InvalidArgumentException
1701
	 */
1702
	public function inferReciprocalComponent($remoteClass, $remoteRelation) {
1703
		/** @var DataObject $remote */
1704
		$remote = $remoteClass::singleton();
1705
		$class = $remote->getRelationClass($remoteRelation);
1706
1707
		// Validate arguments
1708
		if(!$this->isInDB()) {
1709
			throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1710
		}
1711
		if(empty($class)) {
1712
			throw new InvalidArgumentException(sprintf(
1713
				"%s invoked with invalid relation %s.%s",
1714
				__METHOD__,
1715
				$remoteClass,
1716
				$remoteRelation
1717
			));
1718
		}
1719
		if($class === 'DataObject') {
1720
			throw new InvalidArgumentException(sprintf(
1721
				"%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1722
				"This method does not support polymorphic relationships",
1723
				__METHOD__,
1724
				$remoteClass,
1725
				$remoteRelation
1726
			));
1727
		}
1728
		if(!is_a($this, $class, true)) {
1729
			throw new InvalidArgumentException(sprintf(
1730
				"Relation %s on %s does not refer to objects of type %s",
1731
				$remoteRelation, $remoteClass, get_class($this)
1732
			));
1733
		}
1734
1735
		// Check the relation type to mock
1736
		$relationType = $remote->getRelationType($remoteRelation);
1737
		switch($relationType) {
1738
			case 'has_one': {
1739
				// Mock has_many
1740
				$joinField = "{$remoteRelation}ID";
1741
				$componentClass = ClassInfo::table_for_object_field($remoteClass, $joinField);
1742
				$result = HasManyList::create($componentClass, $joinField);
1743
				if ($this->model) {
1744
					$result->setDataModel($this->model);
1745
				}
1746
				return $result
1747
					->setDataQueryParam($this->getInheritableQueryParams())
1748
					->forForeignID($this->ID);
1749
			}
1750
			case 'belongs_to':
1751
			case 'has_many': {
1752
				// These relations must have a has_one on the other end, so find it
1753
				$joinField = $remote->getRemoteJoinField($remoteRelation, $relationType, $polymorphic);
1754
				if ($polymorphic) {
1755
					throw new InvalidArgumentException(sprintf(
1756
						"%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1757
						"to be a has_one polymorphic. This method does not support polymorphic relationships",
1758
						__METHOD__,
1759
						$remoteClass,
1760
						$remoteRelation
1761
					));
1762
				}
1763
				$joinID = $this->getField($joinField);
1764
				if (empty($joinID)) {
1765
					return null;
1766
				}
1767
				// Get object by joined ID
1768
				return DataObject::get($remoteClass)
1769
					->filter('ID', $joinID)
1770
					->setDataQueryParam($this->getInheritableQueryParams())
1771
					->first();
1772
			}
1773
			case 'many_many':
1774
			case 'belongs_many_many': {
1775
				// Get components and extra fields from parent
1776
				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...
1777
					= $remote->manyManyComponent($remoteRelation);
1778
				$extraFields = $remote->manyManyExtraFieldsForComponent($remoteRelation) ?: array();
1779
1780
				// Reverse parent and component fields and create an inverse ManyManyList
1781
				/** @var ManyManyList $result */
1782
				$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1783
				if($this->model) {
1784
					$result->setDataModel($this->model);
1785
				}
1786
				$this->extend('updateManyManyComponents', $result);
1787
1788
				// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1789
				// foreignID set elsewhere.
1790
				return $result
1791
					->setDataQueryParam($this->getInheritableQueryParams())
1792
					->forForeignID($this->ID);
1793
			}
1794
			default: {
1795
				return null;
1796
			}
1797
		}
1798
	}
1799
1800
	/**
1801
	 * Tries to find the database key on another object that is used to store a
1802
	 * relationship to this class. If no join field can be found it defaults to 'ParentID'.
1803
	 *
1804
	 * If the remote field is polymorphic then $polymorphic is set to true, and the return value
1805
	 * is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
1806
	 *
1807
	 * @param string $component Name of the relation on the current object pointing to the
1808
	 * remote object.
1809
	 * @param string $type the join type - either 'has_many' or 'belongs_to'
1810
	 * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
1811
	 * @return string
1812
	 * @throws Exception
1813
	 */
1814
	public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
1815
		// Extract relation from current object
1816
		if($type === 'has_many') {
1817
			$remoteClass = $this->hasManyComponent($component, false);
1818
		} else {
1819
			$remoteClass = $this->belongsToComponent($component, false);
1820
		}
1821
1822
		if(empty($remoteClass)) {
1823
			throw new Exception("Unknown $type component '$component' on class '$this->class'");
1824
		}
1825
		if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
1826
			throw new Exception(
1827
				"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
1828
			);
1829
		}
1830
1831
		// If presented with an explicit field name (using dot notation) then extract field name
1832
		$remoteField = null;
1833
		if(strpos($remoteClass, '.') !== false) {
1834
			list($remoteClass, $remoteField) = explode('.', $remoteClass);
1835
		}
1836
1837
		// Reference remote has_one to check against
1838
		$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
1839
1840
		// Without an explicit field name, attempt to match the first remote field
1841
		// with the same type as the current class
1842
		if(empty($remoteField)) {
1843
			// look for remote has_one joins on this class or any parent classes
1844
			$remoteRelationsMap = array_flip($remoteRelations);
1845
			foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1846
				if(array_key_exists($class, $remoteRelationsMap)) {
1847
					$remoteField = $remoteRelationsMap[$class];
1848
					break;
1849
				}
1850
			}
1851
		}
1852
1853
		// In case of an indeterminate remote field show an error
1854
		if(empty($remoteField)) {
1855
			$polymorphic = false;
1856
			$message = "No has_one found on class '$remoteClass'";
1857
			if($type == 'has_many') {
1858
				// include a hint for has_many that is missing a has_one
1859
				$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
1860
				$message .= " requires a has_one on '$remoteClass'";
1861
			}
1862
			throw new Exception($message);
1863
		}
1864
1865
		// If given an explicit field name ensure the related class specifies this
1866
		if(empty($remoteRelations[$remoteField])) {
1867
			throw new Exception("Missing expected has_one named '$remoteField'
1868
				on class '$remoteClass' referenced by $type named '$component'
1869
				on class {$this->class}"
1870
			);
1871
		}
1872
1873
		// Inspect resulting found relation
1874
		if($remoteRelations[$remoteField] === 'DataObject') {
1875
			$polymorphic = true;
1876
			return $remoteField; // Composite polymorphic field does not include 'ID' suffix
1877
		} else {
1878
			$polymorphic = false;
1879
			return $remoteField . 'ID';
1880
		}
1881
	}
1882
1883
	/**
1884
	 * Returns a many-to-many component, as a ManyManyList.
1885
	 * @param string $componentName Name of the many-many component
1886
	 * @return ManyManyList The set of components
1887
	 */
1888
	public function getManyManyComponents($componentName) {
1889
		$manyManyComponent = $this->manyManyComponent($componentName);
1890
		if(!$manyManyComponent) {
1891
			throw new InvalidArgumentException(sprintf(
1892
				"DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1893
				$componentName,
1894
				$this->class
1895
			));
1896
		}
1897
1898
		list($parentClass, $componentClass, $parentField, $componentField, $table) = $manyManyComponent;
1899
1900
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1901 View Code Duplication
		if(!$this->ID) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1902
			if(!isset($this->unsavedRelations[$componentName])) {
1903
				$this->unsavedRelations[$componentName] =
1904
					new UnsavedRelationList($parentClass, $componentName, $componentClass);
1905
			}
1906
			return $this->unsavedRelations[$componentName];
1907
		}
1908
1909
		$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
1910
		/** @var ManyManyList $result */
1911
		$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1912
1913
1914
		// Store component data in query meta-data
1915
		$result = $result->alterDataQuery(function($query) use ($extraFields) {
1916
			$query->setQueryParam('Component.ExtraFields', $extraFields);
1917
		});
1918
		
1919
		if($this->model) {
1920
			$result->setDataModel($this->model);
1921
		}
1922
1923
		$this->extend('updateManyManyComponents', $result);
1924
1925
		// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1926
		// foreignID set elsewhere.
1927
		return $result
1928
			->setDataQueryParam($this->getInheritableQueryParams())
1929
			->forForeignID($this->ID);
1930
	}
1931
1932
	/**
1933
	 * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1934
	 * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1935
	 *
1936
	 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1937
	 * 							their classes.
1938
	 */
1939
	public function hasOne() {
1940
		return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1941
	}
1942
1943
	/**
1944
	 * Return data for a specific has_one component.
1945
	 * @param string $component
1946
	 * @return string|null
1947
	 */
1948
	public function hasOneComponent($component) {
1949
		$classes = ClassInfo::ancestry($this, true);
1950
1951
		foreach(array_reverse($classes) as $class) {
1952
			$hasOnes = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
1953
			if(isset($hasOnes[$component])) {
1954
				return $hasOnes[$component];
1955
			}
1956
		}
1957
	}
1958
1959
	/**
1960
	 * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1961
	 * their class name will be returned.
1962
	 *
1963
	 * @param string $component - Name of component
1964
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1965
	 *        the field data stripped off. It defaults to TRUE.
1966
	 * @return string|array
1967
	 */
1968 View Code Duplication
	public function belongsTo($component = null, $classOnly = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1969
		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...
1970
			Deprecation::notice(
1971
				'4.0',
1972
				'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
1973
				Deprecation::SCOPE_GLOBAL
1974
			);
1975
			return $this->belongsToComponent($component, $classOnly);
1976
		}
1977
1978
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1979
		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...
1980
			return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1981
		} else {
1982
			return $belongsTo ? $belongsTo : array();
1983
		}
1984
	}
1985
1986
	/**
1987
	 * Return data for a specific belongs_to component.
1988
	 * @param string $component
1989
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1990
	 *        the field data stripped off. It defaults to TRUE.
1991
	 * @return string|null
1992
	 */
1993 View Code Duplication
	public function belongsToComponent($component, $classOnly = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1994
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1995
1996
		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...
1997
			$belongsTo = $belongsTo[$component];
1998
		} else {
1999
			return null;
2000
		}
2001
2002
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
2003
	}
2004
2005
	/**
2006
	 * Return all of the database fields in this object
2007
	 *
2008
	 * @param string $fieldName Limit the output to a specific field name
2009
	 * @param string $includeTable If returning a single column, prefix the column with the table name
2010
	 * in Table.Column(spec) format
2011
	 * @return array|string|null The database fields, or if searching a single field, just this one field if found
2012
	 * Field will be a string in ClassName(args) format, or Table.ClassName(args) format if $includeTable is true
2013
	 */
2014
	public function db($fieldName = null, $includeTable = false) {
2015
		$classes = ClassInfo::ancestry($this, true);
2016
2017
		// If we're looking for a specific field, we want to hit subclasses first as they may override field types
2018
		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...
2019
			$classes = array_reverse($classes);
2020
		}
2021
2022
		$db = array();
2023
		foreach($classes as $class) {
2024
			// Merge fields with new fields and composite fields
2025
			$fields = self::database_fields($class);
2026
			$compositeFields = self::composite_fields($class, false);
2027
			$db = array_merge($db, $fields, $compositeFields);
2028
2029
			// Check for search field
2030
			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...
2031
				// Return found field
2032
				if(!$includeTable) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $includeTable of type false|string 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...
2033
					return $db[$fieldName];
2034
				}
2035
2036
				// Set table for the given field
2037
				if(in_array($fieldName, $this->config()->fixed_fields)) {
2038
					$table = $this->baseTable();
2039
				} else {
2040
					$table = $class;
2041
				}
2042
				return $table . "." . $db[$fieldName];
2043
			}
2044
		}
2045
2046
		// At end of search complete
2047
		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...
2048
			return null;
2049
		} else {
2050
			return $db;
2051
		}
2052
	}
2053
2054
	/**
2055
	 * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2056
	 * relationships and their classes will be returned.
2057
	 *
2058
	 * @param string $component Deprecated - Name of component
2059
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2060
	 *        the field data stripped off. It defaults to TRUE.
2061
	 * @return string|array|false
2062
	 */
2063 View Code Duplication
	public function hasMany($component = null, $classOnly = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2064
		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...
2065
			Deprecation::notice(
2066
				'4.0',
2067
				'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
2068
				Deprecation::SCOPE_GLOBAL
2069
			);
2070
			return $this->hasManyComponent($component, $classOnly);
2071
		}
2072
2073
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2074
		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...
2075
			return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2076
		} else {
2077
			return $hasMany ? $hasMany : array();
2078
		}
2079
	}
2080
2081
	/**
2082
	 * Return data for a specific has_many component.
2083
	 * @param string $component
2084
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2085
	 *        the field data stripped off. It defaults to TRUE.
2086
	 * @return string|null
2087
	 */
2088 View Code Duplication
	public function hasManyComponent($component, $classOnly = true) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2089
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2090
2091
		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...
2092
			$hasMany = $hasMany[$component];
2093
		} else {
2094
			return null;
2095
		}
2096
2097
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
2098
	}
2099
2100
	/**
2101
	 * Return the many-to-many extra fields specification.
2102
	 *
2103
	 * If you don't specify a component name, it returns all
2104
	 * extra fields for all components available.
2105
	 *
2106
	 * @return array|null
2107
	 */
2108
	public function manyManyExtraFields() {
2109
		return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2110
	}
2111
2112
	/**
2113
	 * Return the many-to-many extra fields specification for a specific component.
2114
	 * @param string $component
2115
	 * @return array|null
2116
	 */
2117
	public function manyManyExtraFieldsForComponent($component) {
2118
		// Get all many_many_extraFields defined in this class or parent classes
2119
		$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2120
		// Extra fields are immediately available
2121
		if(isset($extraFields[$component])) {
2122
			return $extraFields[$component];
2123
		}
2124
2125
		// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
2126
		$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2127
		$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2128
		if($candidate) {
2129
			$relationName = null;
2130
			// Extract class and relation name from dot-notation
2131 View Code Duplication
			if(strpos($candidate, '.') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2132
				list($candidate, $relationName) = explode('.', $candidate, 2);
2133
			}
2134
2135
			// If we've not already found the relation name from dot notation, we need to find a relation that points
2136
			// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
2137
			// so it's safe to assume that it's the correct one
2138
			if(!$relationName) {
2139
				$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2140
2141
				foreach($candidateManyManys as $relation => $relatedClass) {
2142
					if (is_a($this, $relatedClass)) {
2143
						$relationName = $relation;
2144
					}
2145
				}
2146
			}
2147
2148
			// If we've found a matching relation on the target class, see if we can find extra fields for it
2149
			$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
2150
			if(isset($extraFields[$relationName])) {
2151
				return $extraFields[$relationName];
2152
			}
2153
		}
2154
2155
		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...
2156
	}
2157
2158
	/**
2159
	 * Return information about a many-to-many component.
2160
	 * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2161
	 * components are returned.
2162
	 *
2163
	 * @see DataObject::manyManyComponent()
2164
	 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2165
	 */
2166
	public function manyMany() {
2167
		$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
2168
		$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2169
		$items = array_merge($manyManys, $belongsManyManys);
2170
		return $items;
2171
	}
2172
2173
	/**
2174
	 * Return information about a specific many_many component. Returns a numeric array of:
2175
	 * array(
2176
	 * 	<classname>,		The class that relation is defined in e.g. "Product"
2177
	 * 	<candidateName>,	The target class of the relation e.g. "Category"
2178
	 * 	<parentField>,		The field name pointing to <classname>'s table e.g. "ProductID"
2179
	 * 	<childField>,		The field name pointing to <candidatename>'s table e.g. "CategoryID"
2180
	 * 	<joinTable>			The join table between the two classes e.g. "Product_Categories"
2181
	 * )
2182
	 * @param string $component The component name
2183
	 * @return array|null
2184
	 */
2185
	public function manyManyComponent($component) {
2186
		$classes = $this->getClassAncestry();
2187
		foreach($classes as $class) {
2188
			$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
2189
			// Check if the component is defined in many_many on this class
2190
			$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2191
			if($candidate) {
2192
				$parentField = $class . "ID";
2193
				$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
2194
				return array($class, $candidate, $parentField, $childField, "{$class}_$component");
2195
			}
2196
2197
			// Check if the component is defined in belongs_many_many on this class
2198
			$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
2199
			$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
2200
			if($candidate) {
2201
				// Extract class and relation name from dot-notation
2202 View Code Duplication
				if(strpos($candidate, '.') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2203
					list($candidate, $relationName) = explode('.', $candidate, 2);
2204
				}
2205
2206
				$childField = $candidate . "ID";
2207
2208
				// We need to find the inverse component name
2209
				$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2210
				if(!$otherManyMany) {
2211
					throw new LogicException("Inverse component of $candidate not found ({$this->class})");
2212
				}
2213
2214
				// If we've got a relation name (extracted from dot-notation), we can already work out
2215
				// the join table and candidate class name...
2216
				if(isset($relationName) && isset($otherManyMany[$relationName])) {
2217
					$candidateClass = $otherManyMany[$relationName];
2218
					$joinTable = "{$candidate}_{$relationName}";
2219
				} else {
2220
					// ... otherwise, we need to loop over the many_manys and find a relation that
2221
					// matches up to this class
2222
					foreach($otherManyMany as $inverseComponentName => $candidateClass) {
0 ignored issues
show
Bug introduced by
The expression $otherManyMany of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
2223
						if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $candidateClass can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
2224
							$joinTable = "{$candidate}_{$inverseComponentName}";
2225
							break;
2226
						}
2227
					}
2228
				}
2229
2230
				// If we could work out the join table, we've got all the info we need
2231
				if(isset($joinTable)) {
2232
					$parentField = ($class == $candidate) ? "ChildID" : $candidateClass . "ID";
0 ignored issues
show
Bug introduced by
The variable $candidateClass does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2233
					return array($class, $candidate, $parentField, $childField, $joinTable);
2234
				}
2235
2236
				throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
2237
			}
2238
		}
2239
	}
2240
2241
	/**
2242
	 * This returns an array (if it exists) describing the database extensions that are required, or false if none
2243
	 *
2244
	 * This is experimental, and is currently only a Postgres-specific enhancement.
2245
	 *
2246
	 * @return array or false
2247
	 */
2248
	public function database_extensions($class){
2249
		$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2250
2251
		if($extensions)
2252
			return $extensions;
2253
		else
2254
			return false;
2255
	}
2256
2257
	/**
2258
	 * Generates a SearchContext to be used for building and processing
2259
	 * a generic search form for properties on this object.
2260
	 *
2261
	 * @return SearchContext
2262
	 */
2263
	public function getDefaultSearchContext() {
2264
		return new SearchContext(
2265
			$this->class,
2266
			$this->scaffoldSearchFields(),
2267
			$this->defaultSearchFilters()
2268
		);
2269
	}
2270
2271
	/**
2272
	 * Determine which properties on the DataObject are
2273
	 * searchable, and map them to their default {@link FormField}
2274
	 * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2275
	 *
2276
	 * Some additional logic is included for switching field labels, based on
2277
	 * how generic or specific the field type is.
2278
	 *
2279
	 * Used by {@link SearchContext}.
2280
	 *
2281
	 * @param array $_params
2282
	 *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2283
	 *   'restrictFields': Numeric array of a field name whitelist
2284
	 * @return FieldList
2285
	 */
2286
	public function scaffoldSearchFields($_params = null) {
2287
		$params = array_merge(
2288
			array(
2289
				'fieldClasses' => false,
2290
				'restrictFields' => false
2291
			),
2292
			(array)$_params
2293
		);
2294
		$fields = new FieldList();
2295
		foreach($this->searchableFields() as $fieldName => $spec) {
2296
			if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
2297
2298
			// If a custom fieldclass is provided as a string, use it
2299
			if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2300
				$fieldClass = $params['fieldClasses'][$fieldName];
2301
				$field = new $fieldClass($fieldName);
2302
			// If we explicitly set a field, then construct that
2303
			} else if(isset($spec['field'])) {
2304
				// If it's a string, use it as a class name and construct
2305
				if(is_string($spec['field'])) {
2306
					$fieldClass = $spec['field'];
2307
					$field = new $fieldClass($fieldName);
2308
2309
				// If it's a FormField object, then just use that object directly.
2310
				} else if($spec['field'] instanceof FormField) {
2311
					$field = $spec['field'];
2312
2313
				// Otherwise we have a bug
2314
				} else {
2315
					user_error("Bad value for searchable_fields, 'field' value: "
2316
						. var_export($spec['field'], true), E_USER_WARNING);
2317
				}
2318
2319
			// Otherwise, use the database field's scaffolder
2320
			} else {
2321
				$field = $this->relObject($fieldName)->scaffoldSearchField();
2322
			}
2323
2324
			// Allow fields to opt out of search
2325
			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...
2326
				continue;
2327
			}
2328
2329
			if (strstr($fieldName, '.')) {
2330
				$field->setName(str_replace('.', '__', $fieldName));
2331
			}
2332
			$field->setTitle($spec['title']);
2333
2334
			$fields->push($field);
2335
		}
2336
		return $fields;
2337
	}
2338
2339
	/**
2340
	 * Scaffold a simple edit form for all properties on this dataobject,
2341
	 * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2342
	 * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2343
	 *
2344
	 * @uses FormScaffolder
2345
	 *
2346
	 * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2347
	 * @return FieldList
2348
	 */
2349
	public function scaffoldFormFields($_params = null) {
2350
		$params = array_merge(
2351
			array(
2352
				'tabbed' => false,
2353
				'includeRelations' => false,
2354
				'restrictFields' => false,
2355
				'fieldClasses' => false,
2356
				'ajaxSafe' => false
2357
			),
2358
			(array)$_params
2359
		);
2360
2361
		$fs = new FormScaffolder($this);
2362
		$fs->tabbed = $params['tabbed'];
2363
		$fs->includeRelations = $params['includeRelations'];
2364
		$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...
2365
		$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...
2366
		$fs->ajaxSafe = $params['ajaxSafe'];
2367
2368
		return $fs->getFieldList();
2369
	}
2370
2371
	/**
2372
	 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2373
	 * being called on extensions
2374
	 *
2375
	 * @param callable $callback The callback to execute
2376
	 */
2377
	protected function beforeUpdateCMSFields($callback) {
2378
		$this->beforeExtending('updateCMSFields', $callback);
2379
	}
2380
2381
	/**
2382
	 * Centerpiece of every data administration interface in Silverstripe,
2383
	 * which returns a {@link FieldList} suitable for a {@link Form} object.
2384
	 * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2385
	 * generate this set. To customize, overload this method in a subclass
2386
	 * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2387
	 *
2388
	 * <code>
2389
	 * class MyCustomClass extends DataObject {
2390
	 *  static $db = array('CustomProperty'=>'Boolean');
2391
	 *
2392
	 *  function getCMSFields() {
2393
	 *    $fields = parent::getCMSFields();
2394
	 *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2395
	 *    return $fields;
2396
	 *  }
2397
	 * }
2398
	 * </code>
2399
	 *
2400
	 * @see Good example of complex FormField building: SiteTree::getCMSFields()
2401
	 *
2402
	 * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2403
	 */
2404
	public function getCMSFields() {
2405
		$tabbedFields = $this->scaffoldFormFields(array(
2406
			// Don't allow has_many/many_many relationship editing before the record is first saved
2407
			'includeRelations' => ($this->ID > 0),
2408
			'tabbed' => true,
2409
			'ajaxSafe' => true
2410
		));
2411
2412
		$this->extend('updateCMSFields', $tabbedFields);
2413
2414
		return $tabbedFields;
2415
	}
2416
2417
	/**
2418
	 * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2419
	 * including that dataobject's extensions customised actions could be added to the EditForm.
2420
	 *
2421
	 * @return an Empty FieldList(); need to be overload by solid subclass
2422
	 */
2423
	public function getCMSActions() {
2424
		$actions = new FieldList();
2425
		$this->extend('updateCMSActions', $actions);
2426
		return $actions;
2427
	}
2428
2429
2430
	/**
2431
	 * Used for simple frontend forms without relation editing
2432
	 * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2433
	 * by default. To customize, either overload this method in your
2434
	 * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2435
	 *
2436
	 * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2437
	 *
2438
	 * @param array $params See {@link scaffoldFormFields()}
2439
	 * @return FieldList Always returns a simple field collection without TabSet.
2440
	 */
2441
	public function getFrontEndFields($params = null) {
2442
		$untabbedFields = $this->scaffoldFormFields($params);
2443
		$this->extend('updateFrontEndFields', $untabbedFields);
2444
2445
		return $untabbedFields;
2446
	}
2447
2448
	/**
2449
	 * Gets the value of a field.
2450
	 * Called by {@link __get()} and any getFieldName() methods you might create.
2451
	 *
2452
	 * @param string $field The name of the field
2453
	 *
2454
	 * @return mixed The field value
2455
	 */
2456
	public function getField($field) {
2457
		// If we already have an object in $this->record, then we should just return that
2458
		if(isset($this->record[$field]) && is_object($this->record[$field])) {
2459
			return $this->record[$field];
2460
		}
2461
2462
		// Do we have a field that needs to be lazy loaded?
2463 View Code Duplication
		if(isset($this->record[$field.'_Lazy'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2464
			$tableClass = $this->record[$field.'_Lazy'];
2465
			$this->loadLazyFields($tableClass);
2466
		}
2467
2468
		// In case of complex fields, return the DBField object
2469
		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...
2470
			$this->record[$field] = $this->dbObject($field);
2471
		}
2472
2473
		return isset($this->record[$field]) ? $this->record[$field] : null;
2474
	}
2475
2476
	/**
2477
	 * Loads all the stub fields that an initial lazy load didn't load fully.
2478
	 *
2479
	 * @param string $tableClass Base table to load the values from. Others are joined as required.
2480
	 *                   Not specifying a tableClass will load all lazy fields from all tables.
2481
	 * @return bool Flag if lazy loading succeeded
2482
	 */
2483
	protected function loadLazyFields($tableClass = null) {
2484
		if(!$this->isInDB() || !is_numeric($this->ID)) {
2485
			return false;
2486
		}
2487
2488
		if (!$tableClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tableClass of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2489
			$loaded = array();
2490
2491
			foreach ($this->record as $key => $value) {
2492
				if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2493
					$this->loadLazyFields($value);
2494
					$loaded[$value] = $value;
2495
				}
2496
			}
2497
2498
			return false;
2499
		}
2500
2501
		$dataQuery = new DataQuery($tableClass);
2502
2503
		// Reset query parameter context to that of this DataObject
2504
		if($params = $this->getSourceQueryParams()) {
2505
			foreach($params as $key => $value) {
2506
				$dataQuery->setQueryParam($key, $value);
2507
			}
2508
		}
2509
2510
		// Limit query to the current record, unless it has the Versioned extension,
2511
		// in which case it requires special handling through augmentLoadLazyFields()
2512
		$baseTable = ClassInfo::baseDataClass($this);
2513
		$dataQuery->where([
2514
			"\"{$baseTable}\".\"ID\"" => $this->record['ID']
2515
		])->limit(1);
2516
2517
		$columns = array();
2518
2519
		// Add SQL for fields, both simple & multi-value
2520
		// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2521
		$databaseFields = self::database_fields($tableClass);
2522
		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...
2523
			if(!isset($this->record[$k]) || $this->record[$k] === null) {
2524
				$columns[] = $k;
2525
			}
2526
		}
2527
2528
		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...
2529
			$query = $dataQuery->query();
2530
			$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2531
			$this->extend('augmentSQL', $query, $dataQuery);
2532
2533
			$dataQuery->setQueriedColumns($columns);
2534
			$newData = $dataQuery->execute()->record();
2535
2536
			// Load the data into record
2537
			if($newData) {
2538
				foreach($newData as $k => $v) {
2539
					if (in_array($k, $columns)) {
2540
						$this->record[$k] = $v;
2541
						$this->original[$k] = $v;
2542
						unset($this->record[$k . '_Lazy']);
2543
					}
2544
				}
2545
2546
			// No data means that the query returned nothing; assign 'null' to all the requested fields
2547
			} else {
2548
				foreach($columns as $k) {
2549
					$this->record[$k] = null;
2550
					$this->original[$k] = null;
2551
					unset($this->record[$k . '_Lazy']);
2552
				}
2553
			}
2554
		}
2555
		return true;
2556
	}
2557
2558
	/**
2559
	 * Return the fields that have changed.
2560
	 *
2561
	 * The change level affects what the functions defines as "changed":
2562
	 * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2563
	 * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2564
	 *   for example a change from 0 to null would not be included.
2565
	 *
2566
	 * Example return:
2567
	 * <code>
2568
	 * array(
2569
	 *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2570
	 * )
2571
	 * </code>
2572
	 *
2573
	 * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2574
	 * to return all database fields, or an array for an explicit filter. false returns all fields.
2575
	 * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2576
	 * @return array
2577
	 */
2578
	public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
2579
		$changedFields = array();
2580
2581
		// Update the changed array with references to changed obj-fields
2582
		foreach($this->record as $k => $v) {
2583
			// Prevents DBComposite infinite looping on isChanged
2584
			if(is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2585
				continue;
2586
			}
2587
			if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2588
				$this->changed[$k] = self::CHANGE_VALUE;
2589
			}
2590
		}
2591
2592
		if(is_array($databaseFieldsOnly)) {
2593
			$fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2594
		} elseif($databaseFieldsOnly) {
2595
			$fields = array_intersect_key((array)$this->changed, $this->db());
2596
		} else {
2597
			$fields = $this->changed;
2598
		}
2599
2600
		// Filter the list to those of a certain change level
2601
		if($changeLevel > self::CHANGE_STRICT) {
2602
			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...
2603
				if($level < $changeLevel) {
2604
					unset($fields[$name]);
2605
				}
2606
			}
2607
		}
2608
2609
		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...
2610
			$changedFields[$name] = array(
2611
				'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2612
				'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2613
				'level' => $level
2614
			);
2615
		}
2616
2617
		return $changedFields;
2618
	}
2619
2620
	/**
2621
	 * Uses {@link getChangedFields()} to determine if fields have been changed
2622
	 * since loading them from the database.
2623
	 *
2624
	 * @param string $fieldName Name of the database field to check, will check for any if not given
2625
	 * @param int $changeLevel See {@link getChangedFields()}
2626
	 * @return boolean
2627
	 */
2628
	public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
2629
		$fields = $fieldName ? array($fieldName) : true;
2630
		$changed = $this->getChangedFields($fields, $changeLevel);
2631
		if(!isset($fieldName)) {
2632
			return !empty($changed);
2633
		}
2634
		else {
2635
			return array_key_exists($fieldName, $changed);
2636
		}
2637
	}
2638
2639
	/**
2640
	 * Set the value of the field
2641
	 * Called by {@link __set()} and any setFieldName() methods you might create.
2642
	 *
2643
	 * @param string $fieldName Name of the field
2644
	 * @param mixed $val New field value
2645
	 * @return DataObject $this
2646
	 */
2647
	public function setField($fieldName, $val) {
2648
		//if it's a has_one component, destroy the cache
2649
		if (substr($fieldName, -2) == 'ID') {
2650
			unset($this->components[substr($fieldName, 0, -2)]);
2651
		}
2652
2653
		// If we've just lazy-loaded the column, then we need to populate the $original array
2654 View Code Duplication
		if(isset($this->record[$fieldName.'_Lazy'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2655
			$tableClass = $this->record[$fieldName.'_Lazy'];
2656
			$this->loadLazyFields($tableClass);
2657
		}
2658
2659
		// Situation 1: Passing an DBField
2660
		if($val instanceof DBField) {
2661
			$val->setName($fieldName);
2662
			$val->saveInto($this);
2663
2664
			// Situation 1a: Composite fields should remain bound in case they are
2665
			// later referenced to update the parent dataobject
2666
			if($val instanceof DBComposite) {
2667
				$val->bindTo($this);
2668
				$this->record[$fieldName] = $val;
2669
			}
2670
		// Situation 2: Passing a literal or non-DBField object
2671
		} else {
2672
			// If this is a proper database field, we shouldn't be getting non-DBField objects
2673
			if(is_object($val) && $this->db($fieldName)) {
2674
				user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2675
			}
2676
2677
			// if a field is not existing or has strictly changed
2678
			if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2679
				// TODO Add check for php-level defaults which are not set in the db
2680
				// TODO Add check for hidden input-fields (readonly) which are not set in the db
2681
				// At the very least, the type has changed
2682
				$this->changed[$fieldName] = self::CHANGE_STRICT;
2683
2684
				if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
2685
						&& $this->record[$fieldName] != $val)) {
2686
2687
					// Value has changed as well, not just the type
2688
					$this->changed[$fieldName] = self::CHANGE_VALUE;
2689
				}
2690
2691
				// Value is always saved back when strict check succeeds.
2692
				$this->record[$fieldName] = $val;
2693
			}
2694
		}
2695
		return $this;
2696
	}
2697
2698
	/**
2699
	 * Set the value of the field, using a casting object.
2700
	 * This is useful when you aren't sure that a date is in SQL format, for example.
2701
	 * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2702
	 * can be saved into the Image table.
2703
	 *
2704
	 * @param string $fieldName Name of the field
2705
	 * @param mixed $value New field value
2706
	 * @return $this
2707
	 */
2708
	public function setCastedField($fieldName, $value) {
2709
		if(!$fieldName) {
2710
			user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2711
		}
2712
		$fieldObj = $this->dbObject($fieldName);
2713
		if($fieldObj) {
2714
			$fieldObj->setValue($value);
2715
			$fieldObj->saveInto($this);
2716
		} else {
2717
			$this->$fieldName = $value;
2718
		}
2719
		return $this;
2720
	}
2721
2722
	/**
2723
	 * {@inheritdoc}
2724
	 */
2725
	public function castingHelper($field) {
2726
		if ($fieldSpec = $this->db($field)) {
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->db($field); of type array|string|null adds the type array to the return on line 2727 which is incompatible with the return type of the parent method ViewableData::castingHelper of type string.
Loading history...
2727
			return $fieldSpec;
2728
		}
2729
2730
		// many_many_extraFields aren't presented by db(), so we check if the source query params
2731
		// provide us with meta-data for a many_many relation we can inspect for extra fields.
2732
		$queryParams = $this->getSourceQueryParams();
2733
		if (!empty($queryParams['Component.ExtraFields'])) {
2734
			$extraFields = $queryParams['Component.ExtraFields'];
2735
2736
			if (isset($extraFields[$field])) {
2737
				return $extraFields[$field];
2738
			}
2739
		}
2740
2741
		return parent::castingHelper($field);
2742
	}
2743
2744
	/**
2745
	 * Returns true if the given field exists in a database column on any of
2746
	 * the objects tables and optionally look up a dynamic getter with
2747
	 * get<fieldName>().
2748
	 *
2749
	 * @param string $field Name of the field
2750
	 * @return boolean True if the given field exists
2751
	 */
2752
	public function hasField($field) {
2753
		return (
2754
			array_key_exists($field, $this->record)
2755
			|| $this->db($field)
2756
			|| (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...
2757
			|| $this->hasMethod("get{$field}")
2758
		);
2759
	}
2760
2761
	/**
2762
	 * Returns true if the given field exists as a database column
2763
	 *
2764
	 * @param string $field Name of the field
2765
	 *
2766
	 * @return boolean
2767
	 */
2768
	public function hasDatabaseField($field) {
2769
		return $this->db($field)
2770
			&& ! 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...
2771
	}
2772
2773
	/**
2774
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2775
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2776
	 *
2777
	 * @param string $field Name of the field
2778
	 * @return string The field type of the given field
2779
	 */
2780
	public function hasOwnTableDatabaseField($field) {
2781
		return self::has_own_table_database_field($this->class, $field);
2782
	}
2783
2784
	/**
2785
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2786
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2787
	 *
2788
	 * @param string $class Class name to check
2789
	 * @param string $field Name of the field
2790
	 * @return string The field type of the given field
2791
	 */
2792
	public static function has_own_table_database_field($class, $field) {
2793
		$fieldMap = self::database_fields($class);
2794
2795
		// Remove string-based "constructor-arguments" from the DBField definition
2796
		if(isset($fieldMap[$field])) {
2797
			$spec = $fieldMap[$field];
2798
			if(is_string($spec)) return strtok($spec,'(');
2799
			else return $spec['type'];
2800
		}
2801
	}
2802
2803
	/**
2804
	 * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2805
	 * actually looking in the database.
2806
	 *
2807
	 * @param string $dataClass
2808
	 * @return bool
2809
	 */
2810
	public static function has_own_table($dataClass) {
2811
		if(!is_subclass_of($dataClass,'DataObject')) return false;
2812
2813
		$dataClass = ClassInfo::class_name($dataClass);
2814
		if(!isset(self::$_cache_has_own_table[$dataClass])) {
2815
			if(get_parent_class($dataClass) == 'DataObject') {
2816
				self::$_cache_has_own_table[$dataClass] = true;
2817
			} else {
2818
				self::$_cache_has_own_table[$dataClass]
2819
					= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
2820
					|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
2821
			}
2822
		}
2823
		return self::$_cache_has_own_table[$dataClass];
2824
	}
2825
2826
	/**
2827
	 * Returns true if the member is allowed to do the given action.
2828
	 * See {@link extendedCan()} for a more versatile tri-state permission control.
2829
	 *
2830
	 * @param string $perm The permission to be checked, such as 'View'.
2831
	 * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2832
	 * in user.
2833
	 * @param array $context Additional $context to pass to extendedCan()
2834
	 *
2835
	 * @return boolean True if the the member is allowed to do the given action
2836
	 */
2837
	public function can($perm, $member = null, $context = array()) {
2838
		if(!isset($member)) {
2839
			$member = Member::currentUser();
2840
		}
2841
		if(Permission::checkMember($member, "ADMIN")) return true;
2842
2843
		if($this->manyManyComponent('Can' . $perm)) {
2844
			if($this->ParentID && $this->SecurityType == 'Inherit') {
2845
				if(!($p = $this->Parent)) {
0 ignored issues
show
Documentation introduced by
The property Parent does not exist on object<DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2846
					return false;
2847
				}
2848
				return $this->Parent->can($perm, $member);
2849
2850
			} else {
2851
				$permissionCache = $this->uninherited('permissionCache');
2852
				$memberID = $member ? $member->ID : 'none';
2853
2854
				if(!isset($permissionCache[$memberID][$perm])) {
2855
					if($member->ID) {
2856
						$groups = $member->Groups();
2857
					}
2858
2859
					$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...
2860
2861
					// TODO Fix relation table hardcoding
2862
					$query = new SQLSelect(
2863
						"\"Page_Can$perm\".PageID",
2864
					array("\"Page_Can$perm\""),
2865
						"GroupID IN ($groupList)");
2866
2867
					$permissionCache[$memberID][$perm] = $query->execute()->column();
2868
2869
					if($perm == "View") {
2870
						// TODO Fix relation table hardcoding
2871
						$query = new SQLSelect("\"SiteTree\".\"ID\"", array(
2872
							"\"SiteTree\"",
2873
							"LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2874
							), "\"Page_CanView\".\"PageID\" IS NULL");
0 ignored issues
show
Documentation introduced by
'"Page_CanView"."PageID" IS NULL' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2875
2876
							$unsecuredPages = $query->execute()->column();
2877
							if($permissionCache[$memberID][$perm]) {
2878
								$permissionCache[$memberID][$perm]
2879
									= array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2880
							} else {
2881
								$permissionCache[$memberID][$perm] = $unsecuredPages;
2882
							}
2883
					}
2884
2885
					Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2886
				}
2887
2888
				if($permissionCache[$memberID][$perm]) {
2889
					return in_array($this->ID, $permissionCache[$memberID][$perm]);
2890
				}
2891
			}
2892
		} else {
2893
			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, CMSMenuTest_CustomTitle, CMSMenuTest_LeftAndMainController, CMSProfileController, CMSSecurity, CampaignAdmin, ChangeSet, ChangeSetItem, ChangeSetItemTest_Versioned, ChangeSetTest_Base, ChangeSetTest_End, ChangeSetTest_EndChild, ChangeSetTest_Mid, CheckboxFieldTest_Article, CheckboxSetFieldTest_Article, CheckboxSetFieldTest_Tag, ClassInfoTest_BaseClass, ClassInfoTest_BaseDataClass, ClassInfoTest_ChildClass, ClassInfoTest_GrandChildClass, ClassInfoTest_HasFields, ClassInfoTest_NoFields, ClassInfoTest_WithRelation, CliController, ComponentSetTest_Player, ComponentSetTest_Team, Controller, ControllerTest_AccessBaseController, ControllerTest_AccessSecuredController, ControllerTest_AccessWildcardSecuredController, ControllerTest_ContainerController, ControllerTest_Controller, ControllerTest_HasAction, ControllerTest_HasAction_Unsecured, ControllerTest_IndexSecuredController, ControllerTest_SubController, ControllerTest_UnsecuredController, CsvBulkLoaderTest_Player, CsvBulkLoaderTest_PlayerContract, CsvBulkLoaderTest_Team, DBClassNameTest_CustomDefault, DBClassNameTest_CustomDefaultSubclass, DBClassNameTest_Object, DBClassNameTest_ObjectSubClass, DBClassNameTest_ObjectSubSubClass, DBClassNameTest_OtherClass, DBCompositeTest_DataObject, DBFileTest_ImageOnly, DBFileTest_Object, DBFileTest_Subclass, DataDifferencerTest_HasOneRelationObject, DataDifferencerTest_Object, DataExtensionTest_CMSFieldsBase, DataExtensionTest_CMSFieldsChild, DataExtensionTest_CMSFieldsGrandchild, DataExtensionTest_Member, DataExtensionTest_MyObject, DataExtensionTest_Player, DataExtensionTest_RelatedObject, DataObject, DataObjectDuplicateTestClass1, DataObjectDuplicateTestClass2, DataObjectDuplicateTestClass3, DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO, DataObjectTest\NamespacedClass, DataObjectTest\RelationClass, DataObjectTest_Bogey, DataObjectTest_CEO, DataObjectTest_Company, DataObjectTest_EquipmentCompany, DataObjectTest_ExtendedTeamComment, DataObjectTest_Fan, DataObjectTest_FieldlessSubTable, DataObjectTest_FieldlessTable, DataObjectTest_Fixture, DataObjectTest_Play, DataObjectTest_Player, DataObjectTest_Ploy, DataObjectTest_Staff, DataObjectTest_SubEquipmentCompany, DataObjectTest_SubTeam, DataObjectTest_Team, DataObjectTest_TeamComment, DataObjectTest_ValidatedObject, DataQueryTest_A, DataQueryTest_B, DataQueryTest_C, DataQueryTest_D, DataQueryTest_E, DataQueryTest_F, DataQueryTest_G, DatabaseAdmin, DatabaseTest_MyObject, DatetimeFieldTest_Model, DecimalTest_DataObject, DevAdminControllerTest_Controller1, DevBuildController, DevelopmentAdmin, DirectorTestRequest_Controller, EmailFieldTest_Controller, FakeController, File, FileTest_MyCustomFile, FixtureBlueprintTest_Article, FixtureBlueprintTest_Page, FixtureBlueprintTest_SiteTree, FixtureFactoryTest_DataObject, FixtureFactoryTest_DataObjectRelation, Folder, FormScaffolderTest_Article, FormScaffolderTest_Author, FormScaffolderTest_Tag, FormTest_Controller, FormTest_ControllerWithSecurityToken, FormTest_ControllerWithStrictPostCheck, FormTest_Player, FormTest_Team, FulltextFilterTest_DataObject, GridFieldAction_Delete_Team, GridFieldAction_Edit_Team, GridFieldAddExistingAutocompleterTest_Controller, GridFieldDetailFormTest_Category, GridFieldDetailFormTest_CategoryController, GridFieldDetailFormTest_Controller, GridFieldDetailFormTest_GroupController, GridFieldDetailFormTest_PeopleGroup, GridFieldDetailFormTest_Person, GridFieldExportButtonTest_NoView, GridFieldExportButtonTest_Team, GridFieldPrintButtonTest_DO, GridFieldSortableHeaderTest_Cheerleader, GridFieldSortableHeaderTest_CheerleaderHat, GridFieldSortableHeaderTest_Mom, GridFieldSortableHeaderTest_Team, GridFieldSortableHeaderTest_TeamGroup, GridFieldTest_Cheerleader, GridFieldTest_Permissions, GridFieldTest_Player, GridFieldTest_Team, GridField_URLHandlerTest_Controller, Group, GroupTest_Member, HTMLEditorFieldTest_Object, HierarchyHideTest_Object, HierarchyHideTest_SubObject, HierarchyTest_Object, Image, InstallerTest, LeftAndMain, LeftAndMainTest_Controller, LeftAndMainTest_Object, ListboxFieldTest_Article, ListboxFieldTest_DataObject, ListboxFieldTest_Tag, LoginAttempt, ManyManyListTest_Category, ManyManyListTest_ExtraFields, ManyManyListTest_IndirectPrimary, ManyManyListTest_Product, ManyManyListTest_Secondary, ManyManyListTest_SecondarySub, Member, MemberDatetimeOptionsetFieldTest_Controller, MemberPassword, ModelAdmin, ModelAdminTest_Admin, ModelAdminTest_Contact, ModelAdminTest_Player, ModelAdminTest_PlayerAdmin, MoneyFieldTest_CustomSetter_Object, MoneyFieldTest_Object, MoneyTest_DataObject, MoneyTest_SubClass, MySQLDatabaseTest_Data, NumericFieldTest_Object, OtherSubclassWithSameField, Permission, PermissionRole, PermissionRoleCode, RememberLoginHash, RequestHandlingFieldTest_Controller, RequestHandlingTest_AllowedController, RequestHandlingTest_Controller, RequestHandlingTest_Cont...rFormWithAllowedActions, RequestHandlingTest_FormActionController, SQLInsertTestBase, SQLSelectTestBase, SQLSelectTestChild, SQLSelectTest_DO, SQLUpdateChild, SQLUpdateTestBase, SSViewerCacheBlockTest_Model, SSViewerCacheBlockTest_VersionedModel, SSViewerTest_Object, SapphireInfo, SapphireREPL, SearchContextTest_Action, SearchContextTest_AllFilterTypes, SearchContextTest_Book, SearchContextTest_Company, SearchContextTest_Deadline, SearchContextTest_Person, SearchContextTest_Project, SearchFilterApplyRelationTest_DO, SearchFilterApplyRelationTest_HasManyChild, SearchFilterApplyRelationTest_HasManyGrantChild, SearchFilterApplyRelationTest_HasManyParent, SearchFilterApplyRelationTest_HasOneChild, SearchFilterApplyRelationTest_HasOneGrantChild, SearchFilterApplyRelationTest_HasOneParent, SearchFilterApplyRelationTest_ManyManyChild, SearchFilterApplyRelationTest_ManyManyGrantChild, SearchFilterApplyRelationTest_ManyManyParent, Security, SecurityAdmin, SecurityTest_NullController, SecurityTest_SecuredController, SilverStripe\Filesystem\...ProtectedFileController, SilverStripe\Framework\Tests\ClassI, SubclassedDBFieldObject, TaskRunner, TestNamespace\SSViewerTest_Controller, TransactionTest_Object, UnsavedRelationListTest_DataObject, Upload, UploadFieldTest_Controller, UploadFieldTest_ExtendedFile, UploadFieldTest_Record, VersionableExtensionsTest_DataObject, VersionedLazySub_DataObject, VersionedLazy_DataObject, VersionedOwnershipTest_Attachment, VersionedOwnershipTest_Banner, VersionedOwnershipTest_CustomRelation, VersionedOwnershipTest_Image, VersionedOwnershipTest_Object, VersionedOwnershipTest_Page, VersionedOwnershipTest_Related, VersionedOwnershipTest_RelatedMany, VersionedOwnershipTest_Subclass, VersionedTest_AnotherSubclass, VersionedTest_DataObject, VersionedTest_PublicStage, VersionedTest_PublicViaExtension, VersionedTest_RelatedWithoutVersion, VersionedTest_SingleStage, VersionedTest_Subclass, VersionedTest_UnversionedWithField, VersionedTest_WithIndexes, XMLDataFormatterTest_DataObject, YamlFixtureTest_DataObject, YamlFixtureTest_DataObjectRelation, i18nTestModule, i18nTest_DataObject, i18nTextCollectorTestMyObject, i18nTextCollectorTestMySubObject. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
2894
		}
2895
	}
2896
2897
	/**
2898
	 * Process tri-state responses from permission-alterting extensions.  The extensions are
2899
	 * expected to return one of three values:
2900
	 *
2901
	 *  - false: Disallow this permission, regardless of what other extensions say
2902
	 *  - true: Allow this permission, as long as no other extensions return false
2903
	 *  - NULL: Don't affect the outcome
2904
	 *
2905
	 * This method itself returns a tri-state value, and is designed to be used like this:
2906
	 *
2907
	 * <code>
2908
	 * $extended = $this->extendedCan('canDoSomething', $member);
2909
	 * if($extended !== null) return $extended;
2910
	 * else return $normalValue;
2911
	 * </code>
2912
	 *
2913
	 * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2914
	 * @param Member|int $member
2915
	 * @param array $context Optional context
2916
	 * @return boolean|null
2917
	 */
2918
	public function extendedCan($methodName, $member, $context = array()) {
2919
		$results = $this->extend($methodName, $member, $context);
2920
		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...
2921
			// Remove NULLs
2922
			$results = array_filter($results, function($v) {return !is_null($v);});
2923
			// If there are any non-NULL responses, then return the lowest one of them.
2924
			// If any explicitly deny the permission, then we don't get access
2925
			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...
2926
		}
2927
		return null;
2928
	}
2929
2930
	/**
2931
	 * @param Member $member
2932
	 * @return boolean
2933
	 */
2934 View Code Duplication
	public function canView($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2935
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2934 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2936
		if($extended !== null) {
2937
			return $extended;
2938
		}
2939
		return Permission::check('ADMIN', 'any', $member);
2940
	}
2941
2942
	/**
2943
	 * @param Member $member
2944
	 * @return boolean
2945
	 */
2946 View Code Duplication
	public function canEdit($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2947
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2946 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2948
		if($extended !== null) {
2949
			return $extended;
2950
		}
2951
		return Permission::check('ADMIN', 'any', $member);
2952
	}
2953
2954
	/**
2955
	 * @param Member $member
2956
	 * @return boolean
2957
	 */
2958 View Code Duplication
	public function canDelete($member = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2959
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2958 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2960
		if($extended !== null) {
2961
			return $extended;
2962
		}
2963
		return Permission::check('ADMIN', 'any', $member);
2964
	}
2965
2966
	/**
2967
	 * @param Member $member
2968
	 * @param array $context Additional context-specific data which might
2969
	 * affect whether (or where) this object could be created.
2970
	 * @return boolean
2971
	 */
2972 View Code Duplication
	public function canCreate($member = null, $context = array()) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2973
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2972 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2974
		if($extended !== null) {
2975
			return $extended;
2976
		}
2977
		return Permission::check('ADMIN', 'any', $member);
2978
	}
2979
2980
	/**
2981
	 * Debugging used by Debug::show()
2982
	 *
2983
	 * @return string HTML data representing this object
2984
	 */
2985
	public function debug() {
2986
		$val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2987
		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...
2988
			$val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2989
		}
2990
		$val .= "</ul>\n";
2991
		return $val;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $val; (string) is incompatible with the return type of the parent method ViewableData::Debug of type ViewableData_Debugger.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
2992
	}
2993
2994
	/**
2995
	 * Return the DBField object that represents the given field.
2996
	 * This works similarly to obj() with 2 key differences:
2997
	 *   - it still returns an object even when the field has no value.
2998
	 *   - it only matches fields and not methods
2999
	 *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
3000
	 *
3001
	 * @param string $fieldName Name of the field
3002
	 * @return DBField The field as a DBField object
3003
	 */
3004
	public function dbObject($fieldName) {
3005
		$value = isset($this->record[$fieldName])
3006
			? $this->record[$fieldName]
3007
			: null;
3008
3009
		// If we have a DBField object in $this->record, then return that
3010
		if(is_object($value)) {
3011
			return $value;
3012
		}
3013
3014
		// Build and populate new field otherwise
3015
		$helper = $this->db($fieldName, true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a false|string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3016
		if($helper) {
3017
			list($table, $spec) = explode('.', $helper);
3018
			$obj = Object::create_from_string($spec, $fieldName);
3019
			$obj->setTable($table);
3020
			$obj->setValue($value, $this, false);
3021
			return $obj;
3022
		}
3023
	}
3024
3025
	/**
3026
	 * Traverses to a DBField referenced by relationships between data objects.
3027
	 *
3028
	 * The path to the related field is specified with dot separated syntax
3029
	 * (eg: Parent.Child.Child.FieldName).
3030
	 *
3031
	 * @param string $fieldPath
3032
	 *
3033
	 * @return mixed DBField of the field on the object or a DataList instance.
3034
	 */
3035
	public function relObject($fieldPath) {
3036
		$object = null;
0 ignored issues
show
Unused Code introduced by
$object is not used, you could remove the assignment.

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

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

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

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

Loading history...
3037
3038
		if(strpos($fieldPath, '.') !== false) {
3039
			$parts = explode('.', $fieldPath);
3040
			$fieldName = array_pop($parts);
3041
3042
			// Traverse dot syntax
3043
			$component = $this;
3044
3045
			foreach($parts as $relation) {
3046
				if($component instanceof SS_List) {
3047
					if(method_exists($component,$relation)) {
3048
						$component = $component->$relation();
3049
					} else {
3050
						$component = $component->relation($relation);
3051
					}
3052
				} else {
3053
					$component = $component->$relation();
3054
				}
3055
			}
3056
3057
			$object = $component->dbObject($fieldName);
3058
3059
		} else {
3060
			$object = $this->dbObject($fieldPath);
3061
		}
3062
3063
		return $object;
3064
	}
3065
3066
	/**
3067
	 * Traverses to a field referenced by relationships between data objects, returning the value
3068
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3069
	 *
3070
	 * @param $fieldName string
3071
	 * @return string | null - will return null on a missing value
3072
	 */
3073 View Code Duplication
	public function relField($fieldName) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3074
		$component = $this;
3075
3076
		// We're dealing with relations here so we traverse the dot syntax
3077
		if(strpos($fieldName, '.') !== false) {
3078
			$relations = explode('.', $fieldName);
3079
			$fieldName = array_pop($relations);
3080
			foreach($relations as $relation) {
3081
				// Inspect $component for element $relation
3082
				if($component->hasMethod($relation)) {
3083
					// Check nested method
3084
					$component = $component->$relation();
3085
				} elseif($component instanceof SS_List) {
3086
					// Select adjacent relation from DataList
3087
					$component = $component->relation($relation);
3088
				} elseif($component instanceof DataObject
3089
					&& ($dbObject = $component->dbObject($relation))
3090
				) {
3091
					// Select db object
3092
					$component = $dbObject;
3093
				} else {
3094
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
3095
				}
3096
			}
3097
		}
3098
3099
		// Bail if the component is null
3100
		if(!$component) {
3101
			return null;
3102
		}
3103
		if($component->hasMethod($fieldName)) {
3104
			return $component->$fieldName();
3105
		}
3106
		return $component->$fieldName;
3107
	}
3108
3109
	/**
3110
	 * Temporary hack to return an association name, based on class, to get around the mangle
3111
	 * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3112
	 *
3113
	 * @return String
3114
	 */
3115
	public function getReverseAssociation($className) {
3116
		if (is_array($this->manyMany())) {
3117
			$many_many = array_flip($this->manyMany());
3118
			if (array_key_exists($className, $many_many)) return $many_many[$className];
3119
		}
3120
		if (is_array($this->hasMany())) {
3121
			$has_many = array_flip($this->hasMany());
3122
			if (array_key_exists($className, $has_many)) return $has_many[$className];
3123
		}
3124
		if (is_array($this->hasOne())) {
3125
			$has_one = array_flip($this->hasOne());
3126
			if (array_key_exists($className, $has_one)) return $has_one[$className];
3127
		}
3128
3129
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by DataObject::getReverseAssociation of type string.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3181
		}
3182
3183
		$result->setDataModel(DataModel::inst());
3184
		return $result;
3185
	}
3186
3187
3188
	/**
3189
	 * Return the first item matching the given query.
3190
	 * All calls to get_one() are cached.
3191
	 *
3192
	 * @param string $callerClass The class of objects to be returned
3193
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3194
	 * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3195
	 * @param boolean $cache Use caching
3196
	 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3197
	 *
3198
	 * @return DataObject The first item matching the query
3199
	 */
3200
	public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
3201
		$SNG = singleton($callerClass);
3202
3203
		$cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3204
		$cacheKey = md5(var_export($cacheComponents, true));
3205
3206
		// Flush destroyed items out of the cache
3207
		if($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
3208
				&& self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
3209
				&& self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
3210
3211
			self::$_cache_get_one[$callerClass][$cacheKey] = false;
3212
		}
3213
		if(!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3214
			$dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
3215
			$item = $dl->First();
3216
3217
			if($cache) {
3218
				self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3219
				if(!self::$_cache_get_one[$callerClass][$cacheKey]) {
3220
					self::$_cache_get_one[$callerClass][$cacheKey] = false;
3221
				}
3222
			}
3223
		}
3224
		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...
3225
	}
3226
3227
	/**
3228
	 * Flush the cached results for all relations (has_one, has_many, many_many)
3229
	 * Also clears any cached aggregate data.
3230
	 *
3231
	 * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3232
	 *                            When false will just clear session-local cached data
3233
	 * @return DataObject $this
3234
	 */
3235
	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...
3236
		if($this->class == 'DataObject') {
3237
			self::$_cache_get_one = array();
3238
			return $this;
3239
		}
3240
3241
		$classes = ClassInfo::ancestry($this->class);
3242
		foreach($classes as $class) {
3243
			if(isset(self::$_cache_get_one[$class])) unset(self::$_cache_get_one[$class]);
3244
		}
3245
3246
		$this->extend('flushCache');
3247
3248
		$this->components = array();
3249
		return $this;
3250
	}
3251
3252
	/**
3253
	 * Flush the get_one global cache and destroy associated objects.
3254
	 */
3255
	public static function flush_and_destroy_cache() {
3256
		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...
3257
			if(is_array($items)) foreach($items as $item) {
3258
				if($item) $item->destroy();
3259
			}
3260
		}
3261
		self::$_cache_get_one = array();
3262
	}
3263
3264
	/**
3265
	 * Reset all global caches associated with DataObject.
3266
	 */
3267
	public static function reset() {
3268
		DBClassName::clear_classname_cache();
3269
		self::$_cache_has_own_table = array();
3270
		self::$_cache_get_one = array();
3271
		self::$_cache_composite_fields = array();
3272
		self::$_cache_database_fields = array();
3273
		self::$_cache_get_class_ancestry = array();
3274
		self::$_cache_field_labels = array();
3275
	}
3276
3277
	/**
3278
	 * Return the given element, searching by ID
3279
	 *
3280
	 * @param string $callerClass The class of the object to be returned
3281
	 * @param int $id The id of the element
3282
	 * @param boolean $cache See {@link get_one()}
3283
	 *
3284
	 * @return DataObject The element
3285
	 */
3286
	public static function get_by_id($callerClass, $id, $cache = true) {
3287
		if(!is_numeric($id)) {
3288
			user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3289
		}
3290
3291
		// Check filter column
3292
		if(is_subclass_of($callerClass, 'DataObject')) {
3293
			$baseClass = ClassInfo::baseDataClass($callerClass);
3294
			$column = "\"$baseClass\".\"ID\"";
3295
		} else{
3296
			// This simpler code will be used by non-DataObject classes that implement DataObjectInterface
3297
			$column = '"ID"';
3298
		}
3299
3300
		// Relegate to get_one
3301
		return DataObject::get_one($callerClass, array($column => $id), $cache);
3302
	}
3303
3304
	/**
3305
	 * Get the name of the base table for this object
3306
	 */
3307
	public function baseTable() {
3308
		$tableClasses = ClassInfo::dataClassesFor($this->class);
3309
		return array_shift($tableClasses);
3310
	}
3311
3312
	/**
3313
	 * @var Array Parameters used in the query that built this object.
3314
	 * This can be used by decorators (e.g. lazy loading) to
3315
	 * run additional queries using the same context.
3316
	 */
3317
	protected $sourceQueryParams;
3318
3319
	/**
3320
	 * @see $sourceQueryParams
3321
	 * @return array
3322
	 */
3323
	public function getSourceQueryParams() {
3324
		return $this->sourceQueryParams;
3325
	}
3326
3327
	/**
3328
	 * Get list of parameters that should be inherited to relations on this object
3329
	 *
3330
	 * @return array
3331
	 */
3332
	public function getInheritableQueryParams() {
3333
		$params = $this->getSourceQueryParams();
3334
		$this->extend('updateInheritableQueryParams', $params);
3335
		return $params;
3336
	}
3337
3338
	/**
3339
	 * @see $sourceQueryParams
3340
	 * @param array
3341
	 */
3342
	public function setSourceQueryParams($array) {
3343
		$this->sourceQueryParams = $array;
3344
	}
3345
3346
	/**
3347
	 * @see $sourceQueryParams
3348
	 * @param array
3349
	 */
3350
	public function setSourceQueryParam($key, $value) {
3351
		$this->sourceQueryParams[$key] = $value;
3352
	}
3353
3354
	/**
3355
	 * @see $sourceQueryParams
3356
	 * @return Mixed
3357
	 */
3358
	public function getSourceQueryParam($key) {
3359
		if(isset($this->sourceQueryParams[$key])) return $this->sourceQueryParams[$key];
3360
		else return null;
3361
	}
3362
3363
	//-------------------------------------------------------------------------------------------//
3364
3365
	/**
3366
	 * Return the database indexes on this table.
3367
	 * This array is indexed by the name of the field with the index, and
3368
	 * the value is the type of index.
3369
	 */
3370
	public function databaseIndexes() {
3371
		$has_one = $this->uninherited('has_one',true);
0 ignored issues
show
Unused Code introduced by
The call to DataObject::uninherited() has too many arguments starting with true.

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

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

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

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

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

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

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

Loading history...
3373
		//$fileIndexes = $this->uninherited('fileIndexes', true);
3374
3375
		$indexes = array();
3376
3377
		if($has_one) {
3378
			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...
3379
				$indexes[$relationshipName . 'ID'] = true;
3380
			}
3381
		}
3382
3383
		if($classIndexes) {
3384
			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...
3385
				$indexes[$indexName] = $indexType;
3386
			}
3387
		}
3388
3389
		if(get_parent_class($this) == "DataObject") {
3390
			$indexes['ClassName'] = true;
3391
		}
3392
3393
		return $indexes;
3394
	}
3395
3396
	/**
3397
	 * Check the database schema and update it as necessary.
3398
	 *
3399
	 * @uses DataExtension->augmentDatabase()
3400
	 */
3401
	public function requireTable() {
3402
		// Only build the table if we've actually got fields
3403
		$fields = self::database_fields($this->class);
3404
		$extensions = self::database_extensions($this->class);
3405
3406
		$indexes = $this->databaseIndexes();
3407
3408
		// Validate relationship configuration
3409
		$this->validateModelDefinitions();
3410
3411
		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...
3412
			$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
3413
			DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
0 ignored issues
show
Documentation introduced by
$fields is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$indexes is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3414
				$extensions);
3415
		} else {
3416
			DB::dont_require_table($this->class);
3417
		}
3418
3419
		// Build any child tables for many_many items
3420
		if($manyMany = $this->uninherited('many_many', true)) {
0 ignored issues
show
Unused Code introduced by
The call to DataObject::uninherited() has too many arguments starting with true.

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

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

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

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

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

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

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

Loading history...
3422
			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...
3423
				// Build field list
3424
				$manymanyFields = array(
3425
					"{$this->class}ID" => "Int",
3426
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => "Int",
3427
				);
3428
				if(isset($extras[$relationship])) {
3429
					$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
3430
				}
3431
3432
				// Build index list
3433
				$manymanyIndexes = array(
3434
					"{$this->class}ID" => true,
3435
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true,
3436
				);
3437
3438
				DB::require_table("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null,
0 ignored issues
show
Documentation introduced by
$manymanyFields is of type array<?,string>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$manymanyIndexes is of type array<?,boolean>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3439
					$extensions);
3440
			}
3441
		}
3442
3443
		// Let any extentions make their own database fields
3444
		$this->extend('augmentDatabase', $dummy);
3445
	}
3446
3447
	/**
3448
	 * Validate that the configured relations for this class use the correct syntaxes
3449
	 * @throws LogicException
3450
	 */
3451
	protected function validateModelDefinitions() {
3452
		$modelDefinitions = array(
3453
			'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
3454
			'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
3455
			'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
3456
			'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
3457
			'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
3458
			'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
3459
			'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
3460
		);
3461
3462
		foreach($modelDefinitions as $defType => $relations) {
3463
			if( ! $relations) continue;
3464
3465
			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...
3466
				if($defType === 'many_many_extraFields') {
3467
					if(!is_array($v)) {
3468
						throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
3469
							. var_export($k, true) . " => " . var_export($v, true)
3470
							. ". Each many_many_extraFields entry should map to a field specification array.");
3471
					}
3472
				} else {
3473
					if(!is_string($k) || is_numeric($k) || !is_string($v)) {
3474
						throw new LogicException("$this->class::$defType has a bad entry: "
3475
							. var_export($k, true). " => " . var_export($v, true) . ".  Each map key should be a
3476
							 relationship name, and the map value should be the data class to join to.");
3477
					}
3478
				}
3479
			}
3480
		}
3481
	}
3482
3483
	/**
3484
	 * Add default records to database. This function is called whenever the
3485
	 * database is built, after the database tables have all been created. Overload
3486
	 * this to add default records when the database is built, but make sure you
3487
	 * call parent::requireDefaultRecords().
3488
	 *
3489
	 * @uses DataExtension->requireDefaultRecords()
3490
	 */
3491
	public function requireDefaultRecords() {
3492
		$defaultRecords = $this->stat('default_records');
3493
3494
		if(!empty($defaultRecords)) {
3495
			$hasData = DataObject::get_one($this->class);
3496
			if(!$hasData) {
3497
				$className = $this->class;
3498
				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...
3499
					$obj = $this->model->$className->newObject($record);
3500
					$obj->write();
3501
				}
3502
				DB::alteration_message("Added default records to $className table","created");
3503
			}
3504
		}
3505
3506
		// Let any extentions make their own database default data
3507
		$this->extend('requireDefaultRecords', $dummy);
3508
	}
3509
3510
	/**
3511
	 * Get the default searchable fields for this object, as defined in the
3512
	 * $searchable_fields list. If searchable fields are not defined on the
3513
	 * data object, uses a default selection of summary fields.
3514
	 *
3515
	 * @return array
3516
	 */
3517
	public function searchableFields() {
3518
		// can have mixed format, need to make consistent in most verbose form
3519
		$fields = $this->stat('searchable_fields');
3520
		$labels = $this->fieldLabels();
3521
3522
		// fallback to summary fields (unless empty array is explicitly specified)
3523
		if( ! $fields && ! is_array($fields)) {
3524
			$summaryFields = array_keys($this->summaryFields());
3525
			$fields = array();
3526
3527
			// remove the custom getters as the search should not include them
3528
			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...
3529
				foreach($summaryFields as $key => $name) {
3530
					$spec = $name;
3531
3532
					// Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3533 View Code Duplication
					if(($fieldPos = strpos($name, '.')) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3534
						$name = substr($name, 0, $fieldPos);
3535
					}
3536
3537
					if($this->hasDatabaseField($name)) {
3538
						$fields[] = $name;
3539
					} elseif($this->relObject($spec)) {
3540
						$fields[] = $spec;
3541
					}
3542
				}
3543
			}
3544
		}
3545
3546
		// we need to make sure the format is unified before
3547
		// augmenting fields, so extensions can apply consistent checks
3548
		// but also after augmenting fields, because the extension
3549
		// might use the shorthand notation as well
3550
3551
		// rewrite array, if it is using shorthand syntax
3552
		$rewrite = array();
3553
		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...
3554
			$identifer = (is_int($name)) ? $specOrName : $name;
3555
3556
			if(is_int($name)) {
3557
				// Format: array('MyFieldName')
3558
				$rewrite[$identifer] = array();
3559
			} elseif(is_array($specOrName)) {
3560
				// Format: array('MyFieldName' => array(
3561
				//   'filter => 'ExactMatchFilter',
3562
				//   'field' => 'NumericField', // optional
3563
				//   'title' => 'My Title', // optional
3564
				// ))
3565
				$rewrite[$identifer] = array_merge(
3566
					array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3567
					(array)$specOrName
3568
				);
3569
			} else {
3570
				// Format: array('MyFieldName' => 'ExactMatchFilter')
3571
				$rewrite[$identifer] = array(
3572
					'filter' => $specOrName,
3573
				);
3574
			}
3575
			if(!isset($rewrite[$identifer]['title'])) {
3576
				$rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3577
					? $labels[$identifer] : FormField::name_to_label($identifer);
3578
			}
3579
			if(!isset($rewrite[$identifer]['filter'])) {
3580
				$rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3581
			}
3582
		}
3583
3584
		$fields = $rewrite;
3585
3586
		// apply DataExtensions if present
3587
		$this->extend('updateSearchableFields', $fields);
3588
3589
		return $fields;
3590
	}
3591
3592
	/**
3593
	 * Get any user defined searchable fields labels that
3594
	 * exist. Allows overriding of default field names in the form
3595
	 * interface actually presented to the user.
3596
	 *
3597
	 * The reason for keeping this separate from searchable_fields,
3598
	 * which would be a logical place for this functionality, is to
3599
	 * avoid bloating and complicating the configuration array. Currently
3600
	 * much of this system is based on sensible defaults, and this property
3601
	 * would generally only be set in the case of more complex relationships
3602
	 * between data object being required in the search interface.
3603
	 *
3604
	 * Generates labels based on name of the field itself, if no static property
3605
	 * {@link self::field_labels} exists.
3606
	 *
3607
	 * @uses $field_labels
3608
	 * @uses FormField::name_to_label()
3609
	 *
3610
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3611
	 *
3612
	 * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3613
	 */
3614
	public function fieldLabels($includerelations = true) {
3615
		$cacheKey = $this->class . '_' . $includerelations;
3616
3617
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
3618
			$customLabels = $this->stat('field_labels');
3619
			$autoLabels = array();
3620
3621
			// get all translated static properties as defined in i18nCollectStatics()
3622
			$ancestry = ClassInfo::ancestry($this->class);
3623
			$ancestry = array_reverse($ancestry);
3624
			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...
3625
				if($ancestorClass == 'ViewableData') break;
3626
				$types = array(
3627
					'db'        => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3628
				);
3629
				if($includerelations){
3630
					$types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3631
					$types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3632
					$types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3633
					$types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3634
				}
3635
				foreach($types as $type => $attrs) {
3636
					foreach($attrs as $name => $spec) {
3637
						$autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}",FormField::name_to_label($name));
3638
					}
3639
				}
3640
			}
3641
3642
			$labels = array_merge((array)$autoLabels, (array)$customLabels);
3643
			$this->extend('updateFieldLabels', $labels);
3644
			self::$_cache_field_labels[$cacheKey] = $labels;
3645
		}
3646
3647
		return self::$_cache_field_labels[$cacheKey];
3648
	}
3649
3650
	/**
3651
	 * Get a human-readable label for a single field,
3652
	 * see {@link fieldLabels()} for more details.
3653
	 *
3654
	 * @uses fieldLabels()
3655
	 * @uses FormField::name_to_label()
3656
	 *
3657
	 * @param string $name Name of the field
3658
	 * @return string Label of the field
3659
	 */
3660
	public function fieldLabel($name) {
3661
		$labels = $this->fieldLabels();
3662
		return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3663
	}
3664
3665
	/**
3666
	 * Get the default summary fields for this object.
3667
	 *
3668
	 * @todo use the translation apparatus to return a default field selection for the language
3669
	 *
3670
	 * @return array
3671
	 */
3672
	public function summaryFields() {
3673
		$fields = $this->stat('summary_fields');
3674
3675
		// if fields were passed in numeric array,
3676
		// convert to an associative array
3677
		if($fields && array_key_exists(0, $fields)) {
3678
			$fields = array_combine(array_values($fields), array_values($fields));
3679
		}
3680
3681
		if (!$fields) {
3682
			$fields = array();
3683
			// try to scaffold a couple of usual suspects
3684
			if ($this->hasField('Name')) $fields['Name'] = 'Name';
3685
			if ($this->hasDataBaseField('Title')) $fields['Title'] = 'Title';
3686
			if ($this->hasField('Description')) $fields['Description'] = 'Description';
3687
			if ($this->hasField('FirstName')) $fields['FirstName'] = 'First Name';
3688
		}
3689
		$this->extend("updateSummaryFields", $fields);
3690
3691
		// Final fail-over, just list ID field
3692
		if(!$fields) $fields['ID'] = 'ID';
3693
3694
		// Localize fields (if possible)
3695
		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...
3696
			// only attempt to localize if the label definition is the same as the field name.
3697
			// this will preserve any custom labels set in the summary_fields configuration
3698
			if(isset($fields[$name]) && $name === $fields[$name]) {
3699
				$fields[$name] = $label;
3700
			}
3701
		}
3702
3703
		return $fields;
3704
	}
3705
3706
	/**
3707
	 * Defines a default list of filters for the search context.
3708
	 *
3709
	 * If a filter class mapping is defined on the data object,
3710
	 * it is constructed here. Otherwise, the default filter specified in
3711
	 * {@link DBField} is used.
3712
	 *
3713
	 * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3714
	 *
3715
	 * @return array
3716
	 */
3717
	public function defaultSearchFilters() {
3718
		$filters = array();
3719
3720
		foreach($this->searchableFields() as $name => $spec) {
3721
			$filterClass = $spec['filter'];
0 ignored issues
show
Unused Code introduced by
$filterClass is not used, you could remove the assignment.

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

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

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

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

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