Completed
Pull Request — master (#5304)
by Damian
10:55
created

DataObject::i18n_pluralise()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 8
rs 9.4285
cc 1
eloc 6
nc 1
nop 2
1
<?php
2
3
use SilverStripe\Model\FieldType\DBPolymorphicForeignKey;
4
use SilverStripe\Model\FieldType\DBField;
5
use SilverStripe\Model\FieldType\DBDatetime;
6
use SilverStripe\Model\FieldType\DBPrimaryKey;
7
use SilverStripe\Model\FieldType\DBComposite;
8
use SilverStripe\Model\FieldType\DBClassName;
9
10
/**
11
 * A single database record & abstract class for the data-access-model.
12
 *
13
 * <h2>Extensions</h2>
14
 *
15
 * See {@link Extension} and {@link DataExtension}.
16
 *
17
 * <h2>Permission Control</h2>
18
 *
19
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
20
 * strings which can be selected on a group-by-group basis.
21
 *
22
 * <code>
23
 * class Article extends DataObject implements PermissionProvider {
24
 *  static $api_access = true;
25
 *
26
 *  function canView($member = false) {
27
 *    return Permission::check('ARTICLE_VIEW');
28
 *  }
29
 *  function canEdit($member = false) {
30
 *    return Permission::check('ARTICLE_EDIT');
31
 *  }
32
 *  function canDelete() {
33
 *    return Permission::check('ARTICLE_DELETE');
34
 *  }
35
 *  function canCreate() {
36
 *    return Permission::check('ARTICLE_CREATE');
37
 *  }
38
 *  function providePermissions() {
39
 *    return array(
40
 *      'ARTICLE_VIEW' => 'Read an article object',
41
 *      'ARTICLE_EDIT' => 'Edit an article object',
42
 *      'ARTICLE_DELETE' => 'Delete an article object',
43
 *      'ARTICLE_CREATE' => 'Create an article object',
44
 *    );
45
 *  }
46
 * }
47
 * </code>
48
 *
49
 * Object-level access control by {@link Group} membership:
50
 * <code>
51
 * class Article extends DataObject {
52
 *   static $api_access = true;
53
 *
54
 *   function canView($member = false) {
55
 *     if(!$member) $member = Member::currentUser();
56
 *     return $member->inGroup('Subscribers');
57
 *   }
58
 *   function canEdit($member = false) {
59
 *     if(!$member) $member = Member::currentUser();
60
 *     return $member->inGroup('Editors');
61
 *   }
62
 *
63
 *   // ...
64
 * }
65
 * </code>
66
 *
67
 * If any public method on this class is prefixed with an underscore,
68
 * the results are cached in memory through {@link cachedCall()}.
69
 *
70
 *
71
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
72
 *  and defineMethods()
73
 *
74
 * @package framework
75
 * @subpackage model
76
 *
77
 * @property integer ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
78
 * @property string ClassName Class name of the DataObject
79
 * @property string LastEdited Date and time of DataObject's last modification.
80
 * @property string Created Date and time of DataObject creation.
81
 */
82
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider {
83
84
	/**
85
	 * Human-readable singular name.
86
	 * @var string
87
	 * @config
88
	 */
89
	private static $singular_name = null;
90
91
	/**
92
	 * Human-readable plural name
93
	 * @var string
94
	 * @config
95
	 */
96
	private static $plural_name = null;
97
98
	/**
99
	 * Allow API access to this object?
100
	 * @todo Define the options that can be set here
101
	 * @config
102
	 */
103
	private static $api_access = false;
104
105
	/**
106
	 * Allows specification of a default value for the ClassName field.
107
	 * Configure this value only in subclasses of DataObject.
108
	 *
109
	 * @config
110
	 * @var string
111
	 */
112
	private static $default_classname = null;
113
114
	/**
115
	 * True if this DataObject has been destroyed.
116
	 * @var boolean
117
	 */
118
	public $destroyed = false;
119
120
	/**
121
	 * The DataModel from this this object comes
122
	 */
123
	protected $model;
124
125
	/**
126
	 * Data stored in this objects database record. An array indexed by fieldname.
127
	 *
128
	 * Use {@link toMap()} if you want an array representation
129
	 * of this object, as the $record array might contain lazy loaded field aliases.
130
	 *
131
	 * @var array
132
	 */
133
	protected $record;
134
135
	/**
136
	 * Represents a field that hasn't changed (before === after, thus before == after)
137
	 */
138
	const CHANGE_NONE = 0;
139
140
	/**
141
	 * Represents a field that has changed type, although not the loosely defined value.
142
	 * (before !== after && before == after)
143
	 * E.g. change 1 to true or "true" to true, but not true to 0.
144
	 * Value changes are by nature also considered strict changes.
145
	 */
146
	const CHANGE_STRICT = 1;
147
148
	/**
149
	 * Represents a field that has changed the loosely defined value
150
	 * (before != after, thus, before !== after))
151
	 * E.g. change false to true, but not false to 0
152
	 */
153
	const CHANGE_VALUE = 2;
154
155
	/**
156
	 * An array indexed by fieldname, true if the field has been changed.
157
	 * Use {@link getChangedFields()} and {@link isChanged()} to inspect
158
	 * the changed state.
159
	 *
160
	 * @var array
161
	 */
162
	private $changed;
163
164
	/**
165
	 * The database record (in the same format as $record), before
166
	 * any changes.
167
	 * @var array
168
	 */
169
	protected $original;
170
171
	/**
172
	 * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
173
	 * @var boolean
174
	 */
175
	protected $brokenOnDelete = false;
176
177
	/**
178
	 * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
179
	 * @var boolean
180
	 */
181
	protected $brokenOnWrite = false;
182
183
	/**
184
	 * @config
185
	 * @var boolean Should dataobjects be validated before they are written?
186
	 * Caution: Validation can contain safeguards against invalid/malicious data,
187
	 * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
188
	 * to only disable validation for very specific use cases.
189
	 */
190
	private static $validation_enabled = true;
191
192
	/**
193
	 * Static caches used by relevant functions.
194
	 */
195
	protected static $_cache_has_own_table = array();
196
	protected static $_cache_get_one;
197
	protected static $_cache_get_class_ancestry;
198
	protected static $_cache_composite_fields = array();
199
	protected static $_cache_database_fields = array();
200
	protected static $_cache_field_labels = array();
201
202
	/**
203
	 * Base fields which are not defined in static $db
204
	 *
205
	 * @config
206
	 * @var array
207
	 */
208
	private static $fixed_fields = array(
209
		'ID' => 'PrimaryKey',
210
		'ClassName' => 'DBClassName',
211
		'LastEdited' => 'SS_Datetime',
212
		'Created' => 'SS_Datetime',
213
	);
214
215
	/**
216
	 * Core dataobject extensions
217
	 *
218
	 * @config
219
	 * @var array
220
	 */
221
	private static $extensions = array(
222
		'AssetControl' => '\\SilverStripe\\Filesystem\\AssetControlExtension'
223
	);
224
225
	/**
226
	 * Non-static relationship cache, indexed by component name.
227
	 */
228
	protected $components;
229
230
	/**
231
	 * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
232
	 */
233
	protected $unsavedRelations;
234
235
	/**
236
	 * Return the complete map of fields to specification on this object, including fixed_fields.
237
	 * "ID" will be included on every table.
238
	 *
239
	 * Composite DB field specifications are returned by reference if necessary, but not in the return
240
	 * array.
241
	 *
242
	 * Can be called directly on an object. E.g. Member::database_fields()
243
	 *
244
	 * @param string $class Class name to query from
245
	 * @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
246
	 */
247
	public static function database_fields($class = null) {
248
		if(empty($class)) {
249
			$class = get_called_class();
250
		}
251
252
		// Refresh cache
253
		self::cache_database_fields($class);
254
255
		// Return cached values
256
		return self::$_cache_database_fields[$class];
257
	}
258
259
	/**
260
	 * Cache all database and composite fields for the given class.
261
	 * Will do nothing if already cached
262
	 *
263
	 * @param string $class Class name to cache
264
	 */
265
	protected static function cache_database_fields($class) {
266
		// Skip if already cached
267
		if( isset(self::$_cache_database_fields[$class])
268
			&& isset(self::$_cache_composite_fields[$class])
269
		) {
270
			return;
271
		}
272
273
		$compositeFields = array();
274
		$dbFields = array();
275
276
		// Ensure fixed fields appear at the start
277
		$fixedFields = self::config()->fixed_fields;
0 ignored issues
show
Documentation introduced by
The property fixed_fields does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
278
		if(get_parent_class($class) === 'DataObject') {
279
			// Merge fixed with ClassName spec and custom db fields
280
			$dbFields = $fixedFields;
281
		} else {
282
			$dbFields['ID'] = $fixedFields['ID'];
283
		}
284
285
		// Check each DB value as either a field or composite field
286
		$db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array();
287
		foreach($db as $fieldName => $fieldSpec) {
0 ignored issues
show
Bug introduced by
The expression $db of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
288
			$fieldClass = strtok($fieldSpec, '(');
289
			if(singleton($fieldClass) instanceof DBComposite) {
290
				$compositeFields[$fieldName] = $fieldSpec;
291
			} else {
292
				$dbFields[$fieldName] = $fieldSpec;
293
			}
294
		}
295
296
		// Add in all has_ones
297
		$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED) ?: array();
298
		foreach($hasOne as $fieldName => $hasOneClass) {
0 ignored issues
show
Bug introduced by
The expression $hasOne of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
299
			if($hasOneClass === 'DataObject') {
300
				$compositeFields[$fieldName] = 'PolymorphicForeignKey';
301
			} else {
302
				$dbFields["{$fieldName}ID"] = 'ForeignKey';
303
			}
304
		}
305
306
		// Merge composite fields into DB
307
		foreach($compositeFields as $fieldName => $fieldSpec) {
308
			$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
309
			$fieldObj->setTable($class);
310
			$nestedFields = $fieldObj->compositeDatabaseFields();
311
			foreach($nestedFields as $nestedName => $nestedSpec) {
312
				$dbFields["{$fieldName}{$nestedName}"] = $nestedSpec;
313
			}
314
		}
315
316
		// Return cached results
317
		self::$_cache_database_fields[$class] = $dbFields;
318
		self::$_cache_composite_fields[$class] = $compositeFields;
319
	}
320
321
	/**
322
	 * Get all database columns explicitly defined on a class in {@link DataObject::$db}
323
	 * and {@link DataObject::$has_one}. Resolves instances of {@link DBComposite}
324
	 * into the actual database fields, rather than the name of the field which
325
	 * might not equate a database column.
326
	 *
327
	 * Does not include "base fields" like "ID", "ClassName", "Created", "LastEdited",
328
	 * see {@link database_fields()}.
329
	 *
330
	 * Can be called directly on an object. E.g. Member::custom_database_fields()
331
	 *
332
	 * @uses DBComposite->compositeDatabaseFields()
333
	 *
334
	 * @param string $class Class name to query from
335
	 * @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
336
	 */
337
	public static function custom_database_fields($class = null) {
338
		if(empty($class)) {
339
			$class = get_called_class();
340
		}
341
342
		// Get all fields
343
		$fields = self::database_fields($class);
344
345
		// Remove fixed fields. This assumes that NO fixed_fields are composite
346
		$fields = array_diff_key($fields, self::config()->fixed_fields);
347
		return $fields;
348
	}
349
350
	/**
351
	 * Returns the field class if the given db field on the class is a composite field.
352
	 * Will check all applicable ancestor classes and aggregate results.
353
	 *
354
	 * @param string $class Class to check
355
	 * @param string $name Field to check
356
	 * @param boolean $aggregated True if parent classes should be checked, or false to limit to this class
357
	 * @return string|false Class spec name of composite field if it exists, or false if not
358
	 */
359
	public static function is_composite_field($class, $name, $aggregated = true) {
360
		$fields = self::composite_fields($class, $aggregated);
361
		return isset($fields[$name]) ? $fields[$name] : false;
362
	}
363
364
	/**
365
	 * Returns a list of all the composite if the given db field on the class is a composite field.
366
	 * Will check all applicable ancestor classes and aggregate results.
367
	 *
368
	 * Can be called directly on an object. E.g. Member::composite_fields(), or Member::composite_fields(null, true)
369
	 * to aggregate.
370
	 *
371
	 * Includes composite has_one (Polymorphic) fields
372
	 *
373
	 * @param string $class Name of class to check
374
	 * @param bool $aggregated Include fields in entire hierarchy, rather than just on this table
375
	 * @return array List of composite fields and their class spec
376
	 */
377
	public static function composite_fields($class = null, $aggregated = true) {
378
		// Check $class
379
		if(empty($class)) {
380
			$class = get_called_class();
381
		}
382
		if($class === 'DataObject') {
383
			return array();
384
		}
385
386
		// Refresh cache
387
		self::cache_database_fields($class);
388
389
		// Get fields for this class
390
		$compositeFields = self::$_cache_composite_fields[$class];
391
		if(!$aggregated) {
392
			return $compositeFields;
393
		}
394
395
		// Recursively merge
396
		return array_merge(
397
			$compositeFields,
398
			self::composite_fields(get_parent_class($class))
399
		);
400
	}
401
402
	/**
403
	 * Construct a new DataObject.
404
	 *
405
	 * @param array|null $record This will be null for a new database record.  Alternatively, you can pass an array of
406
	 * field values.  Normally this contructor is only used by the internal systems that get objects from the database.
407
	 * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
408
	 *                             Singletons don't have their defaults set.
409
	 * @param DataModel $model
410
	 * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
411
	 */
412
	public function __construct($record = null, $isSingleton = false, $model = null, $queryParams = array()) {
413
		parent::__construct();
414
415
		// Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
416
		$this->setSourceQueryParams($queryParams);
417
418
		// Set the fields data.
419
		if(!$record) {
420
			$record = array(
421
				'ID' => 0,
422
				'ClassName' => get_class($this),
423
				'RecordClassName' => get_class($this)
424
			);
425
		}
426
427
		if(!is_array($record) && !is_a($record, "stdClass")) {
428
			if(is_object($record)) $passed = "an object of type '$record->class'";
429
			else $passed = "The value '$record'";
430
431
			user_error("DataObject::__construct passed $passed.  It's supposed to be passed an array,"
432
				. " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
433
				E_USER_WARNING);
434
			$record = null;
435
		}
436
437
		if(is_a($record, "stdClass")) {
438
			$record = (array)$record;
439
		}
440
441
		// Set $this->record to $record, but ignore NULLs
442
		$this->record = array();
443
		foreach($record as $k => $v) {
0 ignored issues
show
Bug introduced by
The expression $record of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
444
			// Ensure that ID is stored as a number and not a string
445
			// To do: this kind of clean-up should be done on all numeric fields, in some relatively
446
			// performant manner
447
			if($v !== null) {
448
				if($k == 'ID' && is_numeric($v)) $this->record[$k] = (int)$v;
449
				else $this->record[$k] = $v;
450
			}
451
		}
452
453
		// Identify fields that should be lazy loaded, but only on existing records
454
		if(!empty($record['ID'])) {
455
			$currentObj = get_class($this);
456
			while($currentObj != 'DataObject') {
457
				$fields = self::custom_database_fields($currentObj);
458
				foreach($fields as $field => $type) {
459
					if(!array_key_exists($field, $record)) $this->record[$field.'_Lazy'] = $currentObj;
460
				}
461
				$currentObj = get_parent_class($currentObj);
462
			}
463
		}
464
465
		$this->original = $this->record;
466
467
		// Keep track of the modification date of all the data sourced to make this page
468
		// From this we create a Last-Modified HTTP header
469
		if(isset($record['LastEdited'])) {
470
			HTTP::register_modification_date($record['LastEdited']);
471
		}
472
473
		// this must be called before populateDefaults(), as field getters on a DataObject
474
		// may call getComponent() and others, which rely on $this->model being set.
475
		$this->model = $model ? $model : DataModel::inst();
476
477
		// Must be called after parent constructor
478
		if(!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
479
			$this->populateDefaults();
480
		}
481
482
		// prevent populateDefaults() and setField() from marking overwritten defaults as changed
483
		$this->changed = array();
484
	}
485
486
	/**
487
	 * Set the DataModel
488
	 * @param DataModel $model
489
	 * @return DataObject $this
490
	 */
491
	public function setDataModel(DataModel $model) {
492
		$this->model = $model;
493
		return $this;
494
	}
495
496
	/**
497
	 * Destroy all of this objects dependant objects and local caches.
498
	 * You'll need to call this to get the memory of an object that has components or extensions freed.
499
	 */
500
	public function destroy() {
501
		//$this->destroyed = true;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
502
		gc_collect_cycles();
503
		$this->flushCache(false);
504
	}
505
506
	/**
507
	 * Create a duplicate of this node.
508
	 * Note: now also duplicates relations.
509
	 *
510
	 * @param bool $doWrite Perform a write() operation before returning the object.
511
	 * If this is true, it will create the duplicate in the database.
512
	 * @return DataObject A duplicate of this node. The exact type will be the type of this node.
513
	 */
514
	public function duplicate($doWrite = true) {
515
		$className = $this->class;
516
		$clone = new $className( $this->toMap(), false, $this->model );
517
		$clone->ID = 0;
518
519
		$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
520
		if($doWrite) {
521
			$clone->write();
522
			$this->duplicateManyManyRelations($this, $clone);
523
		}
524
		$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
525
526
		return $clone;
527
	}
528
529
	/**
530
	 * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object
531
	 * The destinationObject must be written to the database already and have an ID. Writing is performed
532
	 * automatically when adding the new relations.
533
	 *
534
	 * @param DataObject $sourceObject the source object to duplicate from
535
	 * @param DataObject $destinationObject the destination object to populate with the duplicated relations
536
	 * @return DataObject with the new many_many relations copied in
537
	 */
538
	protected function duplicateManyManyRelations($sourceObject, $destinationObject) {
539
		if (!$destinationObject || $destinationObject->ID < 1) {
540
			user_error("Can't duplicate relations for an object that has not been written to the database",
541
				E_USER_ERROR);
542
		}
543
544
		//duplicate complex relations
545
		// DO NOT copy has_many relations, because copying the relation would result in us changing the has_one
546
		// relation on the other side of this relation to point at the copy and no longer the original (being a
547
		// has_one, it can only point at one thing at a time). So, all relations except has_many can and are copied
548
		if ($sourceObject->hasOne()) foreach($sourceObject->hasOne() as $name => $type) {
549
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
550
		}
551
		if ($sourceObject->manyMany()) foreach($sourceObject->manyMany() as $name => $type) {
552
			//many_many include belongs_many_many
553
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
554
		}
555
556
		return $destinationObject;
557
	}
558
559
	/**
560
	 * Helper function to duplicate relations from one object to another
561
	 * @param $sourceObject the source object to duplicate from
562
	 * @param $destinationObject the destination object to populate with the duplicated relations
563
	 * @param $name the name of the relation to duplicate (e.g. members)
564
	 */
565
	private function duplicateRelations($sourceObject, $destinationObject, $name) {
566
		$relations = $sourceObject->$name();
567
		if ($relations) {
568
			if ($relations instanceOf RelationList) {   //many-to-something relation
569
				if ($relations->Count() > 0) {  //with more than one thing it is related to
570
					foreach($relations as $relation) {
571
						$destinationObject->$name()->add($relation);
572
					}
573
				}
574
			} else {    //one-to-one relation
575
				$destinationObject->{"{$name}ID"} = $relations->ID;
576
			}
577
		}
578
	}
579
580
	public function getObsoleteClassName() {
581
		$className = $this->getField("ClassName");
582
		if (!ClassInfo::exists($className)) return $className;
583
	}
584
585
	public function getClassName() {
586
		$className = $this->getField("ClassName");
587
		if (!ClassInfo::exists($className)) return get_class($this);
588
		return $className;
589
	}
590
591
	/**
592
	 * Set the ClassName attribute. {@link $class} is also updated.
593
	 * Warning: This will produce an inconsistent record, as the object
594
	 * instance will not automatically switch to the new subclass.
595
	 * Please use {@link newClassInstance()} for this purpose,
596
	 * or destroy and reinstanciate the record.
597
	 *
598
	 * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
599
	 * @return DataObject $this
600
	 */
601
	public function setClassName($className) {
602
		$className = trim($className);
603
		if(!$className || !is_subclass_of($className, 'DataObject')) return;
604
605
		$this->class = $className;
606
		$this->setField("ClassName", $className);
607
		return $this;
608
	}
609
610
	/**
611
	 * Create a new instance of a different class from this object's record.
612
	 * This is useful when dynamically changing the type of an instance. Specifically,
613
	 * it ensures that the instance of the class is a match for the className of the
614
	 * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
615
	 * property manually before calling this method, as it will confuse change detection.
616
	 *
617
	 * If the new class is different to the original class, defaults are populated again
618
	 * because this will only occur automatically on instantiation of a DataObject if
619
	 * there is no record, or the record has no ID. In this case, we do have an ID but
620
	 * we still need to repopulate the defaults.
621
	 *
622
	 * @param string $newClassName The name of the new class
623
	 *
624
	 * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
625
	 */
626
	public function newClassInstance($newClassName) {
627
		$originalClass = $this->ClassName;
628
		$newInstance = new $newClassName(array_merge(
629
			$this->record,
630
			array(
631
				'ClassName' => $originalClass,
632
				'RecordClassName' => $originalClass,
633
			)
634
		), false, $this->model);
635
636
		if($newClassName != $originalClass) {
637
			$newInstance->setClassName($newClassName);
638
			$newInstance->populateDefaults();
639
			$newInstance->forceChange();
640
		}
641
642
		return $newInstance;
643
	}
644
645
	/**
646
	 * Adds methods from the extensions.
647
	 * Called by Object::__construct() once per class.
648
	 */
649
	public function defineMethods() {
650
		parent::defineMethods();
651
652
		// Define the extra db fields - this is only necessary for extensions added in the
653
		// class definition.  Object::add_extension() will call this at definition time for
654
		// those objects, which is a better mechanism.  Perhaps extensions defined inside the
655
		// class def can somehow be applied at definiton time also?
656
		if($this->extension_instances) foreach($this->extension_instances as $i => $instance) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extension_instances of type Extension[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
657
			if(!$instance->class) {
658
				$class = get_class($instance);
659
				user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls"
660
					. " parent::__construct()", E_USER_ERROR);
661
			}
662
		}
663
664
		if($this->class == 'DataObject') return;
665
666
		// Set up accessors for joined items
667
		if($manyMany = $this->manyMany()) {
668
			foreach($manyMany as $relationship => $class) {
669
				$this->addWrapperMethod($relationship, 'getManyManyComponents');
670
			}
671
		}
672
		if($hasMany = $this->hasMany()) {
673
674
			foreach($hasMany as $relationship => $class) {
0 ignored issues
show
Bug introduced by
The expression $hasMany of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
675
				$this->addWrapperMethod($relationship, 'getComponents');
676
			}
677
678
		}
679
		if($hasOne = $this->hasOne()) {
680
			foreach($hasOne as $relationship => $class) {
681
				$this->addWrapperMethod($relationship, 'getComponent');
682
			}
683
		}
684
		if($belongsTo = $this->belongsTo()) foreach(array_keys($belongsTo) as $relationship) {
685
			$this->addWrapperMethod($relationship, 'getComponent');
686
		}
687
	}
