Completed
Pull Request — 3.2 (#5154)
by Jono
10:39
created

DataObject::singleton()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 4
rs 10
cc 1
eloc 3
nc 1
nop 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
	 * Creates a class instance by the "singleton" design pattern.
427
	 * It will always return the same instance for this class,
428
	 * which can be used for performance reasons and as a simple
429
	 * way to access instance methods which don't rely on instance
430
	 * data (e.g. the custom SilverStripe static handling).
431
	 *
432
	 * @return static The singleton instance
433
	 */
434
	public static function singleton() {
435
		$class = get_called_class();
436
		return Injector::inst()->get($class, true, array(null, true));
437
	}
438
439
	/**
440
	 * Construct a new DataObject.
441
	 *
442
	 * @param array|null $record This will be null for a new database record.  Alternatively, you can pass an array of
443
	 * field values.  Normally this contructor is only used by the internal systems that get objects from the database.
444
	 * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
445
	 *                             Singletons don't have their defaults set.
446
	 */
447
	public function __construct($record = null, $isSingleton = false, $model = null) {
448
		parent::__construct();
449
450
		// Set the fields data.
451
		if(!$record) {
452
			$record = array(
453
				'ID' => 0,
454
				'ClassName' => get_class($this),
455
				'RecordClassName' => get_class($this)
456
			);
457
		}
458
459
		if(!is_array($record) && !is_a($record, "stdClass")) {
460
			if(is_object($record)) $passed = "an object of type '$record->class'";
461
			else $passed = "The value '$record'";
462
463
			user_error("DataObject::__construct passed $passed.  It's supposed to be passed an array,"
464
				. " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
465
				E_USER_WARNING);
466
			$record = null;
467
		}
468
469
		if(is_a($record, "stdClass")) {
470
			$record = (array)$record;
471
		}
472
473
		// Set $this->record to $record, but ignore NULLs
474
		$this->record = array();
475
		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...
476
			// Ensure that ID is stored as a number and not a string
477
			// To do: this kind of clean-up should be done on all numeric fields, in some relatively
478
			// performant manner
479
			if($v !== null) {
480
				if($k == 'ID' && is_numeric($v)) $this->record[$k] = (int)$v;
481
				else $this->record[$k] = $v;
482
			}
483
		}
484
485
		// Identify fields that should be lazy loaded, but only on existing records
486
		if(!empty($record['ID'])) {
487
			$currentObj = get_class($this);
488
			while($currentObj != 'DataObject') {
489
				$fields = self::custom_database_fields($currentObj);
490
				foreach($fields as $field => $type) {
491
					if(!array_key_exists($field, $record)) $this->record[$field.'_Lazy'] = $currentObj;
492
				}
493
				$currentObj = get_parent_class($currentObj);
494
			}
495
		}
496
497
		$this->original = $this->record;
498
499
		// Keep track of the modification date of all the data sourced to make this page
500
		// From this we create a Last-Modified HTTP header
501
		if(isset($record['LastEdited'])) {
502
			HTTP::register_modification_date($record['LastEdited']);
503
		}
504
505
		// this must be called before populateDefaults(), as field getters on a DataObject
506
		// may call getComponent() and others, which rely on $this->model being set.
507
		$this->model = $model ? $model : DataModel::inst();
508
509
		// Must be called after parent constructor
510
		if(!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
511
			$this->populateDefaults();
512
		}
513
514
		// prevent populateDefaults() and setField() from marking overwritten defaults as changed
515
		$this->changed = array();
516
	}
517
518
	/**
519
	 * Set the DataModel
520
	 * @param DataModel $model
521
	 * @return DataObject $this
522
	 */
523
	public function setDataModel(DataModel $model) {
524
		$this->model = $model;
525
		return $this;
526
	}
527
528
	/**
529
	 * Destroy all of this objects dependant objects and local caches.
530
	 * You'll need to call this to get the memory of an object that has components or extensions freed.
531
	 */
532
	public function destroy() {
533
		//$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...
534
		gc_collect_cycles();
535
		$this->flushCache(false);
536
	}
537
538
	/**
539
	 * Create a duplicate of this node.
540
	 * Note: now also duplicates relations.
541
	 *
542
	 * @param $doWrite Perform a write() operation before returning the object.  If this is true, it will create the
543
	 *                 duplicate in the database.
544
	 * @return DataObject A duplicate of this node. The exact type will be the type of this node.
545
	 */
546
	public function duplicate($doWrite = true) {
547
		$className = $this->class;
548
		$clone = new $className( $this->toMap(), false, $this->model );
549
		$clone->ID = 0;
550
551
		$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
552
		if($doWrite) {
553
			$clone->write();
554
			$this->duplicateManyManyRelations($this, $clone);
555
		}
556
		$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
557
558
		return $clone;
559
	}
560
561
	/**
562
	 * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object
563
	 * The destinationObject must be written to the database already and have an ID. Writing is performed
564
	 * automatically when adding the new relations.
565
	 *
566
	 * @param $sourceObject the source object to duplicate from
567
	 * @param $destinationObject the destination object to populate with the duplicated relations
568
	 * @return DataObject with the new many_many relations copied in
569
	 */
570
	protected function duplicateManyManyRelations($sourceObject, $destinationObject) {
571
		if (!$destinationObject || $destinationObject->ID < 1) {
572
			user_error("Can't duplicate relations for an object that has not been written to the database",
573
				E_USER_ERROR);
574
		}
575
576
		//duplicate complex relations
577
		// DO NOT copy has_many relations, because copying the relation would result in us changing the has_one
578
		// relation on the other side of this relation to point at the copy and no longer the original (being a
579
		// has_one, it can only point at one thing at a time). So, all relations except has_many can and are copied
580
		if ($sourceObject->hasOne()) foreach($sourceObject->hasOne() as $name => $type) {
581
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
582
		}
583
		if ($sourceObject->manyMany()) foreach($sourceObject->manyMany() as $name => $type) {
584
			//many_many include belongs_many_many
585
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
586
		}
587
588
		return $destinationObject;
589
	}
590
591
	/**
592
	 * Helper function to duplicate relations from one object to another
593
	 * @param $sourceObject the source object to duplicate from
594
	 * @param $destinationObject the destination object to populate with the duplicated relations
595
	 * @param $name the name of the relation to duplicate (e.g. members)
596
	 */
597
	private function duplicateRelations($sourceObject, $destinationObject, $name) {
598
		$relations = $sourceObject->$name();
599
		if ($relations) {
600
			if ($relations instanceOf RelationList) {   //many-to-something relation
601
				if ($relations->Count() > 0) {  //with more than one thing it is related to
602
					foreach($relations as $relation) {
603
						$destinationObject->$name()->add($relation);
604
					}
605
				}
606
			} else {    //one-to-one relation
607
				$destinationObject->{"{$name}ID"} = $relations->ID;
608
			}
609
		}
610
	}
611
612
	public function getObsoleteClassName() {
613
		$className = $this->getField("ClassName");
614
		if (!ClassInfo::exists($className)) return $className;
615
	}
616
617
	public function getClassName() {
618
		$className = $this->getField("ClassName");
619
		if (!ClassInfo::exists($className)) return get_class($this);
620
		return $className;
621
	}
622
623
	/**
624
	 * Set the ClassName attribute. {@link $class} is also updated.
625
	 * Warning: This will produce an inconsistent record, as the object
626
	 * instance will not automatically switch to the new subclass.
627
	 * Please use {@link newClassInstance()} for this purpose,
628
	 * or destroy and reinstanciate the record.
629
	 *
630
	 * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
631
	 * @return DataObject $this
632
	 */
633
	public function setClassName($className) {
634
		$className = trim($className);
635
		if(!$className || !is_subclass_of($className, 'DataObject')) return;
636
637
		$this->class = $className;
638
		$this->setField("ClassName", $className);
639
		return $this;
640
	}
641
642
	/**
643
	 * Create a new instance of a different class from this object's record.
644
	 * This is useful when dynamically changing the type of an instance. Specifically,
645
	 * it ensures that the instance of the class is a match for the className of the
646
	 * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
647
	 * property manually before calling this method, as it will confuse change detection.
648
	 *
649
	 * If the new class is different to the original class, defaults are populated again
650
	 * because this will only occur automatically on instantiation of a DataObject if
651
	 * there is no record, or the record has no ID. In this case, we do have an ID but
652
	 * we still need to repopulate the defaults.
653
	 *
654
	 * @param string $newClassName The name of the new class
655
	 *
656
	 * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
657
	 */
658
	public function newClassInstance($newClassName) {
659
		$originalClass = $this->ClassName;
660
		$newInstance = new $newClassName(array_merge(
661
			$this->record,
662
			array(
663
				'ClassName' => $originalClass,
664
				'RecordClassName' => $originalClass,
665
			)
666
		), false, $this->model);
667
668
		if($newClassName != $originalClass) {
669
			$newInstance->setClassName($newClassName);
670
			$newInstance->populateDefaults();
671
			$newInstance->forceChange();
672
		}
673
674
		return $newInstance;
675
	}
676
677
	/**
678
	 * Adds methods from the extensions.
679
	 * Called by Object::__construct() once per class.
680
	 */
681
	public function defineMethods() {
682
		parent::defineMethods();
683
684
		// Define the extra db fields - this is only necessary for extensions added in the
685
		// class definition.  Object::add_extension() will call this at definition time for
686
		// those objects, which is a better mechanism.  Perhaps extensions defined inside the
687
		// class def can somehow be applied at definiton time also?
688
		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...
689
			if(!$instance->class) {
690
				$class = get_class($instance);
691
				user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls"
692
					. " parent::__construct()", E_USER_ERROR);
693
			}
694
		}
695
696
		if($this->class == 'DataObject') return;
697
698
		// Set up accessors for joined items
699
		if($manyMany = $this->manyMany()) {
700
			foreach($manyMany as $relationship => $class) {
701
				$this->addWrapperMethod($relationship, 'getManyManyComponents');
702
			}
703
		}
704
		if($hasMany = $this->hasMany()) {
705
706
			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...
707
				$this->addWrapperMethod($relationship, 'getComponents');
708
			}
709
710
		}
711
		if($hasOne = $this->hasOne()) {
712
			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...
713
				$this->addWrapperMethod($relationship, 'getComponent');
714
			}
715
		}
716
		if($belongsTo = $this->belongsTo()) foreach(array_keys($belongsTo) as $relationship) {
717
			$this->addWrapperMethod($relationship, 'getComponent');
718
		}
719
	}
720
721
	/**
722
	 * Returns true if this object "exists", i.e., has a sensible value.
723
	 * The default behaviour for a DataObject is to return true if
724
	 * the object exists in the database, you can override this in subclasses.
725
	 *
726
	 * @return boolean true if this object exists
727
	 */
728
	public function exists() {
729
		return (isset($this->record['ID']) && $this->record['ID'] > 0);
730
	}
731
732
	/**
733
	 * Returns TRUE if all values (other than "ID") are
734
	 * considered empty (by weak boolean comparison).
735
	 * Only checks for fields listed in {@link custom_database_fields()}
736
	 *
737
	 * @todo Use DBField->hasValue()
738
	 *
739
	 * @return boolean
740
	 */
741
	public function isEmpty(){
742
		$isEmpty = true;
743
		$customFields = self::custom_database_fields(get_class($this));
744
		if($map = $this->toMap()){
745
			foreach($map as $k=>$v){
746
				// only look at custom fields
747
				if(!array_key_exists($k, $customFields)) continue;
748
749
				$dbObj = ($v instanceof DBField) ? $v : $this->dbObject($k);
750
				$isEmpty = ($isEmpty && !$dbObj->exists());
751
			}
752
		}
753
		return $isEmpty;
754
	}
755
756
	/**
757
	 * Get the user friendly singular name of this DataObject.
758
	 * If the name is not defined (by redefining $singular_name in the subclass),
759
	 * this returns the class name.
760
	 *
761
	 * @return string User friendly singular name of this DataObject
762
	 */
763
	public function singular_name() {
764
		if(!$name = $this->stat('singular_name')) {
765
			$name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $this->class))));
766
		}
767
768
		return $name;
769
	}
770
771
	/**
772
	 * Get the translated user friendly singular name of this DataObject
773
	 * same as singular_name() but runs it through the translating function
774
	 *
775
	 * Translating string is in the form:
776
	 *     $this->class.SINGULARNAME
777
	 * Example:
778
	 *     Page.SINGULARNAME
779
	 *
780
	 * @return string User friendly translated singular name of this DataObject
781
	 */
782
	public function i18n_singular_name() {
783
		return _t($this->class.'.SINGULARNAME', $this->singular_name());
784
	}
785
786
	/**
787
	 * Get the user friendly plural name of this DataObject
788
	 * If the name is not defined (by renaming $plural_name in the subclass),
789
	 * this returns a pluralised version of the class name.
790
	 *
791
	 * @return string User friendly plural name of this DataObject
792
	 */
