Completed
Pull Request — 3.5 (#8009)
by Daniel
09:26
created

DataObject::duplicateRelations()   D

Complexity

Conditions 9
Paths 9

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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

There are different options of fixing this problem.

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

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

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

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

There are different options of fixing this problem.

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

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

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

Loading history...
1034
					$leftComponent = $leftObj->getComponent($relationship);
1035
					$rightComponent = $rightObj->getComponent($relationship);
1036
					if($leftComponent->exists() && $rightComponent->exists() && $priority == 'right') {
1037
						$leftObj->{$relationship . 'ID'} = $rightObj->{$relationship . 'ID'};
1038
					}
1039
				}
1040
			}
1041
		}
1042
1043
		return true;
1044
	}
1045
1046
	/**
1047
	 * Forces the record to think that all its data has changed.
1048
	 * Doesn't write to the database. Only sets fields as changed
1049
	 * if they are not already marked as changed.
1050
	 *
1051
	 * @return $this
1052
	 */
1053
	public function forceChange() {
1054
		// Ensure lazy fields loaded
1055
		$this->loadLazyFields();
1056
1057
		// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1058
		$fieldNames = array_unique(array_merge(
1059
			array_keys($this->record),
1060
			array_keys($this->inheritedDatabaseFields())
1061
		));
1062
1063
		foreach($fieldNames as $fieldName) {
1064
			if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
1065
			// Populate the null values in record so that they actually get written
1066
			if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
1067
		}
1068
1069
		// @todo Find better way to allow versioned to write a new version after forceChange
1070
		if($this->isChanged('Version')) unset($this->changed['Version']);
1071
		return $this;
1072
	}
1073
1074
	/**
1075
	 * Validate the current object.
1076
	 *
1077
	 * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1078
	 * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1079
	 *
1080
	 * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1081
	 * and onAfterWrite() won't get called either.
1082
	 *
1083
	 * It is expected that you call validate() in your own application to test that an object is valid before
1084
	 * attempting a write, and respond appropriately if it isn't.
1085
	 *
1086
	 * @see {@link ValidationResult}
1087
	 * @return ValidationResult
1088
	 */
1089
	protected function validate() {
1090
		$result = ValidationResult::create();
1091
		$this->extend('validate', $result);
1092
		return $result;
1093
	}
1094
1095
	/**
1096
	 * Public accessor for {@see DataObject::validate()}
1097
	 *
1098
	 * @return ValidationResult
1099
	 */
1100
	public function doValidate() {
1101
		// validate will be public in 4.0
1102
		return $this->validate();
1103
	}
1104
1105
	/**
1106
	 * Event handler called before writing to the database.
1107
	 * You can overload this to clean up or otherwise process data before writing it to the
1108
	 * database.  Don't forget to call parent::onBeforeWrite(), though!
1109
	 *
1110
	 * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1111
	 *
1112
	 * @uses DataExtension->onBeforeWrite()
1113
	 */
1114
	protected function onBeforeWrite() {
1115
		$this->brokenOnWrite = false;
1116
1117
		$dummy = null;
1118
		$this->extend('onBeforeWrite', $dummy);
1119
	}
1120
1121
	/**
1122
	 * Event handler called after writing to the database.
1123
	 * You can overload this to act upon changes made to the data after it is written.
1124
	 * $this->changed will have a record
1125
	 * database.  Don't forget to call parent::onAfterWrite(), though!
1126
	 *
1127
	 * @uses DataExtension->onAfterWrite()
1128
	 */
1129
	protected function onAfterWrite() {
1130
		$dummy = null;
1131
		$this->extend('onAfterWrite', $dummy);
1132
	}
1133
1134
	/**
1135
	 * Event handler called before deleting from the database.
1136
	 * You can overload this to clean up or otherwise process data before delete this
1137
	 * record.  Don't forget to call parent::onBeforeDelete(), though!
1138
	 *
1139
	 * @uses DataExtension->onBeforeDelete()
1140
	 */
1141
	protected function onBeforeDelete() {
1142
		$this->brokenOnDelete = false;
1143
1144
		$dummy = null;
1145
		$this->extend('onBeforeDelete', $dummy);
1146
	}
1147
1148
	protected function onAfterDelete() {
1149
		$this->extend('onAfterDelete');
1150
	}
1151
1152
	/**
1153
	 * Load the default values in from the self::$defaults array.
1154
	 * Will traverse the defaults of the current class and all its parent classes.
1155
	 * Called by the constructor when creating new records.
1156
	 *
1157
	 * @uses DataExtension->populateDefaults()
1158
	 * @return DataObject $this
1159
	 */
1160
	public function populateDefaults() {
1161
		$classes = array_reverse(ClassInfo::ancestry($this));
1162
1163
		foreach($classes as $class) {
1164
			$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1165
1166
			if($defaults && !is_array($defaults)) {
1167
				user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
1168
					E_USER_WARNING);
1169
				$defaults = null;
1170
			}
1171
1172
			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...
1173
				// SRM 2007-03-06: Stricter check
1174
				if(!isset($this->$fieldName) || $this->$fieldName === null) {
1175
					$this->$fieldName = $fieldValue;
1176
				}
1177
				// Set many-many defaults with an array of ids
1178
				if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
1179
					$manyManyJoin = $this->$fieldName();
1180
					$manyManyJoin->setByIdList($fieldValue);
1181
				}
1182
			}
1183
			if($class == 'DataObject') {
1184
				break;
1185
			}
1186
		}
1187
1188
		$this->extend('populateDefaults');
1189
		return $this;
1190
	}
1191
1192
	/**
1193
	 * Determine validation of this object prior to write
1194
	 *
1195
	 * @return ValidationException Exception generated by this write, or null if valid
1196
	 */
1197
	protected function validateWrite() {
1198
		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...
1199
			return new ValidationException(
1200
				"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...
1201
				"you need to change the ClassName before you can write it",
1202
				E_USER_WARNING
1203
			);
1204
		}
1205
1206
		if(Config::inst()->get('DataObject', 'validation_enabled')) {
1207
			$result = $this->validate();
1208
			if (!$result->valid()) {
1209
				return new ValidationException(
1210
					$result,
1211
					$result->message(),
1212
					E_USER_WARNING
1213
				);
1214
			}
1215
		}
1216
	}
1217
1218
	/**
1219
	 * Prepare an object prior to write
1220
	 *
1221
	 * @throws ValidationException
1222
	 */