688
689
	/**
690
	 * Returns true if this object "exists", i.e., has a sensible value.
691
	 * The default behaviour for a DataObject is to return true if
692
	 * the object exists in the database, you can override this in subclasses.
693
	 *
694
	 * @return boolean true if this object exists
695
	 */
696
	public function exists() {
697
		return (isset($this->record['ID']) && $this->record['ID'] > 0);
698
	}
699
700
	/**
701
	 * Returns TRUE if all values (other than "ID") are
702
	 * considered empty (by weak boolean comparison).
703
	 *
704
	 * @return boolean
705
	 */
706
	public function isEmpty() {
707
		$fixed = $this->config()->fixed_fields;
0 ignored issues
show
Documentation introduced by
The property fixed_fields does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
708
		foreach($this->toMap() as $field => $value){
709
			// only look at custom fields
710
			if(isset($fixed[$field])) {
711
				continue;
712
			}
713
714
			$dbObject = $this->dbObject($field);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $dbObject is correct as $this->dbObject($field) (which targets DataObject::dbObject()) seems to always return null.

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

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

}

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

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

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

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

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

foreach ($a as $b) {
}

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


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

// $b is now guaranteed to be defined here.
Loading history...
913
					$this->$relatedFieldName = $relObj->ID;
914
					$relObj->flushCache();
915
				} else {
916
					user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
917
				}
918
			} else {
919
				$this->$k = $v;
920
			}
921
		}
922
		return $this;
923
	}
924
925
	/**
926
	 * Pass changes as a map, and try to
927
	 * get automatic casting for these fields.
928
	 * Doesn't write to the database. To write the data,
929
	 * use the write() method.
930
	 *
931
	 * @param array $data A map of field name to data values to update.
932
	 * @return DataObject $this
933
	 */
934
	public function castedUpdate($data) {
935
		foreach($data as $k => $v) {
936
			$this->setCastedField($k,$v);
937
		}
938
		return $this;
939
	}
940
941
	/**
942
	 * Merges data and relations from another object of same class,
943
	 * without conflict resolution. Allows to specify which
944
	 * dataset takes priority in case its not empty.
945
	 * has_one-relations are just transferred with priority 'right'.
946
	 * has_many and many_many-relations are added regardless of priority.
947
	 *
948
	 * Caution: has_many/many_many relations are moved rather than duplicated,
949
	 * meaning they are not connected to the merged object any longer.
950
	 * Caution: Just saves updated has_many/many_many relations to the database,
951
	 * doesn't write the updated object itself (just writes the object-properties).
952
	 * Caution: Does not delete the merged object.
953
	 * Caution: Does now overwrite Created date on the original object.
954
	 *
955
	 * @param $obj DataObject
956
	 * @param $priority String left|right Determines who wins in case of a conflict (optional)
957
	 * @param $includeRelations Boolean Merge any existing relations (optional)
958
	 * @param $overwriteWithEmpty Boolean Overwrite existing left values with empty right values.
959
	 *                            Only applicable with $priority='right'. (optional)
960
	 * @return Boolean
961
	 */
962
	public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) {
963
		$leftObj = $this;
964
965
		if($leftObj->ClassName != $rightObj->ClassName) {
966
			// we can't merge similiar subclasses because they might have additional relations
967
			user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
968
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
969
			return false;
970
		}
971
972
		if(!$rightObj->ID) {
973
			user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
974
				to make sure all relations are transferred properly.').", E_USER_WARNING);
975
			return false;
976
		}
977
978
		// makes sure we don't merge data like ID or ClassName
979
		$leftData = $leftObj->db();
980
		$rightData = $rightObj->db();
981
982
		foreach($rightData as $key=>$rightSpec) {
983
			// Don't merge ID
984
			if($key === 'ID') {
985
				continue;
986
			}
987
988
			// Only merge relations if allowed
989
			if($rightSpec === 'ForeignKey' && !$includeRelations) {
990
				continue;
991
			}
992
993
			// don't merge conflicting values if priority is 'left'
994
			if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
995
				continue;
996
			}
997
998
			// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
999
			if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
1000
				continue;
1001
			}
1002
1003
			// TODO remove redundant merge of has_one fields
1004
			$leftObj->{$key} = $rightObj->{$key};
1005
		}
1006
1007
		// merge relations
1008
		if($includeRelations) {
1009
			if($manyMany = $this->manyMany()) {
1010
				foreach($manyMany as $relationship => $class) {
1011
					$leftComponents = $leftObj->getManyManyComponents($relationship);
1012
					$rightComponents = $rightObj->getManyManyComponents($relationship);
1013
					if($rightComponents && $rightComponents->exists()) {
1014
						$leftComponents->addMany($rightComponents->column('ID'));
1015
					}
1016
					$leftComponents->write();
1017
				}
1018
			}
1019
1020
			if($hasMany = $this->hasMany()) {
1021
				foreach($hasMany as $relationship => $class) {
0 ignored issues
show
Bug introduced by
The expression $hasMany of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
1022
					$leftComponents = $leftObj->getComponents($relationship);
1023
					$rightComponents = $rightObj->getComponents($relationship);
1024
					if($rightComponents && $rightComponents->exists()) {
1025
						$leftComponents->addMany($rightComponents->column('ID'));
1026
					}
1027
					$leftComponents->write();
1028
				}
1029
1030
			}
1031
		}
1032
1033
		return true;
1034
	}
1035
1036
	/**
1037
	 * Forces the record to think that all its data has changed.
1038
	 * Doesn't write to the database. Only sets fields as changed
1039
	 * if they are not already marked as changed.
1040
	 *
1041
	 * @return DataObject $this
1042
	 */