793
	public function plural_name() {
794
		if($name = $this->stat('plural_name')) {
795
			return $name;
796
		} else {
797
			$name = $this->singular_name();
798
			//if the penultimate character is not a vowel, replace "y" with "ies"
799
			if (preg_match('/[^aeiou]y$/i', $name)) {
800
				$name = substr($name,0,-1) . 'ie';
801
			}
802
			return ucfirst($name . 's');
803
		}
804
	}
805
806
	/**
807
	 * Get the translated user friendly plural name of this DataObject
808
	 * Same as plural_name but runs it through the translation function
809
	 * Translation string is in the form:
810
	 *      $this->class.PLURALNAME
811
	 * Example:
812
	 *      Page.PLURALNAME
813
	 *
814
	 * @return string User friendly translated plural name of this DataObject
815
	 */
816
	public function i18n_plural_name()
817
	{
818
		$name = $this->plural_name();
819
		return _t($this->class.'.PLURALNAME', $name);
820
	}
821
822
	/**
823
	 * Standard implementation of a title/label for a specific
824
	 * record. Tries to find properties 'Title' or 'Name',
825
	 * and falls back to the 'ID'. Useful to provide
826
	 * user-friendly identification of a record, e.g. in errormessages
827
	 * or UI-selections.
828
	 *
829
	 * Overload this method to have a more specialized implementation,
830
	 * e.g. for an Address record this could be:
831
	 * <code>
832
	 * function getTitle() {
833
	 *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
834
	 * }
835
	 * </code>
836
	 *
837
	 * @return string
838
	 */
839
	public function getTitle() {
840
		if($this->hasDatabaseField('Title')) return $this->getField('Title');
841
		if($this->hasDatabaseField('Name')) return $this->getField('Name');
842
843
		return "#{$this->ID}";
844
	}
845
846
	/**
847
	 * Returns the associated database record - in this case, the object itself.
848
	 * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
849
	 *
850
	 * @return DataObject Associated database record
851
	 */
852
	public function data() {
853
		return $this;
854
	}
855
856
	/**
857
	 * Convert this object to a map.
858
	 *
859
	 * @return array The data as a map.
860
	 */
861
	public function toMap() {
862
		$this->loadLazyFields();
863
		return $this->record;
864
	}
865
866
	/**
867
	 * Return all currently fetched database fields.
868
	 *
869
	 * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
870
	 * Obviously, this makes it a lot faster.
871
	 *
872
	 * @return array The data as a map.
873
	 */
874
	public function getQueriedDatabaseFields() {
875
		return $this->record;
876
	}
877
878
	/**
879
	 * Update a number of fields on this object, given a map of the desired changes.
880
	 *
881
	 * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
882
	 * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
883
	 *
884
	 * update() doesn't write the main object, but if you use the dot syntax, it will write()
885
	 * the related objects that it alters.
886
	 *
887
	 * @param array $data A map of field name to data values to update.
888
	 * @return DataObject $this
889
	 */
890
	public function update($data) {
891
		foreach($data as $k => $v) {
892
			// Implement dot syntax for updates
893
			if(strpos($k,'.') !== false) {
894
				$relations = explode('.', $k);
895
				$fieldName = array_pop($relations);
896
				$relObj = $this;
897
				foreach($relations as $i=>$relation) {
898
					// no support for has_many or many_many relationships,
899
					// as the updater wouldn't know which object to write to (or create)
900
					if($relObj->$relation() instanceof DataObject) {
901
						$parentObj = $relObj;
902
						$relObj = $relObj->$relation();
903
						// If the intermediate relationship objects have been created, then write them
904
						if($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
905
							$relObj->write();
906
							$relatedFieldName = $relation."ID";
907
							$parentObj->$relatedFieldName = $relObj->ID;
908
							$parentObj->write();
909
						}
910
					} else {
911
						user_error(
912
							"DataObject::update(): Can't traverse relationship '$relation'," .
913
							"it has to be a has_one relationship or return a single DataObject",
914
							E_USER_NOTICE
915
						);
916
						// unset relation object so we don't write properties to the wrong object
917
						unset($relObj);
918
						break;
919
					}
920
				}
921
922
				if($relObj) {
923
					$relObj->$fieldName = $v;
924
					$relObj->write();
925
					$relatedFieldName = $relation."ID";
0 ignored issues
show
Bug introduced by
The variable $relation seems to be defined by a foreach iteration on line 897. 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...
926
					$this->$relatedFieldName = $relObj->ID;
927
					$relObj->flushCache();
928
				} else {
929
					user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
930
				}
931
			} else {
932
				$this->$k = $v;
933
			}
934
		}
935
		return $this;
936
	}
937
938
	/**
939
	 * Pass changes as a map, and try to
940
	 * get automatic casting for these fields.
941
	 * Doesn't write to the database. To write the data,
942
	 * use the write() method.
943
	 *
944
	 * @param array $data A map of field name to data values to update.
945
	 * @return DataObject $this
946
	 */
947
	public function castedUpdate($data) {
948
		foreach($data as $k => $v) {
949
			$this->setCastedField($k,$v);
950
		}
951
		return $this;
952
	}
953
954
	/**
955
	 * Merges data and relations from another object of same class,
956
	 * without conflict resolution. Allows to specify which
957
	 * dataset takes priority in case its not empty.
958
	 * has_one-relations are just transferred with priority 'right'.
959
	 * has_many and many_many-relations are added regardless of priority.
960
	 *
961
	 * Caution: has_many/many_many relations are moved rather than duplicated,
962
	 * meaning they are not connected to the merged object any longer.
963
	 * Caution: Just saves updated has_many/many_many relations to the database,
964
	 * doesn't write the updated object itself (just writes the object-properties).
965
	 * Caution: Does not delete the merged object.
966
	 * Caution: Does now overwrite Created date on the original object.
967
	 *
968
	 * @param $obj DataObject
969
	 * @param $priority String left|right Determines who wins in case of a conflict (optional)
970
	 * @param $includeRelations Boolean Merge any existing relations (optional)
971
	 * @param $overwriteWithEmpty Boolean Overwrite existing left values with empty right values.
972
	 *                            Only applicable with $priority='right'. (optional)
973
	 * @return Boolean
974
	 */
975
	public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) {
976
		$leftObj = $this;
977
978
		if($leftObj->ClassName != $rightObj->ClassName) {
979
			// we can't merge similiar subclasses because they might have additional relations
980
			user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
981
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
982
			return false;
983
		}
984
985
		if(!$rightObj->ID) {
986
			user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
987
				to make sure all relations are transferred properly.').", E_USER_WARNING);
988
			return false;
989
		}
990
991
		// makes sure we don't merge data like ID or ClassName
992
		$leftData = $leftObj->inheritedDatabaseFields();
993
		$rightData = $rightObj->inheritedDatabaseFields();
994
995
		foreach($rightData as $key=>$rightVal) {
996
			// don't merge conflicting values if priority is 'left'
997
			if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) continue;
998
999
			// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
1000
			if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) continue;
1001
1002
			// TODO remove redundant merge of has_one fields
1003
			$leftObj->{$key} = $rightObj->{$key};
1004
		}
1005
1006
		// merge relations
1007
		if($includeRelations) {
1008
			if($manyMany = $this->manyMany()) {
1009
				foreach($manyMany as $relationship => $class) {
1010
					$leftComponents = $leftObj->getManyManyComponents($relationship);
1011
					$rightComponents = $rightObj->getManyManyComponents($relationship);
1012
					if($rightComponents && $rightComponents->exists()) {
1013
						$leftComponents->addMany($rightComponents->column('ID'));
1014
					}
1015
					$leftComponents->write();
1016
				}
1017
			}
1018
1019
			if($hasMany = $this->hasMany()) {
1020
				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...
1021
					$leftComponents = $leftObj->getComponents($relationship);
1022
					$rightComponents = $rightObj->getComponents($relationship);
1023
					if($rightComponents && $rightComponents->exists()) {
1024
						$leftComponents->addMany($rightComponents->column('ID'));
1025
					}
1026
					$leftComponents->write();
1027
				}
1028
1029
			}
1030
1031
			if($hasOne = $this->hasOne()) {
1032
				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...
1033
					$leftComponent = $leftObj->getComponent($relationship);
1034
					$rightComponent = $rightObj->getComponent($relationship);
1035
					if($leftComponent->exists() && $rightComponent->exists() && $priority == 'right') {
1036
						$leftObj->{$relationship . 'ID'} = $rightObj->{$relationship . 'ID'};
1037
					}
1038
				}
1039
			}
1040
		}
1041
1042
		return true;
1043
	}
1044
1045
	/**
1046
	 * Forces the record to think that all its data has changed.
1047
	 * Doesn't write to the database. Only sets fields as changed
1048
	 * if they are not already marked as changed.
1049
	 *
1050
	 * @return DataObject $this
1051
	 */
1052
	public function forceChange() {
1053
		// Ensure lazy fields loaded
1054
		$this->loadLazyFields();
1055
1056
		// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1057
		$fieldNames = array_unique(array_merge(
1058
			array_keys($this->record),
1059
			array_keys($this->inheritedDatabaseFields())));
1060
1061
		foreach($fieldNames as $fieldName) {
1062
			if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
1063
			// Populate the null values in record so that they actually get written
1064
			if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
1065
		}
1066
1067
		// @todo Find better way to allow versioned to write a new version after forceChange
1068
		if($this->isChanged('Version')) unset($this->changed['Version']);
1069
		return $this;
1070
	}
1071
1072
	/**
1073
	 * Validate the current object.
1074
	 *
1075
	 * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1076
	 * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1077
	 *
1078
	 * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1079
	 * and onAfterWrite() won't get called either.
1080
	 *
1081
	 * It is expected that you call validate() in your own application to test that an object is valid before
1082
	 * attempting a write, and respond appropriately if it isn't.
1083
	 *
1084
	 * @see {@link ValidationResult}
1085
	 * @return ValidationResult
1086
	 */
1087
	protected function validate() {
1088
		$result = ValidationResult::create();
1089
		$this->extend('validate', $result);
1090
		return $result;
1091
	}
1092
1093
	/**
1094
	 * Public accessor for {@see DataObject::validate()}
1095
	 *
1096
	 * @return ValidationResult
1097
	 */
1098
	public function doValidate() {
1099
		// validate will be public in 4.0
1100
		return $this->validate();
1101
	}
1102
1103
	/**
1104
	 * Event handler called before writing to the database.
1105
	 * You can overload this to clean up or otherwise process data before writing it to the
1106
	 * database.  Don't forget to call parent::onBeforeWrite(), though!
1107
	 *
1108
	 * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1109
	 *
1110
	 * @uses DataExtension->onBeforeWrite()
1111
	 */
1112
	protected function onBeforeWrite() {
1113
		$this->brokenOnWrite = false;
1114
1115
		$dummy = null;
1116
		$this->extend('onBeforeWrite', $dummy);
1117
	}
1118
1119
	/**
1120
	 * Event handler called after writing to the database.
1121
	 * You can overload this to act upon changes made to the data after it is written.
1122
	 * $this->changed will have a record
1123
	 * database.  Don't forget to call parent::onAfterWrite(), though!
1124
	 *
1125
	 * @uses DataExtension->onAfterWrite()
1126
	 */
1127
	protected function onAfterWrite() {
1128
		$dummy = null;
1129
		$this->extend('onAfterWrite', $dummy);
1130
	}
1131
1132
	/**
1133
	 * Event handler called before deleting from the database.
1134
	 * You can overload this to clean up or otherwise process data before delete this
1135
	 * record.  Don't forget to call parent::onBeforeDelete(), though!
1136
	 *
1137
	 * @uses DataExtension->onBeforeDelete()
1138
	 */
1139
	protected function onBeforeDelete() {
1140
		$this->brokenOnDelete = false;
1141
1142
		$dummy = null;
1143
		$this->extend('onBeforeDelete', $dummy);
1144
	}
1145
1146
	protected function onAfterDelete() {
1147
		$this->extend('onAfterDelete');
1148
	}
1149
1150
	/**
1151
	 * Load the default values in from the self::$defaults array.
1152
	 * Will traverse the defaults of the current class and all its parent classes.
1153
	 * Called by the constructor when creating new records.
1154
	 *
1155
	 * @uses DataExtension->populateDefaults()
1156
	 * @return DataObject $this
1157
	 */
1158
	public function populateDefaults() {
1159
		$classes = array_reverse(ClassInfo::ancestry($this));
1160
1161
		foreach($classes as $class) {
1162
			$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1163
1164
			if($defaults && !is_array($defaults)) {
1165
				user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
1166
					E_USER_WARNING);
1167
				$defaults = null;
1168
			}
1169
1170
			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...
1171
				// SRM 2007-03-06: Stricter check
1172
				if(!isset($this->$fieldName) || $this->$fieldName === null) {
1173
					$this->$fieldName = $fieldValue;
1174
				}
1175
				// Set many-many defaults with an array of ids
1176
				if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
1177
					$manyManyJoin = $this->$fieldName();
1178
					$manyManyJoin->setByIdList($fieldValue);
1179
				}
1180
			}
1181
			if($class == 'DataObject') {
1182
				break;
1183
			}
