Completed
Push — namespace-model ( 9b3f38...c67c40 )
by Sam
16:21 queued 05:15
created

DataObject::getComponents()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 38
Code Lines 23

Duplication

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

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
584
				if ($relations->Count() > 0) {  //with more than one thing it is related to
585
					foreach($relations as $relation) {
586
						$destinationObject->$name()->add($relation);
587
					}
588
				}
589
			} else {    //one-to-one relation
590
				$destinationObject->{"{$name}ID"} = $relations->ID;
591
			}
592
		}
593
	}
594
595
	public function getObsoleteClassName() {
596
		$className = $this->getField("ClassName");
597
		if (!ClassInfo::exists($className)) return $className;
598
	}
599
600
	public function getClassName() {
601
		$className = $this->getField("ClassName");
602
		if (!ClassInfo::exists($className)) return get_class($this);
603
		return $className;
604
	}
605
606
	/**
607
	 * Set the ClassName attribute. {@link $class} is also updated.
608
	 * Warning: This will produce an inconsistent record, as the object
609
	 * instance will not automatically switch to the new subclass.
610
	 * Please use {@link newClassInstance()} for this purpose,
611
	 * or destroy and reinstanciate the record.
612
	 *
613
	 * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
614
	 * @return DataObject $this
615
	 */
616
	public function setClassName($className) {
617
		$className = trim($className);
618
		if(!$className || !is_subclass_of($className, 'SilverStripe\Model\DataObject')) return;
619
620
		$this->class = $className;
621
		$this->setField("ClassName", $className);
622
		return $this;
623
	}
624
625
	/**
626
	 * Create a new instance of a different class from this object's record.
627
	 * This is useful when dynamically changing the type of an instance. Specifically,
628
	 * it ensures that the instance of the class is a match for the className of the
629
	 * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
630
	 * property manually before calling this method, as it will confuse change detection.
631
	 *
632
	 * If the new class is different to the original class, defaults are populated again
633
	 * because this will only occur automatically on instantiation of a DataObject if
634
	 * there is no record, or the record has no ID. In this case, we do have an ID but
635
	 * we still need to repopulate the defaults.
636
	 *
637
	 * @param string $newClassName The name of the new class
638
	 *
639
	 * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
640
	 */
641
	public function newClassInstance($newClassName) {
642
		$originalClass = $this->ClassName;
643
		$newInstance = new $newClassName(array_merge(
644
			$this->record,
645
			array(
646
				'ClassName' => $originalClass,
647
				'RecordClassName' => $originalClass,
648
			)
649
		), false, $this->model);
650
651
		if($newClassName != $originalClass) {
652
			$newInstance->setClassName($newClassName);
653
			$newInstance->populateDefaults();
654
			$newInstance->forceChange();
655
		}
656
657
		return $newInstance;
658
	}
659
660
	/**
661
	 * Adds methods from the extensions.
662
	 * Called by Object::__construct() once per class.
663
	 */
664
	public function defineMethods() {
665
		parent::defineMethods();
666
667
		// Define the extra db fields - this is only necessary for extensions added in the
668
		// class definition.  Object::add_extension() will call this at definition time for
669
		// those objects, which is a better mechanism.  Perhaps extensions defined inside the
670
		// class def can somehow be applied at definiton time also?
671
		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...
672
			if(!$instance->class) {
673
				$class = get_class($instance);
674
				user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls"
675
					. " parent::__construct()", E_USER_ERROR);
676
			}
677
		}
678
679
		if($this->class == 'SilverStripe\Model\DataObject') return;
680
681
		// Set up accessors for joined items
682
		if($manyMany = $this->manyMany()) {
683
			foreach($manyMany as $relationship => $class) {
684
				$this->addWrapperMethod($relationship, 'getManyManyComponents');
685
			}
686
		}
687
		if($hasMany = $this->hasMany()) {
688
689
			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...
690
				$this->addWrapperMethod($relationship, 'getComponents');
691
			}
692
693
		}
694
		if($hasOne = $this->hasOne()) {
695
			foreach($hasOne as $relationship => $class) {
696
				$this->addWrapperMethod($relationship, 'getComponent');
697
			}
698
		}
699
		if($belongsTo = $this->belongsTo()) foreach(array_keys($belongsTo) as $relationship) {
700
			$this->addWrapperMethod($relationship, 'getComponent');
701
		}
702
	}
703
704
	/**
705
	 * Returns true if this object "exists", i.e., has a sensible value.
706
	 * The default behaviour for a DataObject is to return true if
707
	 * the object exists in the database, you can override this in subclasses.
708
	 *
709
	 * @return boolean true if this object exists
710
	 */
711
	public function exists() {
712
		return (isset($this->record['ID']) && $this->record['ID'] > 0);
713
	}
714
715
	/**
716
	 * Returns TRUE if all values (other than "ID") are
717
	 * considered empty (by weak boolean comparison).
718
	 *
719
	 * @return boolean
720
	 */
721
	public function isEmpty() {
722
		$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...
723
		foreach($this->toMap() as $field => $value){
724
			// only look at custom fields
725
			if(isset($fixed[$field])) {
726
				continue;
727
			}
728
729
			$dbObject = $this->dbObject($field);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $dbObject is correct as $this->dbObject($field) (which targets SilverStripe\Model\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...
730
			if(!$dbObject) {
731
				continue;
732
			}
733
			if($dbObject->exists()) {
734
				return false;
735
			}
736
		}
737
		return true;
738
	}
739
740
	/**
741
	 * Pluralise this item given a specific count.
742
	 *
743
	 * E.g. "0 Pages", "1 File", "3 Images"
744
	 *
745
	 * @param string $count
746
	 * @param bool $prependNumber Include number in result. Defaults to true.
747
	 * @return string
748
	 */
749
	public function i18n_pluralise($count, $prependNumber = true) {
750
		return i18n::pluralise(
751
			$this->i18n_singular_name(),
752
			$this->i18n_plural_name(),
753
			$count,
754
			$prependNumber
755
		);
756
	}
757
758
	/**
759
	 * Get the user friendly singular name of this DataObject.
760
	 * If the name is not defined (by redefining $singular_name in the subclass),
761
	 * this returns the class name.
762
	 *
763
	 * @return string User friendly singular name of this DataObject
764
	 */
765
	public function singular_name() {
766
		if(!$name = $this->stat('singular_name')) {
767
			$name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $this->class))));
768
		}
769
770
		return $name;
771
	}
772
773
	/**
774
	 * Get the translated user friendly singular name of this DataObject
775
	 * same as singular_name() but runs it through the translating function
776
	 *
777
	 * Translating string is in the form:
778
	 *     $this->class.SINGULARNAME
779
	 * Example:
780
	 *     Page.SINGULARNAME
781
	 *
782
	 * @return string User friendly translated singular name of this DataObject
783
	 */
784
	public function i18n_singular_name() {
785
		return _t($this->class.'.SINGULARNAME', $this->singular_name());
786
	}
787
788
	/**
789
	 * Get the user friendly plural name of this DataObject
790
	 * If the name is not defined (by renaming $plural_name in the subclass),
791
	 * this returns a pluralised version of the class name.
792
	 *
793
	 * @return string User friendly plural name of this DataObject
794
	 */
795
	public function plural_name() {
796
		if($name = $this->stat('plural_name')) {
797
			return $name;
798
		} else {
799
			$name = $this->singular_name();
800
			//if the penultimate character is not a vowel, replace "y" with "ies"
801
			if (preg_match('/[^aeiou]y$/i', $name)) {
802
				$name = substr($name,0,-1) . 'ie';
803
			}
804
			return ucfirst($name . 's');
805
		}
806
	}
807
808
	/**
809
	 * Get the translated user friendly plural name of this DataObject
810
	 * Same as plural_name but runs it through the translation function
811
	 * Translation string is in the form:
812
	 *      $this->class.PLURALNAME
813
	 * Example:
814
	 *      Page.PLURALNAME
815
	 *
816
	 * @return string User friendly translated plural name of this DataObject
817
	 */
818
	public function i18n_plural_name()
819
	{
820
		$name = $this->plural_name();
821
		return _t($this->class.'.PLURALNAME', $name);
822
	}
823
824
	/**
825
	 * Standard implementation of a title/label for a specific
826
	 * record. Tries to find properties 'Title' or 'Name',
827
	 * and falls back to the 'ID'. Useful to provide
828
	 * user-friendly identification of a record, e.g. in errormessages
829
	 * or UI-selections.
830
	 *
831
	 * Overload this method to have a more specialized implementation,
832
	 * e.g. for an Address record this could be:
833
	 * <code>
834
	 * function getTitle() {
835
	 *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
836
	 * }
837
	 * </code>
838
	 *
839
	 * @return string
840
	 */
841
	public function getTitle() {
842
		if($this->hasDatabaseField('Title')) return $this->getField('Title');
843
		if($this->hasDatabaseField('Name')) return $this->getField('Name');
844
845
		return "#{$this->ID}";
846
	}
847
848
	/**
849
	 * Returns the associated database record - in this case, the object itself.
850
	 * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
851
	 *
852
	 * @return DataObject Associated database record
853
	 */
854
	public function data() {
855
		return $this;
856
	}
857
858
	/**
859
	 * Convert this object to a map.
860
	 *
861
	 * @return array The data as a map.
862
	 */
863
	public function toMap() {
864
		$this->loadLazyFields();
865
		return $this->record;
866
	}
867
868
	/**
869
	 * Return all currently fetched database fields.
870
	 *
871
	 * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
872
	 * Obviously, this makes it a lot faster.
873
	 *
874
	 * @return array The data as a map.
875
	 */
876
	public function getQueriedDatabaseFields() {
877
		return $this->record;
878
	}
879
880
	/**
881
	 * Update a number of fields on this object, given a map of the desired changes.
882
	 *
883
	 * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
884
	 * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
885
	 *
886
	 * update() doesn't write the main object, but if you use the dot syntax, it will write()
887
	 * the related objects that it alters.
888
	 *
889
	 * @param array $data A map of field name to data values to update.
890
	 * @return DataObject $this
891
	 */
892
	public function update($data) {
893
		foreach($data as $k => $v) {
894
			// Implement dot syntax for updates
895
			if(strpos($k,'.') !== false) {
896
				$relations = explode('.', $k);
897
				$fieldName = array_pop($relations);
898
				$relObj = $this;
899
				foreach($relations as $i=>$relation) {
900
					// no support for has_many or many_many relationships,
901
					// as the updater wouldn't know which object to write to (or create)
902
					if($relObj->$relation() instanceof DataObject) {
903
						$parentObj = $relObj;
904
						$relObj = $relObj->$relation();
905
						// If the intermediate relationship objects have been created, then write them
906
						if($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
907
							$relObj->write();
908
							$relatedFieldName = $relation."ID";
909
							$parentObj->$relatedFieldName = $relObj->ID;
910
							$parentObj->write();
911
						}
912
					} else {
913
						user_error(
914
							"DataObject::update(): Can't traverse relationship '$relation'," .
915
							"it has to be a has_one relationship or return a single DataObject",
916
							E_USER_NOTICE
917
						);
918
						// unset relation object so we don't write properties to the wrong object
919
						unset($relObj);
920
						break;
921
					}
922
				}
923
924
				if($relObj) {
925
					$relObj->$fieldName = $v;
926
					$relObj->write();
927
					$relatedFieldName = $relation."ID";
0 ignored issues
show
Bug introduced by
The variable $relation seems to be defined by a foreach iteration on line 899. 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...
928
					$this->$relatedFieldName = $relObj->ID;
929
					$relObj->flushCache();
930
				} else {
931
					user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
932
				}
933
			} else {
934
				$this->$k = $v;
935
			}
936
		}
937
		return $this;
938
	}
939
940
	/**
941
	 * Pass changes as a map, and try to
942
	 * get automatic casting for these fields.
943
	 * Doesn't write to the database. To write the data,
944
	 * use the write() method.
945
	 *
946
	 * @param array $data A map of field name to data values to update.
947
	 * @return DataObject $this
948
	 */
949
	public function castedUpdate($data) {
950
		foreach($data as $k => $v) {
951
			$this->setCastedField($k,$v);
952
		}
953
		return $this;
954
	}
955
956
	/**
957
	 * Merges data and relations from another object of same class,
958
	 * without conflict resolution. Allows to specify which
959
	 * dataset takes priority in case its not empty.
960
	 * has_one-relations are just transferred with priority 'right'.
961
	 * has_many and many_many-relations are added regardless of priority.
962
	 *
963
	 * Caution: has_many/many_many relations are moved rather than duplicated,
964
	 * meaning they are not connected to the merged object any longer.
965
	 * Caution: Just saves updated has_many/many_many relations to the database,
966
	 * doesn't write the updated object itself (just writes the object-properties).
967
	 * Caution: Does not delete the merged object.
968
	 * Caution: Does now overwrite Created date on the original object.
969
	 *
970
	 * @param $obj DataObject
971
	 * @param $priority String left|right Determines who wins in case of a conflict (optional)
972
	 * @param $includeRelations Boolean Merge any existing relations (optional)
973
	 * @param $overwriteWithEmpty Boolean Overwrite existing left values with empty right values.
974
	 *                            Only applicable with $priority='right'. (optional)
975
	 * @return Boolean
976
	 */