1223
	protected function preWrite() {
1224
		// Validate this object
1225
		if($writeException = $this->validateWrite()) {
1226
			// Used by DODs to clean up after themselves, eg, Versioned
1227
			$this->invokeWithExtensions('onAfterSkippedWrite');
1228
			throw $writeException;
1229
		}
1230
1231
		// Check onBeforeWrite
1232
		$this->brokenOnWrite = true;
1233
		$this->onBeforeWrite();
1234
		if($this->brokenOnWrite) {
1235
			user_error("$this->class has a broken onBeforeWrite() function."
1236
				. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1237
		}
1238
	}
1239
1240
	/**
1241
	 * Detects and updates all changes made to this object
1242
	 *
1243
	 * @param bool $forceChanges If set to true, force all fields to be treated as changed
1244
	 * @return bool True if any changes are detected
1245
	 */
1246
	protected function updateChanges($forceChanges = false)
1247
	{
1248
		if($forceChanges) {
1249
			// Force changes, but only for loaded fields
1250
			foreach($this->record as $field => $value) {
1251
				$this->changed[$field] = static::CHANGE_VALUE;
1252
			}
1253
			return true;
1254
		}
1255
		return $this->isChanged();
1256
	}
1257
1258
	/**
1259
	 * Writes a subset of changes for a specific table to the given manipulation
1260
	 *
1261
	 * @param string $baseTable Base table
1262
	 * @param string $now Timestamp to use for the current time
1263
	 * @param bool $isNewRecord Whether this should be treated as a new record write
1264
	 * @param array $manipulation Manipulation to write to
1265
	 * @param string $class Table and Class to select and write to
1266
	 */
1267
	protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
1268
		$manipulation[$class] = array();
1269
1270
		// Extract records for this table
1271
		foreach($this->record as $fieldName => $fieldValue) {
1272
1273
			// Check if this record pertains to this table, and
1274
			// we're not attempting to reset the BaseTable->ID
1275
			if(	empty($this->changed[$fieldName])
1276
				|| ($class === $baseTable && $fieldName === 'ID')
1277
				|| (!self::has_own_table_database_field($class, $fieldName)
1278
					&& !self::is_composite_field($class, $fieldName, false))
1279
			) {
1280
				continue;
1281
			}
1282
1283
1284
			// if database column doesn't correlate to a DBField instance...
1285
			$fieldObj = $this->dbObject($fieldName);
1286
			if(!$fieldObj) {
1287
				$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1288
			}
1289
1290
			// Ensure DBField is repopulated and written to the manipulation
1291
			$fieldObj->setValue($fieldValue, $this->record);
1292
			$fieldObj->writeToManipulation($manipulation[$class]);
1293
		}
1294
1295
		// Ensure update of Created and LastEdited columns
1296
		if($baseTable === $class) {
1297
			$manipulation[$class]['fields']['LastEdited'] = $now;
1298
			if($isNewRecord) {
1299
				$manipulation[$class]['fields']['Created']
1300
					= empty($this->record['Created'])
1301
						? $now
1302
						: $this->record['Created'];
1303
				$manipulation[$class]['fields']['ClassName'] = $this->class;
1304
			}
1305
		}
1306
1307
		// Inserts done one the base table are performed in another step, so the manipulation should instead
1308
		// attempt an update, as though it were a normal update.
1309
		$manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update';
1310
		$manipulation[$class]['id'] = $this->record['ID'];
1311
	}
1312
1313
	/**
1314
	 * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1315
	 *
1316
	 * Does nothing if an ID is already assigned for this record
1317
	 *
1318
	 * @param string $baseTable Base table
1319
	 * @param string $now Timestamp to use for the current time
1320
	 */
1321
	protected function writeBaseRecord($baseTable, $now) {
1322
		// Generate new ID if not specified
1323
		if($this->isInDB()) return;
1324
1325
		// Perform an insert on the base table
1326
		$insert = new SQLInsert('"'.$baseTable.'"');
1327
		$insert
1328
			->assign('"Created"', $now)
1329
			->execute();
1330
		$this->changed['ID'] = self::CHANGE_VALUE;
1331
		$this->record['ID'] = DB::get_generated_id($baseTable);
1332
	}
1333
1334
	/**
1335
	 * Generate and write the database manipulation for all changed fields
1336
	 *
1337
	 * @param string $baseTable Base table
1338
	 * @param string $now Timestamp to use for the current time
1339
	 * @param bool $isNewRecord If this is a new record
1340
	 */
1341
	protected function writeManipulation($baseTable, $now, $isNewRecord) {
1342
		// Generate database manipulations for each class
1343
		$manipulation = array();
1344
		foreach($this->getClassAncestry() as $class) {
1345
			if(self::has_own_table($class)) {
1346
				$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1347
			}
1348
		}
1349
1350
		// Allow extensions to extend this manipulation
1351
		$this->extend('augmentWrite', $manipulation);
1352
1353
		// New records have their insert into the base data table done first, so that they can pass the
1354
		// generated ID on to the rest of the manipulation
1355
		if($isNewRecord) {
1356
			$manipulation[$baseTable]['command'] = 'update';
1357
		}
1358
1359
		// Perform the manipulation
1360
		DB::manipulate($manipulation);
1361
	}
1362
1363
	/**
1364
	 * Writes all changes to this object to the database.
1365
	 *  - It will insert a record whenever ID isn't set, otherwise update.
1366
	 *  - All relevant tables will be updated.
1367
	 *  - $this->onBeforeWrite() gets called beforehand.
1368
	 *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1369
	 *
1370
	 *  @uses DataExtension->augmentWrite()
1371
	 *
1372
	 * @param boolean $showDebug Show debugging information
1373
	 * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1374
	 * @param boolean $forceWrite Write to database even if there are no changes
1375
	 * @param boolean $writeComponents Call write() on all associated component instances which were previously
1376
	 *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1377
	 *                                 {@link getManyManyComponents()} (Default: false)
1378
	 * @return int The ID of the record
1379
	 * @throws ValidationException Exception that can be caught and handled by the calling function
1380
	 */
1381
	public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1382
		$now = SS_Datetime::now()->Rfc2822();
1383
1384
		// Execute pre-write tasks
1385
		$this->preWrite();
1386
1387
		// Check if we are doing an update or an insert
1388
		$isNewRecord = !$this->isInDB() || $forceInsert;
1389
1390
		// Check changes exist, abort if there are none
1391
		$hasChanges = $this->updateChanges($isNewRecord);
1392
		if($hasChanges || $forceWrite || $isNewRecord) {
1393
1394
			// Ensure Created and LastEdited are populated
1395
			if(!isset($this->record['Created'])) {
1396
				$this->record['Created'] = $now;
1397
			}
1398
			$this->record['LastEdited'] = $now;
1399
			// New records have their insert into the base data table done first, so that they can pass the
1400
			// generated primary key on to the rest of the manipulation
1401
			$baseTable = ClassInfo::baseDataClass($this->class);
1402
			$this->writeBaseRecord($baseTable, $now);
1403
1404
			// Write the DB manipulation for all changed fields
1405
			$this->writeManipulation($baseTable, $now, $isNewRecord);
1406
1407
			// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1408
			$this->writeRelations();
1409
			$this->onAfterWrite();
1410
			$this->changed = array();
1411
		} else {
1412
			if($showDebug) Debug::message("no changes for DataObject");
1413
1414
			// Used by DODs to clean up after themselves, eg, Versioned
1415
			$this->invokeWithExtensions('onAfterSkippedWrite');
1416
		}
1417
1418
		// Write relations as necessary
1419
		if($writeComponents) $this->writeComponents(true);
1420
1421
		// Clears the cache for this object so get_one returns the correct object.
1422
		$this->flushCache();
1423
1424
		return $this->record['ID'];
1425
	}
1426
1427
	/**
1428
	 * Writes cached relation lists to the database, if possible
1429
	 */
1430
	public function writeRelations() {
1431
		if(!$this->isInDB()) return;
1432
1433
		// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1434
		if($this->unsavedRelations) {
1435
			foreach($this->unsavedRelations as $name => $list) {
1436
				$list->changeToList($this->$name());
1437
			}
1438
			$this->unsavedRelations = array();
1439
		}
1440
	}
1441
1442
	/**
1443
	 * Write the cached components to the database. Cached components could refer to two different instances of the
1444
	 * same record.
1445
	 *
1446
	 * @param $recursive Recursively write components
1447
	 * @return DataObject $this
1448
	 */
1449
	public function writeComponents($recursive = false) {
1450
		if(!$this->components) return $this;
1451
1452
		foreach($this->components as $component) {
1453
			$component->write(false, false, false, $recursive);
1454
		}
1455
		return $this;
1456
	}
1457
1458
	/**
1459
	 * Delete this data object.
1460
	 * $this->onBeforeDelete() gets called.
1461
	 * Note that in Versioned objects, both Stage and Live will be deleted.
1462
	 *  @uses DataExtension->augmentSQL()
1463
	 */