1184
		}
1185
1186
		$this->extend('populateDefaults');
1187
		return $this;
1188
	}
1189
1190
	/**
1191
	 * Determine validation of this object prior to write
1192
	 *
1193
	 * @return ValidationException Exception generated by this write, or null if valid
1194
	 */
1195
	protected function validateWrite() {
1196
		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...
1197
			return new ValidationException(
1198
				"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...
1199
				"you need to change the ClassName before you can write it",
1200
				E_USER_WARNING
1201
			);
1202
		}
1203
1204
		if(Config::inst()->get('DataObject', 'validation_enabled')) {
1205
			$result = $this->validate();
1206
			if (!$result->valid()) {
1207
				return new ValidationException(
1208
					$result,
1209
					$result->message(),
1210
					E_USER_WARNING
1211
				);
1212
			}
1213
		}
1214
	}
1215
1216
	/**
1217
	 * Prepare an object prior to write
1218
	 *
1219
	 * @throws ValidationException
1220
	 */
1221
	protected function preWrite() {
1222
		// Validate this object
1223
		if($writeException = $this->validateWrite()) {
1224
			// Used by DODs to clean up after themselves, eg, Versioned
1225
			$this->invokeWithExtensions('onAfterSkippedWrite');
1226
			throw $writeException;
1227
		}
1228
1229
		// Check onBeforeWrite
1230
		$this->brokenOnWrite = true;
1231
		$this->onBeforeWrite();
1232
		if($this->brokenOnWrite) {
1233
			user_error("$this->class has a broken onBeforeWrite() function."
1234
				. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1235
		}
1236
	}
1237
1238
	/**
1239
	 * Detects and updates all changes made to this object
1240
	 *
1241
	 * @param bool $forceChanges If set to true, force all fields to be treated as changed
1242
	 * @return bool True if any changes are detected
1243
	 */
1244
	protected function updateChanges($forceChanges = false) {
1245
		// Update the changed array with references to changed obj-fields
1246
		foreach($this->record as $field => $value) {
1247
			// Only mark ID as changed if $forceChanges
1248
			if($field === 'ID' && !$forceChanges) continue;
1249
			// Determine if this field should be forced, or can mark itself, changed
1250
			if($forceChanges
1251
				|| !$this->isInDB()
1252
				|| (is_object($value) && method_exists($value, 'isChanged') && $value->isChanged())
1253
			) {
1254
				$this->changed[$field] = self::CHANGE_VALUE;
1255
			}
1256
		}
1257
1258
		// Check changes exist, abort if there are no changes
1259
		return $this->changed && (bool)array_filter($this->changed);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->changed of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1260
	}
1261
1262
	/**
1263
	 * Writes a subset of changes for a specific table to the given manipulation
1264
	 *
1265
	 * @param string $baseTable Base table
1266
	 * @param string $now Timestamp to use for the current time
1267
	 * @param bool $isNewRecord Whether this should be treated as a new record write
1268
	 * @param array $manipulation Manipulation to write to
1269
	 * @param string $class Table and Class to select and write to
1270
	 */
1271
	protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
1272
		$manipulation[$class] = array();
1273
1274
		// Extract records for this table
1275
		foreach($this->record as $fieldName => $fieldValue) {
1276
1277
			// Check if this record pertains to this table, and
1278
			// we're not attempting to reset the BaseTable->ID
1279
			if(	empty($this->changed[$fieldName])
1280
				|| ($class === $baseTable && $fieldName === 'ID')
1281
				|| (!self::has_own_table_database_field($class, $fieldName)
1282
					&& !self::is_composite_field($class, $fieldName, false))
1283
			) {
1284
				continue;
1285
			}
1286
1287
1288
			// if database column doesn't correlate to a DBField instance...
1289
			$fieldObj = $this->dbObject($fieldName);
1290
			if(!$fieldObj) {
1291
				$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1292
			}
1293
1294
			// Ensure DBField is repopulated and written to the manipulation
1295
			$fieldObj->setValue($fieldValue, $this->record);
1296
			$fieldObj->writeToManipulation($manipulation[$class]);
1297
		}
1298
1299
		// Ensure update of Created and LastEdited columns
1300
		if($baseTable === $class) {
1301
			$manipulation[$class]['fields']['LastEdited'] = $now;
1302
			if($isNewRecord) {
1303
				$manipulation[$class]['fields']['Created']
1304
					= empty($this->record['Created'])
1305
						? $now
1306
						: $this->record['Created'];
1307
				$manipulation[$class]['fields']['ClassName'] = $this->class;
1308
			}
1309
		}
1310
1311
		// Inserts done one the base table are performed in another step, so the manipulation should instead
1312
		// attempt an update, as though it were a normal update.
1313
		$manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update';
1314
		$manipulation[$class]['id'] = $this->record['ID'];
1315
	}
1316
1317
	/**
1318
	 * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1319
	 *
1320
	 * Does nothing if an ID is already assigned for this record
1321
	 *
1322
	 * @param string $baseTable Base table
1323
	 * @param string $now Timestamp to use for the current time
1324
	 */
1325
	protected function writeBaseRecord($baseTable, $now) {
1326
		// Generate new ID if not specified
1327
		if($this->isInDB()) return;
1328
1329
		// Perform an insert on the base table
1330
		$insert = new SQLInsert('"'.$baseTable.'"');
1331
		$insert
1332
			->assign('"Created"', $now)
1333
			->execute();
1334
		$this->changed['ID'] = self::CHANGE_VALUE;
1335
		$this->record['ID'] = DB::get_generated_id($baseTable);
1336
	}
1337
1338
	/**
1339
	 * Generate and write the database manipulation for all changed fields
1340
	 *
1341
	 * @param string $baseTable Base table
1342
	 * @param string $now Timestamp to use for the current time
1343
	 * @param bool $isNewRecord If this is a new record
1344
	 */
1345
	protected function writeManipulation($baseTable, $now, $isNewRecord) {
1346
		// Generate database manipulations for each class
1347
		$manipulation = array();
1348
		foreach($this->getClassAncestry() as $class) {
1349
			if(self::has_own_table($class)) {
1350
				$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1351
			}
1352
		}
1353
1354
		// Allow extensions to extend this manipulation
1355
		$this->extend('augmentWrite', $manipulation);
1356
1357
		// New records have their insert into the base data table done first, so that they can pass the
1358
		// generated ID on to the rest of the manipulation
1359
		if($isNewRecord) {
1360
			$manipulation[$baseTable]['command'] = 'update';
1361
		}
1362
1363
		// Perform the manipulation
1364
		DB::manipulate($manipulation);
1365
	}
1366
1367
	/**
1368
	 * Writes all changes to this object to the database.
1369
	 *  - It will insert a record whenever ID isn't set, otherwise update.
1370
	 *  - All relevant tables will be updated.
1371
	 *  - $this->onBeforeWrite() gets called beforehand.
1372
	 *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1373
	 *
1374
	 *  @uses DataExtension->augmentWrite()
1375
	 *
1376
	 * @param boolean $showDebug Show debugging information
1377
	 * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1378
	 * @param boolean $forceWrite Write to database even if there are no changes
1379
	 * @param boolean $writeComponents Call write() on all associated component instances which were previously
1380
	 *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1381
	 *                                 {@link getManyManyComponents()} (Default: false)
1382
	 * @return int The ID of the record
1383
	 * @throws ValidationException Exception that can be caught and handled by the calling function
1384
	 */
1385
	public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1386
		$now = SS_Datetime::now()->Rfc2822();
1387
1388
		// Execute pre-write tasks
1389
		$this->preWrite();
1390
1391
		// Check if we are doing an update or an insert
1392
		$isNewRecord = !$this->isInDB() || $forceInsert;
1393
1394
		// Check changes exist, abort if there are none
1395
		$hasChanges = $this->updateChanges($forceInsert);
1396
		if($hasChanges || $forceWrite || $isNewRecord) {
1397
			// New records have their insert into the base data table done first, so that they can pass the
1398
			// generated primary key on to the rest of the manipulation
1399
			$baseTable = ClassInfo::baseDataClass($this->class);
1400
			$this->writeBaseRecord($baseTable, $now);
1401
1402
			// Write the DB manipulation for all changed fields
1403
			$this->writeManipulation($baseTable, $now, $isNewRecord);
1404
1405
			// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1406
			$this->writeRelations();
1407
			$this->onAfterWrite();
1408
			$this->changed = array();
1409
		} else {
1410
			if($showDebug) Debug::message("no changes for DataObject");
1411
1412
			// Used by DODs to clean up after themselves, eg, Versioned
1413
			$this->invokeWithExtensions('onAfterSkippedWrite');
1414
		}
1415
1416
		// Ensure Created and LastEdited are populated
1417
		if(!isset($this->record['Created'])) {
1418
			$this->record['Created'] = $now;
1419
		}
1420
		$this->record['LastEdited'] = $now;
1421
1422
		// Write relations as necessary
1423
		if($writeComponents) $this->writeComponents(true);
1424
1425
		// Clears the cache for this object so get_one returns the correct object.
1426
		$this->flushCache();
1427
1428
		return $this->record['ID'];
1429
	}
1430
1431
	/**
1432
	 * Writes cached relation lists to the database, if possible
1433
	 */
1434
	public function writeRelations() {
1435
		if(!$this->isInDB()) return;
1436
1437
		// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1438
		if($this->unsavedRelations) {
1439
			foreach($this->unsavedRelations as $name => $list) {
1440
				$list->changeToList($this->$name());
1441
			}
1442
			$this->unsavedRelations = array();
1443
		}
1444
	}
1445
1446
	/**
1447
	 * Write the cached components to the database. Cached components could refer to two different instances of the
1448
	 * same record.
1449
	 *
1450
	 * @param $recursive Recursively write components
1451
	 * @return DataObject $this
1452
	 */
1453
	public function writeComponents($recursive = false) {
1454
		if(!$this->components) return $this;
1455
1456
		foreach($this->components as $component) {
1457
			$component->write(false, false, false, $recursive);
1458
		}
1459
		return $this;
1460
	}
1461
1462
	/**
1463
	 * Delete this data object.
1464
	 * $this->onBeforeDelete() gets called.
1465
	 * Note that in Versioned objects, both Stage and Live will be deleted.
1466
	 *  @uses DataExtension->augmentSQL()
1467
	 */
1468
	public function delete() {
1469
		$this->brokenOnDelete = true;
1470
		$this->onBeforeDelete();
1471
		if($this->brokenOnDelete) {
1472
			user_error("$this->class has a broken onBeforeDelete() function."
1473
				. " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1474
		}
1475
1476
		// Deleting a record without an ID shouldn't do anything
1477
		if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1478
1479
		// TODO: This is quite ugly.  To improve:
1480
		//  - move the details of the delete code in the DataQuery system
1481
		//  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1482
		//    obviously, that means getting requireTable() to configure cascading deletes ;-)
1483
		$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1484
		foreach($srcQuery->queriedTables() as $table) {
1485
			$delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1486
			$delete->execute();
1487
		}
1488
		// Remove this item out of any caches
1489
		$this->flushCache();
1490
1491
		$this->onAfterDelete();
1492
1493
		$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...
1494
		$this->ID = 0;
1495
	}
1496
1497
	/**
1498
	 * Delete the record with the given ID.
1499
	 *
1500
	 * @param string $className The class name of the record to be deleted
1501
	 * @param int $id ID of record to be deleted
1502
	 */
1503
	public static function delete_by_id($className, $id) {
1504
		$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...
1505
		if($obj) {
1506
			$obj->delete();
1507
		} else {
1508
			user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1509
		}
1510
	}
1511
1512
	/**
1513
	 * Get the class ancestry, including the current class name.
1514
	 * The ancestry will be returned as an array of class names, where the 0th element
1515
	 * will be the class that inherits directly from DataObject, and the last element
1516
	 * will be the current class.
1517
	 *
1518
	 * @return array Class ancestry
1519
	 */
1520
	public function getClassAncestry() {
1521
		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...
1522
			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...
1523
			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...
1524
				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...
1525
			}
1526
		}
1527
		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...
1528
	}
1529
1530
	/**
1531
	 * Return a component object from a one to one relationship, as a DataObject.
1532
	 * If no component is available, an 'empty component' will be returned for
1533
	 * non-polymorphic relations, or for polymorphic relations with a class set.
1534
	 *
1535
	 * @param string $componentName Name of the component
1536
	 *
1537
	 * @return DataObject The component object. It's exact type will be that of the component.
1538
	 */