977
	public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) {
978
		$leftObj = $this;
979
980
		if($leftObj->ClassName != $rightObj->ClassName) {
981
			// we can't merge similiar subclasses because they might have additional relations
982
			user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
983
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
984
			return false;
985
		}
986
987
		if(!$rightObj->ID) {
988
			user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
989
				to make sure all relations are transferred properly.').", E_USER_WARNING);
990
			return false;
991
		}
992
993
		// makes sure we don't merge data like ID or ClassName
994
		$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...
995
		$rightData = $rightObj->db();
996
997
		foreach($rightData as $key=>$rightSpec) {
998
			// Don't merge ID
999
			if($key === 'ID') {
1000
				continue;
1001
			}
1002
1003
			// Only merge relations if allowed
1004
			if($rightSpec === 'ForeignKey' && !$includeRelations) {
1005
				continue;
1006
			}
1007
1008
			// don't merge conflicting values if priority is 'left'
1009
			if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
1010
				continue;
1011
			}
1012
1013
			// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
1014
			if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
1015
				continue;
1016
			}
1017
1018
			// TODO remove redundant merge of has_one fields
1019
			$leftObj->{$key} = $rightObj->{$key};
1020
		}
1021
1022
		// merge relations
1023
		if($includeRelations) {
1024 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...
1025
				foreach($manyMany as $relationship => $class) {
1026
					$leftComponents = $leftObj->getManyManyComponents($relationship);
1027
					$rightComponents = $rightObj->getManyManyComponents($relationship);
1028
					if($rightComponents && $rightComponents->exists()) {
1029
						$leftComponents->addMany($rightComponents->column('ID'));
1030
					}
1031
					$leftComponents->write();
1032
				}
1033
			}
1034
1035 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...
1036
				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...
1037
					$leftComponents = $leftObj->getComponents($relationship);
1038
					$rightComponents = $rightObj->getComponents($relationship);
1039
					if($rightComponents && $rightComponents->exists()) {
1040
						$leftComponents->addMany($rightComponents->column('ID'));
1041
					}
1042
					$leftComponents->write();
1043
				}
1044
1045
			}
1046
		}
1047
1048
		return true;
1049
	}
1050
1051
	/**
1052
	 * Forces the record to think that all its data has changed.
1053
	 * Doesn't write to the database. Only sets fields as changed
1054
	 * if they are not already marked as changed.
1055
	 *
1056
	 * @return DataObject $this
1057
	 */
1058
	public function forceChange() {
1059
		// Ensure lazy fields loaded
1060
		$this->loadLazyFields();
1061
1062
		// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1063
		$fieldNames = array_unique(array_merge(
1064
			array_keys($this->record),
1065
			array_keys($this->db())
1066
		));
1067
1068
		foreach($fieldNames as $fieldName) {
1069
			if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
1070
			// Populate the null values in record so that they actually get written
1071
			if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
1072
		}
1073
1074
		// @todo Find better way to allow versioned to write a new version after forceChange
1075
		if($this->isChanged('Version')) unset($this->changed['Version']);
1076
		return $this;
1077
	}
1078
1079
	/**
1080
	 * Validate the current object.
1081
	 *
1082
	 * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1083
	 * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1084
	 *
1085
	 * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1086
	 * and onAfterWrite() won't get called either.
1087
	 *
1088
	 * It is expected that you call validate() in your own application to test that an object is valid before
1089
	 * attempting a write, and respond appropriately if it isn't.
1090
	 *
1091
	 * @see {@link ValidationResult}
1092
	 * @return ValidationResult
1093
	 */
1094
	public function validate() {
1095
		$result = ValidationResult::create();
1096
		$this->extend('validate', $result);
1097
		return $result;
1098
	}
1099
1100
	/**
1101
	 * Public accessor for {@see DataObject::validate()}
1102
	 *
1103
	 * @return ValidationResult
1104
	 */
1105
	public function doValidate() {
1106
		Deprecation::notice('5.0', 'Use validate');
1107
		return $this->validate();
1108
	}
1109
1110
	/**
1111
	 * Event handler called before writing to the database.
1112
	 * You can overload this to clean up or otherwise process data before writing it to the
1113
	 * database.  Don't forget to call parent::onBeforeWrite(), though!
1114
	 *
1115
	 * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1116
	 *
1117
	 * @uses DataExtension->onBeforeWrite()
1118
	 */
1119
	protected function onBeforeWrite() {
1120
		$this->brokenOnWrite = false;
1121
1122
		$dummy = null;
1123
		$this->extend('onBeforeWrite', $dummy);
1124
	}
1125
1126
	/**
1127
	 * Event handler called after writing to the database.
1128
	 * You can overload this to act upon changes made to the data after it is written.
1129
	 * $this->changed will have a record
1130
	 * database.  Don't forget to call parent::onAfterWrite(), though!
1131
	 *
1132
	 * @uses DataExtension->onAfterWrite()
1133
	 */
1134
	protected function onAfterWrite() {
1135
		$dummy = null;
1136
		$this->extend('onAfterWrite', $dummy);
1137
	}
1138
1139
	/**
1140
	 * Event handler called before deleting from the database.
1141
	 * You can overload this to clean up or otherwise process data before delete this
1142
	 * record.  Don't forget to call parent::onBeforeDelete(), though!
1143
	 *
1144
	 * @uses DataExtension->onBeforeDelete()
1145
	 */
1146
	protected function onBeforeDelete() {
1147
		$this->brokenOnDelete = false;
1148
1149
		$dummy = null;
1150
		$this->extend('onBeforeDelete', $dummy);
1151
	}
1152
1153
	protected function onAfterDelete() {
1154
		$this->extend('onAfterDelete');
1155
	}
1156
1157
	/**
1158
	 * Load the default values in from the self::$defaults array.
1159
	 * Will traverse the defaults of the current class and all its parent classes.
1160
	 * Called by the constructor when creating new records.
1161
	 *
1162
	 * @uses DataExtension->populateDefaults()
1163
	 * @return DataObject $this
1164
	 */
1165
	public function populateDefaults() {
1166
		$classes = array_reverse(ClassInfo::ancestry($this));
1167
1168
		foreach($classes as $class) {
1169
			$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1170
1171
			if($defaults && !is_array($defaults)) {
1172
				user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
1173
					E_USER_WARNING);
1174
				$defaults = null;
1175
			}
1176
1177
			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...
1178
				// SRM 2007-03-06: Stricter check
1179
				if(!isset($this->$fieldName) || $this->$fieldName === null) {
1180
					$this->$fieldName = $fieldValue;
1181
				}
1182
				// Set many-many defaults with an array of ids
1183
				if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
1184
					$manyManyJoin = $this->$fieldName();
1185
					$manyManyJoin->setByIdList($fieldValue);
1186
				}
1187
			}
1188
			if($class == 'SilverStripe\Model\DataObject') {
1189
				break;
1190
			}
1191
		}
1192
1193
		$this->extend('populateDefaults');
1194
		return $this;
1195
	}
1196
1197
	/**
1198
	 * Determine validation of this object prior to write
1199
	 *
1200
	 * @return ValidationException Exception generated by this write, or null if valid
1201
	 */
1202
	protected function validateWrite() {
1203
		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...
1204
			return new ValidationException(
1205
				"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...
1206
				"you need to change the ClassName before you can write it",
1207
				E_USER_WARNING
1208
			);
1209
		}
1210
1211
		if(Config::inst()->get('SilverStripe\Model\DataObject', 'validation_enabled')) {
1212
			$result = $this->validate();
1213
			if (!$result->valid()) {
1214
				return new ValidationException(
1215
					$result,
1216
					$result->message(),
1217
					E_USER_WARNING
1218
				);
1219
			}
1220
		}
1221
	}
1222
1223
	/**
1224
	 * Prepare an object prior to write
1225
	 *
1226
	 * @throws ValidationException
1227
	 */
1228
	protected function preWrite() {
1229
		// Validate this object
1230
		if($writeException = $this->validateWrite()) {
1231
			// Used by DODs to clean up after themselves, eg, Versioned
1232
			$this->invokeWithExtensions('onAfterSkippedWrite');
1233
			throw $writeException;
1234
		}
1235
1236
		// Check onBeforeWrite
1237
		$this->brokenOnWrite = true;
1238
		$this->onBeforeWrite();
1239
		if($this->brokenOnWrite) {
1240
			user_error("$this->class has a broken onBeforeWrite() function."
1241
				. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1242
		}
1243
	}
1244
1245
	/**
1246
	 * Detects and updates all changes made to this object
1247
	 *
1248
	 * @param bool $forceChanges If set to true, force all fields to be treated as changed
1249
	 * @return bool True if any changes are detected
1250
	 */
1251
	protected function updateChanges($forceChanges = false) {
1252
		// Update the changed array with references to changed obj-fields
1253
		foreach($this->record as $field => $value) {
1254
			// Only mark ID as changed if $forceChanges
1255
			if($field === 'ID' && !$forceChanges) continue;
1256
			// Determine if this field should be forced, or can mark itself, changed
1257
			if($forceChanges
1258
				|| !$this->isInDB()
1259
				|| (is_object($value) && method_exists($value, 'isChanged') && $value->isChanged())
1260
			) {
1261
				$this->changed[$field] = self::CHANGE_VALUE;
1262
			}
1263
		}
1264
1265
		// Check changes exist, abort if there are no changes
1266
		return $this->changed && (bool)array_filter($this->changed);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->changed 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...
1267
	}
1268
1269
	/**
1270
	 * Writes a subset of changes for a specific table to the given manipulation
1271
	 *
1272
	 * @param string $baseTable Base table
1273
	 * @param string $now Timestamp to use for the current time
1274
	 * @param bool $isNewRecord Whether this should be treated as a new record write
1275
	 * @param array $manipulation Manipulation to write to
1276
	 * @param string $class Table and Class to select and write to
1277
	 */
1278
	protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
1279
		$manipulation[$class] = array();
1280
1281
		// Extract records for this table
1282
		foreach($this->record as $fieldName => $fieldValue) {
1283
1284
			// Check if this record pertains to this table, and
1285
			// we're not attempting to reset the BaseTable->ID
1286
			if(	empty($this->changed[$fieldName])
1287
				|| ($class === $baseTable && $fieldName === 'ID')
1288
				|| (!self::has_own_table_database_field($class, $fieldName)
1289
					&& !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...
1290
			) {
1291
				continue;
1292
			}
1293
1294
1295
			// if database column doesn't correlate to a DBField instance...
1296
			$fieldObj = $this->dbObject($fieldName);
1297
			if(!$fieldObj) {
1298
				$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1299
			}
1300
1301
			// Write to manipulation
1302
			$fieldObj->writeToManipulation($manipulation[$class]);
1303
		}
1304
1305
		// Ensure update of Created and LastEdited columns
1306
		if($baseTable === $class) {
1307
			$manipulation[$class]['fields']['LastEdited'] = $now;
1308
			if($isNewRecord) {
1309
				$manipulation[$class]['fields']['Created']
1310
					= empty($this->record['Created'])
1311
						? $now
1312
						: $this->record['Created'];
1313
				$manipulation[$class]['fields']['ClassName'] = $this->class;
1314
			}
1315
		}
1316
1317
		// Inserts done one the base table are performed in another step, so the manipulation should instead
1318
		// attempt an update, as though it were a normal update.
1319
		$manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update';
1320
		$manipulation[$class]['id'] = $this->record['ID'];
1321
	}
1322
1323
	/**
1324
	 * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1325
	 *
1326
	 * Does nothing if an ID is already assigned for this record
1327
	 *
1328
	 * @param string $baseTable Base table
1329
	 * @param string $now Timestamp to use for the current time
1330
	 */
1331
	protected function writeBaseRecord($baseTable, $now) {
1332
		// Generate new ID if not specified
1333
		if($this->isInDB()) return;
1334
1335
		// Perform an insert on the base table
1336
		$insert = new SQLInsert('"'.$baseTable.'"');
1337
		$insert
1338
			->assign('"Created"', $now)
1339
			->execute();
1340
		$this->changed['ID'] = self::CHANGE_VALUE;
1341
		$this->record['ID'] = DB::get_generated_id($baseTable);
1342
	}
1343
1344
	/**
1345
	 * Generate and write the database manipulation for all changed fields
1346
	 *
1347
	 * @param string $baseTable Base table
1348
	 * @param string $now Timestamp to use for the current time
1349
	 * @param bool $isNewRecord If this is a new record
1350
	 */
1351
	protected function writeManipulation($baseTable, $now, $isNewRecord) {
1352
		// Generate database manipulations for each class
1353
		$manipulation = array();
1354
		foreach($this->getClassAncestry() as $class) {
1355
			if(self::has_own_table($class)) {
1356
				$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1357
			}
1358
		}
1359
1360
		// Allow extensions to extend this manipulation
1361
		$this->extend('augmentWrite', $manipulation);
1362
1363
		// New records have their insert into the base data table done first, so that they can pass the
1364
		// generated ID on to the rest of the manipulation
1365
		if($isNewRecord) {
1366
			$manipulation[$baseTable]['command'] = 'update';
1367
		}
1368
1369
		// Perform the manipulation
1370
		DB::manipulate($manipulation);
1371
	}
1372
1373
	/**
1374
	 * Writes all changes to this object to the database.
1375
	 *  - It will insert a record whenever ID isn't set, otherwise update.
1376
	 *  - All relevant tables will be updated.
1377
	 *  - $this->onBeforeWrite() gets called beforehand.
1378
	 *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1379
	 *
1380
	 *  @uses DataExtension->augmentWrite()
1381
	 *
1382
	 * @param boolean $showDebug Show debugging information
1383
	 * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1384
	 * @param boolean $forceWrite Write to database even if there are no changes
1385
	 * @param boolean $writeComponents Call write() on all associated component instances which were previously
1386
	 *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1387
	 *                                 {@link getManyManyComponents()} (Default: false)
1388
	 * @return int The ID of the record
1389
	 * @throws ValidationException Exception that can be caught and handled by the calling function
1390
	 */
1391
	public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1392
		$now = DBDatetime::now()->Rfc2822();
1393
1394
		// Execute pre-write tasks
1395
		$this->preWrite();
1396
1397
		// Check if we are doing an update or an insert
1398
		$isNewRecord = !$this->isInDB() || $forceInsert;
1399
1400
		// Check changes exist, abort if there are none
1401
		$hasChanges = $this->updateChanges($forceInsert);
1402
		if($hasChanges || $forceWrite || $isNewRecord) {
1403
			// New records have their insert into the base data table done first, so that they can pass the
1404
			// generated primary key on to the rest of the manipulation
1405
			$baseTable = ClassInfo::baseDataClass($this->class);
1406
			$this->writeBaseRecord($baseTable, $now);
1407
1408
			// Write the DB manipulation for all changed fields
1409
			$this->writeManipulation($baseTable, $now, $isNewRecord);
1410
1411
			// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1412
			$this->writeRelations();
1413
			$this->onAfterWrite();
1414
			$this->changed = array();
1415
		} else {
1416
			if($showDebug) Debug::message("no changes for DataObject");
1417
1418
			// Used by DODs to clean up after themselves, eg, Versioned
1419
			$this->invokeWithExtensions('onAfterSkippedWrite');
1420
		}
1421
1422
		// Ensure Created and LastEdited are populated
1423
		if(!isset($this->record['Created'])) {
1424
			$this->record['Created'] = $now;
1425
		}
1426
		$this->record['LastEdited'] = $now;
1427
1428
		// Write relations as necessary
1429
		if($writeComponents) $this->writeComponents(true);
1430
1431
		// Clears the cache for this object so get_one returns the correct object.
1432
		$this->flushCache();
1433
1434
		return $this->record['ID'];
1435
	}