1464
	public function delete() {
1465
		$this->brokenOnDelete = true;
1466
		$this->onBeforeDelete();
1467
		if($this->brokenOnDelete) {
1468
			user_error("$this->class has a broken onBeforeDelete() function."
1469
				. " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1470
		}
1471
1472
		// Deleting a record without an ID shouldn't do anything
1473
		if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1474
1475
		// TODO: This is quite ugly.  To improve:
1476
		//  - move the details of the delete code in the DataQuery system
1477
		//  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1478
		//    obviously, that means getting requireTable() to configure cascading deletes ;-)
1479
		$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1480
		foreach($srcQuery->queriedTables() as $table) {
1481
			$delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1482
			$delete->execute();
1483
		}
1484
		// Remove this item out of any caches
1485
		$this->flushCache();
1486
1487
		$this->onAfterDelete();
1488
1489
		$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...
1490
		$this->ID = 0;
1491
	}
1492
1493
	/**
1494
	 * Delete the record with the given ID.
1495
	 *
1496
	 * @param string $className The class name of the record to be deleted
1497
	 * @param int $id ID of record to be deleted
1498
	 */
1499
	public static function delete_by_id($className, $id) {
1500
		$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...
1501
		if($obj) {
1502
			$obj->delete();
1503
		} else {
1504
			user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1505
		}
1506
	}
1507
1508
	/**
1509
	 * Get the class ancestry, including the current class name.
1510
	 * The ancestry will be returned as an array of class names, where the 0th element
1511
	 * will be the class that inherits directly from DataObject, and the last element
1512
	 * will be the current class.
1513
	 *
1514
	 * @return array Class ancestry
1515
	 */
1516
	public function getClassAncestry() {
1517
		if(!isset(DataObject::$_cache_get_class_ancestry[$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...
1518
			DataObject::$_cache_get_class_ancestry[$this->class] = array($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...
1519
			while(($class=get_parent_class(DataObject::$_cache_get_class_ancestry[$this->class][0])) != "DataObject") {
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...
1520
				array_unshift(DataObject::$_cache_get_class_ancestry[$this->class], $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...
1521
			}
1522
		}
1523
		return DataObject::$_cache_get_class_ancestry[$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...
1524
	}
1525
1526
	/**
1527
	 * Return a component object from a one to one relationship, as a DataObject.
1528
	 * If no component is available, an 'empty component' will be returned for
1529
	 * non-polymorphic relations, or for polymorphic relations with a class set.
1530
	 *
1531
	 * @param string $componentName Name of the component
1532
	 *
1533
	 * @return DataObject The component object. It's exact type will be that of the component.
1534
	 */
1535
	public function getComponent($componentName) {
1536
		if(isset($this->components[$componentName])) {
1537
			return $this->components[$componentName];
1538
		}
1539
1540
		if($class = $this->hasOneComponent($componentName)) {
1541
			$joinField = $componentName . 'ID';
1542
			$joinID    = $this->getField($joinField);
1543
1544
			// Extract class name for polymorphic relations
1545
			if($class === 'DataObject') {
1546
				$class = $this->getField($componentName . 'Class');
1547
				if(empty($class)) return null;
1548
			}
1549
1550
			if($joinID) {
1551
				$component = DataObject::get_by_id($class, $joinID);
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...
1552
			}
1553
1554
			if(empty($component)) {
1555
				$component = $this->model->$class->newObject();
1556
			}
1557
		} elseif($class = $this->belongsToComponent($componentName)) {
1558
1559
			$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
1560
			$joinID    = $this->ID;
1561
1562
			if($joinID) {
1563
1564
				$filter = $polymorphic
1565
					? array(
1566
						"{$joinField}ID" => $joinID,
1567
						"{$joinField}Class" => $this->class
1568
					)
1569
					: array(
1570
						$joinField => $joinID
1571
					);
1572
				$component = DataObject::get($class)->filter($filter)->first();
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...
1573
			}
1574
1575
			if(empty($component)) {
1576
				$component = $this->model->$class->newObject();
1577
				if($polymorphic) {
1578
					$component->{$joinField.'ID'} = $this->ID;
1579
					$component->{$joinField.'Class'} = $this->class;
1580
				} else {
1581
					$component->$joinField = $this->ID;
1582
				}
1583
			}
1584
		} else {
1585
			throw new Exception("DataObject->getComponent(): Could not find component '$componentName'.");
1586
		}
1587
1588
		$this->components[$componentName] = $component;
1589
		return $component;
1590
	}
1591
1592
	/**
1593
	 * Returns a one-to-many relation as a HasManyList
1594
	 *
1595
	 * @param string $componentName Name of the component
1596
	 * @param string|null $filter Deprecated. A filter to be inserted into the WHERE clause
1597
	 * @param string|null|array $sort Deprecated. A sort expression to be inserted into the ORDER BY clause. If omitted,
1598
	 *                                the static field $default_sort on the component class will be used.
1599
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1600
	 * @param string|null|array $limit Deprecated. A limit expression to be inserted into the LIMIT clause
1601
	 *
1602
	 * @return HasManyList The components of the one-to-many relationship.
1603
	 */
1604
	public function getComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
1605
		$result = null;
1606
1607
		if(!$componentClass = $this->hasManyComponent($componentName)) {
1608
			user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'"
1609
				. " on class '$this->class'", E_USER_ERROR);
1610
		}
1611
1612
		if($join) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $join 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...
1613
			throw new \InvalidArgumentException(
1614
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
1615
			);
1616
		}
1617
1618
		if($filter !== null || $sort !== null || $limit !== null) {
1619
			Deprecation::notice('4.0', 'The $filter, $sort and $limit parameters for DataObject::getComponents()
1620
				have been deprecated. Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
1621
		}
1622
1623
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1624
		if(!$this->ID) {
1625
			if(!isset($this->unsavedRelations[$componentName])) {
1626
				$this->unsavedRelations[$componentName] =
1627
					new UnsavedRelationList($this->class, $componentName, $componentClass);
0 ignored issues
show
Security Bug introduced by
It seems like $componentClass defined by $this->hasManyComponent($componentName) on line 1607 can also be of type false; however, UnsavedRelationList::__construct() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1628
			}
1629
			return $this->unsavedRelations[$componentName];
1630
		}
1631
1632
		// Determine type and nature of foreign relation
1633
		$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
1634
		if($polymorphic) {
1635
			$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1636
		} else {
1637
			$result = HasManyList::create($componentClass, $joinField);
1638
		}
1639
1640
		if($this->model) $result->setDataModel($this->model);
1641
1642
		return $result
1643
			->forForeignID($this->ID)
1644
			->where($filter)
1645
			->limit($limit)
1646
			->sort($sort);
1647
	}
1648
1649
	/**
1650
	 * @deprecated
1651
	 */
1652
	public function getComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
1653
		Deprecation::notice('4.0', "Use getComponents to get a filtered DataList for an object's relation");
1654
		return $this->getComponents($componentName, $filter, $sort, $join, $limit);
1655
	}
1656
1657
	/**
1658
	 * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1659
	 *
1660
	 * @param $relationName Relation name.
1661
	 * @return string Class name, or null if not found.
1662
	 */
1663
	public function getRelationClass($relationName) {
1664
		// Go through all relationship configuration fields.
1665
		$candidates = array_merge(
1666
			($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1667
			($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1668
			($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
1669
			($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
1670
			($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1671
		);
1672
1673
		if (isset($candidates[$relationName])) {
1674
			$remoteClass = $candidates[$relationName];
1675
1676
			// If dot notation is present, extract just the first part that contains the class.
1677
			if(($fieldPos = strpos($remoteClass, '.'))!==false) {
1678
				return substr($remoteClass, 0, $fieldPos);
1679
			}
1680
1681
			// Otherwise just return the class
1682
			return $remoteClass;
1683
		}
1684
1685
		return null;
1686
	}
1687
1688
	/**
1689
	 * Tries to find the database key on another object that is used to store a
1690
	 * relationship to this class. If no join field can be found it defaults to 'ParentID'.
1691
	 *
1692
	 * If the remote field is polymorphic then $polymorphic is set to true, and the return value
1693
	 * is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
1694
	 *
1695
	 * @param string $component Name of the relation on the current object pointing to the
1696
	 * remote object.
1697
	 * @param string $type the join type - either 'has_many' or 'belongs_to'
1698
	 * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
1699
	 * @return string
1700
	 */
1701
	public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
1702
		// Extract relation from current object
1703
		if($type === 'has_many') {
1704
			$remoteClass = $this->hasManyComponent($component, false);
1705
		} else {
1706
			$remoteClass = $this->belongsToComponent($component, false);
1707
		}
1708
1709
		if(empty($remoteClass)) {
1710
			throw new Exception("Unknown $type component '$component' on class '$this->class'");
1711
		}
1712
		if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
1713
			throw new Exception(
1714
				"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
1715
			);
1716
		}
1717
1718
		// If presented with an explicit field name (using dot notation) then extract field name
1719
		$remoteField = null;
1720
		if(strpos($remoteClass, '.') !== false) {
1721
			list($remoteClass, $remoteField) = explode('.', $remoteClass);
1722
		}
1723
1724
		// Reference remote has_one to check against
1725
		$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
1726
1727
		// Without an explicit field name, attempt to match the first remote field
1728
		// with the same type as the current class
1729
		if(empty($remoteField)) {
1730
			// look for remote has_one joins on this class or any parent classes
1731
			$remoteRelationsMap = array_flip($remoteRelations);
1732
			foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1733
				if(array_key_exists($class, $remoteRelationsMap)) {
1734
					$remoteField = $remoteRelationsMap[$class];
1735
					break;
1736
				}
1737
			}
1738
		}
1739
1740
		// In case of an indeterminate remote field show an error
1741
		if(empty($remoteField)) {
1742
			$polymorphic = false;
1743
			$message = "No has_one found on class '$remoteClass'";
1744
			if($type == 'has_many') {
1745
				// include a hint for has_many that is missing a has_one
1746
				$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
1747
				$message .= " requires a has_one on '$remoteClass'";
1748
			}
1749
			throw new Exception($message);
1750
		}
1751
1752
		// If given an explicit field name ensure the related class specifies this
1753
		if(empty($remoteRelations[$remoteField])) {
1754
			throw new Exception("Missing expected has_one named '$remoteField'
1755
				on class '$remoteClass' referenced by $type named '$component'
1756
				on class {$this->class}"
1757
			);
1758
		}
1759
1760
		// Inspect resulting found relation
1761
		if($remoteRelations[$remoteField] === 'DataObject') {
1762
			$polymorphic = true;
1763
			return $remoteField; // Composite polymorphic field does not include 'ID' suffix
1764
		} else {
1765
			$polymorphic = false;
1766
			return $remoteField . 'ID';
1767
		}
1768
	}
1769
1770
	/**
1771
	 * Returns a many-to-many component, as a ManyManyList.
1772
	 * @param string $componentName Name of the many-many component
1773
	 * @return ManyManyList The set of components
1774
	 *
1775
	 * @todo Implement query-params
1776
	 */
1777
	public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
1778
		list($parentClass, $componentClass, $parentField, $componentField, $table)
1779
			= $this->manyManyComponent($componentName);
1780
1781
		if($filter !== null || $sort !== null || $join !== null || $limit !== null) {
1782
			Deprecation::notice('4.0', 'The $filter, $sort, $join and $limit parameters for
1783
				DataObject::getManyManyComponents() have been deprecated.
1784
				Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
1785
		}
1786
1787
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1788
		if(!$this->ID) {
1789
			if(!isset($this->unsavedRelations[$componentName])) {
1790
				$this->unsavedRelations[$componentName] =
1791
					new UnsavedRelationList($parentClass, $componentName, $componentClass);
1792
			}
1793
			return $this->unsavedRelations[$componentName];
1794
		}
1795
1796
		$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
1797
		$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1798
1799
1800
		// Store component data in query meta-data
1801
		$result = $result->alterDataQuery(function($query) use ($extraFields) {
1802
			$query->setQueryParam('Component.ExtraFields', $extraFields);
1803
		});
1804
		
1805
		if($this->model) $result->setDataModel($this->model);
1806
1807
		$this->extend('updateManyManyComponents', $result);
1808
1809
		// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1810
		// foreignID set elsewhere.
1811
		return $result
1812
			->forForeignID($this->ID)
1813
			->where($filter)
1814
			->sort($sort)
1815
			->limit($limit);
1816
	}
1817
1818
	/**
1819
	 * @deprecated 4.0 Method has been replaced by hasOne() and hasOneComponent()
1820
	 * @param string $component
1821
	 * @return array|null
1822
	 */
1823
	public function has_one($component = null) {
1824
		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...
1825
			Deprecation::notice('4.0', 'Please use hasOneComponent() instead');
1826
			return $this->hasOneComponent($component);
1827
		}
1828
1829
		Deprecation::notice('4.0', 'Please use hasOne() instead');
1830
		return $this->hasOne();
1831
	}
1832
1833
	/**
1834
	 * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1835
	 * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1836
	 *
1837
	 * @param string $component Deprecated - Name of component
1838
	 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1839
	 * 							their classes.
1840
	 */
1841
	public function hasOne($component = null) {
1842
		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...
1843
			Deprecation::notice(
1844
				'4.0',
1845
				'Please use DataObject::hasOneComponent() instead of passing a component name to hasOne()',
1846
				Deprecation::SCOPE_GLOBAL
1847
			);
1848
			return $this->hasOneComponent($component);
1849
		}
1850
1851
		return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1852
	}
1853
1854
	/**
1855
	 * Return data for a specific has_one component.
1856
	 * @param string $component
1857
	 * @return string|null
1858
	 */
1859
	public function hasOneComponent($component) {
1860
		$hasOnes = (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1861
1862
		if(isset($hasOnes[$component])) {
1863
			return $hasOnes[$component];
1864
		}
1865
	}
1866
1867
	/**
1868
	 * @deprecated 4.0 Method has been replaced by belongsTo() and belongsToComponent()
1869
	 * @param string $component
1870
	 * @param bool $classOnly
1871
	 * @return array|null
1872
	 */
1873
	public function belongs_to($component = null, $classOnly = true) {
1874
		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...
1875
			Deprecation::notice('4.0', 'Please use belongsToComponent() instead');
1876
			return $this->belongsToComponent($component, $classOnly);
1877
		}
1878
1879
		Deprecation::notice('4.0', 'Please use belongsTo() instead');
1880
		return $this->belongsTo(null, $classOnly);
1881
	}
1882
1883
	/**
1884
	 * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1885
	 * their class name will be returned.
1886
	 *
1887
	 * @param string $component - Name of component
1888
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1889
	 *        the field data stripped off. It defaults to TRUE.
1890
	 * @return string|array
1891
	 */
1892
	public function belongsTo($component = null, $classOnly = true) {
1893
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1894
			Deprecation::notice(
1895
				'4.0',
1896
				'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
1897
				Deprecation::SCOPE_GLOBAL
1898
			);
1899
			return $this->belongsToComponent($component, $classOnly);
1900
		}
1901
1902
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1903
		if($belongsTo && $classOnly) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $belongsTo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1904
			return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1905
		} else {
1906
			return $belongsTo ? $belongsTo : array();
1907
		}
1908
	}
1909
1910
	/**
1911
	 * Return data for a specific belongs_to component.
1912
	 * @param string $component
1913
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1914
	 *        the field data stripped off. It defaults to TRUE.
1915
	 * @return string|false
1916
	 */
1917
	public function belongsToComponent($component, $classOnly = true) {
1918
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1919
1920
		if($belongsTo && array_key_exists($component, $belongsTo)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $belongsTo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1921
			$belongsTo = $belongsTo[$component];
1922
		} else {
1923
			return false;
1924
		}
1925
1926
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
1927
	}
1928
1929
	/**
1930
	 * Return all of the database fields defined in self::$db and all the parent classes.
1931
	 * Doesn't include any fields specified by self::$has_one.  Use $this->hasOne() to get these fields
1932
	 *
1933
	 * @param string $fieldName Limit the output to a specific field name
1934
	 * @return array The database fields
1935
	 */
1936
	public function db($fieldName = null) {
1937
		$classes = ClassInfo::ancestry($this, true);
1938
1939
		// If we're looking for a specific field, we want to hit subclasses first as they may override field types
1940
		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...
1941
			$classes = array_reverse($classes);
1942
		}
1943
1944
		$items = array();
1945
		foreach($classes as $class) {
1946
			if(isset(self::$_cache_db[$class])) {
1947
				$dbItems = self::$_cache_db[$class];
1948
			} else {
1949
				$dbItems = (array) Config::inst()->get($class, 'db', Config::UNINHERITED);
1950
				self::$_cache_db[$class] = $dbItems;
1951
			}
1952
1953
			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...
1954
				if(isset($dbItems[$fieldName])) {
1955
					return $dbItems[$fieldName];
1956
				}
1957
			} else {
1958
				$items = isset($items) ? array_merge((array) $items, $dbItems) : $dbItems;
1959
			}
1960
		}