1539
	public function getComponent($componentName) {
1540
		if(isset($this->components[$componentName])) {
1541
			return $this->components[$componentName];
1542
		}
1543
1544
		if($class = $this->hasOneComponent($componentName)) {
1545
			$joinField = $componentName . 'ID';
1546
			$joinID    = $this->getField($joinField);
1547
1548
			// Extract class name for polymorphic relations
1549
			if($class === 'DataObject') {
1550
				$class = $this->getField($componentName . 'Class');
1551
				if(empty($class)) return null;
1552
			}
1553
1554
			if($joinID) {
1555
				$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...
1556
			}
1557
1558
			if(empty($component)) {
1559
				$component = $this->model->$class->newObject();
1560
			}
1561
		} elseif($class = $this->belongsToComponent($componentName)) {
1562
1563
			$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
1564
			$joinID    = $this->ID;
1565
1566
			if($joinID) {
1567
1568
				$filter = $polymorphic
1569
					? array(
1570
						"{$joinField}ID" => $joinID,
1571
						"{$joinField}Class" => $this->class
1572
					)
1573
					: array(
1574
						$joinField => $joinID
1575
					);
1576
				$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...
1577
			}
1578
1579
			if(empty($component)) {
1580
				$component = $this->model->$class->newObject();
1581
				if($polymorphic) {
1582
					$component->{$joinField.'ID'} = $this->ID;
1583
					$component->{$joinField.'Class'} = $this->class;
1584
				} else {
1585
					$component->$joinField = $this->ID;
1586
				}
1587
			}
1588
		} else {
1589
			throw new Exception("DataObject->getComponent(): Could not find component '$componentName'.");
1590
		}
1591
1592
		$this->components[$componentName] = $component;
1593
		return $component;
1594
	}
1595
1596
	/**
1597
	 * Returns a one-to-many relation as a HasManyList
1598
	 *
1599
	 * @param string $componentName Name of the component
1600
	 * @param string|null $filter Deprecated. A filter to be inserted into the WHERE clause
1601
	 * @param string|null|array $sort Deprecated. A sort expression to be inserted into the ORDER BY clause. If omitted,
1602
	 *                                the static field $default_sort on the component class will be used.
1603
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1604
	 * @param string|null|array $limit Deprecated. A limit expression to be inserted into the LIMIT clause
1605
	 *
1606
	 * @return HasManyList The components of the one-to-many relationship.
1607
	 */
1608
	public function getComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
1609
		$result = null;
1610
1611
		if(!$componentClass = $this->hasManyComponent($componentName)) {
1612
			user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'"
1613
				. " on class '$this->class'", E_USER_ERROR);
1614
		}
1615
1616
		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...
1617
			throw new \InvalidArgumentException(
1618
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
1619
			);
1620
		}
1621
1622
		if($filter !== null || $sort !== null || $limit !== null) {
1623
			Deprecation::notice('4.0', 'The $filter, $sort and $limit parameters for DataObject::getComponents()
1624
				have been deprecated. Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
1625
		}
1626
1627
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1628
		if(!$this->ID) {
1629
			if(!isset($this->unsavedRelations[$componentName])) {
1630
				$this->unsavedRelations[$componentName] =
1631
					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 1611 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...
1632
			}
1633
			return $this->unsavedRelations[$componentName];
1634
		}
1635
1636
		// Determine type and nature of foreign relation
1637
		$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
1638
		if($polymorphic) {
1639
			$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1640
		} else {
1641
			$result = HasManyList::create($componentClass, $joinField);
1642
		}
1643
1644
		if($this->model) $result->setDataModel($this->model);
1645
1646
		return $result
1647
			->forForeignID($this->ID)
1648
			->where($filter)
1649
			->limit($limit)
1650
			->sort($sort);
1651
	}
1652
1653
	/**
1654
	 * @deprecated
1655
	 */
1656
	public function getComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
1657
		Deprecation::notice('4.0', "Use getComponents to get a filtered DataList for an object's relation");
1658
		return $this->getComponents($componentName, $filter, $sort, $join, $limit);
1659
	}
1660
1661
	/**
1662
	 * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1663
	 *
1664
	 * @param $relationName Relation name.
1665
	 * @return string Class name, or null if not found.
1666
	 */
1667
	public function getRelationClass($relationName) {
1668
		// Go through all relationship configuration fields.
1669
		$candidates = array_merge(
1670
			($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1671
			($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1672
			($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
1673
			($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
1674
			($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1675
		);
1676
1677
		if (isset($candidates[$relationName])) {
1678
			$remoteClass = $candidates[$relationName];
1679
1680
			// If dot notation is present, extract just the first part that contains the class.
1681
			if(($fieldPos = strpos($remoteClass, '.'))!==false) {
1682
				return substr($remoteClass, 0, $fieldPos);
1683
			}
1684
1685
			// Otherwise just return the class
1686
			return $remoteClass;
1687
		}
1688
1689
		return null;
1690
	}
1691
1692
	/**
1693
	 * Tries to find the database key on another object that is used to store a
1694
	 * relationship to this class. If no join field can be found it defaults to 'ParentID'.
1695
	 *
1696
	 * If the remote field is polymorphic then $polymorphic is set to true, and the return value
1697
	 * is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
1698
	 *
1699
	 * @param string $component Name of the relation on the current object pointing to the
1700
	 * remote object.
1701
	 * @param string $type the join type - either 'has_many' or 'belongs_to'
1702
	 * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
1703
	 * @return string
1704
	 */
1705
	public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
1706
		// Extract relation from current object
1707
		if($type === 'has_many') {
1708
			$remoteClass = $this->hasManyComponent($component, false);
1709
		} else {
1710
			$remoteClass = $this->belongsToComponent($component, false);
1711
		}
1712
1713
		if(empty($remoteClass)) {
1714
			throw new Exception("Unknown $type component '$component' on class '$this->class'");
1715
		}
1716
		if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
1717
			throw new Exception(
1718
				"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
1719
			);
1720
		}
1721
1722
		// If presented with an explicit field name (using dot notation) then extract field name
1723
		$remoteField = null;
1724
		if(strpos($remoteClass, '.') !== false) {
1725
			list($remoteClass, $remoteField) = explode('.', $remoteClass);
1726
		}
1727
1728
		// Reference remote has_one to check against
1729
		$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
1730
1731
		// Without an explicit field name, attempt to match the first remote field
1732
		// with the same type as the current class
1733
		if(empty($remoteField)) {
1734
			// look for remote has_one joins on this class or any parent classes
1735
			$remoteRelationsMap = array_flip($remoteRelations);
1736
			foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1737
				if(array_key_exists($class, $remoteRelationsMap)) {
1738
					$remoteField = $remoteRelationsMap[$class];
1739
					break;
1740
				}
1741
			}
1742
		}
1743
1744
		// In case of an indeterminate remote field show an error
1745
		if(empty($remoteField)) {
1746
			$polymorphic = false;
1747
			$message = "No has_one found on class '$remoteClass'";
1748
			if($type == 'has_many') {
1749
				// include a hint for has_many that is missing a has_one
1750
				$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
1751
				$message .= " requires a has_one on '$remoteClass'";
1752
			}
1753
			throw new Exception($message);
1754
		}
1755
1756
		// If given an explicit field name ensure the related class specifies this
1757
		if(empty($remoteRelations[$remoteField])) {
1758
			throw new Exception("Missing expected has_one named '$remoteField'
1759
				on class '$remoteClass' referenced by $type named '$component'
1760
				on class {$this->class}"
1761
			);
1762
		}
1763
1764
		// Inspect resulting found relation
1765
		if($remoteRelations[$remoteField] === 'DataObject') {
1766
			$polymorphic = true;
1767
			return $remoteField; // Composite polymorphic field does not include 'ID' suffix
1768
		} else {
1769
			$polymorphic = false;
1770
			return $remoteField . 'ID';
1771
		}
1772
	}
1773
1774
	/**
1775
	 * Returns a many-to-many component, as a ManyManyList.
1776
	 * @param string $componentName Name of the many-many component
1777
	 * @return ManyManyList The set of components
1778
	 *
1779
	 * @todo Implement query-params
1780
	 */
1781
	public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
1782
		list($parentClass, $componentClass, $parentField, $componentField, $table)
1783
			= $this->manyManyComponent($componentName);
1784
1785
		if($filter !== null || $sort !== null || $join !== null || $limit !== null) {
1786
			Deprecation::notice('4.0', 'The $filter, $sort, $join and $limit parameters for
1787
				DataObject::getManyManyComponents() have been deprecated.
1788
				Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
1789
		}
1790
1791
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1792
		if(!$this->ID) {
1793
			if(!isset($this->unsavedRelations[$componentName])) {
1794
				$this->unsavedRelations[$componentName] =
1795
					new UnsavedRelationList($parentClass, $componentName, $componentClass);
1796
			}
1797
			return $this->unsavedRelations[$componentName];
1798
		}
1799
1800
		$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
1801
		$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1802
1803
		if($this->model) $result->setDataModel($this->model);
1804
1805
		$this->extend('updateManyManyComponents', $result);
1806
1807
		// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1808
		// foreignID set elsewhere.
1809
		return $result
1810
			->forForeignID($this->ID)
1811
			->where($filter)
1812
			->sort($sort)
1813
			->limit($limit);
1814
	}
1815
1816
	/**
1817
	 * @deprecated 4.0 Method has been replaced by hasOne() and hasOneComponent()
1818
	 * @param string $component
1819
	 * @return array|null
1820
	 */
1821
	public function has_one($component = null) {
1822
		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...
1823
			Deprecation::notice('4.0', 'Please use hasOneComponent() instead');
1824
			return $this->hasOneComponent($component);
1825
		}
1826
1827
		Deprecation::notice('4.0', 'Please use hasOne() instead');
1828
		return $this->hasOne();
1829
	}
1830
1831
	/**
1832
	 * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1833
	 * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1834
	 *
1835
	 * @param string $component Deprecated - Name of component
1836
	 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1837
	 * 							their classes.
1838
	 */
1839
	public function hasOne($component = null) {
1840
		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...
1841
			Deprecation::notice(
1842
				'4.0',
1843
				'Please use DataObject::hasOneComponent() instead of passing a component name to hasOne()',
1844
				Deprecation::SCOPE_GLOBAL
1845
			);
1846
			return $this->hasOneComponent($component);
1847
		}
1848
1849
		return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1850
	}
1851
1852
	/**
1853
	 * Return data for a specific has_one component.
1854
	 * @param string $component
1855
	 * @return string|null
1856
	 */
1857
	public function hasOneComponent($component) {
1858
		$hasOnes = (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1859
1860
		if(isset($hasOnes[$component])) {
1861
			return $hasOnes[$component];
1862
		}
1863
	}
1864
1865
	/**
1866
	 * @deprecated 4.0 Method has been replaced by belongsTo() and belongsToComponent()
1867
	 * @param string $component
1868
	 * @param bool $classOnly
1869
	 * @return array|null
1870
	 */
1871
	public function belongs_to($component = null, $classOnly = true) {
1872
		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...
1873
			Deprecation::notice('4.0', 'Please use belongsToComponent() instead');
1874
			return $this->belongsToComponent($component, $classOnly);
1875
		}
1876
1877
		Deprecation::notice('4.0', 'Please use belongsTo() instead');
1878
		return $this->belongsTo(null, $classOnly);
1879
	}
1880
1881
	/**
1882
	 * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1883
	 * their class name will be returned.
1884
	 *
1885
	 * @param string $component - Name of component
1886
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1887
	 *        the field data stripped off. It defaults to TRUE.
1888
	 * @return string|array
1889
	 */
1890
	public function belongsTo($component = null, $classOnly = true) {
1891
		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...
1892
			Deprecation::notice(
1893
				'4.0',
1894
				'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
1895
				Deprecation::SCOPE_GLOBAL
1896
			);
1897
			return $this->belongsToComponent($component, $classOnly);
1898
		}
1899
1900
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1901
		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...
1902
			return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1903
		} else {
1904
			return $belongsTo ? $belongsTo : array();
1905
		}
1906
	}
1907
1908
	/**
1909
	 * Return data for a specific belongs_to component.
1910
	 * @param string $component
1911
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1912
	 *        the field data stripped off. It defaults to TRUE.
1913
	 * @return string|false
1914
	 */
1915
	public function belongsToComponent($component, $classOnly = true) {
1916
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1917
1918
		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...
1919
			$belongsTo = $belongsTo[$component];
1920
		} else {
1921
			return false;
1922
		}
1923
1924
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
1925
	}
1926
1927
	/**
1928
	 * Return all of the database fields defined in self::$db and all the parent classes.
1929
	 * Doesn't include any fields specified by self::$has_one.  Use $this->hasOne() to get these fields
1930
	 *
1931
	 * @param string $fieldName Limit the output to a specific field name
1932
	 * @return array The database fields
1933
	 */
1934
	public function db($fieldName = null) {
1935
		$classes = ClassInfo::ancestry($this, true);
1936
1937
		// If we're looking for a specific field, we want to hit subclasses first as they may override field types
1938
		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...
1939
			$classes = array_reverse($classes);
1940
		}
1941
1942
		$items = array();
1943
		foreach($classes as $class) {
1944
			if(isset(self::$_cache_db[$class])) {
1945
				$dbItems = self::$_cache_db[$class];
1946
			} else {
1947
				$dbItems = (array) Config::inst()->get($class, 'db', Config::UNINHERITED);
1948
				self::$_cache_db[$class] = $dbItems;
1949
			}
1950
1951
			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...
1952
				if(isset($dbItems[$fieldName])) {
1953
					return $dbItems[$fieldName];
1954
				}
1955
			} else {
1956
				$items = isset($items) ? array_merge((array) $items, $dbItems) : $dbItems;
1957
			}