1436
1437
	/**
1438
	 * Writes cached relation lists to the database, if possible
1439
	 */
1440
	public function writeRelations() {
1441
		if(!$this->isInDB()) return;
1442
1443
		// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1444
		if($this->unsavedRelations) {
1445
			foreach($this->unsavedRelations as $name => $list) {
1446
				$list->changeToList($this->$name());
1447
			}
1448
			$this->unsavedRelations = array();
1449
		}
1450
	}
1451
1452
	/**
1453
	 * Write the cached components to the database. Cached components could refer to two different instances of the
1454
	 * same record.
1455
	 *
1456
	 * @param $recursive Recursively write components
1457
	 * @return DataObject $this
1458
	 */
1459
	public function writeComponents($recursive = false) {
1460
		if(!$this->components) return $this;
1461
1462
		foreach($this->components as $component) {
1463
			$component->write(false, false, false, $recursive);
1464
		}
1465
		return $this;
1466
	}
1467
1468
	/**
1469
	 * Delete this data object.
1470
	 * $this->onBeforeDelete() gets called.
1471
	 * Note that in Versioned objects, both Stage and Live will be deleted.
1472
	 *  @uses DataExtension->augmentSQL()
1473
	 */
1474
	public function delete() {
1475
		$this->brokenOnDelete = true;
1476
		$this->onBeforeDelete();
1477
		if($this->brokenOnDelete) {
1478
			user_error("$this->class has a broken onBeforeDelete() function."
1479
				. " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1480
		}
1481
1482
		// Deleting a record without an ID shouldn't do anything
1483
		if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1484
1485
		// TODO: This is quite ugly.  To improve:
1486
		//  - move the details of the delete code in the DataQuery system
1487
		//  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1488
		//    obviously, that means getting requireTable() to configure cascading deletes ;-)
1489
		$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1490
		foreach($srcQuery->queriedTables() as $table) {
1491
			$delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1492
			$delete->execute();
1493
		}
1494
		// Remove this item out of any caches
1495
		$this->flushCache();
1496
1497
		$this->onAfterDelete();
1498
1499
		$this->OldID = $this->ID;
0 ignored issues
show
Documentation introduced by
The property OldID does not exist on object<SilverStripe\Model\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...
1500
		$this->ID = 0;
1501
	}
1502
1503
	/**
1504
	 * Delete the record with the given ID.
1505
	 *
1506
	 * @param string $className The class name of the record to be deleted
1507
	 * @param int $id ID of record to be deleted
1508
	 */
1509
	public static function delete_by_id($className, $id) {
1510
		$obj = DataObject::get_by_id($className, $id);
1511
		if($obj) {
1512
			$obj->delete();
1513
		} else {
1514
			user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1515
		}
1516
	}
1517
1518
	/**
1519
	 * Get the class ancestry, including the current class name.
1520
	 * The ancestry will be returned as an array of class names, where the 0th element
1521
	 * will be the class that inherits directly from DataObject, and the last element
1522
	 * will be the current class.
1523
	 *
1524
	 * @return array Class ancestry
1525
	 */
1526
	public function getClassAncestry() {
1527
		if(!isset(self::$_cache_get_class_ancestry[$this->class])) {
1528
			self::$_cache_get_class_ancestry[$this->class] = array($this->class);
1529
			while(($class=get_parent_class(self::$_cache_get_class_ancestry[$this->class][0])) != 'SilverStripe\Model\DataObject') {
1530
				array_unshift(self::$_cache_get_class_ancestry[$this->class], $class);
1531
			}
1532
		}
1533
		return self::$_cache_get_class_ancestry[$this->class];
1534
	}
1535
1536
	/**
1537
	 * Return a component object from a one to one relationship, as a DataObject.
1538
	 * If no component is available, an 'empty component' will be returned for
1539
	 * non-polymorphic relations, or for polymorphic relations with a class set.
1540
	 *
1541
	 * @param string $componentName Name of the component
1542
	 * @return DataObject The component object. It's exact type will be that of the component.
1543
	 * @throws Exception
1544
	 */
1545
	public function getComponent($componentName) {
1546
		if(isset($this->components[$componentName])) {
1547
			return $this->components[$componentName];
1548
		}
1549
1550
		if($class = $this->hasOneComponent($componentName)) {
1551
			$joinField = $componentName . 'ID';
1552
			$joinID    = $this->getField($joinField);
1553
1554
			// Extract class name for polymorphic relations
1555
			if($class === 'SilverStripe\Model\DataObject') {
1556
				$class = $this->getField($componentName . 'Class');
1557
				if(empty($class)) return null;
1558
			}
1559
1560
			if($joinID) {
1561
				// Ensure that the selected object originates from the same stage, subsite, etc
1562
				$component = DataObject::get($class)
1563
					->filter('ID', $joinID)
1564
					->setDataQueryParam($this->getInheritableQueryParams())
1565
					->first();
1566
			}
1567
1568
			if(empty($component)) {
1569
				$component = $this->model->$class->newObject();
1570
			}
1571
		} elseif($class = $this->belongsToComponent($componentName)) {
1572
			$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
1573
			$joinID = $this->ID;
1574
1575
			if($joinID) {
1576
				// Prepare filter for appropriate join type
1577
				if($polymorphic) {
1578
					$filter = array(
1579
						"{$joinField}ID" => $joinID,
1580
						"{$joinField}Class" => $this->class
1581
					);
1582
				} else {
1583
					$filter = array(
1584
						$joinField => $joinID
1585
					);
1586
				}
1587
1588
				// Ensure that the selected object originates from the same stage, subsite, etc
1589
				$component = DataObject::get($class)
1590
					->filter($filter)
1591
					->setDataQueryParam($this->getInheritableQueryParams())
1592
					->first();
1593
			}
1594
1595
			if(empty($component)) {
1596
				$component = $this->model->$class->newObject();
1597
				if($polymorphic) {
1598
					$component->{$joinField.'ID'} = $this->ID;
1599
					$component->{$joinField.'Class'} = $this->class;
1600
				} else {
1601
					$component->$joinField = $this->ID;
1602
				}
1603
			}
1604
		} else {
1605
			throw new InvalidArgumentException(
1606
				"DataObject->getComponent(): Could not find component '$componentName'."
1607
			);
1608
		}
1609
1610
		$this->components[$componentName] = $component;
1611
		return $component;
1612
	}
1613
1614
	/**
1615
	 * Returns a one-to-many relation as a HasManyList
1616
	 *
1617
	 * @param string $componentName Name of the component
1618
	 * @return HasManyList The components of the one-to-many relationship.
1619
	 */
1620
	public function getComponents($componentName) {
1621
		$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...
1622
1623
		$componentClass = $this->hasManyComponent($componentName);
1624
		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...
1625
			throw new InvalidArgumentException(sprintf(
1626
				"DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1627
				$componentName,
1628
				$this->class
1629
			));
1630
		}
1631
1632
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1633 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...
1634
			if(!isset($this->unsavedRelations[$componentName])) {
1635
				$this->unsavedRelations[$componentName] =
1636
					new UnsavedRelationList($this->class, $componentName, $componentClass);
1637
			}
1638
			return $this->unsavedRelations[$componentName];
1639
		}
1640
1641
		// Determine type and nature of foreign relation
1642
		$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
1643
		/** @var HasManyList $result */
1644
		if($polymorphic) {
1645
			$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1646
		} else {
1647
			$result = HasManyList::create($componentClass, $joinField);
1648
		}
1649
1650
		if($this->model) {
1651
			$result->setDataModel($this->model);
1652
		}
1653
1654
		return $result
1655
			->setDataQueryParam($this->getInheritableQueryParams())
1656
			->forForeignID($this->ID);
1657
	}
1658
1659
	/**
1660
	 * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1661
	 *
1662
	 * @param string $relationName Relation name.
1663
	 * @return string Class name, or null if not found.
1664
	 */