1043
	public function forceChange() {
1044
		// Ensure lazy fields loaded
1045
		$this->loadLazyFields();
1046
1047
		// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1048
		$fieldNames = array_unique(array_merge(
1049
			array_keys($this->record),
1050
			array_keys($this->db())
1051
		));
1052
1053
		foreach($fieldNames as $fieldName) {
1054
			if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
1055
			// Populate the null values in record so that they actually get written
1056
			if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
1057
		}
1058
1059
		// @todo Find better way to allow versioned to write a new version after forceChange
1060
		if($this->isChanged('Version')) unset($this->changed['Version']);
1061
		return $this;
1062
	}
1063
1064
	/**
1065
	 * Validate the current object.
1066
	 *
1067
	 * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1068
	 * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1069
	 *
1070
	 * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1071
	 * and onAfterWrite() won't get called either.
1072
	 *
1073
	 * It is expected that you call validate() in your own application to test that an object is valid before
1074
	 * attempting a write, and respond appropriately if it isn't.
1075
	 *
1076
	 * @see {@link ValidationResult}
1077
	 * @return ValidationResult
1078
	 */
1079
	public function validate() {
1080
		$result = ValidationResult::create();
1081
		$this->extend('validate', $result);
1082
		return $result;
1083
	}
1084
1085
	/**
1086
	 * Public accessor for {@see DataObject::validate()}
1087
	 *
1088
	 * @return ValidationResult
1089
	 */
1090
	public function doValidate() {
1091
		Deprecation::notice('5.0', 'Use validate');
1092
		return $this->validate();
1093
	}
1094
1095
	/**
1096
	 * Event handler called before writing to the database.
1097
	 * You can overload this to clean up or otherwise process data before writing it to the
1098
	 * database.  Don't forget to call parent::onBeforeWrite(), though!
1099
	 *
1100
	 * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1101
	 *
1102
	 * @uses DataExtension->onBeforeWrite()
1103
	 */
1104
	protected function onBeforeWrite() {
1105
		$this->brokenOnWrite = false;
1106
1107
		$dummy = null;
1108
		$this->extend('onBeforeWrite', $dummy);
1109
	}
1110
1111
	/**
1112
	 * Event handler called after writing to the database.
1113
	 * You can overload this to act upon changes made to the data after it is written.
1114
	 * $this->changed will have a record
1115
	 * database.  Don't forget to call parent::onAfterWrite(), though!
1116
	 *
1117
	 * @uses DataExtension->onAfterWrite()
1118
	 */
1119
	protected function onAfterWrite() {
1120
		$dummy = null;
1121
		$this->extend('onAfterWrite', $dummy);
1122
	}
1123
1124
	/**
1125
	 * Event handler called before deleting from the database.
1126
	 * You can overload this to clean up or otherwise process data before delete this
1127
	 * record.  Don't forget to call parent::onBeforeDelete(), though!
1128
	 *
1129
	 * @uses DataExtension->onBeforeDelete()
1130
	 */
1131
	protected function onBeforeDelete() {
1132
		$this->brokenOnDelete = false;
1133
1134
		$dummy = null;
1135
		$this->extend('onBeforeDelete', $dummy);
1136
	}
1137
1138
	protected function onAfterDelete() {
1139
		$this->extend('onAfterDelete');
1140
	}
1141
1142
	/**
1143
	 * Load the default values in from the self::$defaults array.
1144
	 * Will traverse the defaults of the current class and all its parent classes.
1145
	 * Called by the constructor when creating new records.
1146
	 *
1147
	 * @uses DataExtension->populateDefaults()
1148
	 * @return DataObject $this
1149
	 */
1150
	public function populateDefaults() {
1151
		$classes = array_reverse(ClassInfo::ancestry($this));
1152
1153
		foreach($classes as $class) {
1154
			$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1155
1156
			if($defaults && !is_array($defaults)) {
1157
				user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
1158
					E_USER_WARNING);
1159
				$defaults = null;
1160
			}
1161
1162
			if($defaults) foreach($defaults as $fieldName => $fieldValue) {
0 ignored issues
show
Bug introduced by
The expression $defaults of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
1163
				// SRM 2007-03-06: Stricter check
1164
				if(!isset($this->$fieldName) || $this->$fieldName === null) {
1165
					$this->$fieldName = $fieldValue;
1166
				}
1167
				// Set many-many defaults with an array of ids
1168
				if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
1169
					$manyManyJoin = $this->$fieldName();
1170
					$manyManyJoin->setByIdList($fieldValue);
1171
				}
1172
			}
1173
			if($class == 'DataObject') {
1174
				break;
1175
			}
1176
		}
1177
1178
		$this->extend('populateDefaults');
1179
		return $this;
1180
	}
1181
1182
	/**
1183
	 * Determine validation of this object prior to write
1184
	 *
1185
	 * @return ValidationException Exception generated by this write, or null if valid
1186
	 */
1187
	protected function validateWrite() {
1188
		if ($this->ObsoleteClassName) {
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

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

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

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

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

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

Loading history...
1191
				"you need to change the ClassName before you can write it",
1192
				E_USER_WARNING
1193
			);
1194
		}
1195
1196
		if(Config::inst()->get('DataObject', 'validation_enabled')) {
1197
			$result = $this->validate();
1198
			if (!$result->valid()) {
1199
				return new ValidationException(
1200
					$result,
1201
					$result->message(),
1202
					E_USER_WARNING
1203
				);
1204
			}
1205
		}
1206
	}
1207
1208
	/**
1209
	 * Prepare an object prior to write
1210
	 *
1211
	 * @throws ValidationException
1212
	 */
1213
	protected function preWrite() {
1214
		// Validate this object
1215
		if($writeException = $this->validateWrite()) {
1216
			// Used by DODs to clean up after themselves, eg, Versioned
1217
			$this->invokeWithExtensions('onAfterSkippedWrite');
1218
			throw $writeException;
1219
		}
1220
1221
		// Check onBeforeWrite
1222
		$this->brokenOnWrite = true;
1223
		$this->onBeforeWrite();
1224
		if($this->brokenOnWrite) {
1225
			user_error("$this->class has a broken onBeforeWrite() function."
1226
				. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1227
		}
1228
	}
1229
1230
	/**
1231
	 * Detects and updates all changes made to this object
1232
	 *
1233
	 * @param bool $forceChanges If set to true, force all fields to be treated as changed
1234
	 * @return bool True if any changes are detected
1235
	 */
1236
	protected function updateChanges($forceChanges = false) {
1237
		// Update the changed array with references to changed obj-fields
1238
		foreach($this->record as $field => $value) {
1239
			// Only mark ID as changed if $forceChanges
1240
			if($field === 'ID' && !$forceChanges) continue;
1241
			// Determine if this field should be forced, or can mark itself, changed
1242
			if($forceChanges
1243
				|| !$this->isInDB()
1244
				|| (is_object($value) && method_exists($value, 'isChanged') && $value->isChanged())
1245
			) {
1246
				$this->changed[$field] = self::CHANGE_VALUE;
1247
			}
1248
		}
1249
1250
		// Check changes exist, abort if there are no changes
1251
		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...
1252
	}
1253
1254
	/**
1255
	 * Writes a subset of changes for a specific table to the given manipulation
1256
	 *
1257
	 * @param string $baseTable Base table
1258
	 * @param string $now Timestamp to use for the current time
1259
	 * @param bool $isNewRecord Whether this should be treated as a new record write
1260
	 * @param array $manipulation Manipulation to write to
1261
	 * @param string $class Table and Class to select and write to
1262
	 */
1263
	protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
1264
		$manipulation[$class] = array();
1265
1266
		// Extract records for this table
1267
		foreach($this->record as $fieldName => $fieldValue) {
1268
1269
			// Check if this record pertains to this table, and
1270
			// we're not attempting to reset the BaseTable->ID
1271
			if(	empty($this->changed[$fieldName])
1272
				|| ($class === $baseTable && $fieldName === 'ID')
1273
				|| (!self::has_own_table_database_field($class, $fieldName)
1274
					&& !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...
1275
			) {
1276
				continue;
1277
			}
1278
1279
1280
			// if database column doesn't correlate to a DBField instance...
1281
			$fieldObj = $this->dbObject($fieldName);
1282
			if(!$fieldObj) {
1283
				$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1284
			}
1285
1286
			// Write to manipulation
1287
			$fieldObj->writeToManipulation($manipulation[$class]);
1288
		}
1289
1290
		// Ensure update of Created and LastEdited columns
1291
		if($baseTable === $class) {
1292
			$manipulation[$class]['fields']['LastEdited'] = $now;
1293
			if($isNewRecord) {
1294
				$manipulation[$class]['fields']['Created']
1295
					= empty($this->record['Created'])
1296
						? $now
1297
						: $this->record['Created'];
1298
				$manipulation[$class]['fields']['ClassName'] = $this->class;
1299
			}
1300
		}
1301
1302
		// Inserts done one the base table are performed in another step, so the manipulation should instead
1303
		// attempt an update, as though it were a normal update.
1304
		$manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update';
1305
		$manipulation[$class]['id'] = $this->record['ID'];
1306
	}
1307
1308
	/**
1309
	 * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1310
	 *
1311
	 * Does nothing if an ID is already assigned for this record
1312
	 *
1313
	 * @param string $baseTable Base table
1314
	 * @param string $now Timestamp to use for the current time
1315
	 */
1316
	protected function writeBaseRecord($baseTable, $now) {
1317
		// Generate new ID if not specified
1318
		if($this->isInDB()) return;
1319
1320
		// Perform an insert on the base table
1321
		$insert = new SQLInsert('"'.$baseTable.'"');
1322
		$insert
1323
			->assign('"Created"', $now)
1324
			->execute();
1325
		$this->changed['ID'] = self::CHANGE_VALUE;
1326
		$this->record['ID'] = DB::get_generated_id($baseTable);
1327
	}
1328
1329
	/**
1330
	 * Generate and write the database manipulation for all changed fields
1331
	 *
1332
	 * @param string $baseTable Base table
1333
	 * @param string $now Timestamp to use for the current time
1334
	 * @param bool $isNewRecord If this is a new record
1335
	 */
1336
	protected function writeManipulation($baseTable, $now, $isNewRecord) {
1337
		// Generate database manipulations for each class
1338
		$manipulation = array();
1339
		foreach($this->getClassAncestry() as $class) {
1340
			if(self::has_own_table($class)) {
1341
				$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1342
			}
1343
		}
1344
1345
		// Allow extensions to extend this manipulation
1346
		$this->extend('augmentWrite', $manipulation);
1347
1348
		// New records have their insert into the base data table done first, so that they can pass the
1349
		// generated ID on to the rest of the manipulation
1350
		if($isNewRecord) {
1351
			$manipulation[$baseTable]['command'] = 'update';
1352
		}
1353
1354
		// Perform the manipulation
1355
		DB::manipulate($manipulation);
1356
	}
1357
1358
	/**
1359
	 * Writes all changes to this object to the database.
1360
	 *  - It will insert a record whenever ID isn't set, otherwise update.
1361
	 *  - All relevant tables will be updated.
1362
	 *  - $this->onBeforeWrite() gets called beforehand.
1363
	 *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1364
	 *
1365
	 *  @uses DataExtension->augmentWrite()
1366
	 *
1367
	 * @param boolean $showDebug Show debugging information
1368
	 * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1369
	 * @param boolean $forceWrite Write to database even if there are no changes
1370
	 * @param boolean $writeComponents Call write() on all associated component instances which were previously
1371
	 *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1372
	 *                                 {@link getManyManyComponents()} (Default: false)
1373
	 * @return int The ID of the record
1374
	 * @throws ValidationException Exception that can be caught and handled by the calling function
1375
	 */
1376
	public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1377
		$now = DBDatetime::now()->Rfc2822();
1378
1379
		// Execute pre-write tasks
1380
		$this->preWrite();
1381
1382
		// Check if we are doing an update or an insert
1383
		$isNewRecord = !$this->isInDB() || $forceInsert;
1384
1385
		// Check changes exist, abort if there are none
1386
		$hasChanges = $this->updateChanges($forceInsert);
1387
		if($hasChanges || $forceWrite || $isNewRecord) {
1388
			// New records have their insert into the base data table done first, so that they can pass the
1389
			// generated primary key on to the rest of the manipulation
1390
			$baseTable = ClassInfo::baseDataClass($this->class);
1391
			$this->writeBaseRecord($baseTable, $now);
1392
1393
			// Write the DB manipulation for all changed fields
1394
			$this->writeManipulation($baseTable, $now, $isNewRecord);
1395
1396
			// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1397
			$this->writeRelations();
1398
			$this->onAfterWrite();
1399
			$this->changed = array();
1400
		} else {
1401
			if($showDebug) Debug::message("no changes for DataObject");
1402
1403
			// Used by DODs to clean up after themselves, eg, Versioned
1404
			$this->invokeWithExtensions('onAfterSkippedWrite');
1405
		}
1406
1407
		// Ensure Created and LastEdited are populated
1408
		if(!isset($this->record['Created'])) {
1409
			$this->record['Created'] = $now;
1410
		}
1411
		$this->record['LastEdited'] = $now;
1412
1413
		// Write relations as necessary
1414
		if($writeComponents) $this->writeComponents(true);
1415
1416
		// Clears the cache for this object so get_one returns the correct object.
1417
		$this->flushCache();
1418
1419
		return $this->record['ID'];
1420
	}
1421
1422
	/**
1423
	 * Writes cached relation lists to the database, if possible
1424
	 */
1425
	public function writeRelations() {
1426
		if(!$this->isInDB()) return;
1427
1428
		// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1429
		if($this->unsavedRelations) {
1430
			foreach($this->unsavedRelations as $name => $list) {
1431
				$list->changeToList($this->$name());
1432
			}
1433
			$this->unsavedRelations = array();
1434
		}
1435
	}
1436
1437
	/**
1438
	 * Write the cached components to the database. Cached components could refer to two different instances of the
1439
	 * same record.
1440
	 *
1441
	 * @param $recursive Recursively write components
1442
	 * @return DataObject $this
1443
	 */
1444
	public function writeComponents($recursive = false) {
1445
		if(!$this->components) return $this;
1446
1447
		foreach($this->components as $component) {
1448
			$component->write(false, false, false, $recursive);
1449
		}
1450
		return $this;
1451
	}
1452
1453
	/**
1454
	 * Delete this data object.
1455
	 * $this->onBeforeDelete() gets called.
1456
	 * Note that in Versioned objects, both Stage and Live will be deleted.
1457
	 *  @uses DataExtension->augmentSQL()
1458
	 */