1961
1962
		return $items;
1963
	}
1964
1965
	/**
1966
	 * @deprecated 4.0 Method has been replaced by hasMany() and hasManyComponent()
1967
	 * @param string $component
1968
	 * @param bool $classOnly
1969
	 * @return array|null
1970
	 */
1971
	public function has_many($component = null, $classOnly = true) {
1972
		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...
1973
			Deprecation::notice('4.0', 'Please use hasManyComponent() instead');
1974
			return $this->hasManyComponent($component, $classOnly);
1975
		}
1976
1977
		Deprecation::notice('4.0', 'Please use hasMany() instead');
1978
		return $this->hasMany(null, $classOnly);
1979
	}
1980
1981
	/**
1982
	 * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1983
	 * relationships and their classes will be returned.
1984
	 *
1985
	 * @param string $component Deprecated - Name of component
1986
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1987
	 *        the field data stripped off. It defaults to TRUE.
1988
	 * @return string|array|false
1989
	 */
1990
	public function hasMany($component = null, $classOnly = true) {
1991
		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...
1992
			Deprecation::notice(
1993
				'4.0',
1994
				'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
1995
				Deprecation::SCOPE_GLOBAL
1996
			);
1997
			return $this->hasManyComponent($component, $classOnly);
1998
		}
1999
2000
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2001
		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...