1665
	public function getRelationClass($relationName) {
1666
		// Go through all relationship configuration fields.
1667
		$candidates = array_merge(
1668
			($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1669
			($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1670
			($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
1671
			($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
1672
			($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1673
		);
1674
1675
		if (isset($candidates[$relationName])) {
1676
			$remoteClass = $candidates[$relationName];
1677
1678
			// If dot notation is present, extract just the first part that contains the class.
1679 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...
1680
				return substr($remoteClass, 0, $fieldPos);
1681
			}
1682
1683
			// Otherwise just return the class
1684
			return $remoteClass;
1685
		}
1686
1687
		return null;
1688
	}
1689
1690
	/**
1691
	 * Given a relation name, determine the relation type
1692
	 *
1693
	 * @param string $component Name of component
1694
	 * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1695
	 */
1696
	public function getRelationType($component) {
1697
		$types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1698
		foreach($types as $type) {
1699
			$relations = Config::inst()->get($this->class, $type);
1700
			if($relations && isset($relations[$component])) {
1701
				return $type;
1702
			}
1703
		}
1704
		return null;
1705
	}
1706
1707
	/**
1708
	 * Given a relation declared on a remote class, generate a substitute component for the opposite
1709
	 * side of the relation.
1710
	 *
1711
	 * Notes on behaviour:
1712
	 *  - This can still be used on components that are defined on both sides, but do not need to be.
1713
	 *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1714
	 *  - Cannot be used on polymorphic relationships
1715
	 *  - Cannot be used on unsaved objects.
1716
	 *
1717
	 * @param string $remoteClass
1718
	 * @param string $remoteRelation
1719
	 * @return DataList|DataObject The component, either as a list or single object
1720
	 * @throws BadMethodCallException
1721
	 * @throws InvalidArgumentException
1722
	 */
1723
	public function inferReciprocalComponent($remoteClass, $remoteRelation) {
1724
		/** @var DataObject $remote */
1725
		$remote = $remoteClass::singleton();
1726
		$class = $remote->getRelationClass($remoteRelation);
1727
1728
		// Validate arguments
1729
		if(!$this->isInDB()) {
1730
			throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1731
		}
1732
		if(empty($class)) {
1733
			throw new InvalidArgumentException(sprintf(
1734
				"%s invoked with invalid relation %s.%s",
1735
				__METHOD__,
1736
				$remoteClass,
1737
				$remoteRelation
1738
			));
1739
		}
1740
		if($class === 'SilverStripe\Model\DataObject') {
1741
			throw new InvalidArgumentException(sprintf(
1742
				"%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1743
				"This method does not support polymorphic relationships",
1744
				__METHOD__,
1745
				$remoteClass,
1746
				$remoteRelation
1747
			));
1748
		}
1749
		if(!is_a($this, $class, true)) {
1750
			throw new InvalidArgumentException(sprintf(
1751
				"Relation %s on %s does not refer to objects of type %s",
1752
				$remoteRelation, $remoteClass, get_class($this)
1753
			));
1754
		}
1755
1756
		// Check the relation type to mock
1757
		$relationType = $remote->getRelationType($remoteRelation);
1758
		switch($relationType) {
1759
			case 'has_one': {
1760
				// Mock has_many
1761
				$joinField = "{$remoteRelation}ID";
1762
				$componentClass = ClassInfo::table_for_object_field($remoteClass, $joinField);
1763
				$result = HasManyList::create($componentClass, $joinField);
1764
				if ($this->model) {
1765
					$result->setDataModel($this->model);
1766
				}
1767
				return $result
1768
					->setDataQueryParam($this->getInheritableQueryParams())
1769
					->forForeignID($this->ID);
1770
			}
1771
			case 'belongs_to':
1772
			case 'has_many': {
1773
				// These relations must have a has_one on the other end, so find it
1774
				$joinField = $remote->getRemoteJoinField($remoteRelation, $relationType, $polymorphic);
1775
				if ($polymorphic) {
1776
					throw new InvalidArgumentException(sprintf(
1777
						"%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1778
						"to be a has_one polymorphic. This method does not support polymorphic relationships",
1779
						__METHOD__,
1780
						$remoteClass,
1781
						$remoteRelation
1782
					));
1783
				}
1784
				$joinID = $this->getField($joinField);
1785
				if (empty($joinID)) {
1786
					return null;
1787
				}
1788
				// Get object by joined ID
1789
				return DataObject::get($remoteClass)
1790
					->filter('ID', $joinID)
1791
					->setDataQueryParam($this->getInheritableQueryParams())
1792
					->first();
1793
			}
1794
			case 'many_many':
1795
			case 'belongs_many_many': {
1796
				// Get components and extra fields from parent
1797
				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...
1798
					= $remote->manyManyComponent($remoteRelation);
1799
				$extraFields = $remote->manyManyExtraFieldsForComponent($remoteRelation) ?: array();
1800
1801
				// Reverse parent and component fields and create an inverse ManyManyList
1802
				/** @var ManyManyList $result */
1803
				$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1804
				if($this->model) {
1805
					$result->setDataModel($this->model);
1806
				}
1807
				$this->extend('updateManyManyComponents', $result);
1808
1809
				// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1810
				// foreignID set elsewhere.
1811
				return $result
1812
					->setDataQueryParam($this->getInheritableQueryParams())
1813
					->forForeignID($this->ID);
1814
			}
1815
			default: {
1816
				return null;
1817
			}
1818
		}
1819
	}
1820
1821
	/**
1822
	 * Tries to find the database key on another object that is used to store a
1823
	 * relationship to this class. If no join field can be found it defaults to 'ParentID'.
1824
	 *
1825
	 * If the remote field is polymorphic then $polymorphic is set to true, and the return value
1826
	 * is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
1827
	 *
1828
	 * @param string $component Name of the relation on the current object pointing to the
1829
	 * remote object.
1830
	 * @param string $type the join type - either 'has_many' or 'belongs_to'
1831
	 * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
1832
	 * @return string
1833
	 * @throws Exception
1834
	 */
1835
	public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
1836
		// Extract relation from current object
1837
		if($type === 'has_many') {
1838
			$remoteClass = $this->hasManyComponent($component, false);
1839
		} else {
1840
			$remoteClass = $this->belongsToComponent($component, false);
1841
		}
1842
1843
		if(empty($remoteClass)) {
1844
			throw new Exception("Unknown $type component '$component' on class '$this->class'");
1845
		}
1846
		if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
1847
			throw new Exception(
1848
				"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
1849
			);
1850
		}
1851
1852
		// If presented with an explicit field name (using dot notation) then extract field name
1853
		$remoteField = null;
1854
		if(strpos($remoteClass, '.') !== false) {
1855
			list($remoteClass, $remoteField) = explode('.', $remoteClass);
1856
		}
1857
1858
		// Reference remote has_one to check against
1859
		$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
1860
1861
		// Without an explicit field name, attempt to match the first remote field
1862
		// with the same type as the current class
1863
		if(empty($remoteField)) {
1864
			// look for remote has_one joins on this class or any parent classes
1865
			$remoteRelationsMap = array_flip($remoteRelations);
1866
			foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1867
				if(array_key_exists($class, $remoteRelationsMap)) {
1868
					$remoteField = $remoteRelationsMap[$class];
1869
					break;
1870
				}
1871
			}
1872
		}
1873
1874
		// In case of an indeterminate remote field show an error
1875
		if(empty($remoteField)) {
1876
			$polymorphic = false;
1877
			$message = "No has_one found on class '$remoteClass'";
1878
			if($type == 'has_many') {
1879
				// include a hint for has_many that is missing a has_one
1880
				$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
1881
				$message .= " requires a has_one on '$remoteClass'";
1882
			}
1883
			throw new Exception($message);
1884
		}
1885
1886
		// If given an explicit field name ensure the related class specifies this
1887
		if(empty($remoteRelations[$remoteField])) {
1888
			throw new Exception("Missing expected has_one named '$remoteField'
1889
				on class '$remoteClass' referenced by $type named '$component'
1890
				on class {$this->class}"
1891
			);
1892
		}
1893
1894
		// Inspect resulting found relation
1895
		if($remoteRelations[$remoteField] === 'SilverStripe\Model\DataObject') {
1896
			$polymorphic = true;
1897
			return $remoteField; // Composite polymorphic field does not include 'ID' suffix
1898
		} else {
1899
			$polymorphic = false;
1900
			return $remoteField . 'ID';
1901
		}
1902
	}
1903
1904
	/**
1905
	 * Returns a many-to-many component, as a ManyManyList.
1906
	 * @param string $componentName Name of the many-many component
1907
	 * @return ManyManyList The set of components
1908
	 */
1909
	public function getManyManyComponents($componentName) {
1910
		$manyManyComponent = $this->manyManyComponent($componentName);
1911
		if(!$manyManyComponent) {
1912
			throw new InvalidArgumentException(sprintf(
1913
				"DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1914
				$componentName,
1915
				$this->class
1916
			));
1917
		}
1918
1919
		list($parentClass, $componentClass, $parentField, $componentField, $table) = $manyManyComponent;
1920
1921
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1922 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...
1923
			if(!isset($this->unsavedRelations[$componentName])) {
1924
				$this->unsavedRelations[$componentName] =
1925
					new UnsavedRelationList($parentClass, $componentName, $componentClass);
1926
			}
1927
			return $this->unsavedRelations[$componentName];
1928
		}
1929
1930
		$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
1931
		/** @var ManyManyList $result */
1932
		$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1933
		if($this->model) {
1934
			$result->setDataModel($this->model);
1935
		}
1936
1937
		$this->extend('updateManyManyComponents', $result);
1938
1939
		// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1940
		// foreignID set elsewhere.
1941
		return $result
1942
			->setDataQueryParam($this->getInheritableQueryParams())
1943
			->forForeignID($this->ID);
1944
	}
1945
1946
	/**
1947
	 * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1948
	 * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1949
	 *
1950
	 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1951
	 * 							their classes.
1952
	 */
1953
	public function hasOne() {
1954
		return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1955
	}
1956
1957
	/**
1958
	 * Return data for a specific has_one component.
1959
	 * @param string $component
1960
	 * @return string|null
1961
	 */
1962
	public function hasOneComponent($component) {
1963
		$classes = ClassInfo::ancestry($this, true);
1964
1965
		foreach(array_reverse($classes) as $class) {
1966
			$hasOnes = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
1967
			if(isset($hasOnes[$component])) {
1968
				return $hasOnes[$component];
1969
			}
1970
		}
1971
	}
1972
1973
	/**
1974
	 * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1975
	 * their class name will be returned.
1976
	 *
1977
	 * @param string $component - Name of component
1978
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1979
	 *        the field data stripped off. It defaults to TRUE.
1980
	 * @return string|array
1981
	 */
1982 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...
1983
		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...
1984
			Deprecation::notice(
1985
				'4.0',
1986
				'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
1987
				Deprecation::SCOPE_GLOBAL
1988
			);
1989
			return $this->belongsToComponent($component, $classOnly);
1990
		}
1991
1992
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1993
		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...
1994
			return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1995
		} else {
1996
			return $belongsTo ? $belongsTo : array();
1997
		}
1998
	}
1999
2000
	/**
2001
	 * Return data for a specific belongs_to component.
2002
	 * @param string $component
2003
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2004
	 *        the field data stripped off. It defaults to TRUE.
2005
	 * @return string|null
2006
	 */
2007 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...
2008
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
2009
2010
		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...
2011
			$belongsTo = $belongsTo[$component];
2012
		} else {
2013
			return null;
2014
		}
2015
2016
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
2017
	}
2018
2019
	/**
2020
	 * Return all of the database fields in this object
2021
	 *
2022
	 * @param string $fieldName Limit the output to a specific field name
2023
	 * @param string $includeTable If returning a single column, prefix the column with the table name
2024
	 * in Table.Column(spec) format
2025
	 * @return array|string|null The database fields, or if searching a single field, just this one field if found
2026
	 * Field will be a string in ClassName(args) format, or Table.ClassName(args) format if $includeTable is true
2027
	 */
2028
	public function db($fieldName = null, $includeTable = false) {
2029
		$classes = ClassInfo::ancestry($this, true);
2030
2031
		// If we're looking for a specific field, we want to hit subclasses first as they may override field types
2032
		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...
2033
			$classes = array_reverse($classes);
2034
		}
2035
2036
		$db = array();
2037
		foreach($classes as $class) {
2038
			// Merge fields with new fields and composite fields
2039
			$fields = self::database_fields($class);
2040
			$compositeFields = self::composite_fields($class, false);
2041
			$db = array_merge($db, $fields, $compositeFields);
2042
2043
			// Check for search field
2044
			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...
2045
				// Return found field
2046
				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...
2047
					return $db[$fieldName];
2048
				}
2049
2050
				// Set table for the given field
2051
				if(in_array($fieldName, $this->config()->fixed_fields)) {
2052
					$table = $this->baseTable();
2053
				} else {
2054
					$table = $class;
2055
				}
2056
				return $table . "." . $db[$fieldName];
2057
			}
2058
		}
2059
2060
		// At end of search complete
2061
		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...
2062
			return null;
2063
		} else {
2064
			return $db;
2065
		}
2066
	}
2067
2068
	/**
2069
	 * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2070
	 * relationships and their classes will be returned.
2071
	 *
2072
	 * @param string $component Deprecated - Name of component
2073
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2074
	 *        the field data stripped off. It defaults to TRUE.
2075
	 * @return string|array|false
2076
	 */
2077 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...
2078
		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...
2079
			Deprecation::notice(
2080
				'4.0',
2081
				'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
2082
				Deprecation::SCOPE_GLOBAL
2083
			);
2084
			return $this->hasManyComponent($component, $classOnly);
2085
		}
2086
2087
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2088
		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...
2089
			return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2090
		} else {
2091
			return $hasMany ? $hasMany : array();
2092
		}
2093
	}
2094
2095
	/**
2096
	 * Return data for a specific has_many component.
2097
	 * @param string $component
2098
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2099
	 *        the field data stripped off. It defaults to TRUE.
2100
	 * @return string|null
2101
	 */
2102 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...
2103
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2104
2105
		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...
2106
			$hasMany = $hasMany[$component];
2107
		} else {
2108
			return null;
2109
		}
2110
2111
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
2112
	}
2113
2114
	/**
2115
	 * Return the many-to-many extra fields specification.
2116
	 *
2117
	 * If you don't specify a component name, it returns all
2118
	 * extra fields for all components available.
2119
	 *
2120
	 * @return array|null
2121
	 */
2122
	public function manyManyExtraFields() {
2123
		return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2124
	}
2125
2126
	/**
2127
	 * Return the many-to-many extra fields specification for a specific component.
2128
	 * @param string $component
2129
	 * @return array|null
2130
	 */
2131
	public function manyManyExtraFieldsForComponent($component) {
2132
		// Get all many_many_extraFields defined in this class or parent classes
2133
		$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2134
		// Extra fields are immediately available
2135
		if(isset($extraFields[$component])) {
2136
			return $extraFields[$component];
2137
		}
2138
2139
		// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
2140
		$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2141
		$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2142
		if($candidate) {
2143
			$relationName = null;
2144
			// Extract class and relation name from dot-notation
2145 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...
2146
				list($candidate, $relationName) = explode('.', $candidate, 2);
2147
			}
2148
2149
			// If we've not already found the relation name from dot notation, we need to find a relation that points
2150
			// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
2151
			// so it's safe to assume that it's the correct one
2152
			if(!$relationName) {
2153
				$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2154
2155
				foreach($candidateManyManys as $relation => $relatedClass) {
2156
					if (is_a($this, $relatedClass)) {
2157
						$relationName = $relation;
2158
					}
2159
				}
2160
			}
2161
2162
			// If we've found a matching relation on the target class, see if we can find extra fields for it
2163
			$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
2164
			if(isset($extraFields[$relationName])) {
2165
				return $extraFields[$relationName];
2166
			}
2167
		}