1459
	public function delete() {
1460
		$this->brokenOnDelete = true;
1461
		$this->onBeforeDelete();
1462
		if($this->brokenOnDelete) {
1463
			user_error("$this->class has a broken onBeforeDelete() function."
1464
				. " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1465
		}
1466
1467
		// Deleting a record without an ID shouldn't do anything
1468
		if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1469
1470
		// TODO: This is quite ugly.  To improve:
1471
		//  - move the details of the delete code in the DataQuery system
1472
		//  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1473
		//    obviously, that means getting requireTable() to configure cascading deletes ;-)
1474
		$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1475
		foreach($srcQuery->queriedTables() as $table) {
1476
			$delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1477
			$delete->execute();
1478
		}
1479
		// Remove this item out of any caches
1480
		$this->flushCache();
1481
1482
		$this->onAfterDelete();
1483
1484
		$this->OldID = $this->ID;
0 ignored issues
show
Documentation introduced by
The property OldID does not exist on object<DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1485
		$this->ID = 0;
1486
	}
1487
1488
	/**
1489
	 * Delete the record with the given ID.
1490
	 *
1491
	 * @param string $className The class name of the record to be deleted
1492
	 * @param int $id ID of record to be deleted
1493
	 */
1494
	public static function delete_by_id($className, $id) {
1495
		$obj = DataObject::get_by_id($className, $id);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1496
		if($obj) {
1497
			$obj->delete();
1498
		} else {
1499
			user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1500
		}
1501
	}
1502
1503
	/**
1504
	 * Get the class ancestry, including the current class name.
1505
	 * The ancestry will be returned as an array of class names, where the 0th element
1506
	 * will be the class that inherits directly from DataObject, and the last element
1507
	 * will be the current class.
1508
	 *
1509
	 * @return array Class ancestry
1510
	 */
1511
	public function getClassAncestry() {
1512
		if(!isset(self::$_cache_get_class_ancestry[$this->class])) {
1513
			self::$_cache_get_class_ancestry[$this->class] = array($this->class);
1514
			while(($class=get_parent_class(self::$_cache_get_class_ancestry[$this->class][0])) != "DataObject") {
1515
				array_unshift(self::$_cache_get_class_ancestry[$this->class], $class);
1516
			}
1517
		}
1518
		return self::$_cache_get_class_ancestry[$this->class];
1519
	}
1520
1521
	/**
1522
	 * Return a component object from a one to one relationship, as a DataObject.
1523
	 * If no component is available, an 'empty component' will be returned for
1524
	 * non-polymorphic relations, or for polymorphic relations with a class set.
1525
	 *
1526
	 * @param string $componentName Name of the component
1527
	 * @return DataObject The component object. It's exact type will be that of the component.
1528
	 * @throws Exception
1529
	 */
1530
	public function getComponent($componentName) {
1531
		if(isset($this->components[$componentName])) {
1532
			return $this->components[$componentName];
1533
		}
1534
1535
		if($class = $this->hasOneComponent($componentName)) {
1536
			$joinField = $componentName . 'ID';
1537
			$joinID    = $this->getField($joinField);
1538
1539
			// Extract class name for polymorphic relations
1540
			if($class === 'DataObject') {
1541
				$class = $this->getField($componentName . 'Class');
1542
				if(empty($class)) return null;
1543
			}
1544
1545
			if($joinID) {
1546
				// Ensure that the selected object originates from the same stage, subsite, etc
1547
				$component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1548
					->filter('ID', $joinID)
1549
					->setDataQueryParam($this->getInheritableQueryParams())
1550
					->first();
1551
			}
1552
1553
			if(empty($component)) {
1554
				$component = $this->model->$class->newObject();
1555
			}
1556
		} elseif($class = $this->belongsToComponent($componentName)) {
1557
			$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
1558
			$joinID = $this->ID;
1559
1560
			if($joinID) {
1561
				// Prepare filter for appropriate join type
1562
				if($polymorphic) {
1563
					$filter = array(
1564
						"{$joinField}ID" => $joinID,
1565
						"{$joinField}Class" => $this->class
1566
					);
1567
				} else {
1568
					$filter = array(
1569
						$joinField => $joinID
1570
					);
1571
				}
1572
1573
				// Ensure that the selected object originates from the same stage, subsite, etc
1574
				$component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1575
					->filter($filter)
1576
					->setDataQueryParam($this->getInheritableQueryParams())
1577
					->first();
1578
			}
1579
1580
			if(empty($component)) {
1581
				$component = $this->model->$class->newObject();
1582
				if($polymorphic) {
1583
					$component->{$joinField.'ID'} = $this->ID;
1584
					$component->{$joinField.'Class'} = $this->class;
1585
				} else {
1586
					$component->$joinField = $this->ID;
1587
				}
1588
			}
1589
		} else {
1590
			throw new InvalidArgumentException(
1591
				"DataObject->getComponent(): Could not find component '$componentName'."
1592
			);
1593
		}
1594
1595
		$this->components[$componentName] = $component;
1596
		return $component;
1597
	}
1598
1599
	/**
1600
	 * Returns a one-to-many relation as a HasManyList
1601
	 *
1602
	 * @param string $componentName Name of the component
1603
	 * @return HasManyList The components of the one-to-many relationship.
1604
	 */
1605
	public function getComponents($componentName) {
1606
		$result = null;
1607
1608
		$componentClass = $this->hasManyComponent($componentName);
1609
		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...
1610
			throw new InvalidArgumentException(sprintf(
1611
				"DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1612
				$componentName,
1613
				$this->class
1614
			));
1615
		}
1616
1617
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1618
		if(!$this->ID) {
1619
			if(!isset($this->unsavedRelations[$componentName])) {
1620
				$this->unsavedRelations[$componentName] =
1621
					new UnsavedRelationList($this->class, $componentName, $componentClass);
1622
			}
1623
			return $this->unsavedRelations[$componentName];
1624
		}
1625
1626
		// Determine type and nature of foreign relation
1627
		$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
1628
		/** @var HasManyList $result */
1629
		if($polymorphic) {
1630
			$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1631
		} else {
1632
			$result = HasManyList::create($componentClass, $joinField);
1633
		}
1634
1635
		if($this->model) {
1636
			$result->setDataModel($this->model);
1637
		}
1638
1639
		return $result
1640
			->setDataQueryParam($this->getInheritableQueryParams())
1641
			->forForeignID($this->ID);
1642
	}
1643
1644
	/**
1645
	 * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1646
	 *
1647
	 * @param string $relationName Relation name.
1648
	 * @return string Class name, or null if not found.
1649
	 */
1650
	public function getRelationClass($relationName) {
1651
		// Go through all relationship configuration fields.
1652
		$candidates = array_merge(
1653
			($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1654
			($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1655
			($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
1656
			($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
1657
			($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1658
		);
1659
1660
		if (isset($candidates[$relationName])) {
1661
			$remoteClass = $candidates[$relationName];
1662
1663
			// If dot notation is present, extract just the first part that contains the class.
1664
			if(($fieldPos = strpos($remoteClass, '.'))!==false) {
1665
				return substr($remoteClass, 0, $fieldPos);
1666
			}
1667
1668
			// Otherwise just return the class
1669
			return $remoteClass;
1670
		}
1671
1672
		return null;
1673
	}
1674
1675
	/**
1676
	 * Given a relation name, determine the relation type
1677
	 *
1678
	 * @param string $component Name of component
1679
	 * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1680
	 */
1681
	public function getRelationType($component) {
1682
		$types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1683
		foreach($types as $type) {
1684
			$relations = Config::inst()->get($this->class, $type);
1685
			if($relations && isset($relations[$component])) {
1686
				return $type;
1687
			}
1688
		}
1689
		return null;
1690
	}
1691
1692
	/**
1693
	 * Given a relation declared on a remote class, generate a substitute component for the opposite
1694
	 * side of the relation.
1695
	 *
1696
	 * Notes on behaviour:
1697
	 *  - This can still be used on components that are defined on both sides, but do not need to be.
1698
	 *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1699
	 *  - Cannot be used on polymorphic relationships
1700
	 *  - Cannot be used on unsaved objects.
1701
	 *
1702
	 * @param string $remoteClass
1703
	 * @param string $remoteRelation
1704
	 * @return DataList|DataObject The component, either as a list or single object
1705
	 * @throws BadMethodCallException
1706
	 * @throws InvalidArgumentException
1707
	 */
1708
	public function inferReciprocalComponent($remoteClass, $remoteRelation) {
1709
		/** @var DataObject $remote */
1710
		$remote = $remoteClass::singleton();
1711
		$class = $remote->getRelationClass($remoteRelation);
1712
1713
		// Validate arguments
1714
		if(!$this->isInDB()) {
1715
			throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1716
		}
1717
		if(empty($class)) {
1718
			throw new InvalidArgumentException(sprintf(
1719
				"%s invoked with invalid relation %s.%s",
1720
				__METHOD__,
1721
				$remoteClass,
1722
				$remoteRelation
1723
			));
1724
		}
1725
		if($class === 'DataObject') {
1726
			throw new InvalidArgumentException(sprintf(
1727
				"%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1728
				"This method does not support polymorphic relationships",
1729
				__METHOD__,
1730
				$remoteClass,
1731
				$remoteRelation
1732
			));
1733
		}
1734
		if(!is_a($this, $class, true)) {
1735
			throw new InvalidArgumentException(sprintf(
1736
				"Relation %s on %s does not refer to objects of type %s",
1737
				$remoteRelation, $remoteClass, get_class($this)
1738
			));
1739
		}
1740
1741
		// Check the relation type to mock
1742
		$relationType = $remote->getRelationType($remoteRelation);
1743
		switch($relationType) {
1744
			case 'has_one': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

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

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

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

Loading history...
1745
				// Mock has_many
1746
				$joinField = "{$remoteRelation}ID";
1747
				$componentClass = ClassInfo::table_for_object_field($remoteClass, $joinField);
1748
				$result = HasManyList::create($componentClass, $joinField);
1749
				if ($this->model) {
1750
					$result->setDataModel($this->model);
1751
				}
1752
				return $result
1753
					->setDataQueryParam($this->getInheritableQueryParams())
1754
					->forForeignID($this->ID);
1755
			}
1756
			case 'belongs_to':
1757
			case 'has_many': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

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

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

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

Loading history...
1758
				// These relations must have a has_one on the other end, so find it
1759
				$joinField = $remote->getRemoteJoinField($remoteRelation, $relationType, $polymorphic);
1760
				if ($polymorphic) {
1761
					throw new InvalidArgumentException(sprintf(
1762
						"%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1763
						"to be a has_one polymorphic. This method does not support polymorphic relationships",
1764
						__METHOD__,
1765
						$remoteClass,
1766
						$remoteRelation
1767
					));
1768
				}
1769
				$joinID = $this->getField($joinField);
1770
				if (empty($joinID)) {
1771
					return null;
1772
				}
1773
				// Get object by joined ID
1774
				return DataObject::get($remoteClass)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1775
					->filter('ID', $joinID)
1776
					->setDataQueryParam($this->getInheritableQueryParams())
1777
					->first();
1778
			}
1779
			case 'many_many':
1780
			case 'belongs_many_many': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

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

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

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

Loading history...
1781
				// Get components and extra fields from parent
1782
				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...
1783
					= $remote->manyManyComponent($remoteRelation);
1784
				$extraFields = $remote->manyManyExtraFieldsForComponent($remoteRelation) ?: array();
1785
1786
				// Reverse parent and component fields and create an inverse ManyManyList
1787
				/** @var ManyManyList $result */
1788
				$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1789
				if($this->model) {
1790
					$result->setDataModel($this->model);
1791
				}
1792
				$this->extend('updateManyManyComponents', $result);
1793
1794
				// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1795
				// foreignID set elsewhere.
1796
				return $result
1797
					->setDataQueryParam($this->getInheritableQueryParams())
1798
					->forForeignID($this->ID);
1799
			}
1800
			default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

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

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

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

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

Loading history...
1801
				return null;
1802
			}
1803
		}
1804
	}
1805
1806
	/**
1807
	 * Tries to find the database key on another object that is used to store a
1808
	 * relationship to this class. If no join field can be found it defaults to 'ParentID'.
1809
	 *
1810
	 * If the remote field is polymorphic then $polymorphic is set to true, and the return value
1811
	 * is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
1812
	 *
1813
	 * @param string $component Name of the relation on the current object pointing to the
1814
	 * remote object.
1815
	 * @param string $type the join type - either 'has_many' or 'belongs_to'
1816
	 * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
1817
	 * @return string
1818
	 * @throws Exception
1819
	 */
1820
	public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
1821
		// Extract relation from current object
1822
		if($type === 'has_many') {
1823
			$remoteClass = $this->hasManyComponent($component, false);
1824
		} else {
1825
			$remoteClass = $this->belongsToComponent($component, false);
1826
		}
1827
1828
		if(empty($remoteClass)) {
1829
			throw new Exception("Unknown $type component '$component' on class '$this->class'");
1830
		}
1831
		if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
1832
			throw new Exception(
1833
				"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
1834
			);
1835
		}
1836
1837
		// If presented with an explicit field name (using dot notation) then extract field name
1838
		$remoteField = null;
1839
		if(strpos($remoteClass, '.') !== false) {
1840
			list($remoteClass, $remoteField) = explode('.', $remoteClass);
1841
		}
1842
1843
		// Reference remote has_one to check against
1844
		$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
1845
1846
		// Without an explicit field name, attempt to match the first remote field
1847
		// with the same type as the current class
1848
		if(empty($remoteField)) {
1849
			// look for remote has_one joins on this class or any parent classes
1850
			$remoteRelationsMap = array_flip($remoteRelations);
1851
			foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1852
				if(array_key_exists($class, $remoteRelationsMap)) {
1853
					$remoteField = $remoteRelationsMap[$class];
1854
					break;
1855
				}
1856
			}
1857
		}
1858
1859
		// In case of an indeterminate remote field show an error
1860
		if(empty($remoteField)) {
1861
			$polymorphic = false;
1862
			$message = "No has_one found on class '$remoteClass'";
1863
			if($type == 'has_many') {
1864
				// include a hint for has_many that is missing a has_one
1865
				$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
1866
				$message .= " requires a has_one on '$remoteClass'";
1867
			}
1868
			throw new Exception($message);
1869
		}
1870
1871
		// If given an explicit field name ensure the related class specifies this
1872
		if(empty($remoteRelations[$remoteField])) {
1873
			throw new Exception("Missing expected has_one named '$remoteField'
1874
				on class '$remoteClass' referenced by $type named '$component'
1875
				on class {$this->class}"
1876
			);
1877
		}