2002
			return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2003
		} else {
2004
			return $hasMany ? $hasMany : array();
2005
		}
2006
	}
2007
2008
	/**
2009
	 * Return data for a specific has_many component.
2010
	 * @param string $component
2011
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2012
	 *        the field data stripped off. It defaults to TRUE.
2013
	 * @return string|false
2014
	 */
2015
	public function hasManyComponent($component, $classOnly = true) {
2016
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2017
2018
		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...
2019
			$hasMany = $hasMany[$component];
2020
		} else {
2021
			return false;
2022
		}
2023
2024
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
2025
	}
2026
2027
	/**
2028
	 * @deprecated 4.0 Method has been replaced by manyManyExtraFields() and
2029
	 *                 manyManyExtraFieldsForComponent()
2030
	 * @param string $component
2031
	 * @return array
2032
	 */
2033
	public function many_many_extraFields($component = null) {
2034
		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...
2035
			Deprecation::notice('4.0', 'Please use manyManyExtraFieldsForComponent() instead');
2036
			return $this->manyManyExtraFieldsForComponent($component);
2037
		}
2038
2039
		Deprecation::notice('4.0', 'Please use manyManyExtraFields() instead');
2040
		return $this->manyManyExtraFields();
2041
	}
2042
2043
	/**
2044
	 * Return the many-to-many extra fields specification.
2045
	 *
2046
	 * If you don't specify a component name, it returns all
2047
	 * extra fields for all components available.
2048
	 *
2049
	 * @param string $component Deprecated - Name of component
2050
	 * @return array|null
2051
	 */
2052
	public function manyManyExtraFields($component = null) {
2053
		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...
2054
			Deprecation::notice(
2055
				'4.0',
2056
				'Please use DataObject::manyManyExtraFieldsForComponent() instead of passing a component name
2057
					to manyManyExtraFields()',
2058
				Deprecation::SCOPE_GLOBAL
2059
			);
2060
			return $this->manyManyExtraFieldsForComponent($component);
2061
		}
2062
2063
		return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2064
	}
2065
2066
	/**
2067
	 * Return the many-to-many extra fields specification for a specific component.
2068
	 * @param string $component
2069
	 * @return array|null
2070
	 */
2071
	public function manyManyExtraFieldsForComponent($component) {
2072
		// Get all many_many_extraFields defined in this class or parent classes
2073
		$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2074
		// Extra fields are immediately available
2075
		if(isset($extraFields[$component])) {
2076
			return $extraFields[$component];
2077
		}
2078
2079
		// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
2080
		$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2081
		$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2082
		if($candidate) {
2083
			$relationName = null;
2084
			// Extract class and relation name from dot-notation
2085
			if(strpos($candidate, '.') !== false) {
2086
				list($candidate, $relationName) = explode('.', $candidate, 2);
2087
			}
2088
2089
			// If we've not already found the relation name from dot notation, we need to find a relation that points
2090
			// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
2091
			// so it's safe to assume that it's the correct one
2092
			if(!$relationName) {
2093
				$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2094
2095
				foreach($candidateManyManys as $relation => $relatedClass) {
2096
					if (is_a($this, $relatedClass)) {
2097
						$relationName = $relation;
2098
					}
2099
				}
2100
			}
2101
2102
			// If we've found a matching relation on the target class, see if we can find extra fields for it
2103
			$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
2104
			if(isset($extraFields[$relationName])) {
2105
				return $extraFields[$relationName];
2106
			}
2107
		}
2108
2109
		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...
2110
	}
2111
2112
	/**
2113
	 * @deprecated 4.0 Method has been renamed to manyMany()
2114
	 * @param string $component
2115
	 * @return array|null
2116
	 */
2117
	public function many_many($component = null) {
2118
		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...
2119
			Deprecation::notice('4.0', 'Please use manyManyComponent() instead');
2120
			return $this->manyManyComponent($component);
2121
		}
2122
2123
		Deprecation::notice('4.0', 'Please use manyMany() instead');
2124
		return $this->manyMany();
2125
	}
2126
2127
	/**
2128
	 * Return information about a many-to-many component.
2129
	 * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2130
	 * components are returned.
2131
	 *
2132
	 * @see DataObject::manyManyComponent()
2133
	 * @param string $component Deprecated - Name of component
2134
	 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2135
	 */
2136
	public function manyMany($component = null) {
2137
		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...
2138
			Deprecation::notice(
2139
				'4.0',
2140
				'Please use DataObject::manyManyComponent() instead of passing a component name to manyMany()',
2141
				Deprecation::SCOPE_GLOBAL
2142
			);
2143
			return $this->manyManyComponent($component);
2144
		}
2145
2146
		$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
2147
		$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2148
2149
		$items = array_merge($manyManys, $belongsManyManys);
2150
		return $items;
2151
	}
2152
2153
	/**
2154
	 * Return information about a specific many_many component. Returns a numeric array of:
2155
	 * array(
2156
	 * 	<classname>,		The class that relation is defined in e.g. "Product"
2157
	 * 	<candidateName>,	The target class of the relation e.g. "Category"
2158
	 * 	<parentField>,		The field name pointing to <classname>'s table e.g. "ProductID"
2159
	 * 	<childField>,		The field name pointing to <candidatename>'s table e.g. "CategoryID"
2160
	 * 	<joinTable>			The join table between the two classes e.g. "Product_Categories"
2161
	 * )
2162
	 * @param string $component The component name
2163
	 * @return array|null
2164
	 */