2168
2169
		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...
2170
	}
2171
2172
	/**
2173
	 * Return information about a many-to-many component.
2174
	 * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2175
	 * components are returned.
2176
	 *
2177
	 * @see DataObject::manyManyComponent()
2178
	 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2179
	 */
2180
	public function manyMany() {
2181
		$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
2182
		$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2183
		$items = array_merge($manyManys, $belongsManyManys);
2184
		return $items;
2185
	}
2186
2187
	/**
2188
	 * Return information about a specific many_many component. Returns a numeric array of:
2189
	 * array(
2190
	 * 	<classname>,		The class that relation is defined in e.g. "Product"
2191
	 * 	<candidateName>,	The target class of the relation e.g. "Category"
2192
	 * 	<parentField>,		The field name pointing to <classname>'s table e.g. "ProductID"
2193
	 * 	<childField>,		The field name pointing to <candidatename>'s table e.g. "CategoryID"
2194
	 * 	<joinTable>			The join table between the two classes e.g. "Product_Categories"
2195
	 * )
2196
	 * @param string $component The component name
2197
	 * @return array|null
2198
	 */
2199
	public function manyManyComponent($component) {
2200
		$classes = $this->getClassAncestry();
2201
		foreach($classes as $class) {
2202
			$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
2203
			// Check if the component is defined in many_many on this class
2204
			$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2205
			if($candidate) {
2206
				$parentField = $class . "ID";
2207
				$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
2208
				return array($class, $candidate, $parentField, $childField, "{$class}_$component");
2209
			}
2210
2211
			// Check if the component is defined in belongs_many_many on this class
2212
			$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
2213
			$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
2214
			if($candidate) {
2215
				// Extract class and relation name from dot-notation
2216 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...
2217
					list($candidate, $relationName) = explode('.', $candidate, 2);
2218
				}
2219
2220
				$childField = $candidate . "ID";
2221
2222
				// We need to find the inverse component name
2223
				$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2224
				if(!$otherManyMany) {
2225
					throw new LogicException("Inverse component of $candidate not found ({$this->class})");
2226
				}
2227
2228
				// If we've got a relation name (extracted from dot-notation), we can already work out
2229
				// the join table and candidate class name...
2230
				if(isset($relationName) && isset($otherManyMany[$relationName])) {
2231
					$candidateClass = $otherManyMany[$relationName];
2232
					$joinTable = "{$candidate}_{$relationName}";
2233
				} else {
2234
					// ... otherwise, we need to loop over the many_manys and find a relation that
2235
					// matches up to this class
2236
					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...
2237
						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...
2238
							$joinTable = "{$candidate}_{$inverseComponentName}";
2239
							break;
2240
						}
2241
					}
2242
				}
2243
2244
				// If we could work out the join table, we've got all the info we need
2245
				if(isset($joinTable)) {
2246
					$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...
2247
					return array($class, $candidate, $parentField, $childField, $joinTable);
2248
				}
2249
2250
				throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
2251
			}
2252
		}
2253
	}
2254
2255
	/**
2256
	 * This returns an array (if it exists) describing the database extensions that are required, or false if none
2257
	 *
2258
	 * This is experimental, and is currently only a Postgres-specific enhancement.
2259
	 *
2260
	 * @return array or false
2261
	 */
2262
	public function database_extensions($class){
2263
		$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2264
2265
		if($extensions)
2266
			return $extensions;
2267
		else
2268
			return false;
2269
	}
2270
2271
	/**
2272
	 * Generates a SearchContext to be used for building and processing
2273
	 * a generic search form for properties on this object.
2274
	 *
2275
	 * @return SearchContext
2276
	 */
2277
	public function getDefaultSearchContext() {
2278
		return new SearchContext(
2279
			$this->class,
2280
			$this->scaffoldSearchFields(),
2281
			$this->defaultSearchFilters()
2282
		);
2283
	}
2284
2285
	/**
2286
	 * Determine which properties on the DataObject are
2287
	 * searchable, and map them to their default {@link FormField}
2288
	 * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2289
	 *
2290
	 * Some additional logic is included for switching field labels, based on
2291
	 * how generic or specific the field type is.
2292
	 *
2293
	 * Used by {@link SearchContext}.
2294
	 *
2295
	 * @param array $_params
2296
	 *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2297
	 *   'restrictFields': Numeric array of a field name whitelist
2298
	 * @return FieldList
2299
	 */
2300
	public function scaffoldSearchFields($_params = null) {
2301
		$params = array_merge(
2302
			array(
2303
				'fieldClasses' => false,
2304
				'restrictFields' => false
2305
			),
2306
			(array)$_params
2307
		);
2308
		$fields = new FieldList();
2309
		foreach($this->searchableFields() as $fieldName => $spec) {
2310
			if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
2311
2312
			// If a custom fieldclass is provided as a string, use it
2313
			if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2314
				$fieldClass = $params['fieldClasses'][$fieldName];
2315
				$field = new $fieldClass($fieldName);
2316
			// If we explicitly set a field, then construct that
2317
			} else if(isset($spec['field'])) {
2318
				// If it's a string, use it as a class name and construct
2319
				if(is_string($spec['field'])) {
2320
					$fieldClass = $spec['field'];
2321
					$field = new $fieldClass($fieldName);
2322
2323
				// If it's a FormField object, then just use that object directly.
2324
				} else if($spec['field'] instanceof FormField) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\Model\FormField does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
2325
					$field = $spec['field'];
2326
2327
				// Otherwise we have a bug
2328
				} else {
2329
					user_error("Bad value for searchable_fields, 'field' value: "
2330
						. var_export($spec['field'], true), E_USER_WARNING);
2331
				}
2332
2333
			// Otherwise, use the database field's scaffolder
2334
			} else {
2335
				$field = $this->relObject($fieldName)->scaffoldSearchField();
2336
			}
2337
2338
			// Allow fields to opt out of search
2339
			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...
2340
				continue;
2341
			}
2342
2343
			if (strstr($fieldName, '.')) {
2344
				$field->setName(str_replace('.', '__', $fieldName));
2345
			}
2346
			$field->setTitle($spec['title']);
2347
2348
			$fields->push($field);
2349
		}
2350
		return $fields;
2351
	}
2352
2353
	/**
2354
	 * Scaffold a simple edit form for all properties on this dataobject,
2355
	 * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2356
	 * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2357
	 *
2358
	 * @uses FormScaffolder
2359
	 *
2360
	 * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2361
	 * @return FieldList
2362
	 */
2363
	public function scaffoldFormFields($_params = null) {
2364
		$params = array_merge(
2365
			array(
2366
				'tabbed' => false,
2367
				'includeRelations' => false,
2368
				'restrictFields' => false,
2369
				'fieldClasses' => false,
2370
				'ajaxSafe' => false
2371
			),
2372
			(array)$_params
2373
		);
2374
2375
		$fs = new FormScaffolder($this);
2376
		$fs->tabbed = $params['tabbed'];
2377
		$fs->includeRelations = $params['includeRelations'];
2378
		$fs->restrictFields = $params['restrictFields'];
2379
		$fs->fieldClasses = $params['fieldClasses'];
2380
		$fs->ajaxSafe = $params['ajaxSafe'];
2381
2382
		return $fs->getFieldList();
2383
	}
2384
2385
	/**
2386
	 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2387
	 * being called on extensions
2388
	 *
2389
	 * @param callable $callback The callback to execute
2390
	 */
2391
	protected function beforeUpdateCMSFields($callback) {
2392
		$this->beforeExtending('updateCMSFields', $callback);
2393
	}
2394
2395
	/**
2396
	 * Centerpiece of every data administration interface in Silverstripe,
2397
	 * which returns a {@link FieldList} suitable for a {@link Form} object.
2398
	 * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2399
	 * generate this set. To customize, overload this method in a subclass
2400
	 * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2401
	 *
2402
	 * <code>
2403
	 * class MyCustomClass extends DataObject {
2404
	 *  static $db = array('CustomProperty'=>'Boolean');
2405
	 *
2406
	 *  function getCMSFields() {
2407
	 *    $fields = parent::getCMSFields();
2408
	 *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2409
	 *    return $fields;
2410
	 *  }
2411
	 * }
2412
	 * </code>
2413
	 *
2414
	 * @see Good example of complex FormField building: SiteTree::getCMSFields()
2415
	 *
2416
	 * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2417
	 */
2418
	public function getCMSFields() {
2419
		$tabbedFields = $this->scaffoldFormFields(array(
2420
			// Don't allow has_many/many_many relationship editing before the record is first saved
2421
			'includeRelations' => ($this->ID > 0),
2422
			'tabbed' => true,
2423
			'ajaxSafe' => true
2424
		));
2425
2426
		$this->extend('updateCMSFields', $tabbedFields);
2427
2428
		return $tabbedFields;
2429
	}
2430
2431
	/**
2432
	 * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2433
	 * including that dataobject's extensions customised actions could be added to the EditForm.
2434
	 *
2435
	 * @return an Empty FieldList(); need to be overload by solid subclass
2436
	 */
2437
	public function getCMSActions() {
2438
		$actions = new FieldList();
2439
		$this->extend('updateCMSActions', $actions);
2440
		return $actions;
2441
	}
2442
2443
2444
	/**
2445
	 * Used for simple frontend forms without relation editing
2446
	 * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2447
	 * by default. To customize, either overload this method in your
2448
	 * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2449
	 *
2450
	 * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2451
	 *
2452
	 * @param array $params See {@link scaffoldFormFields()}
2453
	 * @return FieldList Always returns a simple field collection without TabSet.
2454
	 */
2455
	public function getFrontEndFields($params = null) {
2456
		$untabbedFields = $this->scaffoldFormFields($params);
2457
		$this->extend('updateFrontEndFields', $untabbedFields);
2458
2459
		return $untabbedFields;
2460
	}
2461
2462
	/**
2463
	 * Gets the value of a field.
2464
	 * Called by {@link __get()} and any getFieldName() methods you might create.
2465
	 *
2466
	 * @param string $field The name of the field
2467
	 *
2468
	 * @return mixed The field value
2469
	 */
2470
	public function getField($field) {
2471
		// If we already have an object in $this->record, then we should just return that
2472
		if(isset($this->record[$field]) && is_object($this->record[$field])) {
2473
			return $this->record[$field];
2474
		}
2475
2476
		// Do we have a field that needs to be lazy loaded?
2477 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...
2478
			$tableClass = $this->record[$field.'_Lazy'];
2479
			$this->loadLazyFields($tableClass);
2480
		}
2481
2482
		// In case of complex fields, return the DBField object
2483
		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...
2484
			$this->record[$field] = $this->dbObject($field);
2485
		}
2486
2487
		return isset($this->record[$field]) ? $this->record[$field] : null;
2488
	}
2489
2490
	/**
2491
	 * Loads all the stub fields that an initial lazy load didn't load fully.
2492
	 *
2493
	 * @param string $tableClass Base table to load the values from. Others are joined as required.
2494
	 * Not specifying a tableClass will load all lazy fields from all tables.
2495
	 */
2496
	protected function loadLazyFields($tableClass = null) {
2497
		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...
2498
			$loaded = array();
2499
2500
			foreach ($this->record as $key => $value) {
2501
				if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2502
					$this->loadLazyFields($value);
2503
					$loaded[$value] = $value;
2504
				}
2505
			}
2506
2507
			return;
2508
		}
2509
2510
		$dataQuery = new DataQuery($tableClass);
2511
2512
		// Reset query parameter context to that of this DataObject
2513
		if($params = $this->getSourceQueryParams()) {
2514
			foreach($params as $key => $value) {
2515
				$dataQuery->setQueryParam($key, $value);
2516
			}
2517
		}
2518
2519
		// TableField sets the record ID to "new" on new row data, so don't try doing anything in that case
2520
		if(!is_numeric($this->record['ID'])) {
2521
			return;
2522
		}
2523
2524
		// Limit query to the current record, unless it has the Versioned extension,
2525
		// in which case it requires special handling through augmentLoadLazyFields()
2526
		$baseTable = ClassInfo::baseDataClass($this);
2527
		$dataQuery->where([
2528
			"\"{$baseTable}\".\"ID\"" => $this->record['ID']
2529
		])->limit(1);
2530
2531
		$columns = array();
2532
2533
		// Add SQL for fields, both simple & multi-value
2534
		// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2535
		$databaseFields = self::database_fields($tableClass);
2536
		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...
2537
			if(!isset($this->record[$k]) || $this->record[$k] === null) {
2538
				$columns[] = $k;
2539
			}
2540
		}
2541
2542
		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...
2543
			$query = $dataQuery->query();
2544
			$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2545
			$this->extend('augmentSQL', $query, $dataQuery);
2546
2547
			$dataQuery->setQueriedColumns($columns);