1878
1879
		// Inspect resulting found relation
1880
		if($remoteRelations[$remoteField] === 'DataObject') {
1881
			$polymorphic = true;
1882
			return $remoteField; // Composite polymorphic field does not include 'ID' suffix
1883
		} else {
1884
			$polymorphic = false;
1885
			return $remoteField . 'ID';
1886
		}
1887
	}
1888
1889
	/**
1890
	 * Returns a many-to-many component, as a ManyManyList.
1891
	 * @param string $componentName Name of the many-many component
1892
	 * @return ManyManyList The set of components
1893
	 */
1894
	public function getManyManyComponents($componentName) {
1895
		$manyManyComponent = $this->manyManyComponent($componentName);
1896
		if(!$manyManyComponent) {
1897
			throw new InvalidArgumentException(sprintf(
1898
				"DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1899
				$componentName,
1900
				$this->class
1901
			));
1902
		}
1903
1904
		list($parentClass, $componentClass, $parentField, $componentField, $table) = $manyManyComponent;
1905
1906
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1907
		if(!$this->ID) {
1908
			if(!isset($this->unsavedRelations[$componentName])) {
1909
				$this->unsavedRelations[$componentName] =
1910
					new UnsavedRelationList($parentClass, $componentName, $componentClass);
1911
			}
1912
			return $this->unsavedRelations[$componentName];
1913
		}
1914
1915
		$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
1916
		/** @var ManyManyList $result */
1917
		$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1918
		if($this->model) {
1919
			$result->setDataModel($this->model);
1920
		}
1921
1922
		$this->extend('updateManyManyComponents', $result);
1923
1924
		// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1925
		// foreignID set elsewhere.
1926
		return $result
1927
			->setDataQueryParam($this->getInheritableQueryParams())
1928
			->forForeignID($this->ID);
1929
	}
1930
1931
	/**
1932
	 * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1933
	 * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1934
	 *
1935
	 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1936
	 * 							their classes.
1937
	 */
1938
	public function hasOne() {
1939
		return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1940
	}
1941
1942
	/**
1943
	 * Return data for a specific has_one component.
1944
	 * @param string $component
1945
	 * @return string|null
1946
	 */
1947
	public function hasOneComponent($component) {
1948
		$classes = ClassInfo::ancestry($this, true);
1949
1950
		foreach(array_reverse($classes) as $class) {
1951
			$hasOnes = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
1952
			if(isset($hasOnes[$component])) {
1953
				return $hasOnes[$component];
1954
			}
1955
		}
1956
	}
1957
1958
	/**
1959
	 * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1960
	 * their class name will be returned.
1961
	 *
1962
	 * @param string $component - Name of component
1963
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1964
	 *        the field data stripped off. It defaults to TRUE.
1965
	 * @return string|array
1966
	 */
1967
	public function belongsTo($component = null, $classOnly = true) {
1968
		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...
1969
			Deprecation::notice(
1970
				'4.0',
1971
				'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
1972
				Deprecation::SCOPE_GLOBAL
1973
			);
1974
			return $this->belongsToComponent($component, $classOnly);
1975
		}
1976
1977
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1978
		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...
1979
			return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1980
		} else {
1981
			return $belongsTo ? $belongsTo : array();
1982
		}
1983
	}
1984
1985
	/**
1986
	 * Return data for a specific belongs_to component.
1987
	 * @param string $component
1988
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1989
	 *        the field data stripped off. It defaults to TRUE.
1990
	 * @return string|null
1991
	 */
1992
	public function belongsToComponent($component, $classOnly = true) {
1993
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1994
1995
		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...
1996
			$belongsTo = $belongsTo[$component];
1997
		} else {
1998
			return null;
1999
		}
2000
2001
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
2002
	}
2003
2004
	/**
2005
	 * Return all of the database fields in this object
2006
	 *
2007
	 * @param string $fieldName Limit the output to a specific field name
2008
	 * @param string $includeTable If returning a single column, prefix the column with the table name
2009
	 * in Table.Column(spec) format
2010
	 * @return array|string|null The database fields, or if searching a single field, just this one field if found
2011
	 * Field will be a string in ClassName(args) format, or Table.ClassName(args) format if $includeTable is true
2012
	 */
2013
	public function db($fieldName = null, $includeTable = false) {
2014
		$classes = ClassInfo::ancestry($this, true);
2015
2016
		// If we're looking for a specific field, we want to hit subclasses first as they may override field types
2017
		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...
2018
			$classes = array_reverse($classes);
2019
		}
2020
2021
		$db = array();
2022
		foreach($classes as $class) {
2023
			// Merge fields with new fields and composite fields
2024
			$fields = self::database_fields($class);
2025
			$compositeFields = self::composite_fields($class, false);
2026
			$db = array_merge($db, $fields, $compositeFields);
2027
2028
			// Check for search field
2029
			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...
2030
				// Return found field
2031
				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...
2032
					return $db[$fieldName];
2033
				}
2034
2035
				// Set table for the given field
2036
				if(in_array($fieldName, $this->config()->fixed_fields)) {
2037
					$table = $this->baseTable();
2038
				} else {
2039
					$table = $class;
2040
				}
2041
				return $table . "." . $db[$fieldName];
2042
			}
2043
		}
2044
2045
		// At end of search complete
2046
		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...
2047
			return null;
2048
		} else {
2049
			return $db;
2050
		}
2051
	}
2052
2053
	/**
2054
	 * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2055
	 * relationships and their classes will be returned.
2056
	 *
2057
	 * @param string $component Deprecated - Name of component
2058
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2059
	 *        the field data stripped off. It defaults to TRUE.
2060
	 * @return string|array|false
2061
	 */
2062
	public function hasMany($component = null, $classOnly = true) {
2063
		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...
2064
			Deprecation::notice(
2065
				'4.0',
2066
				'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
2067
				Deprecation::SCOPE_GLOBAL
2068
			);
2069
			return $this->hasManyComponent($component, $classOnly);
2070
		}
2071
2072
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2073
		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...
2074
			return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2075
		} else {
2076
			return $hasMany ? $hasMany : array();
2077
		}
2078
	}
2079
2080
	/**
2081
	 * Return data for a specific has_many component.
2082
	 * @param string $component
2083
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2084
	 *        the field data stripped off. It defaults to TRUE.
2085
	 * @return string|null
2086
	 */
2087
	public function hasManyComponent($component, $classOnly = true) {
2088
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2089
2090
		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...
2091
			$hasMany = $hasMany[$component];
2092
		} else {
2093
			return null;
2094
		}
2095
2096
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
2097
	}
2098
2099
	/**
2100
	 * Return the many-to-many extra fields specification.
2101
	 *
2102
	 * If you don't specify a component name, it returns all
2103
	 * extra fields for all components available.
2104
	 *
2105
	 * @return array|null
2106
	 */
2107
	public function manyManyExtraFields() {
2108
		return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2109
	}
2110
2111
	/**
2112
	 * Return the many-to-many extra fields specification for a specific component.
2113
	 * @param string $component
2114
	 * @return array|null
2115
	 */
2116
	public function manyManyExtraFieldsForComponent($component) {
2117
		// Get all many_many_extraFields defined in this class or parent classes
2118
		$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2119
		// Extra fields are immediately available
2120
		if(isset($extraFields[$component])) {
2121
			return $extraFields[$component];
2122
		}
2123
2124
		// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
2125
		$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2126
		$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2127
		if($candidate) {
2128
			$relationName = null;
2129
			// Extract class and relation name from dot-notation
2130
			if(strpos($candidate, '.') !== false) {
2131
				list($candidate, $relationName) = explode('.', $candidate, 2);
2132
			}
2133
2134
			// If we've not already found the relation name from dot notation, we need to find a relation that points
2135
			// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
2136
			// so it's safe to assume that it's the correct one
2137
			if(!$relationName) {
2138
				$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2139
2140
				foreach($candidateManyManys as $relation => $relatedClass) {
2141
					if (is_a($this, $relatedClass)) {
2142
						$relationName = $relation;
2143
					}
2144
				}
2145
			}
2146
2147
			// If we've found a matching relation on the target class, see if we can find extra fields for it
2148
			$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
2149
			if(isset($extraFields[$relationName])) {
2150
				return $extraFields[$relationName];
2151
			}
2152
		}
2153
2154
		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...
2155
	}
2156
2157
	/**
2158
	 * Return information about a many-to-many component.
2159
	 * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2160
	 * components are returned.
2161
	 *
2162
	 * @see DataObject::manyManyComponent()
2163
	 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2164
	 */
2165
	public function manyMany() {
2166
		$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
2167
		$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2168
		$items = array_merge($manyManys, $belongsManyManys);
2169
		return $items;
2170
	}
2171
2172
	/**
2173
	 * Return information about a specific many_many component. Returns a numeric array of:
2174
	 * array(
2175
	 * 	<classname>,		The class that relation is defined in e.g. "Product"
2176
	 * 	<candidateName>,	The target class of the relation e.g. "Category"
2177
	 * 	<parentField>,		The field name pointing to <classname>'s table e.g. "ProductID"
2178
	 * 	<childField>,		The field name pointing to <candidatename>'s table e.g. "CategoryID"
2179
	 * 	<joinTable>			The join table between the two classes e.g. "Product_Categories"
2180
	 * )
2181
	 * @param string $component The component name
2182
	 * @return array|null
2183
	 */
2184
	public function manyManyComponent($component) {
2185
		$classes = $this->getClassAncestry();
2186
		foreach($classes as $class) {
2187
			$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
2188
			// Check if the component is defined in many_many on this class
2189
			$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2190
			if($candidate) {
2191
				$parentField = $class . "ID";
2192
				$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
2193
				return array($class, $candidate, $parentField, $childField, "{$class}_$component");
2194
			}
2195
2196
			// Check if the component is defined in belongs_many_many on this class
2197
			$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
2198
			$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
2199
			if($candidate) {
2200
				// Extract class and relation name from dot-notation
2201
				if(strpos($candidate, '.') !== false) {
2202
					list($candidate, $relationName) = explode('.', $candidate, 2);
2203
				}
2204
2205
				$childField = $candidate . "ID";
2206
2207
				// We need to find the inverse component name
2208
				$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2209
				if(!$otherManyMany) {
2210
					throw new LogicException("Inverse component of $candidate not found ({$this->class})");
2211
				}
2212
2213
				// If we've got a relation name (extracted from dot-notation), we can already work out
2214
				// the join table and candidate class name...
2215
				if(isset($relationName) && isset($otherManyMany[$relationName])) {
2216
					$candidateClass = $otherManyMany[$relationName];
2217
					$joinTable = "{$candidate}_{$relationName}";
2218
				} else {
2219
					// ... otherwise, we need to loop over the many_manys and find a relation that
2220
					// matches up to this class
2221
					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...
2222
						if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
2223
							$joinTable = "{$candidate}_{$inverseComponentName}";
2224
							break;
2225
						}
2226
					}
2227
				}
2228
2229
				// If we could work out the join table, we've got all the info we need
2230
				if(isset($joinTable)) {
2231
					$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...
2232
					return array($class, $candidate, $parentField, $childField, $joinTable);
2233
				}
2234
2235
				throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
2236
			}
2237
		}
2238
	}
2239
2240
	/**
2241
	 * This returns an array (if it exists) describing the database extensions that are required, or false if none
2242
	 *
2243
	 * This is experimental, and is currently only a Postgres-specific enhancement.
2244
	 *
2245
	 * @return array or false
2246
	 */
2247
	public function database_extensions($class){
2248
		$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2249
2250
		if($extensions)
2251
			return $extensions;
2252
		else
2253
			return false;
2254
	}
2255
2256
	/**
2257
	 * Generates a SearchContext to be used for building and processing
2258
	 * a generic search form for properties on this object.
2259
	 *
2260
	 * @return SearchContext
2261
	 */
2262
	public function getDefaultSearchContext() {
2263
		return new SearchContext(
2264
			$this->class,
2265
			$this->scaffoldSearchFields(),
2266
			$this->defaultSearchFilters()
2267
		);
2268
	}
2269
2270
	/**
2271
	 * Determine which properties on the DataObject are
2272
	 * searchable, and map them to their default {@link FormField}
2273
	 * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2274
	 *
2275
	 * Some additional logic is included for switching field labels, based on
2276
	 * how generic or specific the field type is.
2277
	 *
2278
	 * Used by {@link SearchContext}.
2279
	 *
2280
	 * @param array $_params
2281
	 *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2282
	 *   'restrictFields': Numeric array of a field name whitelist
2283
	 * @return FieldList
2284
	 */
2285
	public function scaffoldSearchFields($_params = null) {
2286
		$params = array_merge(
2287
			array(
2288
				'fieldClasses' => false,
2289
				'restrictFields' => false
2290
			),
2291
			(array)$_params
2292
		);
2293
		$fields = new FieldList();
2294
		foreach($this->searchableFields() as $fieldName => $spec) {
2295
			if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
2296
2297
			// If a custom fieldclass is provided as a string, use it
2298
			if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2299
				$fieldClass = $params['fieldClasses'][$fieldName];
2300
				$field = new $fieldClass($fieldName);
2301
			// If we explicitly set a field, then construct that
2302
			} else if(isset($spec['field'])) {
2303
				// If it's a string, use it as a class name and construct
2304
				if(is_string($spec['field'])) {
2305
					$fieldClass = $spec['field'];
2306
					$field = new $fieldClass($fieldName);
2307
2308
				// If it's a FormField object, then just use that object directly.
2309
				} else if($spec['field'] instanceof FormField) {
2310
					$field = $spec['field'];
2311
2312
				// Otherwise we have a bug
2313
				} else {
2314
					user_error("Bad value for searchable_fields, 'field' value: "
2315
						. var_export($spec['field'], true), E_USER_WARNING);
2316
				}
2317
2318
			// Otherwise, use the database field's scaffolder
2319
			} else {
2320
				$field = $this->relObject($fieldName)->scaffoldSearchField();
2321
			}
2322
2323
			// Allow fields to opt out of search
2324
			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...
2325
				continue;
2326
			}
2327
2328
			if (strstr($fieldName, '.')) {
2329
				$field->setName(str_replace('.', '__', $fieldName));
2330
			}
2331
			$field->setTitle($spec['title']);
2332
2333
			$fields->push($field);
2334
		}
2335
		return $fields;
2336
	}