1958
		}
1959
1960
		return $items;
1961
	}
1962
1963
	/**
1964
	 * @deprecated 4.0 Method has been replaced by hasMany() and hasManyComponent()
1965
	 * @param string $component
1966
	 * @param bool $classOnly
1967
	 * @return array|null
1968
	 */
1969
	public function has_many($component = null, $classOnly = true) {
1970
		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...
1971
			Deprecation::notice('4.0', 'Please use hasManyComponent() instead');
1972
			return $this->hasManyComponent($component, $classOnly);
1973
		}
1974
1975
		Deprecation::notice('4.0', 'Please use hasMany() instead');
1976
		return $this->hasMany(null, $classOnly);
1977
	}
1978
1979
	/**
1980
	 * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1981
	 * relationships and their classes will be returned.
1982
	 *
1983
	 * @param string $component Deprecated - Name of component
1984
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1985
	 *        the field data stripped off. It defaults to TRUE.
1986
	 * @return string|array|false
1987
	 */
1988
	public function hasMany($component = null, $classOnly = true) {
1989
		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...
1990
			Deprecation::notice(
1991
				'4.0',
1992
				'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
1993
				Deprecation::SCOPE_GLOBAL
1994
			);
1995
			return $this->hasManyComponent($component, $classOnly);
1996
		}
1997
1998
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
1999
		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...
2000
			return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2001
		} else {
2002
			return $hasMany ? $hasMany : array();
2003
		}
2004
	}
2005
2006
	/**
2007
	 * Return data for a specific has_many component.
2008
	 * @param string $component
2009
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2010
	 *        the field data stripped off. It defaults to TRUE.
2011
	 * @return string|false
2012
	 */
2013
	public function hasManyComponent($component, $classOnly = true) {
2014
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2015
2016
		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...
2017
			$hasMany = $hasMany[$component];
2018
		} else {
2019
			return false;
2020
		}
2021
2022
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
2023
	}
2024
2025
	/**
2026
	 * @deprecated 4.0 Method has been replaced by manyManyExtraFields() and
2027
	 *                 manyManyExtraFieldsForComponent()
2028
	 * @param string $component
2029
	 * @return array
2030
	 */
2031
	public function many_many_extraFields($component = null) {
2032
		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...
2033
			Deprecation::notice('4.0', 'Please use manyManyExtraFieldsForComponent() instead');
2034
			return $this->manyManyExtraFieldsForComponent($component);
2035
		}
2036
2037
		Deprecation::notice('4.0', 'Please use manyManyExtraFields() instead');
2038
		return $this->manyManyExtraFields();
2039
	}
2040
2041
	/**
2042
	 * Return the many-to-many extra fields specification.
2043
	 *
2044
	 * If you don't specify a component name, it returns all
2045
	 * extra fields for all components available.
2046
	 *
2047
	 * @param string $component Deprecated - Name of component
2048
	 * @return array|null
2049
	 */
2050
	public function manyManyExtraFields($component = null) {
2051
		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...
2052
			Deprecation::notice(
2053
				'4.0',
2054
				'Please use DataObject::manyManyExtraFieldsForComponent() instead of passing a component name
2055
					to manyManyExtraFields()',
2056
				Deprecation::SCOPE_GLOBAL
2057
			);
2058
			return $this->manyManyExtraFieldsForComponent($component);
2059
		}
2060
2061
		return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2062
	}
2063
2064
	/**
2065
	 * Return the many-to-many extra fields specification for a specific component.
2066
	 * @param string $component
2067
	 * @return array|null
2068
	 */
2069
	public function manyManyExtraFieldsForComponent($component) {
2070
		// Get all many_many_extraFields defined in this class or parent classes
2071
		$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2072
		// Extra fields are immediately available
2073
		if(isset($extraFields[$component])) {
2074
			return $extraFields[$component];
2075
		}
2076
2077
		// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
2078
		$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2079
		$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2080
		if($candidate) {
2081
			$relationName = null;
2082
			// Extract class and relation name from dot-notation
2083
			if(strpos($candidate, '.') !== false) {
2084
				list($candidate, $relationName) = explode('.', $candidate, 2);
2085
			}
2086
2087
			// If we've not already found the relation name from dot notation, we need to find a relation that points
2088
			// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
2089
			// so it's safe to assume that it's the correct one
2090
			if(!$relationName) {
2091
				$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2092
2093
				foreach($candidateManyManys as $relation => $relatedClass) {
2094
					if (is_a($this, $relatedClass)) {
2095
						$relationName = $relation;
2096
					}
2097
				}
2098
			}
2099
2100
			// If we've found a matching relation on the target class, see if we can find extra fields for it
2101
			$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
2102
			if(isset($extraFields[$relationName])) {
2103
				return $extraFields[$relationName];
2104
			}
2105
		}
2106
2107
		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...
2108
	}
2109
2110
	/**
2111
	 * @deprecated 4.0 Method has been renamed to manyMany()
2112
	 * @param string $component
2113
	 * @return array|null
2114
	 */
2115
	public function many_many($component = null) {
2116
		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...
2117
			Deprecation::notice('4.0', 'Please use manyManyComponent() instead');
2118
			return $this->manyManyComponent($component);
2119
		}
2120
2121
		Deprecation::notice('4.0', 'Please use manyMany() instead');
2122
		return $this->manyMany();
2123
	}
2124
2125
	/**
2126
	 * Return information about a many-to-many component.
2127
	 * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2128
	 * components are returned.
2129
	 *
2130
	 * @see DataObject::manyManyComponent()
2131
	 * @param string $component Deprecated - Name of component
2132
	 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2133
	 */
2134
	public function manyMany($component = null) {
2135
		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...
2136
			Deprecation::notice(
2137
				'4.0',
2138
				'Please use DataObject::manyManyComponent() instead of passing a component name to manyMany()',
2139
				Deprecation::SCOPE_GLOBAL
2140
			);
2141
			return $this->manyManyComponent($component);
2142
		}
2143
2144
		$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
2145
		$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2146
2147
		$items = array_merge($manyManys, $belongsManyManys);
2148
		return $items;
2149
	}
2150
2151
	/**
2152
	 * Return information about a specific many_many component. Returns a numeric array of:
2153
	 * array(
2154
	 * 	<classname>,		The class that relation is defined in e.g. "Product"
2155
	 * 	<candidateName>,	The target class of the relation e.g. "Category"
2156
	 * 	<parentField>,		The field name pointing to <classname>'s table e.g. "ProductID"
2157
	 * 	<childField>,		The field name pointing to <candidatename>'s table e.g. "CategoryID"
2158
	 * 	<joinTable>			The join table between the two classes e.g. "Product_Categories"
2159
	 * )
2160
	 * @param string $component The component name
2161
	 * @return array|null
2162
	 */
2163
	public function manyManyComponent($component) {
2164
		$classes = $this->getClassAncestry();
2165
		foreach($classes as $class) {
2166
			$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
2167
			// Check if the component is defined in many_many on this class
2168
			$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2169
			if($candidate) {
2170
				$parentField = $class . "ID";
2171
				$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
2172
				return array($class, $candidate, $parentField, $childField, "{$class}_$component");
2173
			}
2174
2175
			// Check if the component is defined in belongs_many_many on this class
2176
			$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
2177
			$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
2178
			if($candidate) {
2179
				// Extract class and relation name from dot-notation
2180
				if(strpos($candidate, '.') !== false) {
2181
					list($candidate, $relationName) = explode('.', $candidate, 2);
2182
				}
2183
2184
				$childField = $candidate . "ID";
2185
2186
				// We need to find the inverse component name
2187
				$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2188
				if(!$otherManyMany) {
2189
					throw new LogicException("Inverse component of $candidate not found ({$this->class})");
2190
				}
2191
2192
				// If we've got a relation name (extracted from dot-notation), we can already work out
2193
				// the join table and candidate class name...
2194
				if(isset($relationName) && isset($otherManyMany[$relationName])) {
2195
					$candidateClass = $otherManyMany[$relationName];
2196
					$joinTable = "{$candidate}_{$relationName}";
2197
				} else {
2198
					// ... otherwise, we need to loop over the many_manys and find a relation that
2199
					// matches up to this class
2200
					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...
2201
						if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
2202
							$joinTable = "{$candidate}_{$inverseComponentName}";
2203
							break;
2204
						}
2205
					}
2206
				}
2207
2208
				// If we could work out the join table, we've got all the info we need
2209
				if(isset($joinTable)) {
2210
					$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...
2211
					return array($class, $candidate, $parentField, $childField, $joinTable);
2212
				}
2213
2214
				throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
2215
			}
2216
		}
2217
	}
2218
2219
	/**
2220
	 * This returns an array (if it exists) describing the database extensions that are required, or false if none
2221
	 *
2222
	 * This is experimental, and is currently only a Postgres-specific enhancement.
2223
	 *
2224
	 * @return array or false
2225
	 */
2226
	public function database_extensions($class){
2227
		$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2228
2229
		if($extensions)
2230
			return $extensions;
2231
		else
2232
			return false;
2233
	}
2234
2235
	/**
2236
	 * Generates a SearchContext to be used for building and processing
2237
	 * a generic search form for properties on this object.
2238
	 *
2239
	 * @return SearchContext
2240
	 */
2241
	public function getDefaultSearchContext() {
2242
		return new SearchContext(
2243
			$this->class,
2244
			$this->scaffoldSearchFields(),
2245
			$this->defaultSearchFilters()
2246
		);
2247
	}
2248
2249
	/**
2250
	 * Determine which properties on the DataObject are
2251
	 * searchable, and map them to their default {@link FormField}
2252
	 * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2253
	 *
2254
	 * Some additional logic is included for switching field labels, based on
2255
	 * how generic or specific the field type is.
2256
	 *
2257
	 * Used by {@link SearchContext}.
2258
	 *
2259
	 * @param array $_params
2260
	 *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2261
	 *   'restrictFields': Numeric array of a field name whitelist
2262
	 * @return FieldList
2263
	 */
2264
	public function scaffoldSearchFields($_params = null) {
2265
		$params = array_merge(
2266
			array(
2267
				'fieldClasses' => false,
2268
				'restrictFields' => false
2269
			),
2270
			(array)$_params
2271
		);
2272
		$fields = new FieldList();
2273
		foreach($this->searchableFields() as $fieldName => $spec) {
2274
			if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
2275
2276
			// If a custom fieldclass is provided as a string, use it
2277
			if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2278
				$fieldClass = $params['fieldClasses'][$fieldName];
2279
				$field = new $fieldClass($fieldName);
2280
			// If we explicitly set a field, then construct that
2281
			} else if(isset($spec['field'])) {
2282
				// If it's a string, use it as a class name and construct
2283
				if(is_string($spec['field'])) {
2284
					$fieldClass = $spec['field'];
2285
					$field = new $fieldClass($fieldName);
2286
2287
				// If it's a FormField object, then just use that object directly.
2288
				} else if($spec['field'] instanceof FormField) {
2289
					$field = $spec['field'];
2290
2291
				// Otherwise we have a bug
2292
				} else {
2293
					user_error("Bad value for searchable_fields, 'field' value: "
2294
						. var_export($spec['field'], true), E_USER_WARNING);
2295
				}
2296
2297
			// Otherwise, use the database field's scaffolder
2298
			} else {
2299
				$field = $this->relObject($fieldName)->scaffoldSearchField();
2300
			}
2301
2302
			if (strstr($fieldName, '.')) {
2303
				$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...
2304
			}
2305
			$field->setTitle($spec['title']);
2306
2307
			$fields->push($field);
2308
		}
2309
		return $fields;
2310
	}
2311
2312
	/**
2313
	 * Scaffold a simple edit form for all properties on this dataobject,
2314
	 * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2315
	 * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2316
	 *
2317
	 * @uses FormScaffolder
2318
	 *
2319
	 * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2320
	 * @return FieldList
2321
	 */
2322
	public function scaffoldFormFields($_params = null) {
2323
		$params = array_merge(
2324
			array(
2325
				'tabbed' => false,
2326
				'includeRelations' => false,
2327
				'restrictFields' => false,
2328
				'fieldClasses' => false,
2329
				'ajaxSafe' => false
2330
			),
2331
			(array)$_params
2332
		);
2333
2334
		$fs = new FormScaffolder($this);
2335
		$fs->tabbed = $params['tabbed'];
2336
		$fs->includeRelations = $params['includeRelations'];
2337
		$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...
2338
		$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...
2339
		$fs->ajaxSafe = $params['ajaxSafe'];
2340
2341
		return $fs->getFieldList();
2342
	}
2343
2344
	/**
2345
	 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2346
	 * being called on extensions
2347
	 *
2348
	 * @param callable $callback The callback to execute
2349
	 */
2350
	protected function beforeUpdateCMSFields($callback) {
2351
		$this->beforeExtending('updateCMSFields', $callback);
2352
	}