2548
			$newData = $dataQuery->execute()->record();
2549
2550
			// Load the data into record
2551
			if($newData) {
2552
				foreach($newData as $k => $v) {
2553
					if (in_array($k, $columns)) {
2554
						$this->record[$k] = $v;
2555
						$this->original[$k] = $v;
2556
						unset($this->record[$k . '_Lazy']);
2557
					}
2558
				}
2559
2560
			// No data means that the query returned nothing; assign 'null' to all the requested fields
2561
			} else {
2562
				foreach($columns as $k) {
2563
					$this->record[$k] = null;
2564
					$this->original[$k] = null;
2565
					unset($this->record[$k . '_Lazy']);
2566
				}
2567
			}
2568
		}
2569
	}
2570
2571
	/**
2572
	 * Return the fields that have changed.
2573
	 *
2574
	 * The change level affects what the functions defines as "changed":
2575
	 * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2576
	 * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2577
	 *   for example a change from 0 to null would not be included.
2578
	 *
2579
	 * Example return:
2580
	 * <code>
2581
	 * array(
2582
	 *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2583
	 * )
2584
	 * </code>
2585
	 *
2586
	 * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2587
	 * to return all database fields, or an array for an explicit filter. false returns all fields.
2588
	 * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2589
	 * @return array
2590
	 */
2591
	public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
2592
		$changedFields = array();
2593
2594
		// Update the changed array with references to changed obj-fields
2595
		foreach($this->record as $k => $v) {
2596
			// Prevents DBComposite infinite looping on isChanged
2597
			if(is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2598
				continue;
2599
			}
2600
			if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2601
				$this->changed[$k] = self::CHANGE_VALUE;
2602
			}
2603
		}
2604
2605
		if(is_array($databaseFieldsOnly)) {
2606
			$fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2607
		} elseif($databaseFieldsOnly) {
2608
			$fields = array_intersect_key((array)$this->changed, $this->db());
2609
		} else {
2610
			$fields = $this->changed;
2611
		}
2612
2613
		// Filter the list to those of a certain change level
2614
		if($changeLevel > self::CHANGE_STRICT) {
2615
			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...
2616
				if($level < $changeLevel) {
2617
					unset($fields[$name]);
2618
				}
2619
			}
2620
		}
2621
2622
		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...
2623
			$changedFields[$name] = array(
2624
				'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2625
				'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2626
				'level' => $level
2627
			);
2628
		}
2629
2630
		return $changedFields;
2631
	}
2632
2633
	/**
2634
	 * Uses {@link getChangedFields()} to determine if fields have been changed
2635
	 * since loading them from the database.
2636
	 *
2637
	 * @param string $fieldName Name of the database field to check, will check for any if not given
2638
	 * @param int $changeLevel See {@link getChangedFields()}
2639
	 * @return boolean
2640
	 */
2641
	public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
2642
		$fields = $fieldName ? array($fieldName) : false;
2643
		$changed = $this->getChangedFields($fields, $changeLevel);
2644
		if(!isset($fieldName)) {
2645
			return !empty($changed);
2646
		}
2647
		else {
2648
			return array_key_exists($fieldName, $changed);
2649
		}
2650
	}
2651
2652
	/**
2653
	 * Set the value of the field
2654
	 * Called by {@link __set()} and any setFieldName() methods you might create.
2655
	 *
2656
	 * @param string $fieldName Name of the field
2657
	 * @param mixed $val New field value
2658
	 * @return DataObject $this
2659
	 */
2660
	public function setField($fieldName, $val) {
2661
		//if it's a has_one component, destroy the cache
2662
		if (substr($fieldName, -2) == 'ID') {
2663
			unset($this->components[substr($fieldName, 0, -2)]);
2664
		}
2665
2666
		// If we've just lazy-loaded the column, then we need to populate the $original array
2667 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...
2668
			$tableClass = $this->record[$fieldName.'_Lazy'];
2669
			$this->loadLazyFields($tableClass);
2670
		}
2671
2672
		// Situation 1: Passing an DBField
2673
		if($val instanceof DBField) {
2674
			$val->setName($fieldName);
2675
			$val->saveInto($this);
2676
2677
			// Situation 1a: Composite fields should remain bound in case they are
2678
			// later referenced to update the parent dataobject
2679
			if($val instanceof DBComposite) {
2680
				$val->bindTo($this);
2681
				$this->record[$fieldName] = $val;
2682
			}
2683
		// Situation 2: Passing a literal or non-DBField object
2684
		} else {
2685
			// If this is a proper database field, we shouldn't be getting non-DBField objects
2686
			if(is_object($val) && $this->db($fieldName)) {
2687
				user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2688
			}
2689
2690
			// if a field is not existing or has strictly changed
2691
			if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2692
				// TODO Add check for php-level defaults which are not set in the db
2693
				// TODO Add check for hidden input-fields (readonly) which are not set in the db
2694
				// At the very least, the type has changed
2695
				$this->changed[$fieldName] = self::CHANGE_STRICT;
2696
2697
				if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
2698
						&& $this->record[$fieldName] != $val)) {
2699
2700
					// Value has changed as well, not just the type
2701
					$this->changed[$fieldName] = self::CHANGE_VALUE;
2702
				}
2703
2704
				// Value is always saved back when strict check succeeds.
2705
				$this->record[$fieldName] = $val;
2706
			}
2707
		}
2708
		return $this;
2709
	}
2710
2711
	/**
2712
	 * Set the value of the field, using a casting object.
2713
	 * This is useful when you aren't sure that a date is in SQL format, for example.
2714
	 * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2715
	 * can be saved into the Image table.
2716
	 *
2717
	 * @param string $fieldName Name of the field
2718
	 * @param mixed $value New field value
2719
	 * @return $this
2720
	 */
2721
	public function setCastedField($fieldName, $value) {
2722
		if(!$fieldName) {
2723
			user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2724
		}
2725
		$fieldObj = $this->dbObject($fieldName);
2726
		if($fieldObj) {
2727
			$fieldObj->setValue($value);
2728
			$fieldObj->saveInto($this);
2729
		} else {
2730
			$this->$fieldName = $value;
2731
		}
2732
		return $this;
2733
	}
2734
2735
	public function castingHelper($field) {
2736
		// Allows db to act as implicit casting override
2737
		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 2738 which is incompatible with the return type of the parent method ViewableData::castingHelper of type string.
Loading history...
2738
			return $fieldSpec;
2739
		}
2740
		return parent::castingHelper($field);
2741
	}
2742
2743
	/**
2744
	 * Returns true if the given field exists in a database column on any of
2745
	 * the objects tables and optionally look up a dynamic getter with
2746
	 * get<fieldName>().
2747
	 *
2748
	 * @param string $field Name of the field
2749
	 * @return boolean True if the given field exists
2750
	 */
2751
	public function hasField($field) {
2752
		return (
2753
			array_key_exists($field, $this->record)
2754
			|| $this->db($field)
2755
			|| (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...
2756
			|| $this->hasMethod("get{$field}")
2757
		);
2758
	}
2759
2760
	/**
2761
	 * Returns true if the given field exists as a database column
2762
	 *
2763
	 * @param string $field Name of the field
2764
	 *
2765
	 * @return boolean
2766
	 */
2767
	public function hasDatabaseField($field) {
2768
		return $this->db($field)
2769
			&& ! 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...
2770
	}
2771
2772
	/**
2773
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2774
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2775
	 *
2776
	 * @param string $field Name of the field
2777
	 * @return string The field type of the given field
2778
	 */
2779
	public function hasOwnTableDatabaseField($field) {
2780
		return self::has_own_table_database_field($this->class, $field);
2781
	}
2782
2783
	/**
2784
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2785
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2786
	 *
2787
	 * @param string $class Class name to check
2788
	 * @param string $field Name of the field
2789
	 * @return string The field type of the given field
2790
	 */
2791
	public static function has_own_table_database_field($class, $field) {
2792
		$fieldMap = self::database_fields($class);
2793
2794
		// Remove string-based "constructor-arguments" from the DBField definition
2795
		if(isset($fieldMap[$field])) {
2796
			$spec = $fieldMap[$field];
2797
			if(is_string($spec)) return strtok($spec,'(');
2798
			else return $spec['type'];
2799
		}
2800
	}
2801
2802
	/**
2803
	 * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2804
	 * actually looking in the database.
2805
	 *
2806
	 * @param string $dataClass
2807
	 * @return bool
2808
	 */
2809
	public static function has_own_table($dataClass) {
2810
		if(!is_subclass_of($dataClass,'SilverStripe\Model\DataObject')) return false;
2811
2812
		$dataClass = ClassInfo::class_name($dataClass);
2813
		if(!isset(self::$_cache_has_own_table[$dataClass])) {
2814
			if(get_parent_class($dataClass) == 'SilverStripe\Model\DataObject') {
2815
				self::$_cache_has_own_table[$dataClass] = true;
2816
			} else {
2817
				self::$_cache_has_own_table[$dataClass]
2818
					= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
2819
					|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
2820
			}
2821
		}
2822
		return self::$_cache_has_own_table[$dataClass];
2823
	}
2824
2825
	/**
2826
	 * Returns true if the member is allowed to do the given action.
2827
	 * See {@link extendedCan()} for a more versatile tri-state permission control.
2828
	 *
2829
	 * @param string $perm The permission to be checked, such as 'View'.
2830
	 * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2831
	 * in user.
2832
	 * @param array $context Additional $context to pass to extendedCan()
2833
	 *
2834
	 * @return boolean True if the the member is allowed to do the given action
2835
	 */
2836
	public function can($perm, $member = null, $context = array()) {
2837
		if(!isset($member)) {
2838
			$member = Member::currentUser();
2839
		}
2840
		if(Permission::checkMember($member, "ADMIN")) return true;
2841
2842
		if($this->manyManyComponent('Can' . $perm)) {
2843
			if($this->ParentID && $this->SecurityType == 'Inherit') {
2844
				if(!($p = $this->Parent)) {
0 ignored issues
show
Documentation introduced by
The property Parent does not exist on object<SilverStripe\Model\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...
2845
					return false;
2846
				}
2847
				return $this->Parent->can($perm, $member);
2848
2849
			} else {
2850
				$permissionCache = $this->uninherited('permissionCache');
2851
				$memberID = $member ? $member->ID : 'none';
2852
2853
				if(!isset($permissionCache[$memberID][$perm])) {
2854
					if($member->ID) {
2855
						$groups = $member->Groups();
2856
					}
2857
2858
					$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...
2859
2860
					// TODO Fix relation table hardcoding
2861
					$query = new SQLSelect(
2862
						"\"Page_Can$perm\".PageID",
2863
					array("\"Page_Can$perm\""),
2864
						"GroupID IN ($groupList)");
2865
2866
					$permissionCache[$memberID][$perm] = $query->execute()->column();
2867
2868
					if($perm == "View") {
2869
						// TODO Fix relation table hardcoding
2870
						$query = new SQLSelect("\"SiteTree\".\"ID\"", array(
2871
							"\"SiteTree\"",
2872
							"LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2873
							), "\"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...
2874
2875
							$unsecuredPages = $query->execute()->column();
2876
							if($permissionCache[$memberID][$perm]) {
2877
								$permissionCache[$memberID][$perm]
2878
									= array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2879
							} else {
2880
								$permissionCache[$memberID][$perm] = $unsecuredPages;
2881
							}
2882
					}
2883
2884
					Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2885
				}
2886
2887
				if($permissionCache[$memberID][$perm]) {
2888
					return in_array($this->ID, $permissionCache[$memberID][$perm]);
2889
				}
2890
			}
2891
		} else {
2892
			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_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, 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, 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, HierarchyTest_Object, HtmlEditorFieldTest_Object, Image, InstallerTest, LeftAndMain, LeftAndMainTest_Controller, LeftAndMainTest_Object, ListboxFieldTest_Article, ListboxFieldTest_DataObject, ListboxFieldTest_Tag, LoginAttempt, ManyManyListTest_ExtraFields, ManyManyListTest_IndirectPrimary, 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, RestfulServiceTest_Controller, SQLInsertTestBase, SQLSelectTestBase, SQLSelectTestChild, SQLSelectTest_DO, SQLUpdateChild, SQLUpdateTestBase, SSViewerCacheBlockTest_Model, SSViewerCacheBlockTest_VersionedModel, SSViewerTest_Controller, 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, SilverStripe\Model\DataObject, SubclassedDBFieldObject, TaskRunner, 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...
2893
		}
2894
	}
2895
2896
	/**
2897
	 * Process tri-state responses from permission-alterting extensions.  The extensions are
2898
	 * expected to return one of three values:
2899
	 *
2900
	 *  - false: Disallow this permission, regardless of what other extensions say
2901
	 *  - true: Allow this permission, as long as no other extensions return false
2902
	 *  - NULL: Don't affect the outcome
2903
	 *
2904
	 * This method itself returns a tri-state value, and is designed to be used like this:
2905
	 *
2906
	 * <code>
2907
	 * $extended = $this->extendedCan('canDoSomething', $member);
2908
	 * if($extended !== null) return $extended;
2909
	 * else return $normalValue;
2910
	 * </code>
2911
	 *
2912
	 * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2913
	 * @param Member|int $member
2914
	 * @param array $context Optional context
2915
	 * @return boolean|null
2916
	 */
2917
	public function extendedCan($methodName, $member, $context = array()) {
2918
		$results = $this->extend($methodName, $member, $context);
2919
		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...
2920
			// Remove NULLs
2921
			$results = array_filter($results, function($v) {return !is_null($v);});
2922
			// If there are any non-NULL responses, then return the lowest one of them.
2923
			// If any explicitly deny the permission, then we don't get access
2924
			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...
2925
		}
2926
		return null;
2927
	}
2928
2929
	/**
2930
	 * @param Member $member
2931
	 * @return boolean
2932
	 */
2933 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...
2934
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2933 can be null; however, SilverStripe\Model\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...
2935
		if($extended !== null) {
2936
			return $extended;
2937
		}
2938
		return Permission::check('ADMIN', 'any', $member);
2939
	}