2337
2338
	/**
2339
	 * Scaffold a simple edit form for all properties on this dataobject,
2340
	 * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2341
	 * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2342
	 *
2343
	 * @uses FormScaffolder
2344
	 *
2345
	 * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2346
	 * @return FieldList
2347
	 */
2348
	public function scaffoldFormFields($_params = null) {
2349
		$params = array_merge(
2350
			array(
2351
				'tabbed' => false,
2352
				'includeRelations' => false,
2353
				'restrictFields' => false,
2354
				'fieldClasses' => false,
2355
				'ajaxSafe' => false
2356
			),
2357
			(array)$_params
2358
		);
2359
2360
		$fs = new FormScaffolder($this);
2361
		$fs->tabbed = $params['tabbed'];
2362
		$fs->includeRelations = $params['includeRelations'];
2363
		$fs->restrictFields = $params['restrictFields'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $params['restrictFields'] of type false is incompatible with the declared type array of property $restrictFields.

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

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

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

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

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

Loading history...
2365
		$fs->ajaxSafe = $params['ajaxSafe'];
2366
2367
		return $fs->getFieldList();
2368
	}
2369
2370
	/**
2371
	 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2372
	 * being called on extensions
2373
	 *
2374
	 * @param callable $callback The callback to execute
2375
	 */
2376
	protected function beforeUpdateCMSFields($callback) {
2377
		$this->beforeExtending('updateCMSFields', $callback);
2378
	}
2379
2380
	/**
2381
	 * Centerpiece of every data administration interface in Silverstripe,
2382
	 * which returns a {@link FieldList} suitable for a {@link Form} object.
2383
	 * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2384
	 * generate this set. To customize, overload this method in a subclass
2385
	 * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2386
	 *
2387
	 * <code>
2388
	 * class MyCustomClass extends DataObject {
2389
	 *  static $db = array('CustomProperty'=>'Boolean');
2390
	 *
2391
	 *  function getCMSFields() {
2392
	 *    $fields = parent::getCMSFields();
2393
	 *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2394
	 *    return $fields;
2395
	 *  }
2396
	 * }
2397
	 * </code>
2398
	 *
2399
	 * @see Good example of complex FormField building: SiteTree::getCMSFields()
2400
	 *
2401
	 * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2402
	 */
2403
	public function getCMSFields() {
2404
		$tabbedFields = $this->scaffoldFormFields(array(
2405
			// Don't allow has_many/many_many relationship editing before the record is first saved
2406
			'includeRelations' => ($this->ID > 0),
2407
			'tabbed' => true,
2408
			'ajaxSafe' => true
2409
		));
2410
2411
		$this->extend('updateCMSFields', $tabbedFields);
2412
2413
		return $tabbedFields;
2414
	}
2415
2416
	/**
2417
	 * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2418
	 * including that dataobject's extensions customised actions could be added to the EditForm.
2419
	 *
2420
	 * @return an Empty FieldList(); need to be overload by solid subclass
2421
	 */
2422
	public function getCMSActions() {
2423
		$actions = new FieldList();
2424
		$this->extend('updateCMSActions', $actions);
2425
		return $actions;
2426
	}
2427
2428
2429
	/**
2430
	 * Used for simple frontend forms without relation editing
2431
	 * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2432
	 * by default. To customize, either overload this method in your
2433
	 * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2434
	 *
2435
	 * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2436
	 *
2437
	 * @param array $params See {@link scaffoldFormFields()}
2438
	 * @return FieldList Always returns a simple field collection without TabSet.
2439
	 */
2440
	public function getFrontEndFields($params = null) {
2441
		$untabbedFields = $this->scaffoldFormFields($params);
2442
		$this->extend('updateFrontEndFields', $untabbedFields);
2443
2444
		return $untabbedFields;
2445
	}
2446
2447
	/**
2448
	 * Gets the value of a field.
2449
	 * Called by {@link __get()} and any getFieldName() methods you might create.
2450
	 *
2451
	 * @param string $field The name of the field
2452
	 *
2453
	 * @return mixed The field value
2454
	 */
2455
	public function getField($field) {
2456
		// If we already have an object in $this->record, then we should just return that
2457
		if(isset($this->record[$field]) && is_object($this->record[$field])) {
2458
			return $this->record[$field];
2459
		}
2460
2461
		// Do we have a field that needs to be lazy loaded?
2462
		if(isset($this->record[$field.'_Lazy'])) {
2463
			$tableClass = $this->record[$field.'_Lazy'];
2464
			$this->loadLazyFields($tableClass);
2465
		}
2466
2467
		// In case of complex fields, return the DBField object
2468
		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...
2469
			$this->record[$field] = $this->dbObject($field);
2470
		}
2471
2472
		return isset($this->record[$field]) ? $this->record[$field] : null;
2473
	}
2474
2475
	/**
2476
	 * Loads all the stub fields that an initial lazy load didn't load fully.
2477
	 *
2478
	 * @param string $tableClass Base table to load the values from. Others are joined as required.
2479
	 * Not specifying a tableClass will load all lazy fields from all tables.
2480
	 */
2481
	protected function loadLazyFields($tableClass = null) {
2482
		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...
2483
			$loaded = array();
2484
2485
			foreach ($this->record as $key => $value) {
2486
				if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2487
					$this->loadLazyFields($value);
2488
					$loaded[$value] = $value;
2489
				}
2490
			}
2491
2492
			return;
2493
		}
2494
2495
		$dataQuery = new DataQuery($tableClass);
2496
2497
		// Reset query parameter context to that of this DataObject
2498
		if($params = $this->getSourceQueryParams()) {
2499
			foreach($params as $key => $value) {
2500
				$dataQuery->setQueryParam($key, $value);
2501
			}
2502
		}
2503
2504
		// TableField sets the record ID to "new" on new row data, so don't try doing anything in that case
2505
		if(!is_numeric($this->record['ID'])) {
2506
			return;
2507
		}
2508
2509
		// Limit query to the current record, unless it has the Versioned extension,
2510
		// in which case it requires special handling through augmentLoadLazyFields()
2511
		$baseTable = ClassInfo::baseDataClass($this);
2512
		$dataQuery->where([
2513
			"\"{$baseTable}\".\"ID\"" => $this->record['ID']
2514
		])->limit(1);
2515
2516
		$columns = array();
2517
2518
		// Add SQL for fields, both simple & multi-value
2519
		// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2520
		$databaseFields = self::database_fields($tableClass);
2521
		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...
2522
			if(!isset($this->record[$k]) || $this->record[$k] === null) {
2523
				$columns[] = $k;
2524
			}
2525
		}
2526
2527
		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...
2528
			$query = $dataQuery->query();
2529
			$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2530
			$this->extend('augmentSQL', $query, $dataQuery);
2531
2532
			$dataQuery->setQueriedColumns($columns);
2533
			$newData = $dataQuery->execute()->record();
2534
2535
			// Load the data into record
2536
			if($newData) {
2537
				foreach($newData as $k => $v) {
2538
					if (in_array($k, $columns)) {
2539
						$this->record[$k] = $v;
2540
						$this->original[$k] = $v;
2541
						unset($this->record[$k . '_Lazy']);
2542
					}
2543
				}
2544
2545
			// No data means that the query returned nothing; assign 'null' to all the requested fields
2546
			} else {
2547
				foreach($columns as $k) {
2548
					$this->record[$k] = null;
2549
					$this->original[$k] = null;
2550
					unset($this->record[$k . '_Lazy']);
2551
				}
2552
			}
2553
		}
2554
	}
2555
2556
	/**
2557
	 * Return the fields that have changed.
2558
	 *
2559
	 * The change level affects what the functions defines as "changed":
2560
	 * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2561
	 * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2562
	 *   for example a change from 0 to null would not be included.
2563
	 *
2564
	 * Example return:
2565
	 * <code>
2566
	 * array(
2567
	 *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2568
	 * )
2569
	 * </code>
2570
	 *
2571
	 * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2572
	 * to return all database fields, or an array for an explicit filter. false returns all fields.
2573
	 * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2574
	 * @return array
2575
	 */
2576
	public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
2577
		$changedFields = array();
2578
2579
		// Update the changed array with references to changed obj-fields
2580
		foreach($this->record as $k => $v) {
2581
			// Prevents DBComposite infinite looping on isChanged
2582
			if(is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2583
				continue;
2584
			}
2585
			if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2586
				$this->changed[$k] = self::CHANGE_VALUE;
2587
			}
2588
		}
2589
2590
		if(is_array($databaseFieldsOnly)) {
2591
			$fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2592
		} elseif($databaseFieldsOnly) {
2593
			$fields = array_intersect_key((array)$this->changed, $this->db());
2594
		} else {
2595
			$fields = $this->changed;
2596
		}
2597
2598
		// Filter the list to those of a certain change level
2599
		if($changeLevel > self::CHANGE_STRICT) {
2600
			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...
2601
				if($level < $changeLevel) {
2602
					unset($fields[$name]);
2603
				}
2604
			}
2605
		}
2606
2607
		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...
2608
			$changedFields[$name] = array(
2609
				'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2610
				'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2611
				'level' => $level
2612
			);
2613
		}
2614
2615
		return $changedFields;
2616
	}
2617
2618
	/**
2619
	 * Uses {@link getChangedFields()} to determine if fields have been changed
2620
	 * since loading them from the database.
2621
	 *
2622
	 * @param string $fieldName Name of the database field to check, will check for any if not given
2623
	 * @param int $changeLevel See {@link getChangedFields()}
2624
	 * @return boolean
2625
	 */
2626
	public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
2627
		$fields = $fieldName ? array($fieldName) : false;
2628
		$changed = $this->getChangedFields($fields, $changeLevel);
2629
		if(!isset($fieldName)) {
2630
			return !empty($changed);
2631
		}
2632
		else {
2633
			return array_key_exists($fieldName, $changed);
2634
		}
2635
	}
2636
2637
	/**
2638
	 * Set the value of the field
2639
	 * Called by {@link __set()} and any setFieldName() methods you might create.
2640
	 *
2641
	 * @param string $fieldName Name of the field
2642
	 * @param mixed $val New field value
2643
	 * @return DataObject $this
2644
	 */
2645
	public function setField($fieldName, $val) {
2646
		//if it's a has_one component, destroy the cache
2647
		if (substr($fieldName, -2) == 'ID') {
2648
			unset($this->components[substr($fieldName, 0, -2)]);
2649
		}
2650
2651
		// If we've just lazy-loaded the column, then we need to populate the $original array
2652
		if(isset($this->record[$fieldName.'_Lazy'])) {
2653
			$tableClass = $this->record[$fieldName.'_Lazy'];
2654
			$this->loadLazyFields($tableClass);
2655
		}
2656
2657
		// Situation 1: Passing an DBField
2658
		if($val instanceof DBField) {
2659
			$val->setName($fieldName);
2660
			$val->saveInto($this);
2661
2662
			// Situation 1a: Composite fields should remain bound in case they are
2663
			// later referenced to update the parent dataobject
2664
			if($val instanceof DBComposite) {
2665
				$val->bindTo($this);
2666
				$this->record[$fieldName] = $val;
2667
			}
2668
		// Situation 2: Passing a literal or non-DBField object
2669
		} else {
2670
			// If this is a proper database field, we shouldn't be getting non-DBField objects
2671
			if(is_object($val) && $this->db($fieldName)) {
2672
				user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2673
			}
2674
2675
			// if a field is not existing or has strictly changed
2676
			if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2677
				// TODO Add check for php-level defaults which are not set in the db
2678
				// TODO Add check for hidden input-fields (readonly) which are not set in the db
2679
				// At the very least, the type has changed
2680
				$this->changed[$fieldName] = self::CHANGE_STRICT;
2681
2682
				if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
2683
						&& $this->record[$fieldName] != $val)) {
2684
2685
					// Value has changed as well, not just the type
2686
					$this->changed[$fieldName] = self::CHANGE_VALUE;
2687
				}
2688
2689
				// Value is always saved back when strict check succeeds.
2690
				$this->record[$fieldName] = $val;
2691
			}
2692
		}
2693
		return $this;
2694
	}
2695
2696
	/**
2697
	 * Set the value of the field, using a casting object.
2698
	 * This is useful when you aren't sure that a date is in SQL format, for example.
2699
	 * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2700
	 * can be saved into the Image table.
2701
	 *
2702
	 * @param string $fieldName Name of the field
2703
	 * @param mixed $value New field value
2704
	 * @return $this
2705
	 */
2706
	public function setCastedField($fieldName, $value) {
2707
		if(!$fieldName) {
2708
			user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2709
		}
2710
		$fieldObj = $this->dbObject($fieldName);
2711
		if($fieldObj) {
2712
			$fieldObj->setValue($value);
2713
			$fieldObj->saveInto($this);
2714
		} else {
2715
			$this->$fieldName = $value;
2716
		}
2717
		return $this;
2718
	}
2719
2720
	public function castingHelper($field) {
2721
		// Allows db to act as implicit casting override
2722
		if($fieldSpec = $this->db($field)) {
2723
			return $fieldSpec;
2724
		}
2725
		return parent::castingHelper($field);
2726
	}
2727
2728
	/**
2729
	 * Returns true if the given field exists in a database column on any of
2730
	 * the objects tables and optionally look up a dynamic getter with
2731
	 * get<fieldName>().
2732
	 *
2733
	 * @param string $field Name of the field
2734
	 * @return boolean True if the given field exists
2735
	 */