2353
2354
	/**
2355
	 * Centerpiece of every data administration interface in Silverstripe,
2356
	 * which returns a {@link FieldList} suitable for a {@link Form} object.
2357
	 * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2358
	 * generate this set. To customize, overload this method in a subclass
2359
	 * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2360
	 *
2361
	 * <code>
2362
	 * class MyCustomClass extends DataObject {
2363
	 *  static $db = array('CustomProperty'=>'Boolean');
2364
	 *
2365
	 *  function getCMSFields() {
2366
	 *    $fields = parent::getCMSFields();
2367
	 *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2368
	 *    return $fields;
2369
	 *  }
2370
	 * }
2371
	 * </code>
2372
	 *
2373
	 * @see Good example of complex FormField building: SiteTree::getCMSFields()
2374
	 *
2375
	 * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2376
	 */
2377
	public function getCMSFields() {
2378
		$tabbedFields = $this->scaffoldFormFields(array(
2379
			// Don't allow has_many/many_many relationship editing before the record is first saved
2380
			'includeRelations' => ($this->ID > 0),
2381
			'tabbed' => true,
2382
			'ajaxSafe' => true
2383
		));
2384
2385
		$this->extend('updateCMSFields', $tabbedFields);
2386
2387
		return $tabbedFields;
2388
	}
2389
2390
	/**
2391
	 * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2392
	 * including that dataobject's extensions customised actions could be added to the EditForm.
2393
	 *
2394
	 * @return an Empty FieldList(); need to be overload by solid subclass
2395
	 */
2396
	public function getCMSActions() {
2397
		$actions = new FieldList();
2398
		$this->extend('updateCMSActions', $actions);
2399
		return $actions;
2400
	}
2401
2402
2403
	/**
2404
	 * Used for simple frontend forms without relation editing
2405
	 * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2406
	 * by default. To customize, either overload this method in your
2407
	 * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2408
	 *
2409
	 * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2410
	 *
2411
	 * @param array $params See {@link scaffoldFormFields()}
2412
	 * @return FieldList Always returns a simple field collection without TabSet.
2413
	 */
2414
	public function getFrontEndFields($params = null) {
2415
		$untabbedFields = $this->scaffoldFormFields($params);
2416
		$this->extend('updateFrontEndFields', $untabbedFields);
2417
2418
		return $untabbedFields;
2419
	}
2420
2421
	/**
2422
	 * Gets the value of a field.
2423
	 * Called by {@link __get()} and any getFieldName() methods you might create.
2424
	 *
2425
	 * @param string $field The name of the field
2426
	 *
2427
	 * @return mixed The field value
2428
	 */
2429
	public function getField($field) {
2430
		// If we already have an object in $this->record, then we should just return that
2431
		if(isset($this->record[$field]) && is_object($this->record[$field]))  return $this->record[$field];
2432
2433
		// Do we have a field that needs to be lazy loaded?
2434
		if(isset($this->record[$field.'_Lazy'])) {
2435
			$tableClass = $this->record[$field.'_Lazy'];
2436
			$this->loadLazyFields($tableClass);
2437
		}
2438
2439
		// Otherwise, we need to determine if this is a complex field
2440
		if(self::is_composite_field($this->class, $field)) {
2441
			$helper = $this->castingHelper($field);
2442
			$fieldObj = Object::create_from_string($helper, $field);
2443
2444
			$compositeFields = $fieldObj->compositeDatabaseFields();
2445
			foreach ($compositeFields as $compositeName => $compositeType) {
2446
				if(isset($this->record[$field.$compositeName.'_Lazy'])) {
2447
					$tableClass = $this->record[$field.$compositeName.'_Lazy'];
2448
					$this->loadLazyFields($tableClass);
2449
				}
2450
			}
2451
2452
			// write value only if either the field value exists,
2453
			// or a valid record has been loaded from the database
2454
			$value = (isset($this->record[$field])) ? $this->record[$field] : null;
2455
			if($value || $this->exists()) $fieldObj->setValue($value, $this->record, false);
2456
2457
			$this->record[$field] = $fieldObj;
2458
2459
			return $this->record[$field];
2460
		}
2461
2462
		return isset($this->record[$field]) ? $this->record[$field] : null;
2463
	}
2464
2465
	/**
2466
	 * Loads all the stub fields that an initial lazy load didn't load fully.
2467
	 *
2468
	 * @param tableClass Base table to load the values from. Others are joined as required.
2469
	 *                   Not specifying a tableClass will load all lazy fields from all tables.
2470
	 */
2471
	protected function loadLazyFields($tableClass = null) {
2472
		if (!$tableClass) {
2473
			$loaded = array();
2474
2475
			foreach ($this->record as $key => $value) {
2476
				if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2477
					$this->loadLazyFields($value);
2478
					$loaded[$value] = $value;
2479
				}
2480
			}
2481
2482
			return;
2483
		}
2484
2485
		$dataQuery = new DataQuery($tableClass);
2486
2487
		// Reset query parameter context to that of this DataObject
2488
		if($params = $this->getSourceQueryParams()) {
2489
			foreach($params as $key => $value) $dataQuery->setQueryParam($key, $value);
2490
		}
2491
2492
		// TableField sets the record ID to "new" on new row data, so don't try doing anything in that case
2493
		if(!is_numeric($this->record['ID'])) return false;
2494
2495
		// Limit query to the current record, unless it has the Versioned extension,
2496
		// in which case it requires special handling through augmentLoadLazyFields()
2497
		if(!$this->hasExtension('Versioned')) {
2498
			$dataQuery->where("\"$tableClass\".\"ID\" = {$this->record['ID']}")->limit(1);
2499
		}
2500
2501
		$columns = array();
2502
2503
		// Add SQL for fields, both simple & multi-value
2504
		// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2505
		$databaseFields = self::database_fields($tableClass, false);
2506
		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...
2507
			if(!isset($this->record[$k]) || $this->record[$k] === null) {
2508
				$columns[] = $k;
2509
			}
2510
		}
2511
2512
		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...
2513
			$query = $dataQuery->query();
2514
			$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2515
			$this->extend('augmentSQL', $query, $dataQuery);
2516
2517
			$dataQuery->setQueriedColumns($columns);
2518
			$newData = $dataQuery->execute()->record();
2519
2520
			// Load the data into record
2521
			if($newData) {
2522
				foreach($newData as $k => $v) {
2523
					if (in_array($k, $columns)) {
2524
						$this->record[$k] = $v;
2525
						$this->original[$k] = $v;
2526
						unset($this->record[$k . '_Lazy']);
2527
					}
2528
				}
2529
2530
			// No data means that the query returned nothing; assign 'null' to all the requested fields
2531
			} else {
2532
				foreach($columns as $k) {
2533
					$this->record[$k] = null;
2534
					$this->original[$k] = null;
2535
					unset($this->record[$k . '_Lazy']);
2536
				}
2537
			}
2538
		}
2539
	}
2540
2541
	/**
2542
	 * Return the fields that have changed.
2543
	 *
2544
	 * The change level affects what the functions defines as "changed":
2545
	 * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2546
	 * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2547
	 *   for example a change from 0 to null would not be included.
2548
	 *
2549
	 * Example return:
2550
	 * <code>
2551
	 * array(
2552
	 *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2553
	 * )
2554
	 * </code>
2555
	 *
2556
	 * @param boolean $databaseFieldsOnly Get only database fields that have changed
2557
	 * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2558
	 * @return array
2559
	 */
2560
	public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
2561
		$changedFields = array();
2562
2563
		// Update the changed array with references to changed obj-fields
2564
		foreach($this->record as $k => $v) {
2565
			if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2566
				$this->changed[$k] = self::CHANGE_VALUE;
2567
			}
2568
		}
2569
2570
		if($databaseFieldsOnly) {
2571
			$databaseFields = $this->inheritedDatabaseFields();
2572
			$databaseFields['ID'] = true;
2573
			$databaseFields['LastEdited'] = true;
2574
			$databaseFields['Created'] = true;
2575
			$databaseFields['ClassName'] = true;
2576
			$fields = array_intersect_key((array)$this->changed, $databaseFields);
2577
		} else {
2578
			$fields = $this->changed;
2579
		}
2580
2581
		// Filter the list to those of a certain change level
2582
		if($changeLevel > self::CHANGE_STRICT) {
2583
			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...
2584
				if($level < $changeLevel) {
2585
					unset($fields[$name]);
2586
				}
2587
			}
2588
		}
2589
2590
		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...
2591
			$changedFields[$name] = array(
2592
				'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2593
				'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2594
				'level' => $level
2595
			);
2596
		}
2597
2598
		return $changedFields;
2599
	}
2600
2601
	/**
2602
	 * Uses {@link getChangedFields()} to determine if fields have been changed
2603
	 * since loading them from the database.
2604
	 *
2605
	 * @param string $fieldName Name of the database field to check, will check for any if not given
2606
	 * @param int $changeLevel See {@link getChangedFields()}
2607
	 * @return boolean
2608
	 */
2609
	public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
2610
		$changed = $this->getChangedFields(false, $changeLevel);
2611
		if(!isset($fieldName)) {
2612
			return !empty($changed);
2613
		}
2614
		else {
2615
			return array_key_exists($fieldName, $changed);
2616
		}
2617
	}
2618
2619
	/**
2620
	 * Set the value of the field
2621
	 * Called by {@link __set()} and any setFieldName() methods you might create.
2622
	 *
2623
	 * @param string $fieldName Name of the field
2624
	 * @param mixed $val New field value
2625
	 * @return DataObject $this
2626
	 */
2627
	public function setField($fieldName, $val) {
2628
		//if it's a has_one component, destroy the cache
2629
		if (substr($fieldName, -2) == 'ID') {
2630
			unset($this->components[substr($fieldName, 0, -2)]);
2631
		}
2632
		// Situation 1: Passing an DBField
2633
		if($val instanceof DBField) {
2634
			$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...
2635
2636
			// If we've just lazy-loaded the column, then we need to populate the $original array by
2637
			// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
2638
			// on a call to getChanged()?
2639
			$this->getField($fieldName);
2640
2641
			$this->record[$fieldName] = $val;
2642
		// Situation 2: Passing a literal or non-DBField object
2643
		} else {
2644
			// If this is a proper database field, we shouldn't be getting non-DBField objects
2645
			if(is_object($val) && $this->db($fieldName)) {
2646
				user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2647
			}
2648
2649
			// if a field is not existing or has strictly changed
2650
			if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2651
				// TODO Add check for php-level defaults which are not set in the db
2652
				// TODO Add check for hidden input-fields (readonly) which are not set in the db
2653
				// At the very least, the type has changed
2654
				$this->changed[$fieldName] = self::CHANGE_STRICT;
2655
2656
				if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
2657
						&& $this->record[$fieldName] != $val)) {
2658
2659
					// Value has changed as well, not just the type
2660
					$this->changed[$fieldName] = self::CHANGE_VALUE;
2661
				}
2662
2663
				// If we've just lazy-loaded the column, then we need to populate the $original array by
2664
				// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
2665
				// on a call to getChanged()?
2666
				$this->getField($fieldName);
2667
2668
				// Value is always saved back when strict check succeeds.
2669
				$this->record[$fieldName] = $val;
2670
			}
2671
		}
2672
		return $this;
2673
	}
2674
2675
	/**
2676
	 * Set the value of the field, using a casting object.
2677
	 * This is useful when you aren't sure that a date is in SQL format, for example.
2678
	 * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2679
	 * can be saved into the Image table.
2680
	 *
2681
	 * @param string $fieldName Name of the field
2682
	 * @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...
2683
	 * @return DataObject $this
2684
	 */
2685
	public function setCastedField($fieldName, $val) {
2686
		if(!$fieldName) {
2687
			user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2688
		}
2689
		$castingHelper = $this->castingHelper($fieldName);
2690
		if($castingHelper) {
2691
			$fieldObj = Object::create_from_string($castingHelper, $fieldName);
2692
			$fieldObj->setValue($val);
2693
			$fieldObj->saveInto($this);
2694
		} else {
2695
			$this->$fieldName = $val;
2696
		}
2697
		return $this;
2698
	}
2699
2700
	/**
2701
	 * Returns true if the given field exists in a database column on any of
2702
	 * the objects tables and optionally look up a dynamic getter with
2703
	 * get<fieldName>().
2704
	 *
2705
	 * @param string $field Name of the field
2706
	 * @return boolean True if the given field exists
2707
	 */