2940
2941
	/**
2942
	 * @param Member $member
2943
	 * @return boolean
2944
	 */
2945 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...
2946
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2945 can be null; however, SilverStripe\Model\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...
2947
		if($extended !== null) {
2948
			return $extended;
2949
		}
2950
		return Permission::check('ADMIN', 'any', $member);
2951
	}
2952
2953
	/**
2954
	 * @param Member $member
2955
	 * @return boolean
2956
	 */
2957 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...
2958
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2957 can be null; however, SilverStripe\Model\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...
2959
		if($extended !== null) {
2960
			return $extended;
2961
		}
2962
		return Permission::check('ADMIN', 'any', $member);
2963
	}
2964
2965
	/**
2966
	 * @param Member $member
2967
	 * @param array $context Additional context-specific data which might
2968
	 * affect whether (or where) this object could be created.
2969
	 * @return boolean
2970
	 */
2971 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...
2972
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2971 can be null; however, SilverStripe\Model\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...
2973
		if($extended !== null) {
2974
			return $extended;
2975
		}
2976
		return Permission::check('ADMIN', 'any', $member);
2977
	}
2978
2979
	/**
2980
	 * Debugging used by Debug::show()
2981
	 *
2982
	 * @return string HTML data representing this object
2983
	 */
2984
	public function debug() {
2985
		$val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2986
		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...
2987
			$val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2988
		}
2989
		$val .= "</ul>\n";
2990
		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...
2991
	}
2992
2993
	/**
2994
	 * Return the DBField object that represents the given field.
2995
	 * This works similarly to obj() with 2 key differences:
2996
	 *   - it still returns an object even when the field has no value.
2997
	 *   - it only matches fields and not methods
2998
	 *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2999
	 *
3000
	 * @param string $fieldName Name of the field
3001
	 * @return DBField The field as a DBField object
3002
	 */
3003
	public function dbObject($fieldName) {
3004
		$value = isset($this->record[$fieldName])
3005
			? $this->record[$fieldName]
3006
			: null;
3007
3008
		// If we have a DBField object in $this->record, then return that
3009
		if(is_object($value)) {
3010
			return $value;
3011
		}
3012
3013
		// Build and populate new field otherwise
3014
		$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...
3015
		if($helper) {
3016
			list($table, $spec) = explode('.', $helper);
3017
			$obj = Object::create_from_string($spec, $fieldName);
3018
			$obj->setTable($table);
3019
			$obj->setValue($value, $this, false);
3020
			return $obj;
3021
		}
3022
	}
3023
3024
	/**
3025
	 * Traverses to a DBField referenced by relationships between data objects.
3026
	 *
3027
	 * The path to the related field is specified with dot separated syntax
3028
	 * (eg: Parent.Child.Child.FieldName).
3029
	 *
3030
	 * @param string $fieldPath
3031
	 *
3032
	 * @return mixed DBField of the field on the object or a DataList instance.
3033
	 */
3034
	public function relObject($fieldPath) {
3035
		$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...
3036
3037
		if(strpos($fieldPath, '.') !== false) {
3038
			$parts = explode('.', $fieldPath);
3039
			$fieldName = array_pop($parts);
3040
3041
			// Traverse dot syntax
3042
			$component = $this;
3043
3044
			foreach($parts as $relation) {
3045
				if($component instanceof SS_List) {
3046
					if(method_exists($component,$relation)) {
3047
						$component = $component->$relation();
3048
					} else {
3049
						$component = $component->relation($relation);
3050
					}
3051
				} else {
3052
					$component = $component->$relation();
3053
				}
3054
			}
3055
3056
			$object = $component->dbObject($fieldName);
3057
3058
		} else {
3059
			$object = $this->dbObject($fieldPath);
3060
		}
3061
3062
		return $object;
3063
	}
3064
3065
	/**
3066
	 * Traverses to a field referenced by relationships between data objects, returning the value
3067
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3068
	 *
3069
	 * @param $fieldName string
3070
	 * @return string | null - will return null on a missing value
3071
	 */
3072 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...
3073
		$component = $this;
3074
3075
		// We're dealing with relations here so we traverse the dot syntax
3076
		if(strpos($fieldName, '.') !== false) {
3077
			$relations = explode('.', $fieldName);
3078
			$fieldName = array_pop($relations);
3079
			foreach($relations as $relation) {
3080
				// Inspect $component for element $relation
3081
				if($component->hasMethod($relation)) {
3082
					// Check nested method
3083
					$component = $component->$relation();
3084
				} elseif($component instanceof SS_List) {
3085
					// Select adjacent relation from DataList
3086
					$component = $component->relation($relation);
3087
				} elseif($component instanceof DataObject
3088
					&& ($dbObject = $component->dbObject($relation))
3089
				) {
3090
					// Select db object
3091
					$component = $dbObject;
3092
				} else {
3093
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
3094
				}
3095
			}
3096
		}
3097
3098
		// Bail if the component is null
3099
		if(!$component) {
3100
			return null;
3101
		}
3102
		if($component->hasMethod($fieldName)) {
3103
			return $component->$fieldName();
3104
		}
3105
		return $component->$fieldName;
3106
	}
3107
3108
	/**
3109
	 * Temporary hack to return an association name, based on class, to get around the mangle
3110
	 * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3111
	 *
3112
	 * @return String
3113
	 */
3114
	public function getReverseAssociation($className) {
3115
		if (is_array($this->manyMany())) {
3116
			$many_many = array_flip($this->manyMany());
3117
			if (array_key_exists($className, $many_many)) return $many_many[$className];
3118
		}
3119
		if (is_array($this->hasMany())) {
3120
			$has_many = array_flip($this->hasMany());
3121
			if (array_key_exists($className, $has_many)) return $has_many[$className];
3122
		}
3123
		if (is_array($this->hasOne())) {
3124
			$has_one = array_flip($this->hasOne());
3125
			if (array_key_exists($className, $has_one)) return $has_one[$className];
3126
		}
3127
3128
		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 SilverStripe\Model\DataO...::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...
3129
	}
3130
3131
	/**
3132
	 * Return all objects matching the filter
3133
	 * sub-classes are automatically selected and included
3134
	 *
3135
	 * @param string $callerClass The class of objects to be returned
3136
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3137
	 * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3138
	 * @param string|array $sort A sort expression to be inserted into the ORDER
3139
	 * BY clause.  If omitted, self::$default_sort will be used.
3140
	 * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3141
	 * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3142
	 * @param string $containerClass The container class to return the results in.
3143
	 *
3144
	 * @todo $containerClass is Ignored, why?
3145
	 *
3146
	 * @return DataList The objects matching the filter, in the class specified by $containerClass
3147
	 */
3148
	public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
3149
			$containerClass = 'SilverStripe\Model\DataList') {
3150
3151
		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...
3152
			$callerClass = get_called_class();
3153
			if($callerClass == 'SilverStripe\Model\DataObject') {
3154
				throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3155
			}
3156
3157
			if($filter || $sort || $join || $limit || ($containerClass != 'SilverStripe\Model\DataList')) {
3158
				throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3159
					. ' arguments');
3160
			}
3161
3162
			$result = DataList::create(get_called_class());
3163
			$result->setDataModel(DataModel::inst());
3164
			return $result;
3165
		}
3166
3167
		if($join) {
3168
			throw new \InvalidArgumentException(
3169
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3170
			);
3171
		}
3172
3173
		$result = DataList::create($callerClass)->where($filter)->sort($sort);
3174
3175
		if($limit && strpos($limit, ',') !== false) {
3176
			$limitArguments = explode(',', $limit);
3177
			$result = $result->limit($limitArguments[1],$limitArguments[0]);
3178
		} elseif($limit) {
3179
			$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...
3180
		}
3181
3182
		$result->setDataModel(DataModel::inst());
3183
		return $result;
3184
	}
3185
3186
3187
	/**
3188
	 * Return the first item matching the given query.
3189
	 * All calls to get_one() are cached.
3190
	 *
3191
	 * @param string $callerClass The class of objects to be returned
3192
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3193
	 * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3194
	 * @param boolean $cache Use caching
3195
	 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3196
	 *
3197
	 * @return DataObject The first item matching the query
3198
	 */
3199
	public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
3200
		$SNG = singleton($callerClass);
3201
3202
		$cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3203
		$cacheKey = md5(var_export($cacheComponents, true));
3204
3205
		// Flush destroyed items out of the cache
3206
		if($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
3207
				&& self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
3208
				&& self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
3209
3210
			self::$_cache_get_one[$callerClass][$cacheKey] = false;
3211
		}
3212
		if(!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3213
			$dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
3214
			$item = $dl->First();
3215
3216
			if($cache) {
3217
				self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3218
				if(!self::$_cache_get_one[$callerClass][$cacheKey]) {
3219
					self::$_cache_get_one[$callerClass][$cacheKey] = false;
3220
				}
3221
			}
3222
		}
3223
		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...
3224
	}
3225
3226
	/**
3227
	 * Flush the cached results for all relations (has_one, has_many, many_many)
3228
	 * Also clears any cached aggregate data.
3229
	 *
3230
	 * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3231
	 *                            When false will just clear session-local cached data
3232
	 * @return DataObject $this
3233
	 */
3234
	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...
3235
		if($this->class == 'SilverStripe\Model\DataObject') {
3236
			self::$_cache_get_one = array();
3237
			return $this;
3238
		}
3239
3240
		$classes = ClassInfo::ancestry($this->class);
3241
		foreach($classes as $class) {
3242
			if(isset(self::$_cache_get_one[$class])) unset(self::$_cache_get_one[$class]);
3243
		}
3244
3245
		$this->extend('flushCache');
3246
3247
		$this->components = array();
3248
		return $this;
3249
	}
3250
3251
	/**
3252
	 * Flush the get_one global cache and destroy associated objects.
3253
	 */
3254
	public static function flush_and_destroy_cache() {
3255
		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...
3256
			if(is_array($items)) foreach($items as $item) {
3257
				if($item) $item->destroy();
3258
			}
3259
		}
3260
		self::$_cache_get_one = array();
3261
	}
3262
3263
	/**
3264
	 * Reset all global caches associated with DataObject.
3265
	 */
3266
	public static function reset() {
3267
		DBClassName::clear_classname_cache();
3268
		self::$_cache_has_own_table = array();
3269
		self::$_cache_get_one = array();
3270
		self::$_cache_composite_fields = array();
3271
		self::$_cache_database_fields = array();
3272
		self::$_cache_get_class_ancestry = array();
3273
		self::$_cache_field_labels = array();
3274
	}
3275
3276
	/**
3277
	 * Return the given element, searching by ID
3278
	 *
3279
	 * @param string $callerClass The class of the object to be returned
3280
	 * @param int $id The id of the element
3281
	 * @param boolean $cache See {@link get_one()}
3282
	 *
3283
	 * @return DataObject The element
3284
	 */
3285
	public static function get_by_id($callerClass, $id, $cache = true) {
3286
		if(!is_numeric($id)) {
3287
			user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3288
		}
3289
3290
		// Check filter column
3291
		if(is_subclass_of($callerClass, 'SilverStripe\Model\DataObject')) {
3292
			$baseClass = ClassInfo::baseDataClass($callerClass);
3293
			$column = "\"$baseClass\".\"ID\"";
3294
		} else{
3295
			// This simpler code will be used by non-DataObject classes that implement DataObjectInterface
3296
			$column = '"ID"';
3297
		}
3298
3299
		// Relegate to get_one
3300
		return DataObject::get_one($callerClass, array($column => $id), $cache);
3301
	}
3302
3303
	/**
3304
	 * Get the name of the base table for this object
3305
	 */
3306
	public function baseTable() {
3307
		$tableClasses = ClassInfo::dataClassesFor($this->class);
3308
		return array_shift($tableClasses);
3309
	}
3310
3311
	/**
3312
	 * @var Array Parameters used in the query that built this object.
3313
	 * This can be used by decorators (e.g. lazy loading) to
3314
	 * run additional queries using the same context.
3315
	 */
3316
	protected $sourceQueryParams;
3317
3318
	/**
3319
	 * @see $sourceQueryParams
3320
	 * @return array
3321
	 */