2736
	public function hasField($field) {
2737
		return (
2738
			array_key_exists($field, $this->record)
2739
			|| $this->db($field)
2740
			|| (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...
2741
			|| $this->hasMethod("get{$field}")
2742
		);
2743
	}
2744
2745
	/**
2746
	 * Returns true if the given field exists as a database column
2747
	 *
2748
	 * @param string $field Name of the field
2749
	 *
2750
	 * @return boolean
2751
	 */
2752
	public function hasDatabaseField($field) {
2753
		return $this->db($field)
2754
			&& ! 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...
2755
	}
2756
2757
	/**
2758
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2759
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2760
	 *
2761
	 * @param string $field Name of the field
2762
	 * @return string The field type of the given field
2763
	 */
2764
	public function hasOwnTableDatabaseField($field) {
2765
		return self::has_own_table_database_field($this->class, $field);
2766
	}
2767
2768
	/**
2769
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2770
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2771
	 *
2772
	 * @param string $class Class name to check
2773
	 * @param string $field Name of the field
2774
	 * @return string The field type of the given field
2775
	 */
2776
	public static function has_own_table_database_field($class, $field) {
2777
		$fieldMap = self::database_fields($class);
2778
2779
		// Remove string-based "constructor-arguments" from the DBField definition
2780
		if(isset($fieldMap[$field])) {
2781
			$spec = $fieldMap[$field];
2782
			if(is_string($spec)) return strtok($spec,'(');
2783
			else return $spec['type'];
2784
		}
2785
	}
2786
2787
	/**
2788
	 * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2789
	 * actually looking in the database.
2790
	 *
2791
	 * @param string $dataClass
2792
	 * @return bool
2793
	 */
2794
	public static function has_own_table($dataClass) {
2795
		if(!is_subclass_of($dataClass,'DataObject')) return false;
2796
2797
		$dataClass = ClassInfo::class_name($dataClass);
2798
		if(!isset(self::$_cache_has_own_table[$dataClass])) {
2799
			if(get_parent_class($dataClass) == 'DataObject') {
2800
				self::$_cache_has_own_table[$dataClass] = true;
2801
			} else {
2802
				self::$_cache_has_own_table[$dataClass]
2803
					= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
2804
					|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
2805
			}
2806
		}
2807
		return self::$_cache_has_own_table[$dataClass];
2808
	}
2809
2810
	/**
2811
	 * Returns true if the member is allowed to do the given action.
2812
	 * See {@link extendedCan()} for a more versatile tri-state permission control.
2813
	 *
2814
	 * @param string $perm The permission to be checked, such as 'View'.
2815
	 * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2816
	 * in user.
2817
	 * @param array $context Additional $context to pass to extendedCan()
2818
	 *
2819
	 * @return boolean True if the the member is allowed to do the given action
2820
	 */
2821
	public function can($perm, $member = null, $context = array()) {
2822
		if(!isset($member)) {
2823
			$member = Member::currentUser();
2824
		}
2825
		if(Permission::checkMember($member, "ADMIN")) return true;
2826
2827
		if($this->manyManyComponent('Can' . $perm)) {
2828
			if($this->ParentID && $this->SecurityType == 'Inherit') {
2829
				if(!($p = $this->Parent)) {
0 ignored issues
show
Documentation introduced by
The property Parent does not exist on object<DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2830
					return false;
2831
				}
2832
				return $this->Parent->can($perm, $member);
2833
2834
			} else {
2835
				$permissionCache = $this->uninherited('permissionCache');
2836
				$memberID = $member ? $member->ID : 'none';
2837
2838
				if(!isset($permissionCache[$memberID][$perm])) {
2839
					if($member->ID) {
2840
						$groups = $member->Groups();
2841
					}
2842
2843
					$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...
2844
2845
					// TODO Fix relation table hardcoding
2846
					$query = new SQLSelect(
2847
						"\"Page_Can$perm\".PageID",
2848
					array("\"Page_Can$perm\""),
2849
						"GroupID IN ($groupList)");
2850
2851
					$permissionCache[$memberID][$perm] = $query->execute()->column();
2852
2853
					if($perm == "View") {
2854
						// TODO Fix relation table hardcoding
2855
						$query = new SQLSelect("\"SiteTree\".\"ID\"", array(
2856
							"\"SiteTree\"",
2857
							"LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2858
							), "\"Page_CanView\".\"PageID\" IS NULL");
2859
2860
							$unsecuredPages = $query->execute()->column();
2861
							if($permissionCache[$memberID][$perm]) {
2862
								$permissionCache[$memberID][$perm]
2863
									= array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2864
							} else {
2865
								$permissionCache[$memberID][$perm] = $unsecuredPages;
2866
							}
2867
					}
2868
2869
					Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2870
				}
2871
2872
				if($permissionCache[$memberID][$perm]) {
2873
					return in_array($this->ID, $permissionCache[$memberID][$perm]);
2874
				}
2875
			}
2876
		} else {
2877
			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_Mid, CheckboxFieldTest_Article, CheckboxSetFieldTest_Article, CheckboxSetFieldTest_Tag, ClassInfoTest_BaseClass, ClassInfoTest_BaseDataClass, ClassInfoTest_ChildClass, ClassInfoTest_GrandChildClass, ClassInfoTest_HasFields, ClassInfoTest_NoFields, ClassInfoTest_WithRelation, CliController, ComponentSetTest_Player, ComponentSetTest_Team, Controller, ControllerTest_AccessBaseController, ControllerTest_AccessSecuredController, ControllerTest_AccessWildcardSecuredController, ControllerTest_ContainerController, ControllerTest_Controller, ControllerTest_HasAction, ControllerTest_HasAction_Unsecured, ControllerTest_IndexSecuredController, ControllerTest_SubController, ControllerTest_UnsecuredController, CsvBulkLoaderTest_Player, CsvBulkLoaderTest_PlayerContract, CsvBulkLoaderTest_Team, DBClassNameTest_CustomDefault, DBClassNameTest_CustomDefaultSubclass, DBClassNameTest_Object, DBClassNameTest_ObjectSubClass, DBClassNameTest_ObjectSubSubClass, DBClassNameTest_OtherClass, DBCompositeTest_DataObject, DBFileTest_ImageOnly, DBFileTest_Object, DBFileTest_Subclass, DataDifferencerTest_HasOneRelationObject, DataDifferencerTest_Object, DataExtensionTest_CMSFieldsBase, DataExtensionTest_CMSFieldsChild, DataExtensionTest_CMSFieldsGrandchild, DataExtensionTest_Member, DataExtensionTest_MyObject, DataExtensionTest_Player, DataExtensionTest_RelatedObject, DataObject, DataObjectDuplicateTestClass1, DataObjectDuplicateTestClass2, DataObjectDuplicateTestClass3, DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO, DataObjectTest\NamespacedClass, DataObjectTest\RelationClass, DataObjectTest_Bogey, DataObjectTest_CEO, DataObjectTest_Company, DataObjectTest_EquipmentCompany, DataObjectTest_ExtendedTeamComment, DataObjectTest_Fan, DataObjectTest_FieldlessSubTable, DataObjectTest_FieldlessTable, DataObjectTest_Fixture, DataObjectTest_Play, DataObjectTest_Player, DataObjectTest_Ploy, DataObjectTest_Staff, DataObjectTest_SubEquipmentCompany, DataObjectTest_SubTeam, DataObjectTest_Team, DataObjectTest_TeamComment, DataObjectTest_ValidatedObject, DataQueryTest_A, DataQueryTest_B, DataQueryTest_C, DataQueryTest_D, DataQueryTest_E, DataQueryTest_F, DatabaseAdmin, DatabaseTest_MyObject, DatetimeFieldTest_Model, DecimalTest_DataObject, DevAdminControllerTest_Controller1, DevBuildController, DevelopmentAdmin, DirectorTestRequest_Controller, EmailFieldTest_Controller, FakeController, File, FileTest_MyCustomFile, FixtureBlueprintTest_Article, FixtureBlueprintTest_Page, FixtureBlueprintTest_SiteTree, FixtureFactoryTest_DataObject, FixtureFactoryTest_DataObjectRelation, Folder, FormScaffolderTest_Article, FormScaffolderTest_Author, FormScaffolderTest_Tag, FormTest_Controller, FormTest_ControllerWithSecurityToken, FormTest_ControllerWithStrictPostCheck, FormTest_Player, FormTest_Team, FulltextFilterTest_DataObject, GridFieldAction_Delete_Team, GridFieldAction_Edit_Team, GridFieldAddExistingAutocompleterTest_Controller, GridFieldDetailFormTest_Category, GridFieldDetailFormTest_CategoryController, GridFieldDetailFormTest_Controller, GridFieldDetailFormTest_GroupController, GridFieldDetailFormTest_PeopleGroup, GridFieldDetailFormTest_Person, GridFieldExportButtonTest_NoView, GridFieldExportButtonTest_Team, GridFieldPrintButtonTest_DO, GridFieldSortableHeaderTest_Cheerleader, GridFieldSortableHeaderTest_CheerleaderHat, GridFieldSortableHeaderTest_Mom, GridFieldSortableHeaderTest_Team, GridFieldSortableHeaderTest_TeamGroup, GridFieldTest_Cheerleader, GridFieldTest_Permissions, GridFieldTest_Player, GridFieldTest_Team, GridField_URLHandlerTest_Controller, Group, GroupTest_Member, 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, 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...
2878
		}
2879
	}
2880
2881
	/**
2882
	 * Process tri-state responses from permission-alterting extensions.  The extensions are
2883
	 * expected to return one of three values:
2884
	 *
2885
	 *  - false: Disallow this permission, regardless of what other extensions say
2886
	 *  - true: Allow this permission, as long as no other extensions return false
2887
	 *  - NULL: Don't affect the outcome
2888
	 *
2889
	 * This method itself returns a tri-state value, and is designed to be used like this:
2890
	 *
2891
	 * <code>
2892
	 * $extended = $this->extendedCan('canDoSomething', $member);
2893
	 * if($extended !== null) return $extended;
2894
	 * else return $normalValue;
2895
	 * </code>
2896
	 *
2897
	 * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2898
	 * @param Member|int $member
2899
	 * @param array $context Optional context
2900
	 * @return boolean|null
2901
	 */
2902
	public function extendedCan($methodName, $member, $context = array()) {
2903
		$results = $this->extend($methodName, $member, $context);
2904
		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...
2905
			// Remove NULLs
2906
			$results = array_filter($results, function($v) {return !is_null($v);});
2907
			// If there are any non-NULL responses, then return the lowest one of them.
2908
			// If any explicitly deny the permission, then we don't get access
2909
			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...
2910
		}
2911
		return null;
2912
	}
2913
2914
	/**
2915
	 * @param Member $member
2916
	 * @return boolean
2917
	 */
2918
	public function canView($member = null) {
2919
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2918 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2920
		if($extended !== null) {
2921
			return $extended;
2922
		}
2923
		return Permission::check('ADMIN', 'any', $member);
2924
	}
2925
2926
	/**
2927
	 * @param Member $member
2928
	 * @return boolean
2929
	 */
2930
	public function canEdit($member = null) {
2931
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2930 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2932
		if($extended !== null) {
2933
			return $extended;
2934
		}
2935
		return Permission::check('ADMIN', 'any', $member);
2936
	}
2937
2938
	/**
2939
	 * @param Member $member
2940
	 * @return boolean
2941
	 */
2942
	public function canDelete($member = null) {
2943
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2942 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2944
		if($extended !== null) {
2945
			return $extended;
2946
		}
2947
		return Permission::check('ADMIN', 'any', $member);
2948
	}
2949
2950
	/**
2951
	 * @param Member $member
2952
	 * @param array $context Additional context-specific data which might
2953
	 * affect whether (or where) this object could be created.
2954
	 * @return boolean
2955
	 */
2956
	public function canCreate($member = null, $context = array()) {
2957
		$extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2956 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2958
		if($extended !== null) {
2959
			return $extended;
2960
		}
2961
		return Permission::check('ADMIN', 'any', $member);
2962
	}
2963
2964
	/**
2965
	 * Debugging used by Debug::show()
2966
	 *
2967
	 * @return string HTML data representing this object
2968
	 */
2969
	public function debug() {
2970
		$val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2971
		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...
2972
			$val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2973
		}
2974
		$val .= "</ul>\n";
2975
		return $val;
2976
	}
2977
2978
	/**
2979
	 * Return the DBField object that represents the given field.
2980
	 * This works similarly to obj() with 2 key differences:
2981
	 *   - it still returns an object even when the field has no value.
2982
	 *   - it only matches fields and not methods
2983
	 *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2984
	 *
2985
	 * @param string $fieldName Name of the field
2986
	 * @return DBField The field as a DBField object
2987
	 */
2988
	public function dbObject($fieldName) {
2989
		$value = isset($this->record[$fieldName])
2990
			? $this->record[$fieldName]
2991
			: null;
2992
2993
		// If we have a DBField object in $this->record, then return that
2994
		if(is_object($value)) {
2995
			return $value;
2996
		}
2997
2998
		// Build and populate new field otherwise
2999
		$helper = $this->db($fieldName, true);
3000
		if($helper) {
3001
			list($table, $spec) = explode('.', $helper);
3002
			$obj = Object::create_from_string($spec, $fieldName);
3003
			$obj->setTable($table);
3004
			$obj->setValue($value, $this, false);
3005
			return $obj;
3006
		}
3007
	}
3008
3009
	/**
3010
	 * Traverses to a DBField referenced by relationships between data objects.
3011
	 *
3012
	 * The path to the related field is specified with dot separated syntax
3013
	 * (eg: Parent.Child.Child.FieldName).
3014
	 *
3015
	 * @param string $fieldPath
3016
	 *
3017
	 * @return mixed DBField of the field on the object or a DataList instance.
3018
	 */
3019
	public function relObject($fieldPath) {
3020
		$object = null;
3021
3022
		if(strpos($fieldPath, '.') !== false) {
3023
			$parts = explode('.', $fieldPath);
3024
			$fieldName = array_pop($parts);
3025
3026
			// Traverse dot syntax
3027
			$component = $this;
3028
3029
			foreach($parts as $relation) {
3030
				if($component instanceof SS_List) {
3031
					if(method_exists($component,$relation)) {
3032
						$component = $component->$relation();
3033
					} else {
3034
						$component = $component->relation($relation);
3035
					}
3036
				} else {
3037
					$component = $component->$relation();
3038
				}
3039
			}
3040
3041
			$object = $component->dbObject($fieldName);
3042
3043
		} else {
3044
			$object = $this->dbObject($fieldPath);
3045
		}
3046
3047
		return $object;
3048
	}
3049
3050
	/**
3051
	 * Traverses to a field referenced by relationships between data objects, returning the value
3052
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3053
	 *
3054
	 * @param $fieldName string
3055
	 * @return string | null - will return null on a missing value
3056
	 */
3057
	public function relField($fieldName) {
3058
		$component = $this;
3059
3060
		// We're dealing with relations here so we traverse the dot syntax
3061
		if(strpos($fieldName, '.') !== false) {
3062
			$relations = explode('.', $fieldName);
3063
			$fieldName = array_pop($relations);
3064
			foreach($relations as $relation) {
3065
				// Inspect $component for element $relation
3066
				if($component->hasMethod($relation)) {
3067
					// Check nested method
3068
					$component = $component->$relation();
3069
				} elseif($component instanceof SS_List) {
3070
					// Select adjacent relation from DataList
3071
					$component = $component->relation($relation);
3072
				} elseif($component instanceof DataObject
3073
					&& ($dbObject = $component->dbObject($relation))
3074
				) {
3075
					// Select db object
3076
					$component = $dbObject;
3077
				} else {
3078
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
3079
				}
3080
			}
3081
		}
3082
3083
		// Bail if the component is null
3084
		if(!$component) {
3085
			return null;
3086
		}
3087
		if($component->hasMethod($fieldName)) {
3088
			return $component->$fieldName();
3089
		}
3090
		return $component->$fieldName;
3091
	}