2165
	public function manyManyComponent($component) {
2166
		$classes = $this->getClassAncestry();
2167
		foreach($classes as $class) {
2168
			$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
2169
			// Check if the component is defined in many_many on this class
2170
			$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2171
			if($candidate) {
2172
				$parentField = $class . "ID";
2173
				$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
2174
				return array($class, $candidate, $parentField, $childField, "{$class}_$component");
2175
			}
2176
2177
			// Check if the component is defined in belongs_many_many on this class
2178
			$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
2179
			$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
2180
			if($candidate) {
2181
				// Extract class and relation name from dot-notation
2182
				if(strpos($candidate, '.') !== false) {
2183
					list($candidate, $relationName) = explode('.', $candidate, 2);
2184
				}
2185
2186
				$childField = $candidate . "ID";
2187
2188
				// We need to find the inverse component name
2189
				$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2190
				if(!$otherManyMany) {
2191
					throw new LogicException("Inverse component of $candidate not found ({$this->class})");
2192
				}
2193
2194
				// If we've got a relation name (extracted from dot-notation), we can already work out
2195
				// the join table and candidate class name...
2196
				if(isset($relationName) && isset($otherManyMany[$relationName])) {
2197
					$candidateClass = $otherManyMany[$relationName];
2198
					$joinTable = "{$candidate}_{$relationName}";
2199
				} else {
2200
					// ... otherwise, we need to loop over the many_manys and find a relation that
2201
					// matches up to this class
2202
					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...
2203
						if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
2204
							$joinTable = "{$candidate}_{$inverseComponentName}";
2205
							break;
2206
						}
2207
					}
2208
				}
2209
2210
				// If we could work out the join table, we've got all the info we need
2211
				if(isset($joinTable)) {
2212
					$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...
2213
					return array($class, $candidate, $parentField, $childField, $joinTable);
2214
				}
2215
2216
				throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
2217
			}
2218
		}
2219
	}
2220
2221
	/**
2222
	 * This returns an array (if it exists) describing the database extensions that are required, or false if none
2223
	 *
2224
	 * This is experimental, and is currently only a Postgres-specific enhancement.
2225
	 *
2226
	 * @return array or false
2227
	 */
2228
	public function database_extensions($class){
2229
		$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2230
2231
		if($extensions)
2232
			return $extensions;
2233
		else
2234
			return false;
2235
	}
2236
2237
	/**
2238
	 * Generates a SearchContext to be used for building and processing
2239
	 * a generic search form for properties on this object.
2240
	 *
2241
	 * @return SearchContext
2242
	 */
2243
	public function getDefaultSearchContext() {
2244
		return new SearchContext(
2245
			$this->class,
2246
			$this->scaffoldSearchFields(),
2247
			$this->defaultSearchFilters()
2248
		);
2249
	}
2250
2251
	/**
2252
	 * Determine which properties on the DataObject are
2253
	 * searchable, and map them to their default {@link FormField}
2254
	 * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2255
	 *
2256
	 * Some additional logic is included for switching field labels, based on
2257
	 * how generic or specific the field type is.
2258
	 *
2259
	 * Used by {@link SearchContext}.
2260
	 *
2261
	 * @param array $_params
2262
	 *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2263
	 *   'restrictFields': Numeric array of a field name whitelist
2264
	 * @return FieldList
2265
	 */
2266
	public function scaffoldSearchFields($_params = null) {
2267
		$params = array_merge(
2268
			array(
2269
				'fieldClasses' => false,
2270
				'restrictFields' => false
2271
			),
2272
			(array)$_params
2273
		);
2274
		$fields = new FieldList();
2275
		foreach($this->searchableFields() as $fieldName => $spec) {
2276
			if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
2277
2278
			// If a custom fieldclass is provided as a string, use it
2279
			if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2280
				$fieldClass = $params['fieldClasses'][$fieldName];
2281
				$field = new $fieldClass($fieldName);
2282
			// If we explicitly set a field, then construct that
2283
			} else if(isset($spec['field'])) {
2284
				// If it's a string, use it as a class name and construct
2285
				if(is_string($spec['field'])) {
2286
					$fieldClass = $spec['field'];
2287
					$field = new $fieldClass($fieldName);
2288
2289
				// If it's a FormField object, then just use that object directly.
2290
				} else if($spec['field'] instanceof FormField) {
2291
					$field = $spec['field'];
2292
2293
				// Otherwise we have a bug
2294
				} else {
2295
					user_error("Bad value for searchable_fields, 'field' value: "
2296
						. var_export($spec['field'], true), E_USER_WARNING);
2297
				}
2298
2299
			// Otherwise, use the database field's scaffolder
2300
			} else {
2301
				$field = $this->relObject($fieldName)->scaffoldSearchField();
2302
			}
2303
2304
			if (strstr($fieldName, '.')) {
2305
				$field->setName(str_replace('.', '__', $fieldName));
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...
2306
			}
2307
			$field->setTitle($spec['title']);
2308
2309
			$fields->push($field);
2310
		}
2311
		return $fields;
2312
	}
2313
2314
	/**
2315
	 * Scaffold a simple edit form for all properties on this dataobject,
2316
	 * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2317
	 * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2318
	 *
2319
	 * @uses FormScaffolder
2320
	 *
2321
	 * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2322
	 * @return FieldList
2323
	 */
2324
	public function scaffoldFormFields($_params = null) {
2325
		$params = array_merge(
2326
			array(
2327
				'tabbed' => false,
2328
				'includeRelations' => false,
2329
				'restrictFields' => false,
2330
				'fieldClasses' => false,
2331
				'ajaxSafe' => false
2332
			),
2333
			(array)$_params
2334
		);
2335
2336
		$fs = new FormScaffolder($this);
2337
		$fs->tabbed = $params['tabbed'];
2338
		$fs->includeRelations = $params['includeRelations'];
2339
		$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...
2340
		$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...
2341
		$fs->ajaxSafe = $params['ajaxSafe'];
2342
2343
		return $fs->getFieldList();
2344
	}
2345
2346
	/**
2347
	 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2348
	 * being called on extensions
2349
	 *
2350
	 * @param callable $callback The callback to execute
2351
	 */
2352
	protected function beforeUpdateCMSFields($callback) {
2353
		$this->beforeExtending('updateCMSFields', $callback);
2354
	}
2355
2356
	/**
2357
	 * Centerpiece of every data administration interface in Silverstripe,
2358
	 * which returns a {@link FieldList} suitable for a {@link Form} object.
2359
	 * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2360
	 * generate this set. To customize, overload this method in a subclass
2361
	 * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2362
	 *
2363
	 * <code>
2364
	 * class MyCustomClass extends DataObject {
2365
	 *  static $db = array('CustomProperty'=>'Boolean');
2366
	 *
2367
	 *  function getCMSFields() {
2368
	 *    $fields = parent::getCMSFields();
2369
	 *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2370
	 *    return $fields;
2371
	 *  }
2372
	 * }
2373
	 * </code>
2374
	 *
2375
	 * @see Good example of complex FormField building: SiteTree::getCMSFields()
2376
	 *
2377
	 * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2378
	 */
2379
	public function getCMSFields() {
2380
		$tabbedFields = $this->scaffoldFormFields(array(
2381
			// Don't allow has_many/many_many relationship editing before the record is first saved
2382
			'includeRelations' => ($this->ID > 0),
2383
			'tabbed' => true,
2384
			'ajaxSafe' => true
2385
		));
2386
2387
		$this->extend('updateCMSFields', $tabbedFields);
2388
2389
		return $tabbedFields;
2390
	}
2391
2392
	/**
2393
	 * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2394
	 * including that dataobject's extensions customised actions could be added to the EditForm.
2395
	 *
2396
	 * @return an Empty FieldList(); need to be overload by solid subclass
2397
	 */
2398
	public function getCMSActions() {
2399
		$actions = new FieldList();
2400
		$this->extend('updateCMSActions', $actions);
2401
		return $actions;
2402
	}
2403
2404
2405
	/**
2406
	 * Used for simple frontend forms without relation editing
2407
	 * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2408
	 * by default. To customize, either overload this method in your
2409
	 * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2410
	 *
2411
	 * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2412
	 *
2413
	 * @param array $params See {@link scaffoldFormFields()}
2414
	 * @return FieldList Always returns a simple field collection without TabSet.
2415
	 */