2708
	public function hasField($field) {
2709
		return (
2710
			array_key_exists($field, $this->record)
2711
			|| $this->db($field)
2712
			|| (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...
2713
			|| $this->hasMethod("get{$field}")
2714
		);
2715
	}
2716
2717
	/**
2718
	 * Returns true if the given field exists as a database column
2719
	 *
2720
	 * @param string $field Name of the field
2721
	 *
2722
	 * @return boolean
2723
	 */
2724
	public function hasDatabaseField($field) {
2725
		if(isset(self::$fixed_fields[$field])) return true;
2726
2727
		return array_key_exists($field, $this->inheritedDatabaseFields());
2728
	}
2729
2730
	/**
2731
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2732
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2733
	 *
2734
	 * @param string $field Name of the field
2735
	 * @return string The field type of the given field
2736
	 */
2737
	public function hasOwnTableDatabaseField($field) {
2738
		return self::has_own_table_database_field($this->class, $field);
2739
	}
2740
2741
	/**
2742
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2743
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2744
	 *
2745
	 * @param string $class Class name to check
2746
	 * @param string $field Name of the field
2747
	 * @return string The field type of the given field
2748
	 */
2749
	public static function has_own_table_database_field($class, $field) {
2750
		// Since database_fields omits 'ID'
2751
		if($field == "ID") return "Int";
2752
2753
		$fieldMap = self::database_fields($class, false);
2754
2755
		// Remove string-based "constructor-arguments" from the DBField definition
2756
		if(isset($fieldMap[$field])) {
2757
			$spec = $fieldMap[$field];
2758
			if(is_string($spec)) return strtok($spec,'(');
2759
			else return $spec['type'];
2760
		}
2761
	}
2762
2763
	/**
2764
	 * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2765
	 * actually looking in the database.
2766
	 *
2767
	 * @param string $dataClass
2768
	 * @return bool
2769
	 */
2770
	public static function has_own_table($dataClass) {
2771
		if(!is_subclass_of($dataClass,'DataObject')) return false;
2772
2773
		$dataClass = ClassInfo::class_name($dataClass);
2774
		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...
2775
			if(get_parent_class($dataClass) == 'DataObject') {
2776
				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...
2777
			} else {
2778
				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...
2779
					= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
2780
					|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
2781
			}
2782
		}
2783
		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...
2784
	}
2785
2786
	/**
2787
	 * Returns true if the member is allowed to do the given action.
2788
	 * See {@link extendedCan()} for a more versatile tri-state permission control.
2789
	 *
2790
	 * @param string $perm The permission to be checked, such as 'View'.
2791
	 * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2792
	 * in user.
2793
	 *
2794
	 * @return boolean True if the the member is allowed to do the given action
2795
	 */
2796
	public function can($perm, $member = null) {
2797
		if(!isset($member)) {
2798
			$member = Member::currentUser();
2799
		}
2800
		if(Permission::checkMember($member, "ADMIN")) return true;
2801
2802
		if($this->manyManyComponent('Can' . $perm)) {
2803
			if($this->ParentID && $this->SecurityType == 'Inherit') {
2804
				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...
2805
					return false;
2806
				}
2807
				return $this->Parent->can($perm, $member);
2808
2809
			} else {
2810
				$permissionCache = $this->uninherited('permissionCache');
2811
				$memberID = $member ? $member->ID : 'none';
2812
2813
				if(!isset($permissionCache[$memberID][$perm])) {
2814
					if($member->ID) {
2815
						$groups = $member->Groups();
2816
					}
2817
2818
					$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...
2819
2820
					// TODO Fix relation table hardcoding
2821
					$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...
2822
						"\"Page_Can$perm\".PageID",
2823
					array("\"Page_Can$perm\""),
2824
						"GroupID IN ($groupList)");
2825
2826
					$permissionCache[$memberID][$perm] = $query->execute()->column();
2827
2828
					if($perm == "View") {
2829
						// TODO Fix relation table hardcoding
2830
						$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...
2831
							"\"SiteTree\"",
2832
							"LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2833
							), "\"Page_CanView\".\"PageID\" IS NULL");
2834
2835
							$unsecuredPages = $query->execute()->column();
2836
							if($permissionCache[$memberID][$perm]) {
2837
								$permissionCache[$memberID][$perm]
2838
									= array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2839
							} else {
2840
								$permissionCache[$memberID][$perm] = $unsecuredPages;
2841
							}
2842
					}
2843
2844
					Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2845
				}
2846
2847
				if($permissionCache[$memberID][$perm]) {
2848
					return in_array($this->ID, $permissionCache[$memberID][$perm]);
2849
				}
2850
			}
2851
		} else {
2852
			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, 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_Staff, DataObjectTest_SubEquipmentCompany, DataObjectTest_SubTeam, DataObjectTest_Team, DataObjectTest_TeamComment, DataObjectTest_ValidatedObject, DataQueryTest_A, DataQueryTest_B, DataQueryTest_C, DataQueryTest_D, DataQueryTest_E, DataQueryTest_F, DatabaseAdmin, DatabaseTest_MyObject, DatetimeFieldTest_Model, 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, GridFieldPrintButtonTest_DO, GridFieldSortableHeaderTest_Cheerleader, GridFieldSortableHeaderTest_CheerleaderHat, GridFieldSortableHeaderTest_Team, GridFieldTest_Cheerleader, GridFieldTest_Permissions, GridFieldTest_Player, GridFieldTest_Team, GridField_URLHandlerTest_Controller, Group, GroupTest_Member, HierarchyTest_Object, HourlyTask, HtmlEditorFieldTest_Object, Image, Image_Cached, InstallerTest, JSTestRunner, LeftAndMain, LeftAndMainTest_Controller, LeftAndMainTest_Object, ListboxFieldTest_Article, ListboxFieldTest_DataObject, ListboxFieldTest_Tag, LoginAttempt, ManyManyListTest_ExtraFields, ManyManyListTest_IndirectPrimary, ManyManyListTest_Secondary, ManyManyListTest_SecondarySub, Member, MemberDatetimeOptionsetFieldTest_Controller, MemberPassword, ModelAdmin, ModelAdminTest_Admin, ModelAdminTest_Contact, ModelAdminTest_Player, ModelAdminTest_PlayerAdmin, MoneyFieldTest_CustomSetter_Object, MoneyFieldTest_Object, MoneyTest_DataObject, MoneyTest_SubClass, 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, VersionedLazySub_DataObject, VersionedLazy_DataObject, VersionedTest_AnotherSubclass, VersionedTest_DataObject, 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...
2853
		}
2854
	}
2855
2856
	/**
2857
	 * Process tri-state responses from permission-alterting extensions.  The extensions are
2858
	 * expected to return one of three values:
2859
	 *
2860
	 *  - false: Disallow this permission, regardless of what other extensions say
2861
	 *  - true: Allow this permission, as long as no other extensions return false
2862
	 *  - NULL: Don't affect the outcome
2863
	 *
2864
	 * This method itself returns a tri-state value, and is designed to be used like this:
2865
	 *
2866
	 * <code>
2867
	 * $extended = $this->extendedCan('canDoSomething', $member);
2868
	 * if($extended !== null) return $extended;
2869
	 * else return $normalValue;
2870
	 * </code>
2871
	 *
2872
	 * @param String $methodName Method on the same object, e.g. {@link canEdit()}
2873
	 * @param Member|int $member
2874
	 * @return boolean|null
2875
	 */
2876
	public function extendedCan($methodName, $member) {
2877
		$results = $this->extend($methodName, $member);
2878
		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...
2879
			// Remove NULLs
2880
			$results = array_filter($results, function($v) {return !is_null($v);});
2881
			// If there are any non-NULL responses, then return the lowest one of them.
2882
			// If any explicitly deny the permission, then we don't get access
2883
			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...
2884
		}
2885
		return null;
2886
	}
2887
2888
	/**
2889
	 * @param Member $member
2890
	 * @return boolean
2891
	 */
2892
	public function canView($member = null) {
2893
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2892 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...
2894
		if($extended !== null) {
2895
			return $extended;
2896
		}
2897
		return Permission::check('ADMIN', 'any', $member);
2898
	}
2899
2900
	/**
2901
	 * @param Member $member
2902
	 * @return boolean
2903
	 */
2904
	public function canEdit($member = null) {
2905
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2904 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...
2906
		if($extended !== null) {
2907
			return $extended;
2908
		}
2909
		return Permission::check('ADMIN', 'any', $member);
2910
	}
2911
2912
	/**
2913
	 * @param Member $member
2914
	 * @return boolean
2915
	 */
2916
	public function canDelete($member = null) {
2917
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2916 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...
2918
		if($extended !== null) {
2919
			return $extended;
2920
		}
2921
		return Permission::check('ADMIN', 'any', $member);
2922
	}
2923
2924
	/**
2925
	 * @todo Should canCreate be a static method?
2926
	 *
2927
	 * @param Member $member
2928
	 * @return boolean
2929
	 */
2930
	public function canCreate($member = null) {
2931
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2930 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2932
		if($extended !== null) {
2933
			return $extended;
2934
		}
2935
		return Permission::check('ADMIN', 'any', $member);
2936
	}
2937
2938
	/**
2939
	 * Debugging used by Debug::show()
2940
	 *
2941
	 * @return string HTML data representing this object
2942
	 */
2943
	public function debug() {
2944
		$val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2945
		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...
2946
			$val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2947
		}
2948
		$val .= "</ul>\n";
2949
		return $val;
2950
	}
2951
2952
	/**
2953
	 * Return the DBField object that represents the given field.
2954
	 * This works similarly to obj() with 2 key differences:
2955
	 *   - it still returns an object even when the field has no value.
2956
	 *   - it only matches fields and not methods
2957
	 *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2958
	 *
2959
	 * @param string $fieldName Name of the field
2960
	 * @return DBField The field as a DBField object
2961
	 */
2962
	public function dbObject($fieldName) {
2963
		// If we have a CompositeDBField object in $this->record, then return that
2964
		if(isset($this->record[$fieldName]) && is_object($this->record[$fieldName])) {
2965
			return $this->record[$fieldName];
2966
2967
		// Special case for ID field
2968
		} else if($fieldName == 'ID') {
2969
			return new PrimaryKey($fieldName, $this);
2970
2971
		// Special case for ClassName
2972
		} else if($fieldName == 'ClassName') {
2973
			$val = get_class($this);
2974
			return DBField::create_field('Varchar', $val, $fieldName);
2975
2976
		} else if(array_key_exists($fieldName, self::$fixed_fields)) {
2977
			return DBField::create_field(self::$fixed_fields[$fieldName], $this->$fieldName, $fieldName);
2978
2979
		// General casting information for items in $db
2980
		} else if($helper = $this->db($fieldName)) {
2981
			$obj = Object::create_from_string($helper, $fieldName);
2982
			$obj->setValue($this->$fieldName, $this->record, false);
2983
			return $obj;
2984
2985
		// Special case for has_one relationships
2986
		} 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...
2987
			$val = $this->$fieldName;
2988
			return DBField::create_field('ForeignKey', $val, $fieldName, $this);
2989
2990
		// has_one for polymorphic relations do not end in ID
2991
		} else if(($type = $this->hasOneComponent($fieldName)) && ($type === 'DataObject')) {
2992
			$val = $this->$fieldName();
2993
			return DBField::create_field('PolymorphicForeignKey', $val, $fieldName, $this);
2994
2995
		}
2996
	}
2997
2998
	/**
2999
	 * Traverses to a DBField referenced by relationships between data objects.
3000
	 *
3001
	 * The path to the related field is specified with dot separated syntax
3002
	 * (eg: Parent.Child.Child.FieldName).
3003
	 *
3004
	 * @param string $fieldPath
3005
	 *
3006
	 * @return mixed DBField of the field on the object or a DataList instance.
3007
	 */
3008
	public function relObject($fieldPath) {
3009
		$object = null;
3010
3011
		if(strpos($fieldPath, '.') !== false) {
3012
			$parts = explode('.', $fieldPath);
3013
			$fieldName = array_pop($parts);
3014
3015
			// Traverse dot syntax
3016
			$component = $this;
3017
3018
			foreach($parts as $relation) {
3019
				if($component instanceof SS_List) {
3020
					if(method_exists($component,$relation)) {
3021
						$component = $component->$relation();
3022
					} else {
3023
						$component = $component->relation($relation);
3024
					}
3025
				} else {
3026
					$component = $component->$relation();
3027
				}
3028
			}
3029
3030
			$object = $component->dbObject($fieldName);
3031
3032
		} else {
3033
			$object = $this->dbObject($fieldPath);
3034
		}
3035
3036
		return $object;
3037
	}
3038
3039
	/**
3040
	 * Traverses to a field referenced by relationships between data objects, returning the value
3041
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3042
	 *
3043
	 * @param $fieldPath string
3044
	 * @return string | null - will return null on a missing value
3045
	 */
3046
	public function relField($fieldName) {
3047
		$component = $this;
3048
3049
		// We're dealing with relations here so we traverse the dot syntax
3050
		if(strpos($fieldName, '.') !== false) {
3051
			$relations = explode('.', $fieldName);
3052
			$fieldName = array_pop($relations);
3053
			foreach($relations as $relation) {
3054
				// Inspect $component for element $relation
3055
				if($component->hasMethod($relation)) {
3056
					// Check nested method
3057
					$component = $component->$relation();
3058
				} elseif($component instanceof SS_List) {
3059
					// Select adjacent relation from DataList
3060
					$component = $component->relation($relation);
3061
				} elseif($component instanceof DataObject
3062
					&& ($dbObject = $component->dbObject($relation))
3063
				) {
3064
					// Select db object
3065
					$component = $dbObject;
3066
				} else {
3067
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
3068
				}
3069
			}
3070
		}
3071
3072
		// Bail if the component is null
3073
		if(!$component) {
3074
			return null;
3075
		}
3076
		if($component->hasMethod($fieldName)) {
3077
			return $component->$fieldName();
3078
		}
3079
		return $component->$fieldName;
3080
	}