3092
3093
	/**
3094
	 * Temporary hack to return an association name, based on class, to get around the mangle
3095
	 * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3096
	 *
3097
	 * @return String
3098
	 */
3099
	public function getReverseAssociation($className) {
3100
		if (is_array($this->manyMany())) {
3101
			$many_many = array_flip($this->manyMany());
3102
			if (array_key_exists($className, $many_many)) return $many_many[$className];
3103
		}
3104
		if (is_array($this->hasMany())) {
3105
			$has_many = array_flip($this->hasMany());
3106
			if (array_key_exists($className, $has_many)) return $has_many[$className];
3107
		}
3108
		if (is_array($this->hasOne())) {
3109
			$has_one = array_flip($this->hasOne());
3110
			if (array_key_exists($className, $has_one)) return $has_one[$className];
3111
		}
3112
3113
		return false;
3114
	}
3115
3116
	/**
3117
	 * Return all objects matching the filter
3118
	 * sub-classes are automatically selected and included
3119
	 *
3120
	 * @param string $callerClass The class of objects to be returned
3121
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3122
	 * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3123
	 * @param string|array $sort A sort expression to be inserted into the ORDER
3124
	 * BY clause.  If omitted, self::$default_sort will be used.
3125
	 * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3126
	 * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3127
	 * @param string $containerClass The container class to return the results in.
3128
	 *
3129
	 * @todo $containerClass is Ignored, why?
3130
	 *
3131
	 * @return DataList The objects matching the filter, in the class specified by $containerClass
3132
	 */
3133
	public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
3134
			$containerClass = 'DataList') {
3135
3136
		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...
3137
			$callerClass = get_called_class();
3138
			if($callerClass == 'DataObject') {
3139
				throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3140
			}
3141
3142
			if($filter || $sort || $join || $limit || ($containerClass != 'DataList')) {
3143
				throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3144
					. ' arguments');
3145
			}
3146
3147
			$result = DataList::create(get_called_class());
3148
			$result->setDataModel(DataModel::inst());
3149
			return $result;
3150
		}
3151
3152
		if($join) {
3153
			throw new \InvalidArgumentException(
3154
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3155
			);
3156
		}
3157
3158
		$result = DataList::create($callerClass)->where($filter)->sort($sort);
3159
3160
		if($limit && strpos($limit, ',') !== false) {
3161
			$limitArguments = explode(',', $limit);
3162
			$result = $result->limit($limitArguments[1],$limitArguments[0]);
3163
		} elseif($limit) {
3164
			$result = $result->limit($limit);
3165
		}
3166
3167
		$result->setDataModel(DataModel::inst());
3168
		return $result;
3169
	}
3170
3171
3172
	/**
3173
	 * Return the first item matching the given query.
3174
	 * All calls to get_one() are cached.
3175
	 *
3176
	 * @param string $callerClass The class of objects to be returned
3177
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3178
	 * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3179
	 * @param boolean $cache Use caching
3180
	 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3181
	 *
3182
	 * @return DataObject The first item matching the query
3183
	 */
3184
	public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
3185
		$SNG = singleton($callerClass);
3186
3187
		$cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3188
		$cacheKey = md5(var_export($cacheComponents, true));
3189
3190
		// Flush destroyed items out of the cache
3191
		if($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
3192
				&& self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
3193
				&& self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
3194
3195
			self::$_cache_get_one[$callerClass][$cacheKey] = false;
3196
		}
3197
		if(!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3198
			$dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3199
			$item = $dl->First();
3200
3201
			if($cache) {
3202
				self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3203
				if(!self::$_cache_get_one[$callerClass][$cacheKey]) {
3204
					self::$_cache_get_one[$callerClass][$cacheKey] = false;
3205
				}
3206
			}
3207
		}
3208
		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...
3209
	}
3210
3211
	/**
3212
	 * Flush the cached results for all relations (has_one, has_many, many_many)
3213
	 * Also clears any cached aggregate data.
3214
	 *
3215
	 * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3216
	 *                            When false will just clear session-local cached data
3217
	 * @return DataObject $this
3218
	 */
3219
	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...
3220
		if($this->class == 'DataObject') {
3221
			self::$_cache_get_one = array();
3222
			return $this;
3223
		}
3224
3225
		$classes = ClassInfo::ancestry($this->class);
3226
		foreach($classes as $class) {
3227
			if(isset(self::$_cache_get_one[$class])) unset(self::$_cache_get_one[$class]);
3228
		}
3229
3230
		$this->extend('flushCache');
3231
3232
		$this->components = array();
3233
		return $this;
3234
	}
3235
3236
	/**
3237
	 * Flush the get_one global cache and destroy associated objects.
3238
	 */
3239
	public static function flush_and_destroy_cache() {
3240
		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...
3241
			if(is_array($items)) foreach($items as $item) {
3242
				if($item) $item->destroy();
3243
			}
3244
		}
3245
		self::$_cache_get_one = array();
3246
	}
3247
3248
	/**
3249
	 * Reset all global caches associated with DataObject.
3250
	 */
3251
	public static function reset() {
3252
		DBClassName::clear_classname_cache();
3253
		self::$_cache_has_own_table = array();
3254
		self::$_cache_get_one = array();
3255
		self::$_cache_composite_fields = array();
3256
		self::$_cache_database_fields = array();
3257
		self::$_cache_get_class_ancestry = array();
3258
		self::$_cache_field_labels = array();
3259
	}
3260
3261
	/**
3262
	 * Return the given element, searching by ID
3263
	 *
3264
	 * @param string $callerClass The class of the object to be returned
3265
	 * @param int $id The id of the element
3266
	 * @param boolean $cache See {@link get_one()}
3267
	 *
3268
	 * @return DataObject The element
3269
	 */
3270
	public static function get_by_id($callerClass, $id, $cache = true) {
3271
		if(!is_numeric($id)) {
3272
			user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3273
		}
3274
3275
		// Check filter column
3276
		if(is_subclass_of($callerClass, 'DataObject')) {
3277
			$baseClass = ClassInfo::baseDataClass($callerClass);
3278
			$column = "\"$baseClass\".\"ID\"";
3279
		} else{
3280
			// This simpler code will be used by non-DataObject classes that implement DataObjectInterface
3281
			$column = '"ID"';
3282
		}
3283
3284
		// Relegate to get_one
3285
		return DataObject::get_one($callerClass, array($column => $id), $cache);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3286
	}
3287
3288
	/**
3289
	 * Get the name of the base table for this object
3290
	 */
3291
	public function baseTable() {
3292
		$tableClasses = ClassInfo::dataClassesFor($this->class);
3293
		return array_shift($tableClasses);
3294
	}
3295
3296
	/**
3297
	 * @var Array Parameters used in the query that built this object.
3298
	 * This can be used by decorators (e.g. lazy loading) to
3299
	 * run additional queries using the same context.
3300
	 */
3301
	protected $sourceQueryParams;
3302
3303
	/**
3304
	 * @see $sourceQueryParams
3305
	 * @return array
3306
	 */
3307
	public function getSourceQueryParams() {
3308
		return $this->sourceQueryParams;
3309
	}
3310
3311
	/**
3312
	 * Get list of parameters that should be inherited to relations on this object
3313
	 *
3314
	 * @return array
3315
	 */
3316
	public function getInheritableQueryParams() {
3317
		$params = $this->getSourceQueryParams();
3318
		$this->extend('updateInheritableQueryParams', $params);
3319
		return $params;
3320
	}
3321
3322
	/**
3323
	 * @see $sourceQueryParams
3324
	 * @param array
3325
	 */
3326
	public function setSourceQueryParams($array) {
3327
		$this->sourceQueryParams = $array;
3328
	}
3329
3330
	/**
3331
	 * @see $sourceQueryParams
3332
	 * @param array
3333
	 */
3334
	public function setSourceQueryParam($key, $value) {
3335
		$this->sourceQueryParams[$key] = $value;
3336
	}
3337
3338
	/**
3339
	 * @see $sourceQueryParams
3340
	 * @return Mixed
3341
	 */
3342
	public function getSourceQueryParam($key) {
3343
		if(isset($this->sourceQueryParams[$key])) return $this->sourceQueryParams[$key];
3344
		else return null;
3345
	}
3346
3347
	//-------------------------------------------------------------------------------------------//
3348
3349
	/**
3350
	 * Return the database indexes on this table.
3351
	 * This array is indexed by the name of the field with the index, and
3352
	 * the value is the type of index.
3353
	 */
3354
	public function databaseIndexes() {
3355
		$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...
3356
		$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...
3357
		//$fileIndexes = $this->uninherited('fileIndexes', true);
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3358
3359
		$indexes = array();
3360
3361
		if($has_one) {
3362
			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...
3363
				$indexes[$relationshipName . 'ID'] = true;
3364
			}
3365
		}
3366
3367
		if($classIndexes) {
3368
			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...
3369
				$indexes[$indexName] = $indexType;
3370
			}
3371
		}
3372
3373
		if(get_parent_class($this) == "DataObject") {
3374
			$indexes['ClassName'] = true;
3375
		}
3376
3377
		return $indexes;
3378
	}
3379
3380
	/**
3381
	 * Check the database schema and update it as necessary.
3382
	 *
3383
	 * @uses DataExtension->augmentDatabase()
3384
	 */
3385
	public function requireTable() {
3386
		// Only build the table if we've actually got fields
3387
		$fields = self::database_fields($this->class);
3388
		$extensions = self::database_extensions($this->class);
3389
3390
		$indexes = $this->databaseIndexes();
3391
3392
		// Validate relationship configuration
3393
		$this->validateModelDefinitions();
3394
3395
		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...
3396
			$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
3397
			DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
3398
				$extensions);
3399
		} else {
3400
			DB::dont_require_table($this->class);
3401
		}
3402
3403
		// Build any child tables for many_many items
3404
		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...
3405
			$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...
3406
			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...
3407
				// Build field list
3408
				$manymanyFields = array(
3409
					"{$this->class}ID" => "Int",
3410
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => "Int",
3411
				);
3412
				if(isset($extras[$relationship])) {
3413
					$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
3414
				}
3415
3416
				// Build index list
3417
				$manymanyIndexes = array(
3418
					"{$this->class}ID" => true,
3419
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true,
3420
				);
3421
3422
				DB::require_table("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null,
3423
					$extensions);
3424
			}
3425
		}
3426
3427
		// Let any extentions make their own database fields
3428
		$this->extend('augmentDatabase', $dummy);
3429
	}
3430
3431
	/**
3432
	 * Validate that the configured relations for this class use the correct syntaxes
3433
	 * @throws LogicException
3434
	 */
3435
	protected function validateModelDefinitions() {
3436
		$modelDefinitions = array(
3437
			'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
3438
			'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
3439
			'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
3440
			'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
3441
			'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
3442
			'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
3443
			'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
3444
		);
3445
3446
		foreach($modelDefinitions as $defType => $relations) {
3447
			if( ! $relations) continue;
3448
3449
			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...
3450
				if($defType === 'many_many_extraFields') {
3451
					if(!is_array($v)) {
3452
						throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
3453
							. var_export($k, true) . " => " . var_export($v, true)
3454
							. ". Each many_many_extraFields entry should map to a field specification array.");
3455
					}
3456
				} else {
3457
					if(!is_string($k) || is_numeric($k) || !is_string($v)) {
3458
						throw new LogicException("$this->class::$defType has a bad entry: "
3459
							. var_export($k, true). " => " . var_export($v, true) . ".  Each map key should be a
3460
							 relationship name, and the map value should be the data class to join to.");
3461
					}
3462
				}
3463
			}
3464
		}
3465
	}
3466
3467
	/**
3468
	 * Add default records to database. This function is called whenever the
3469
	 * database is built, after the database tables have all been created. Overload
3470
	 * this to add default records when the database is built, but make sure you
3471
	 * call parent::requireDefaultRecords().
3472
	 *
3473
	 * @uses DataExtension->requireDefaultRecords()
3474
	 */
3475
	public function requireDefaultRecords() {
3476
		$defaultRecords = $this->stat('default_records');
3477
3478
		if(!empty($defaultRecords)) {
3479
			$hasData = DataObject::get_one($this->class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3480
			if(!$hasData) {
3481
				$className = $this->class;
3482
				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...
3483
					$obj = $this->model->$className->newObject($record);
3484
					$obj->write();
3485
				}
3486
				DB::alteration_message("Added default records to $className table","created");
3487
			}
3488
		}
3489
3490
		// Let any extentions make their own database default data
3491
		$this->extend('requireDefaultRecords', $dummy);
3492
	}
3493
3494
	/**
3495
	 * Get the default searchable fields for this object, as defined in the
3496
	 * $searchable_fields list. If searchable fields are not defined on the
3497
	 * data object, uses a default selection of summary fields.
3498
	 *
3499
	 * @return array
3500
	 */
3501
	public function searchableFields() {
3502
		// can have mixed format, need to make consistent in most verbose form
3503
		$fields = $this->stat('searchable_fields');
3504
		$labels = $this->fieldLabels();
3505
3506
		// fallback to summary fields (unless empty array is explicitly specified)
3507
		if( ! $fields && ! is_array($fields)) {
3508
			$summaryFields = array_keys($this->summaryFields());
3509
			$fields = array();
3510
3511
			// remove the custom getters as the search should not include them
3512
			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...
3513
				foreach($summaryFields as $key => $name) {
3514
					$spec = $name;
3515
3516
					// Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3517
					if(($fieldPos = strpos($name, '.')) !== false) {
3518
						$name = substr($name, 0, $fieldPos);
3519
					}
3520
3521
					if($this->hasDatabaseField($name)) {
3522
						$fields[] = $name;
3523
					} elseif($this->relObject($spec)) {
3524
						$fields[] = $spec;
3525
					}
3526
				}
3527
			}
3528
		}
3529
3530
		// we need to make sure the format is unified before
3531
		// augmenting fields, so extensions can apply consistent checks
3532
		// but also after augmenting fields, because the extension
3533
		// might use the shorthand notation as well
3534
3535
		// rewrite array, if it is using shorthand syntax
3536
		$rewrite = array();
3537
		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...
3538
			$identifer = (is_int($name)) ? $specOrName : $name;
3539
3540
			if(is_int($name)) {
3541
				// Format: array('MyFieldName')
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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