3322
	public function getSourceQueryParams() {
3323
		return $this->sourceQueryParams;
3324
	}
3325
3326
	/**
3327
	 * Get list of parameters that should be inherited to relations on this object
3328
	 *
3329
	 * @return array
3330
	 */
3331
	public function getInheritableQueryParams() {
3332
		$params = $this->getSourceQueryParams();
3333
		$this->extend('updateInheritableQueryParams', $params);
3334
		return $params;
3335
	}
3336
3337
	/**
3338
	 * @see $sourceQueryParams
3339
	 * @param array
3340
	 */
3341
	public function setSourceQueryParams($array) {
3342
		$this->sourceQueryParams = $array;
3343
	}
3344
3345
	/**
3346
	 * @see $sourceQueryParams
3347
	 * @param array
3348
	 */
3349
	public function setSourceQueryParam($key, $value) {
3350
		$this->sourceQueryParams[$key] = $value;
3351
	}
3352
3353
	/**
3354
	 * @see $sourceQueryParams
3355
	 * @return Mixed
3356
	 */
3357
	public function getSourceQueryParam($key) {
3358
		if(isset($this->sourceQueryParams[$key])) return $this->sourceQueryParams[$key];
3359
		else return null;
3360
	}
3361
3362
	//-------------------------------------------------------------------------------------------//
3363
3364
	/**
3365
	 * Return the database indexes on this table.
3366
	 * This array is indexed by the name of the field with the index, and
3367
	 * the value is the type of index.
3368
	 */
3369
	public function databaseIndexes() {
3370
		$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...
3371
		$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...
3372
		//$fileIndexes = $this->uninherited('fileIndexes', true);
3373
3374
		$indexes = array();
3375
3376
		if($has_one) {
3377
			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...
3378
				$indexes[$relationshipName . 'ID'] = true;
3379
			}
3380
		}
3381
3382
		if($classIndexes) {
3383
			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...
3384
				$indexes[$indexName] = $indexType;
3385
			}
3386
		}
3387
3388
		if(get_parent_class($this) == 'SilverStripe\Model\DataObject') {
3389
			$indexes['ClassName'] = true;
3390
		}
3391
3392
		return $indexes;
3393
	}
3394
3395
	/**
3396
	 * Check the database schema and update it as necessary.
3397
	 *
3398
	 * @uses DataExtension->augmentDatabase()
3399
	 */
3400
	public function requireTable() {
3401
		// Only build the table if we've actually got fields
3402
		$fields = self::database_fields($this->class);
3403
		$extensions = self::database_extensions($this->class);
3404
3405
		$indexes = $this->databaseIndexes();
3406
3407
		// Validate relationship configuration
3408
		$this->validateModelDefinitions();
3409
3410
		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...
3411
			$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
3412
			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...
3413
				$extensions);
3414
		} else {
3415
			DB::dont_require_table($this->class);
3416
		}
3417
3418
		// Build any child tables for many_many items
3419
		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...
3420
			$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...
3421
			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...
3422
				// Build field list
3423
				$manymanyFields = array(
3424
					"{$this->class}ID" => "Int",
3425
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => "Int",
3426
				);
3427
				if(isset($extras[$relationship])) {
3428
					$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
3429
				}
3430
3431
				// Build index list
3432
				$manymanyIndexes = array(
3433
					"{$this->class}ID" => true,
3434
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true,
3435
				);
3436
3437
				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...
3438
					$extensions);
3439
			}
3440
		}
3441
3442
		// Let any extentions make their own database fields
3443
		$this->extend('augmentDatabase', $dummy);
3444
	}
3445
3446
	/**
3447
	 * Validate that the configured relations for this class use the correct syntaxes
3448
	 * @throws LogicException
3449
	 */
3450
	protected function validateModelDefinitions() {
3451
		$modelDefinitions = array(
3452
			'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
3453
			'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
3454
			'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
3455
			'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
3456
			'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
3457
			'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
3458
			'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
3459
		);
3460
3461
		foreach($modelDefinitions as $defType => $relations) {
3462
			if( ! $relations) continue;
3463
3464
			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...
3465
				if($defType === 'many_many_extraFields') {
3466
					if(!is_array($v)) {
3467
						throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
3468
							. var_export($k, true) . " => " . var_export($v, true)
3469
							. ". Each many_many_extraFields entry should map to a field specification array.");
3470
					}
3471
				} else {
3472
					if(!is_string($k) || is_numeric($k) || !is_string($v)) {
3473
						throw new LogicException("$this->class::$defType has a bad entry: "
3474
							. var_export($k, true). " => " . var_export($v, true) . ".  Each map key should be a
3475
							 relationship name, and the map value should be the data class to join to.");
3476
					}
3477
				}
3478
			}
3479
		}
3480
	}
3481
3482
	/**
3483
	 * Add default records to database. This function is called whenever the
3484
	 * database is built, after the database tables have all been created. Overload
3485
	 * this to add default records when the database is built, but make sure you
3486
	 * call parent::requireDefaultRecords().
3487
	 *
3488
	 * @uses DataExtension->requireDefaultRecords()
3489
	 */
3490
	public function requireDefaultRecords() {
3491
		$defaultRecords = $this->stat('default_records');
3492
3493
		if(!empty($defaultRecords)) {
3494
			$hasData = DataObject::get_one($this->class);
3495
			if(!$hasData) {
3496
				$className = $this->class;
3497
				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...
3498
					$obj = $this->model->$className->newObject($record);
3499
					$obj->write();
3500
				}
3501
				DB::alteration_message("Added default records to $className table","created");
3502
			}
3503
		}
3504
3505
		// Let any extentions make their own database default data
3506
		$this->extend('requireDefaultRecords', $dummy);
3507
	}
3508
3509
	/**
3510
	 * Get the default searchable fields for this object, as defined in the
3511
	 * $searchable_fields list. If searchable fields are not defined on the
3512
	 * data object, uses a default selection of summary fields.
3513
	 *
3514
	 * @return array
3515
	 */
3516
	public function searchableFields() {
3517
		// can have mixed format, need to make consistent in most verbose form
3518
		$fields = $this->stat('searchable_fields');
3519
		$labels = $this->fieldLabels();
3520
3521
		// fallback to summary fields (unless empty array is explicitly specified)
3522
		if( ! $fields && ! is_array($fields)) {
3523
			$summaryFields = array_keys($this->summaryFields());
3524
			$fields = array();
3525
3526
			// remove the custom getters as the search should not include them
3527
			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...
3528
				foreach($summaryFields as $key => $name) {
3529
					$spec = $name;
3530
3531
					// Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3532 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...
3533
						$name = substr($name, 0, $fieldPos);
3534
					}
3535
3536
					if($this->hasDatabaseField($name)) {
3537
						$fields[] = $name;
3538
					} elseif($this->relObject($spec)) {
3539
						$fields[] = $spec;
3540
					}
3541
				}
3542
			}
3543
		}
3544
3545
		// we need to make sure the format is unified before
3546
		// augmenting fields, so extensions can apply consistent checks
3547
		// but also after augmenting fields, because the extension
3548
		// might use the shorthand notation as well
3549
3550
		// rewrite array, if it is using shorthand syntax
3551
		$rewrite = array();
3552
		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...
3553
			$identifer = (is_int($name)) ? $specOrName : $name;
3554
3555
			if(is_int($name)) {
3556
				// Format: array('MyFieldName')
3557
				$rewrite[$identifer] = array();
3558
			} elseif(is_array($specOrName)) {
3559
				// Format: array('MyFieldName' => array(
3560
				//   'filter => 'ExactMatchFilter',
3561
				//   'field' => 'NumericField', // optional
3562
				//   'title' => 'My Title', // optional
3563
				// ))
3564
				$rewrite[$identifer] = array_merge(
3565
					array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3566
					(array)$specOrName
3567
				);
3568
			} else {
3569
				// Format: array('MyFieldName' => 'ExactMatchFilter')
3570
				$rewrite[$identifer] = array(
3571
					'filter' => $specOrName,
3572
				);
3573
			}
3574
			if(!isset($rewrite[$identifer]['title'])) {
3575
				$rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3576
					? $labels[$identifer] : FormField::name_to_label($identifer);
3577
			}
3578
			if(!isset($rewrite[$identifer]['filter'])) {
3579
				$rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3580
			}
3581
		}
3582
3583
		$fields = $rewrite;
3584
3585
		// apply DataExtensions if present
3586
		$this->extend('updateSearchableFields', $fields);
3587
3588
		return $fields;
3589
	}
3590
3591
	/**
3592
	 * Get any user defined searchable fields labels that
3593
	 * exist. Allows overriding of default field names in the form
3594
	 * interface actually presented to the user.
3595
	 *
3596
	 * The reason for keeping this separate from searchable_fields,
3597
	 * which would be a logical place for this functionality, is to
3598
	 * avoid bloating and complicating the configuration array. Currently
3599
	 * much of this system is based on sensible defaults, and this property
3600
	 * would generally only be set in the case of more complex relationships
3601
	 * between data object being required in the search interface.
3602
	 *
3603
	 * Generates labels based on name of the field itself, if no static property
3604
	 * {@link self::field_labels} exists.
3605
	 *
3606
	 * @uses $field_labels
3607
	 * @uses FormField::name_to_label()
3608
	 *
3609
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3610
	 *
3611
	 * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3612
	 */
3613
	public function fieldLabels($includerelations = true) {
3614
		$cacheKey = $this->class . '_' . $includerelations;
3615
3616
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
3617
			$customLabels = $this->stat('field_labels');
3618
			$autoLabels = array();
3619
3620
			// get all translated static properties as defined in i18nCollectStatics()
3621
			$ancestry = ClassInfo::ancestry($this->class);
3622
			$ancestry = array_reverse($ancestry);
3623
			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...
3624
				if($ancestorClass == 'ViewableData') break;
3625
				$types = array(
3626
					'db'        => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3627
				);
3628
				if($includerelations){
3629
					$types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3630
					$types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3631
					$types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3632
					$types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3633
				}
3634
				foreach($types as $type => $attrs) {
3635
					foreach($attrs as $name => $spec) {
3636
						$autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}",FormField::name_to_label($name));
3637
					}
3638
				}
3639
			}
3640
3641
			$labels = array_merge((array)$autoLabels, (array)$customLabels);
3642
			$this->extend('updateFieldLabels', $labels);
3643
			self::$_cache_field_labels[$cacheKey] = $labels;
3644
		}
3645
3646
		return self::$_cache_field_labels[$cacheKey];
3647
	}
3648
3649
	/**
3650
	 * Get a human-readable label for a single field,
3651
	 * see {@link fieldLabels()} for more details.
3652
	 *
3653
	 * @uses fieldLabels()
3654
	 * @uses FormField::name_to_label()
3655
	 *
3656
	 * @param string $name Name of the field
3657
	 * @return string Label of the field
3658
	 */
3659
	public function fieldLabel($name) {
3660
		$labels = $this->fieldLabels();
3661
		return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3662
	}
3663
3664
	/**
3665
	 * Get the default summary fields for this object.
3666
	 *
3667
	 * @todo use the translation apparatus to return a default field selection for the language
3668
	 *
3669
	 * @return array
3670
	 */
3671
	public function summaryFields() {
3672
		$fields = $this->stat('summary_fields');
3673
3674
		// if fields were passed in numeric array,
3675
		// convert to an associative array
3676
		if($fields && array_key_exists(0, $fields)) {
3677
			$fields = array_combine(array_values($fields), array_values($fields));
3678
		}
3679
3680
		if (!$fields) {
3681
			$fields = array();
3682
			// try to scaffold a couple of usual suspects
3683
			if ($this->hasField('Name')) $fields['Name'] = 'Name';
3684
			if ($this->hasDataBaseField('Title')) $fields['Title'] = 'Title';
3685
			if ($this->hasField('Description')) $fields['Description'] = 'Description';
3686
			if ($this->hasField('FirstName')) $fields['FirstName'] = 'First Name';
3687
		}
3688
		$this->extend("updateSummaryFields", $fields);
3689
3690
		// Final fail-over, just list ID field
3691
		if(!$fields) $fields['ID'] = 'ID';
3692
3693
		// Localize fields (if possible)
3694
		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...
3695
			// only attempt to localize if the label definition is the same as the field name.
3696
			// this will preserve any custom labels set in the summary_fields configuration
3697
			if(isset($fields[$name]) && $name === $fields[$name]) {
3698
				$fields[$name] = $label;
3699
			}
3700
		}
3701
3702
		return $fields;
3703
	}
3704
3705
	/**
3706
	 * Defines a default list of filters for the search context.
3707
	 *
3708
	 * If a filter class mapping is defined on the data object,
3709
	 * it is constructed here. Otherwise, the default filter specified in
3710
	 * {@link DBField} is used.
3711
	 *
3712
	 * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3713
	 *
3714
	 * @return array
3715
	 */
3716
	public function defaultSearchFilters() {
3717
		$filters = array();
3718
3719
		foreach($this->searchableFields() as $name => $spec) {
3720
			$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...
3721
3722
			if($spec['filter'] instanceof SearchFilter) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\Model\SearchFilter does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

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