3081
3082
	/**
3083
	 * Temporary hack to return an association name, based on class, to get around the mangle
3084
	 * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3085
	 *
3086
	 * @return String
3087
	 */
3088
	public function getReverseAssociation($className) {
3089
		if (is_array($this->manyMany())) {
3090
			$many_many = array_flip($this->manyMany());
3091
			if (array_key_exists($className, $many_many)) return $many_many[$className];
3092
		}
3093
		if (is_array($this->hasMany())) {
3094
			$has_many = array_flip($this->hasMany());
3095
			if (array_key_exists($className, $has_many)) return $has_many[$className];
3096
		}
3097
		if (is_array($this->hasOne())) {
3098
			$has_one = array_flip($this->hasOne());
3099
			if (array_key_exists($className, $has_one)) return $has_one[$className];
3100
		}
3101
3102
		return false;
3103
	}
3104
3105
	/**
3106
	 * Return all objects matching the filter
3107
	 * sub-classes are automatically selected and included
3108
	 *
3109
	 * @param string $callerClass The class of objects to be returned
3110
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3111
	 * Supports parameterised queries. See SQLQuery::addWhere() for syntax examples.
3112
	 * @param string|array $sort A sort expression to be inserted into the ORDER
3113
	 * BY clause.  If omitted, self::$default_sort will be used.
3114
	 * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3115
	 * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3116
	 * @param string $containerClass The container class to return the results in.
3117
	 *
3118
	 * @todo $containerClass is Ignored, why?
3119
	 *
3120
	 * @return DataList The objects matching the filter, in the class specified by $containerClass
3121
	 */
3122
	public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
3123
			$containerClass = 'DataList') {
3124
3125
		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...
3126
			$callerClass = get_called_class();
3127
			if($callerClass == 'DataObject') {
3128
				throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3129
			}
3130
3131
			if($filter || $sort || $join || $limit || ($containerClass != 'DataList')) {
3132
				throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3133
					. ' arguments');
3134
			}
3135
3136
			$result = DataList::create(get_called_class());
3137
			$result->setDataModel(DataModel::inst());
3138
			return $result;
3139
		}
3140
3141
		if($join) {
3142
			throw new \InvalidArgumentException(
3143
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3144
			);
3145
		}
3146
3147
		$result = DataList::create($callerClass)->where($filter)->sort($sort);
3148
3149
		if($limit && strpos($limit, ',') !== false) {
3150
			$limitArguments = explode(',', $limit);
3151
			$result = $result->limit($limitArguments[1],$limitArguments[0]);
3152
		} elseif($limit) {
3153
			$result = $result->limit($limit);
3154
		}
3155
3156
		$result->setDataModel(DataModel::inst());
3157
		return $result;
3158
	}
3159
3160
3161
	/**
3162
	 * @deprecated
3163
	 */
3164
	public function Aggregate($class = null) {
3165
		Deprecation::notice('4.0', 'Call aggregate methods on a DataList directly instead. In templates'
3166
			. ' an example of the new syntax is &lt% cached List(Member).max(LastEdited) %&gt instead'
3167
			. ' (check partial-caching.md documentation for more details.)');
3168
3169
		if($class) {
3170
			$list = new DataList($class);
3171
			$list->setDataModel(DataModel::inst());
3172
		} else if(isset($this)) {
3173
			$list = new DataList(get_class($this));
3174
			$list->setDataModel($this->model);
3175
		} else {
3176
			throw new \InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed"
3177
				. " a classname");
3178
		}
3179
		return $list;
3180
	}
3181
3182
	/**
3183
	 * @deprecated
3184
	 */
3185
	public function RelationshipAggregate($relationship) {
3186
		Deprecation::notice('4.0', 'Call aggregate methods on a relationship directly instead.');
3187
3188
		return $this->$relationship();
3189
	}
3190
3191
	/**
3192
	 * Return the first item matching the given query.
3193
	 * All calls to get_one() are cached.
3194
	 *
3195
	 * @param string $callerClass The class of objects to be returned
3196
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3197
	 * Supports parameterised queries. See SQLQuery::addWhere() for syntax examples.
3198
	 * @param boolean $cache Use caching
3199
	 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3200
	 *
3201
	 * @return DataObject The first item matching the query
3202
	 */
3203
	public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
3204
		$SNG = singleton($callerClass);
3205
3206
		$cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3207
		$cacheKey = md5(var_export($cacheComponents, true));
3208
3209
		// Flush destroyed items out of the cache
3210
		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...
3211
				&& 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...
3212
				&& 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...
3213
3214
			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...
3215
		}
3216
		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...
3217
			$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...
3218
			$item = $dl->First();
3219
3220
			if($cache) {
3221
				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...
3222
				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...
3223
					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...
3224
				}
3225
			}
3226
		}
3227
		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...
3228
	}
3229
3230
	/**
3231
	 * Flush the cached results for all relations (has_one, has_many, many_many)
3232
	 * Also clears any cached aggregate data.
3233
	 *
3234
	 * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3235
	 *                            When false will just clear session-local cached data
3236
	 * @return DataObject $this
3237
	 */
3238
	public function flushCache($persistent = true) {
3239
		if($persistent) Aggregate::flushCache($this->class);
3240
3241
		if($this->class == 'DataObject') {
3242
			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...
3243
			return $this;
3244
		}
3245
3246
		$classes = ClassInfo::ancestry($this->class);
3247
		foreach($classes as $class) {
3248
			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...
3249
		}
3250
3251
		$this->extend('flushCache');
3252
3253
		$this->components = array();
3254
		return $this;
3255
	}
3256
3257
	/**
3258
	 * Flush the get_one global cache and destroy associated objects.
3259
	 */
3260
	public static function flush_and_destroy_cache() {
3261
		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...
3262
			if(is_array($items)) foreach($items as $item) {
3263
				if($item) $item->destroy();
3264
			}
3265
		}
3266
		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...
3267
	}
3268
3269
	/**
3270
	 * Reset all global caches associated with DataObject.
3271
	 */
3272
	public static function reset() {
3273
		self::clear_classname_spec_cache();
3274
		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...
3275
		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...
3276
		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...
3277
		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...
3278
		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...
3279
		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...
3280
		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...
3281
		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...
3282
	}
3283
3284
	/**
3285
	 * Return the given element, searching by ID
3286
	 *
3287
	 * @param string $callerClass The class of the object to be returned
3288
	 * @param int $id The id of the element
3289
	 * @param boolean $cache See {@link get_one()}
3290
	 *
3291
	 * @return DataObject The element
3292
	 */
3293
	public static function get_by_id($callerClass, $id, $cache = true) {
3294
		if(!is_numeric($id)) {
3295
			user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3296
		}
3297
3298
		// Check filter column
3299
		if(is_subclass_of($callerClass, 'DataObject')) {
3300
			$baseClass = ClassInfo::baseDataClass($callerClass);
3301
			$column = "\"$baseClass\".\"ID\"";
3302
		} else{
3303
			// This simpler code will be used by non-DataObject classes that implement DataObjectInterface
3304
			$column = '"ID"';
3305
		}
3306
3307
		// Relegate to get_one
3308
		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...
3309
	}
3310
3311
	/**
3312
	 * Get the name of the base table for this object
3313
	 */
3314
	public function baseTable() {
3315
		$tableClasses = ClassInfo::dataClassesFor($this->class);
3316
		return array_shift($tableClasses);
3317
	}
3318
3319
	/**
3320
	 * @var Array Parameters used in the query that built this object.
3321
	 * This can be used by decorators (e.g. lazy loading) to
3322
	 * run additional queries using the same context.
3323
	 */
3324
	protected $sourceQueryParams;
3325
3326
	/**
3327
	 * @see $sourceQueryParams
3328
	 * @return array
3329
	 */
3330
	public function getSourceQueryParams() {
3331
		return $this->sourceQueryParams;
3332
	}
3333
3334
	/**
3335
	 * @see $sourceQueryParams
3336
	 * @param array
3337
	 */
3338
	public function setSourceQueryParams($array) {
3339
		$this->sourceQueryParams = $array;
3340
	}
3341
3342
	/**
3343
	 * @see $sourceQueryParams
3344
	 * @param array
3345
	 */
3346
	public function setSourceQueryParam($key, $value) {
3347
		$this->sourceQueryParams[$key] = $value;
3348
	}
3349
3350
	/**
3351
	 * @see $sourceQueryParams
3352
	 * @return Mixed
3353
	 */
3354
	public function getSourceQueryParam($key) {
3355
		if(isset($this->sourceQueryParams[$key])) return $this->sourceQueryParams[$key];
3356
		else return null;
3357
	}
3358
3359
	//-------------------------------------------------------------------------------------------//
3360
3361
	/**
3362
	 * Return the database indexes on this table.
3363
	 * This array is indexed by the name of the field with the index, and
3364
	 * the value is the type of index.
3365
	 */
3366
	public function databaseIndexes() {
3367
		$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...
3368
		$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...
3369
		//$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...
3370
3371
		$indexes = array();
3372
3373
		if($has_one) {
3374
			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...
3375
				$indexes[$relationshipName . 'ID'] = true;
3376
			}
3377
		}
3378
3379
		if($classIndexes) {
3380
			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...
3381
				$indexes[$indexName] = $indexType;
3382
			}
3383
		}
3384
3385
		if(get_parent_class($this) == "DataObject") {
3386
			$indexes['ClassName'] = true;
3387
		}
3388
3389
		return $indexes;
3390
	}
3391
3392
	/**
3393
	 * Check the database schema and update it as necessary.
3394
	 *
3395
	 * @uses DataExtension->augmentDatabase()
3396
	 */
3397
	public function requireTable() {
3398
		// Only build the table if we've actually got fields
3399
		$fields = self::database_fields($this->class);
3400
		$extensions = self::database_extensions($this->class);
3401
3402
		$indexes = $this->databaseIndexes();
3403
3404
		// Validate relationship configuration
3405
		$this->validateModelDefinitions();
3406
3407
		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...
3408
			$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
3409
			DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
3410
				$extensions);
3411
		} else {
3412
			DB::dont_require_table($this->class);
3413
		}
3414
3415
		// Build any child tables for many_many items
3416
		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...
3417
			$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...
3418
			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...
3419
				// Build field list
3420
				$manymanyFields = array(
3421
					"{$this->class}ID" => "Int",
3422
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => "Int",
3423
				);
3424
				if(isset($extras[$relationship])) {
3425
					$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
3426
				}
3427
3428
				// Build index list
3429
				$manymanyIndexes = array(
3430
					"{$this->class}ID" => true,
3431
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true,
3432
				);
3433
3434
				DB::require_table("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null,
3435
					$extensions);
3436
			}
3437
		}
3438
3439
		// Let any extentions make their own database fields
3440
		$this->extend('augmentDatabase', $dummy);
3441
	}
3442
3443
	/**
3444
	 * Validate that the configured relations for this class use the correct syntaxes
3445
	 * @throws LogicException
3446
	 */
3447
	protected function validateModelDefinitions() {
3448
		$modelDefinitions = array(
3449
			'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
3450
			'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
3451
			'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
3452
			'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
3453
			'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
3454
			'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
3455
			'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
3456
		);
3457
3458
		foreach($modelDefinitions as $defType => $relations) {
3459
			if( ! $relations) continue;
3460
3461
			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...
3462
				if($defType === 'many_many_extraFields') {
3463
					if(!is_array($v)) {
3464
						throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
3465
							. var_export($k, true) . " => " . var_export($v, true)
3466
							. ". Each many_many_extraFields entry should map to a field specification array.");
3467
					}
3468
				} else {
3469
					if(!is_string($k) || is_numeric($k) || !is_string($v)) {
3470
						throw new LogicException("$this->class::$defType has a bad entry: "
3471
							. var_export($k, true). " => " . var_export($v, true) . ".  Each map key should be a
3472
							 relationship name, and the map value should be the data class to join to.");
3473
					}
3474
				}
3475
			}
3476
		}
3477
	}
3478
3479
	/**
3480
	 * Add default records to database. This function is called whenever the
3481
	 * database is built, after the database tables have all been created. Overload
3482
	 * this to add default records when the database is built, but make sure you
3483
	 * call parent::requireDefaultRecords().
3484
	 *
3485
	 * @uses DataExtension->requireDefaultRecords()
3486
	 */
3487
	public function requireDefaultRecords() {
3488
		$defaultRecords = $this->stat('default_records');
3489
3490
		if(!empty($defaultRecords)) {
3491
			$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...
3492
			if(!$hasData) {
3493
				$className = $this->class;
3494
				foreach($defaultRecords as $record) {
0 ignored issues
show
Bug introduced by
The expression $defaultRecords of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

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