2416
	public function getFrontEndFields($params = null) {
2417
		$untabbedFields = $this->scaffoldFormFields($params);
2418
		$this->extend('updateFrontEndFields', $untabbedFields);
2419
2420
		return $untabbedFields;
2421
	}
2422
2423
	/**
2424
	 * Gets the value of a field.
2425
	 * Called by {@link __get()} and any getFieldName() methods you might create.
2426
	 *
2427
	 * @param string $field The name of the field
2428
	 *
2429
	 * @return mixed The field value
2430
	 */
2431
	public function getField($field) {
2432
		// If we already have an object in $this->record, then we should just return that
2433
		if(isset($this->record[$field]) && is_object($this->record[$field]))  return $this->record[$field];
2434
2435
		// Do we have a field that needs to be lazy loaded?
2436
		if(isset($this->record[$field.'_Lazy'])) {
2437
			$tableClass = $this->record[$field.'_Lazy'];
2438
			$this->loadLazyFields($tableClass);
2439
		}
2440
2441
		// Otherwise, we need to determine if this is a complex field
2442
		if(self::is_composite_field($this->class, $field)) {
2443
			$helper = $this->castingHelper($field);
2444
			$fieldObj = Object::create_from_string($helper, $field);
2445
2446
			$compositeFields = $fieldObj->compositeDatabaseFields();
2447
			foreach ($compositeFields as $compositeName => $compositeType) {
2448
				if(isset($this->record[$field.$compositeName.'_Lazy'])) {
2449
					$tableClass = $this->record[$field.$compositeName.'_Lazy'];
2450
					$this->loadLazyFields($tableClass);
2451
				}
2452
			}
2453
2454
			// write value only if either the field value exists,
2455
			// or a valid record has been loaded from the database
2456
			$value = (isset($this->record[$field])) ? $this->record[$field] : null;
2457
			if($value || $this->exists()) $fieldObj->setValue($value, $this->record, false);
2458
2459
			$this->record[$field] = $fieldObj;
2460
2461
			return $this->record[$field];
2462
		}
2463
2464
		return isset($this->record[$field]) ? $this->record[$field] : null;
2465
	}
2466
2467
	/**
2468
	 * Loads all the stub fields that an initial lazy load didn't load fully.
2469
	 *
2470
	 * @param string $tableClass Base table to load the values from. Others are joined as required.
2471
	 * Not specifying a tableClass will load all lazy fields from all tables.
2472
	 * @return bool Flag if lazy loading succeeded
2473
	 */
2474
	protected function loadLazyFields($tableClass = null) {
2475
		if(!$this->isInDB() || !is_numeric($this->ID)) {
2476
			return false;
2477
		}
2478
2479
		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...
2480
			$loaded = array();
2481
2482
			foreach ($this->record as $key => $value) {
2483
				if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2484
					$this->loadLazyFields($value);
2485
					$loaded[$value] = $value;
2486
				}
2487
			}
2488
2489
			return false;
2490
		}
2491
2492
		$dataQuery = new DataQuery($tableClass);
2493
2494
		// Reset query parameter context to that of this DataObject
2495
		if($params = $this->getSourceQueryParams()) {
2496
			foreach($params as $key => $value) $dataQuery->setQueryParam($key, $value);
2497
		}
2498
2499
		// Limit query to the current record, unless it has the Versioned extension,
2500
		// in which case it requires special handling through augmentLoadLazyFields()
2501
		if(!$this->hasExtension('Versioned')) {
2502
			$dataQuery->where("\"$tableClass\".\"ID\" = {$this->record['ID']}")->limit(1);
2503
		}
2504
2505
		$columns = array();
2506
2507
		// Add SQL for fields, both simple & multi-value
2508
		// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2509
		$databaseFields = self::database_fields($tableClass, false);
2510
		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...
2511
			if(!isset($this->record[$k]) || $this->record[$k] === null) {
2512
				$columns[] = $k;
2513
			}
2514
		}
2515
2516
		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...
2517
			$query = $dataQuery->query();
2518
			$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2519
			$this->extend('augmentSQL', $query, $dataQuery);
2520
2521
			$dataQuery->setQueriedColumns($columns);
2522
			$newData = $dataQuery->execute()->record();
2523
2524
			// Load the data into record
2525
			if($newData) {
2526
				foreach($newData as $k => $v) {
2527
					if (in_array($k, $columns)) {
2528
						$this->record[$k] = $v;
2529
						$this->original[$k] = $v;
2530
						unset($this->record[$k . '_Lazy']);
2531
					}
2532
				}
2533
2534
			// No data means that the query returned nothing; assign 'null' to all the requested fields
2535
			} else {
2536
				foreach($columns as $k) {
2537
					$this->record[$k] = null;
2538
					$this->original[$k] = null;
2539
					unset($this->record[$k . '_Lazy']);
2540
				}
2541
			}
2542
		}
2543
		return true;
2544
	}
2545
2546
	/**
2547
	 * Return the fields that have changed.
2548
	 *
2549
	 * The change level affects what the functions defines as "changed":
2550
	 * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2551
	 * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2552
	 *   for example a change from 0 to null would not be included.
2553
	 *
2554
	 * Example return:
2555
	 * <code>
2556
	 * array(
2557
	 *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2558
	 * )
2559
	 * </code>
2560
	 *
2561
	 * @param boolean $databaseFieldsOnly Get only database fields that have changed
2562
	 * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2563
	 * @return array
2564
	 */
2565
	public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
2566
		$changedFields = array();
2567
2568
		// Update the changed array with references to changed obj-fields
2569
		foreach($this->record as $k => $v) {
2570
			if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2571
				$this->changed[$k] = self::CHANGE_VALUE;
2572
			}
2573
		}
2574
2575
		if($databaseFieldsOnly) {
2576
			// Merge all DB fields together
2577
			$inheritedFields = $this->inheritedDatabaseFields();
2578
			$compositeFields = static::composite_fields(get_class($this));
2579
			$fixedFields = $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...
2580
			$databaseFields = array_merge(
2581
				$inheritedFields,
2582
				$fixedFields,
2583
				$compositeFields
2584
			);
2585
			$fields = array_intersect_key((array)$this->changed, $databaseFields);
2586
		} else {
2587
			$fields = $this->changed;
2588
		}
2589
2590
		// Filter the list to those of a certain change level
2591
		if($changeLevel > self::CHANGE_STRICT) {
2592
			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...
2593
				if($level < $changeLevel) {
2594
					unset($fields[$name]);
2595
				}
2596
			}
2597
		}
2598
2599
		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...
2600
			$changedFields[$name] = array(
2601
				'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2602
				'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2603
				'level' => $level
2604
			);
2605
		}
2606
2607
		return $changedFields;
2608
	}
2609
2610
	/**
2611
	 * Uses {@link getChangedFields()} to determine if fields have been changed
2612
	 * since loading them from the database.
2613
	 *
2614
	 * @param string $fieldName Name of the database field to check, will check for any if not given
2615
	 * @param int $changeLevel See {@link getChangedFields()}
2616
	 * @return boolean
2617
	 */
2618
	public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
2619
		if (!$fieldName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldName 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...
2620
			// Limit "any changes" to db fields only
2621
			$changed = $this->getChangedFields(true, $changeLevel);
2622
			return !empty($changed);
2623
		} else {
2624
			// Given a field name, check all fields
2625
			$changed = $this->getChangedFields(false, $changeLevel);
2626
			return array_key_exists($fieldName, $changed);
2627
		}
2628
	}
2629
2630
	/**
2631
	 * Set the value of the field
2632
	 * Called by {@link __set()} and any setFieldName() methods you might create.
2633
	 *
2634
	 * @param string $fieldName Name of the field
2635
	 * @param mixed $val New field value
2636
	 * @return DataObject $this
2637
	 */
2638
	public function setField($fieldName, $val) {
2639
		//if it's a has_one component, destroy the cache
2640
		if (substr($fieldName, -2) == 'ID') {
2641
			unset($this->components[substr($fieldName, 0, -2)]);
2642
		}
2643
		// Situation 1: Passing an DBField
2644
		if($val instanceof DBField) {
2645
			$val->Name = $fieldName;
0 ignored issues
show
Documentation introduced by
The property Name does not exist on object<DBField>. 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...
2646
2647
			// If we've just lazy-loaded the column, then we need to populate the $original array by
2648
			// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
2649
			// on a call to getChanged()?
2650
			$this->getField($fieldName);
2651
2652
			$this->record[$fieldName] = $val;
2653
		// Situation 2: Passing a literal or non-DBField object
2654
		} else {
2655
			// If this is a proper database field, we shouldn't be getting non-DBField objects
2656
			if(is_object($val) && $this->db($fieldName)) {
2657
				user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2658
			}
2659
2660
			// if a field is not existing or has strictly changed
2661
			if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2662
				// TODO Add check for php-level defaults which are not set in the db
2663
				// TODO Add check for hidden input-fields (readonly) which are not set in the db
2664
				// At the very least, the type has changed
2665
				$this->changed[$fieldName] = self::CHANGE_STRICT;
2666
2667
				if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
2668
						&& $this->record[$fieldName] != $val)) {
2669
2670
					// Value has changed as well, not just the type
2671
					$this->changed[$fieldName] = self::CHANGE_VALUE;
2672
				}
2673
2674
				// If we've just lazy-loaded the column, then we need to populate the $original array by
2675
				// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
2676
				// on a call to getChanged()?
2677
				$this->getField($fieldName);
2678
2679
				// Value is always saved back when strict check succeeds.
2680
				$this->record[$fieldName] = $val;
2681
			}
2682
		}
2683
		return $this;
2684
	}
2685
2686
	/**
2687
	 * Set the value of the field, using a casting object.
2688
	 * This is useful when you aren't sure that a date is in SQL format, for example.
2689
	 * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2690
	 * can be saved into the Image table.
2691
	 *
2692
	 * @param string $fieldName Name of the field
2693
	 * @param mixed $value New field value
0 ignored issues
show
Bug introduced by
There is no parameter named $value. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
2694
	 * @return DataObject $this
2695
	 */
2696
	public function setCastedField($fieldName, $val) {
2697
		if(!$fieldName) {
2698
			user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2699
		}
2700
		$castingHelper = $this->castingHelper($fieldName);
2701
		if($castingHelper) {
2702
			$fieldObj = Object::create_from_string($castingHelper, $fieldName);
2703
			$fieldObj->setValue($val);
2704
			$fieldObj->saveInto($this);
2705
		} else {
2706
			$this->$fieldName = $val;
2707
		}
2708
		return $this;
2709
	}
2710
2711
	/**
2712
	 * {@inheritdoc}
2713
	 */
2714
	public function castingHelper($field) {
2715
		if ($fieldSpec = $this->db($field)) {
2716
			return $fieldSpec;
2717
		}
2718
2719
		// many_many_extraFields aren't presented by db(), so we check if the source query params
2720
		// provide us with meta-data for a many_many relation we can inspect for extra fields.
2721
		$queryParams = $this->getSourceQueryParams();
2722
		if (!empty($queryParams['Component.ExtraFields'])) {
2723
			$extraFields = $queryParams['Component.ExtraFields'];
2724
2725
			if (isset($extraFields[$field])) {
2726
				return $extraFields[$field];
2727
			}
2728
		}
2729
2730
		return parent::castingHelper($field);
2731
	}
2732
2733
	/**
2734
	 * Returns true if the given field exists in a database column on any of
2735
	 * the objects tables and optionally look up a dynamic getter with
2736
	 * get<fieldName>().
2737
	 *
2738
	 * @param string $field Name of the field
2739
	 * @return boolean True if the given field exists
2740
	 */
2741
	public function hasField($field) {
2742
		return (
2743
			array_key_exists($field, $this->record)
2744
			|| $this->db($field)
2745
			|| (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...
2746
			|| $this->hasMethod("get{$field}")
2747
		);
2748
	}
2749
2750
	/**
2751
	 * Returns true if the given field exists as a database column
2752
	 *
2753
	 * @param string $field Name of the field
2754
	 *
2755
	 * @return boolean
2756
	 */
2757
	public function hasDatabaseField($field) {
2758
		if(isset(self::$fixed_fields[$field])) return true;
2759
2760
		return array_key_exists($field, $this->inheritedDatabaseFields());
2761
	}
2762
2763
	/**
2764
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2765
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2766
	 *
2767
	 * @param string $field Name of the field
2768
	 * @return string The field type of the given field
2769
	 */
2770
	public function hasOwnTableDatabaseField($field) {
2771
		return self::has_own_table_database_field($this->class, $field);
2772
	}
2773
2774
	/**
2775
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2776
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2777
	 *
2778
	 * @param string $class Class name to check
2779
	 * @param string $field Name of the field
2780
	 * @return string The field type of the given field
2781
	 */
2782
	public static function has_own_table_database_field($class, $field) {
2783
		// Since database_fields omits 'ID'
2784
		if($field == "ID") return "Int";
2785
2786
		$fieldMap = self::database_fields($class, false);
2787
2788
		// Remove string-based "constructor-arguments" from the DBField definition
2789
		if(isset($fieldMap[$field])) {
2790
			$spec = $fieldMap[$field];
2791
			if(is_string($spec)) return strtok($spec,'(');
2792
			else return $spec['type'];
2793
		}
2794
	}
2795
2796
	/**
2797
	 * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2798
	 * actually looking in the database.
2799
	 *
2800
	 * @param string $dataClass
2801
	 * @return bool
2802
	 */
2803
	public static function has_own_table($dataClass) {
2804
		if(!is_subclass_of($dataClass,'DataObject')) return false;
2805
2806
		$dataClass = ClassInfo::class_name($dataClass);
2807
		if(!isset(DataObject::$cache_has_own_table[$dataClass])) {
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...
2808
			if(get_parent_class($dataClass) == 'DataObject') {
2809
				DataObject::$cache_has_own_table[$dataClass] = true;
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...
2810
			} else {
2811
				DataObject::$cache_has_own_table[$dataClass]
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...
2812
					= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
2813
					|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
2814
			}
2815
		}
2816
		return DataObject::$cache_has_own_table[$dataClass];
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...
2817
	}
2818
2819
	/**
2820
	 * Returns true if the member is allowed to do the given action.
2821
	 * See {@link extendedCan()} for a more versatile tri-state permission control.
2822
	 *
2823
	 * @param string $perm The permission to be checked, such as 'View'.
2824
	 * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2825
	 * in user.
2826
	 *
2827
	 * @return boolean True if the the member is allowed to do the given action
2828
	 */
2829
	public function can($perm, $member = null) {
2830
		if(!isset($member)) {
2831
			$member = Member::currentUser();
2832
		}
2833
		if(Permission::checkMember($member, "ADMIN")) return true;
2834
2835
		if($this->manyManyComponent('Can' . $perm)) {
2836
			if($this->ParentID && $this->SecurityType == 'Inherit') {
2837
				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...
2838
					return false;
2839
				}
2840
				return $this->Parent->can($perm, $member);
2841
2842
			} else {
2843
				$permissionCache = $this->uninherited('permissionCache');
2844
				$memberID = $member ? $member->ID : 'none';
2845
2846
				if(!isset($permissionCache[$memberID][$perm])) {
2847
					if($member->ID) {
2848
						$groups = $member->Groups();
2849
					}
2850
2851
					$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...
2852
2853
					// TODO Fix relation table hardcoding
2854
					$query = new SQLQuery(
0 ignored issues
show
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
2855
						"\"Page_Can$perm\".PageID",
2856
					array("\"Page_Can$perm\""),
2857
						"GroupID IN ($groupList)");
2858
2859
					$permissionCache[$memberID][$perm] = $query->execute()->column();
2860
2861
					if($perm == "View") {
2862
						// TODO Fix relation table hardcoding
2863
						$query = new SQLQuery("\"SiteTree\".\"ID\"", array(
0 ignored issues
show
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

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