Completed
Push — 3.7 ( 11b87a...bb5701 )
by
unknown
09:09 queued 01:14
created

DataObject::setField()   C

Complexity

Conditions 15
Paths 26

Size

Total Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
nc 26
nop 2
dl 0
loc 59
rs 5.9166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * A single database record & abstract class for the data-access-model.
4
 *
5
 * <h2>Extensions</h2>
6
 *
7
 * See {@link Extension} and {@link DataExtension}.
8
 *
9
 * <h2>Permission Control</h2>
10
 *
11
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
12
 * strings which can be selected on a group-by-group basis.
13
 *
14
 * <code>
15
 * class Article extends DataObject implements PermissionProvider {
16
 *  static $api_access = true;
17
 *
18
 *  function canView($member = false) {
19
 *    return Permission::check('ARTICLE_VIEW');
20
 *  }
21
 *  function canEdit($member = false) {
22
 *    return Permission::check('ARTICLE_EDIT');
23
 *  }
24
 *  function canDelete() {
25
 *    return Permission::check('ARTICLE_DELETE');
26
 *  }
27
 *  function canCreate() {
28
 *    return Permission::check('ARTICLE_CREATE');
29
 *  }
30
 *  function providePermissions() {
31
 *    return array(
32
 *      'ARTICLE_VIEW' => 'Read an article object',
33
 *      'ARTICLE_EDIT' => 'Edit an article object',
34
 *      'ARTICLE_DELETE' => 'Delete an article object',
35
 *      'ARTICLE_CREATE' => 'Create an article object',
36
 *    );
37
 *  }
38
 * }
39
 * </code>
40
 *
41
 * Object-level access control by {@link Group} membership:
42
 * <code>
43
 * class Article extends DataObject {
44
 *   static $api_access = true;
45
 *
46
 *   function canView($member = false) {
47
 *     if(!$member) $member = Member::currentUser();
48
 *     return $member->inGroup('Subscribers');
49
 *   }
50
 *   function canEdit($member = false) {
51
 *     if(!$member) $member = Member::currentUser();
52
 *     return $member->inGroup('Editors');
53
 *   }
54
 *
55
 *   // ...
56
 * }
57
 * </code>
58
 *
59
 * If any public method on this class is prefixed with an underscore,
60
 * the results are cached in memory through {@link cachedCall()}.
61
 *
62
 *
63
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
64
 *  and defineMethods()
65
 *
66
 * @package framework
67
 * @subpackage model
68
 *
69
 * @property integer ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
70
 * @property string ClassName Class name of the DataObject
71
 * @property string LastEdited Date and time of DataObject's last modification.
72
 * @property string Created Date and time of DataObject creation.
73
 */
74
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider {
75
76
	/**
77
	 * Human-readable singular name.
78
	 * @var string
79
	 * @config
80
	 */
81
	private static $singular_name = null;
82
83
	/**
84
	 * Human-readable plural name
85
	 * @var string
86
	 * @config
87
	 */
88
	private static $plural_name = null;
89
90
	/**
91
	 * Allow API access to this object?
92
	 * @todo Define the options that can be set here
93
	 * @config
94
	 */
95
	private static $api_access = false;
96
97
	/**
98
	 * True if this DataObject has been destroyed.
99
	 * @var boolean
100
	 */
101
	public $destroyed = false;
102
103
	/**
104
	 * The DataModel from this this object comes
105
	 */
106
	protected $model;
107
108
	/**
109
	 * Data stored in this objects database record. An array indexed by fieldname.
110
	 *
111
	 * Use {@link toMap()} if you want an array representation
112
	 * of this object, as the $record array might contain lazy loaded field aliases.
113
	 *
114
	 * @var array
115
	 */
116
	protected $record;
117
118
	/**
119
	 * Represents a field that hasn't changed (before === after, thus before == after)
120
	 */
121
	const CHANGE_NONE = 0;
122
123
	/**
124
	 * Represents a field that has changed type, although not the loosely defined value.
125
	 * (before !== after && before == after)
126
	 * E.g. change 1 to true or "true" to true, but not true to 0.
127
	 * Value changes are by nature also considered strict changes.
128
	 */
129
	const CHANGE_STRICT = 1;
130
131
	/**
132
	 * Represents a field that has changed the loosely defined value
133
	 * (before != after, thus, before !== after))
134
	 * E.g. change false to true, but not false to 0
135
	 */
136
	const CHANGE_VALUE = 2;
137
138
	/**
139
	 * An array indexed by fieldname, true if the field has been changed.
140
	 * Use {@link getChangedFields()} and {@link isChanged()} to inspect
141
	 * the changed state.
142
	 *
143
	 * @var array
144
	 */
145
	private $changed;
146
147
	/**
148
	 * The database record (in the same format as $record), before
149
	 * any changes.
150
	 * @var array
151
	 */
152
	protected $original;
153
154
	/**
155
	 * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
156
	 * @var boolean
157
	 */
158
	protected $brokenOnDelete = false;
159
160
	/**
161
	 * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
162
	 * @var boolean
163
	 */
164
	protected $brokenOnWrite = false;
165
166
	/**
167
	 * @config
168
	 * @var boolean Should dataobjects be validated before they are written?
169
	 * Caution: Validation can contain safeguards against invalid/malicious data,
170
	 * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
171
	 * to only disable validation for very specific use cases.
172
	 */
173
	private static $validation_enabled = true;
174
175
	/**
176
	 * Static caches used by relevant functions.
177
	 */
178
	public static $cache_has_own_table = array();
179
	protected static $_cache_db = array();
180
	protected static $_cache_get_one;
181
	protected static $_cache_get_class_ancestry;
182
	protected static $_cache_composite_fields = array();
183
	protected static $_cache_is_composite_field = array();
184
	protected static $_cache_custom_database_fields = array();
185
	protected static $_cache_field_labels = array();
186
187
	// base fields which are not defined in static $db
188
	private static $fixed_fields = array(
189
		'ID' => 'Int',
190
		'ClassName' => 'Enum',
191
		'LastEdited' => 'SS_Datetime',
192
		'Created' => 'SS_Datetime',
193
	);
194
195
	/**
196
	 * Non-static relationship cache, indexed by component name.
197
	 */
198
	protected $components;
199
200
	/**
201
	 * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
202
	 */
203
	protected $unsavedRelations;
204
205
	/**
206
	 * Returns when validation on DataObjects is enabled.
207
	 *
208
	 * @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
209
	 * @return bool
210
	 */
211
	public static function get_validation_enabled() {
212
		Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead');
213
		return Config::inst()->get('DataObject', 'validation_enabled');
214
	}
215
216
	/**
217
	 * Set whether DataObjects should be validated before they are written.
218
	 *
219
	 * Caution: Validation can contain safeguards against invalid/malicious data,
220
	 * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
221
	 * to only disable validation for very specific use cases.
222
	 *
223
	 * @param $enable bool
224
	 * @see DataObject::validate()
225
	 * @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
226
	 */
227
	public static function set_validation_enabled($enable) {
228
		Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead');
229
		Config::inst()->update('DataObject', 'validation_enabled', (bool)$enable);
230
	}
231
232
	/**
233
	 * @var [string] - class => ClassName field definition cache for self::database_fields
234
	 */
235
	private static $classname_spec_cache = array();
236
237
	/**
238
	 * Clear all cached classname specs. It's necessary to clear all cached subclassed names
239
	 * for any classes if a new class manifest is generated.
240
	 */
241
	public static function clear_classname_spec_cache() {
242
		self::$classname_spec_cache = array();
243
		PolymorphicForeignKey::clear_classname_spec_cache();
244
	}
245
246
	/**
247
	 * Determines the specification for the ClassName field for the given class
248
	 *
249
	 * @param string $class
250
	 * @param boolean $queryDB Determine if the DB may be queried for additional information
251
	 * @return string Resulting ClassName spec. If $queryDB is true this will include all
252
	 * legacy types that no longer have concrete classes in PHP
253
	 */
254
	public static function get_classname_spec($class, $queryDB = true) {
255
		// Check cache
256
		if(!empty(self::$classname_spec_cache[$class])) return self::$classname_spec_cache[$class];
257
258
		// Build known class names
259
		$classNames = ClassInfo::subclassesFor($class);
260
261
		// Enhance with existing classes in order to prevent legacy details being lost
262
		if($queryDB && DB::get_schema()->hasField($class, 'ClassName')) {
263
			$existing = DB::query("SELECT DISTINCT \"ClassName\" FROM \"{$class}\"")->column();
264
			$classNames = array_unique(array_merge($classNames, $existing));
265
		}
266
		$spec = "Enum('" . implode(', ', $classNames) . "')";
267
268
		// Only cache full information if queried
269
		if($queryDB) self::$classname_spec_cache[$class] = $spec;
270
		return $spec;
271
	}
272
273
	/**
274
	 * Return the complete map of fields on this object, including "Created", "LastEdited" and "ClassName".
275
	 * See {@link custom_database_fields()} for a getter that excludes these "base fields".
276
	 *
277
	 * @param string $class
278
	 * @param boolean $queryDB Determine if the DB may be queried for additional information
279
	 * @return array
280
	 */
281
	public static function database_fields($class, $queryDB = true) {
282
		if(get_parent_class($class) == 'DataObject') {
283
			// Merge fixed with ClassName spec and custom db fields
284
			$fixed = self::$fixed_fields;
285
			unset($fixed['ID']);
286
			return array_merge(
287
				$fixed,
288
				array('ClassName' => self::get_classname_spec($class, $queryDB)),
289
				self::custom_database_fields($class)
290
			);
291
		}
292
293
		return self::custom_database_fields($class);
294
	}
295
296
	/**
297
	 * Get all database columns explicitly defined on a class in {@link DataObject::$db}
298
	 * and {@link DataObject::$has_one}. Resolves instances of {@link CompositeDBField}
299
	 * into the actual database fields, rather than the name of the field which
300
	 * might not equate a database column.
301
	 *
302
	 * Does not include "base fields" like "ID", "ClassName", "Created", "LastEdited",
303
	 * see {@link database_fields()}.
304
	 *
305
	 * @uses CompositeDBField->compositeDatabaseFields()
306
	 *
307
	 * @param string $class
308
	 * @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
309
	 */
310
	public static function custom_database_fields($class) {
311
		if(isset(self::$_cache_custom_database_fields[$class])) {
312
			return self::$_cache_custom_database_fields[$class];
313
		}
314
315
		$fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
316
317
		foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) {
318
			// Remove the original fieldname, it's not an actual database column
319
			unset($fields[$fieldName]);
320
321
			// Add all composite columns
322
			$compositeFields = singleton($fieldClass)->compositeDatabaseFields();
323
			if($compositeFields) foreach($compositeFields as $compositeName => $spec) {
324
				$fields["{$fieldName}{$compositeName}"] = $spec;
325
			}
326
		}
327
328
		// Add has_one relationships
329
		$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
330
		if($hasOne) foreach(array_keys($hasOne) as $field) {
331
332
			// Check if this is a polymorphic relation, in which case the relation
333
			// is a composite field
334
			if($hasOne[$field] === 'DataObject') {
335
				$relationField = DBField::create_field('PolymorphicForeignKey', null, $field);
336
				$relationField->setTable($class);
337
				if($compositeFields = $relationField->compositeDatabaseFields()) {
338
					foreach($compositeFields as $compositeName => $spec) {
339
						$fields["{$field}{$compositeName}"] = $spec;
340
					}
341
				}
342
			} else {
343
				$fields[$field . 'ID'] = 'ForeignKey';
344
			}
345
		}
346
347
		$output = (array) $fields;
348
349
		self::$_cache_custom_database_fields[$class] = $output;
350
351
		return $output;
352
	}
353
354
	/**
355
	 * Returns the field class if the given db field on the class is a composite field.
356
	 * Will check all applicable ancestor classes and aggregate results.
357
	 *
358
	 * @param string $class Class to check
359
	 * @param string $name Field to check
360
	 * @param boolean $aggregated True if parent classes should be checked, or false to limit to this class
361
	 * @return string Class name of composite field if it exists
362
	 */
363
	public static function is_composite_field($class, $name, $aggregated = true) {
364
		$key = $class . '_' . $name . '_' . (string)$aggregated;
365
366
		if(!isset(DataObject::$_cache_is_composite_field[$key])) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
367
			$isComposite = null;
368
369
			if(!isset(DataObject::$_cache_composite_fields[$class])) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
370
				self::cache_composite_fields($class);
371
			}
372
373
			if(isset(DataObject::$_cache_composite_fields[$class][$name])) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
374
				$isComposite = DataObject::$_cache_composite_fields[$class][$name];
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
375
			} elseif($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
376
				$isComposite = self::is_composite_field($parentClass, $name);
377
			}
378
379
			DataObject::$_cache_is_composite_field[$key] = ($isComposite) ? $isComposite : false;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
380
		}
381
382
		return DataObject::$_cache_is_composite_field[$key] ?: null;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
383
	}
384
385
	/**
386
	 * Returns a list of all the composite if the given db field on the class is a composite field.
387
	 * Will check all applicable ancestor classes and aggregate results.
388
	 */
389
	public static function composite_fields($class, $aggregated = true) {
390
		if(!isset(DataObject::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
391
392
		$compositeFields = DataObject::$_cache_composite_fields[$class];
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
393
394
		if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
395
			$compositeFields = array_merge($compositeFields,
396
				self::composite_fields($parentClass));
397
		}
398
399
		return $compositeFields;
400
	}
401
402
	/**
403
	 * Internal cacher for the composite field information
404
	 */
405
	private static function cache_composite_fields($class) {
406
		$compositeFields = array();
407
408
		$fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
409
		if($fields) foreach($fields as $fieldName => $fieldClass) {
0 ignored issues
show
Bug introduced by
The expression $fields of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
410
			if(!is_string($fieldClass)) continue;
411
412
			// Strip off any parameters
413
			$bPos = strpos($fieldClass, '(');
414
			if($bPos !== FALSE) $fieldClass = substr($fieldClass, 0, $bPos);
415
416
			// Test to see if it implements CompositeDBField
417
			if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
418
				$compositeFields[$fieldName] = $fieldClass;
419
			}
420
		}
421
422
		DataObject::$_cache_composite_fields[$class] = $compositeFields;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
423
	}
424
425
	/**
426
	 * Construct a new DataObject.
427
	 *
428
	 * @param array|null $record Used internally for rehydrating an object from database content.
429
	 *                           Bypasses setters on this class, and hence should not be used
430
	 *                           for populating data on new records.
431
	 * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
432
	 *                             Singletons don't have their defaults set.
433
	 */
434
	public function __construct($record = null, $isSingleton = false, $model = null) {
435
		parent::__construct();
436
437
		// Set the fields data.
438
		if(!$record) {
439
			$record = array(
440
				'ID' => 0,
441
				'ClassName' => get_class($this),
442
				'RecordClassName' => get_class($this)
443
			);
444
		}
445
446
		if(!is_array($record) && !is_a($record, "stdClass")) {
447
			if(is_object($record)) $passed = "an object of type '$record->class'";
448
			else $passed = "The value '$record'";
449
450
			user_error("DataObject::__construct passed $passed.  It's supposed to be passed an array,"
451
				. " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
452
				E_USER_WARNING);
453
			$record = null;
454
		}
455
456
		if(is_a($record, "stdClass")) {
457
			$record = (array)$record;
458
		}
459
460
		// Set $this->record to $record, but ignore NULLs
461
		$this->record = array();
462
		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...
463
			// Ensure that ID is stored as a number and not a string
464
			// To do: this kind of clean-up should be done on all numeric fields, in some relatively
465
			// performant manner
466
			if($v !== null) {
467
				if($k == 'ID' && is_numeric($v)) $this->record[$k] = (int)$v;
468
				else $this->record[$k] = $v;
469
			}
470
		}
471
472
		// Identify fields that should be lazy loaded, but only on existing records
473
		if(!empty($record['ID'])) {
474
			$currentObj = get_class($this);
475
			while($currentObj != 'DataObject') {
476
				$fields = self::custom_database_fields($currentObj);
477
				foreach($fields as $field => $type) {
478
					if(!array_key_exists($field, $record)) $this->record[$field.'_Lazy'] = $currentObj;
479
				}
480
				$currentObj = get_parent_class($currentObj);
481
			}
482
		}
483
484
		$this->original = $this->record;
485
486
		// Keep track of the modification date of all the data sourced to make this page
487
		// From this we create a Last-Modified HTTP header
488
		if(isset($record['LastEdited'])) {
489
			HTTP::register_modification_date($record['LastEdited']);
490
		}
491
492
		// this must be called before populateDefaults(), as field getters on a DataObject
493
		// may call getComponent() and others, which rely on $this->model being set.
494
		$this->model = $model ? $model : DataModel::inst();
495
496
		// Must be called after parent constructor
497
		if(!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
498
			$this->populateDefaults();
499
		}
500
501
		// prevent populateDefaults() and setField() from marking overwritten defaults as changed
502
		$this->changed = array();
503
	}
504
505
	/**
506
	 * Set the DataModel
507
	 * @param DataModel $model
508
	 * @return DataObject $this
509
	 */
510
	public function setDataModel(DataModel $model) {
511
		$this->model = $model;
512
		return $this;
513
	}
514
515
	/**
516
	 * Destroy all of this objects dependant objects and local caches.
517
	 * You'll need to call this to get the memory of an object that has components or extensions freed.
518
	 */
519
	public function destroy() {
520
		//$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...
521
		gc_collect_cycles();
522
		$this->flushCache(false);
523
	}
524
525
	/**
526
	 * Create a duplicate of this node.
527
	 * Note: now also duplicates relations.
528
	 *
529
	 * @param $doWrite Perform a write() operation before returning the object.  If this is true, it will create the
530
	 *                 duplicate in the database.
531
	 * @return DataObject A duplicate of this node. The exact type will be the type of this node.
532
	 */
533
	public function duplicate($doWrite = true) {
534
		$className = $this->class;
535
		$map = $this->toMap();
536
		unset($map['Created']);
537
		$clone = new $className( $map, false, $this->model );
538
		$clone->ID = 0;
539
540
		$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
541
		if($doWrite) {
542
			$clone->write();
543
			$this->duplicateManyManyRelations($this, $clone);
544
		}
545
		$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
546
547
		return $clone;
548
	}
549
550
	/**
551
	 * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object
552
	 * The destinationObject must be written to the database already and have an ID. Writing is performed
553
	 * automatically when adding the new relations.
554
	 *
555
	 * @param $sourceObject the source object to duplicate from
556
	 * @param $destinationObject the destination object to populate with the duplicated relations
557
	 * @return DataObject with the new many_many relations copied in
558
	 */
559
	protected function duplicateManyManyRelations($sourceObject, $destinationObject) {
560
		if (!$destinationObject || $destinationObject->ID < 1) {
561
			user_error("Can't duplicate relations for an object that has not been written to the database",
562
				E_USER_ERROR);
563
		}
564
565
		//duplicate complex relations
566
		// DO NOT copy has_many relations, because copying the relation would result in us changing the has_one
567
		// relation on the other side of this relation to point at the copy and no longer the original (being a
568
		// has_one, it can only point at one thing at a time). So, all relations except has_many can and are copied
569
		if ($sourceObject->hasOne()) foreach($sourceObject->hasOne() as $name => $type) {
570
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
571
		}
572
		if ($sourceObject->manyMany()) foreach($sourceObject->manyMany() as $name => $type) {
573
			//many_many include belongs_many_many
574
			$this->duplicateRelations($sourceObject, $destinationObject, $name);
575
		}
576
577
		return $destinationObject;
578
	}
579
580
	/**
581
	 * Helper function to duplicate relations from one object to another
582
	 * @param $sourceObject the source object to duplicate from
583
	 * @param $destinationObject the destination object to populate with the duplicated relations
584
	 * @param $name the name of the relation to duplicate (e.g. members)
585
	 */
586
	private function duplicateRelations($sourceObject, $destinationObject, $name) {
587
		$relations = $sourceObject->$name();
588
		if ($relations) {
589
            if ($relations instanceOf ManyManyList) { //many-to-many relation
590
                $extraFieldNames = $relations->getExtraFields();
591
592
                if ($relations->Count() > 0) {  //with more than one thing it is related to
593
					foreach($relations as $relation) {
594
                        // Merge extra fields
595
                        $extraFields = array();
596
                        foreach ($extraFieldNames as $fieldName => $fieldType) {
597
                            $extraFields[$fieldName] = $relation->getField($fieldName);
598
                        }
599
                        $destinationObject->$name()->add($relation, $extraFields);
600
                    }
601
                }
602
            } else if ($relations instanceOf RelationList) {   //many-to-something relation
603
				if ($relations->Count() > 0) {  //with more than one thing it is related to
604
					foreach($relations as $relation) {
605
						$destinationObject->$name()->add($relation);
606
					}
607
				}
608
			} else {    //one-to-one relation
609
				$destinationObject->{"{$name}ID"} = $relations->ID;
610
			}
611
		}
612
	}
613
614
	public function getObsoleteClassName() {
615
		$className = $this->getField("ClassName");
616
		if (!ClassInfo::exists($className)) return $className;
617
	}
618
619
	public function getClassName() {
620
		$className = $this->getField("ClassName");
621
		if (!ClassInfo::exists($className)) return get_class($this);
622
		return $className;
623
	}
624
625
	/**
626
	 * Set the ClassName attribute. {@link $class} is also updated.
627
	 * Warning: This will produce an inconsistent record, as the object
628
	 * instance will not automatically switch to the new subclass.
629
	 * Please use {@link newClassInstance()} for this purpose,
630
	 * or destroy and reinstanciate the record.
631
	 *
632
	 * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
633
	 * @return DataObject $this
634
	 */
635
	public function setClassName($className) {
636
		$className = trim($className);
637
		if(!$className || !is_subclass_of($className, 'DataObject')) return;
638
639
		$this->class = $className;
640
		$this->setField("ClassName", $className);
641
		return $this;
642
	}
643
644
	/**
645
	 * Create a new instance of a different class from this object's record.
646
	 * This is useful when dynamically changing the type of an instance. Specifically,
647
	 * it ensures that the instance of the class is a match for the className of the
648
	 * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
649
	 * property manually before calling this method, as it will confuse change detection.
650
	 *
651
	 * If the new class is different to the original class, defaults are populated again
652
	 * because this will only occur automatically on instantiation of a DataObject if
653
	 * there is no record, or the record has no ID. In this case, we do have an ID but
654
	 * we still need to repopulate the defaults.
655
	 *
656
	 * @param string $newClassName The name of the new class
657
	 *
658
	 * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
659
	 */
660
	public function newClassInstance($newClassName) {
661
		$originalClass = $this->ClassName;
662
		$newInstance = new $newClassName(array_merge(
663
			$this->record,
664
			array(
665
				'ClassName' => $originalClass,
666
				'RecordClassName' => $originalClass,
667
			)
668
		), false, $this->model);
669
670
		if($newClassName != $originalClass) {
671
			$newInstance->setClassName($newClassName);
672
			$newInstance->populateDefaults();
673
			$newInstance->forceChange();
674
		}
675
676
		return $newInstance;
677
	}
678
679
	/**
680
	 * Adds methods from the extensions.
681
	 * Called by Object::__construct() once per class.
682
	 */
683
	public function defineMethods() {
684
		parent::defineMethods();
685
686
		// Define the extra db fields - this is only necessary for extensions added in the
687
		// class definition.  Object::add_extension() will call this at definition time for
688
		// those objects, which is a better mechanism.  Perhaps extensions defined inside the
689
		// class def can somehow be applied at definiton time also?
690
		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...
691
			if(!$instance->class) {
692
				$class = get_class($instance);
693
				user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls"
694
					. " parent::__construct()", E_USER_ERROR);
695
			}
696
		}
697
698
		if($this->class == 'DataObject') return;
699
700
		// Set up accessors for joined items
701
		if($manyMany = $this->manyMany()) {
702
			foreach($manyMany as $relationship => $class) {
703
				$this->addWrapperMethod($relationship, 'getManyManyComponents');
704
			}
705
		}
706
		if($hasMany = $this->hasMany()) {
707
708
			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...
709
				$this->addWrapperMethod($relationship, 'getComponents');
710
			}
711
712
		}
713
		if($hasOne = $this->hasOne()) {
714
			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...
715
				$this->addWrapperMethod($relationship, 'getComponent');
716
			}
717
		}
718
		if($belongsTo = $this->belongsTo()) foreach(array_keys($belongsTo) as $relationship) {
719
			$this->addWrapperMethod($relationship, 'getComponent');
720
		}
721
	}
722
723
	/**
724
	 * Returns true if this object "exists", i.e., has a sensible value.
725
	 * The default behaviour for a DataObject is to return true if
726
	 * the object exists in the database, you can override this in subclasses.
727
	 *
728
	 * @return boolean true if this object exists
729
	 */
730
	public function exists() {
731
		return (isset($this->record['ID']) && $this->record['ID'] > 0);
732
	}
733
734
	/**
735
	 * Returns TRUE if all values (other than "ID") are
736
	 * considered empty (by weak boolean comparison).
737
	 * Only checks for fields listed in {@link custom_database_fields()}
738
	 *
739
	 * @todo Use DBField->hasValue()
740
	 *
741
	 * @return boolean
742
	 */
743
	public function isEmpty(){
744
		$isEmpty = true;
745
		$customFields = self::custom_database_fields(get_class($this));
746
		if($map = $this->toMap()){
747
			foreach($map as $k=>$v){
748
				// only look at custom fields
749
				if(!array_key_exists($k, $customFields)) continue;
750
751
				$dbObj = ($v instanceof DBField) ? $v : $this->dbObject($k);
752
				$isEmpty = ($isEmpty && !$dbObj->exists());
753
			}
754
		}
755
		return $isEmpty;
756
	}
757
758
	/**
759
	 * Get the user friendly singular name of this DataObject.
760
	 * If the name is not defined (by redefining $singular_name in the subclass),
761
	 * this returns the class name.
762
	 *
763
	 * @return string User friendly singular name of this DataObject
764
	 */
765
	public function singular_name() {
766
		if(!$name = $this->stat('singular_name')) {
767
			$name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $this->class))));
768
		}
769
770
		return $name;
771
	}
772
773
	/**
774
	 * Get the translated user friendly singular name of this DataObject
775
	 * same as singular_name() but runs it through the translating function
776
	 *
777
	 * Translating string is in the form:
778
	 *     $this->class.SINGULARNAME
779
	 * Example:
780
	 *     Page.SINGULARNAME
781
	 *
782
	 * @return string User friendly translated singular name of this DataObject
783
	 */
784
	public function i18n_singular_name() {
785
		return _t($this->class.'.SINGULARNAME', $this->singular_name());
786
	}
787
788
	/**
789
	 * Get the user friendly plural name of this DataObject
790
	 * If the name is not defined (by renaming $plural_name in the subclass),
791
	 * this returns a pluralised version of the class name.
792
	 *
793
	 * @return string User friendly plural name of this DataObject
794
	 */
795
	public function plural_name() {
796
		if($name = $this->stat('plural_name')) {
797
			return $name;
798
		} else {
799
			$name = $this->singular_name();
800
			//if the penultimate character is not a vowel, replace "y" with "ies"
801
			if (preg_match('/[^aeiou]y$/i', $name)) {
802
				$name = substr($name,0,-1) . 'ie';
803
			}
804
			return ucfirst($name . 's');
805
		}
806
	}
807
808
	/**
809
	 * Get the translated user friendly plural name of this DataObject
810
	 * Same as plural_name but runs it through the translation function
811
	 * Translation string is in the form:
812
	 *      $this->class.PLURALNAME
813
	 * Example:
814
	 *      Page.PLURALNAME
815
	 *
816
	 * @return string User friendly translated plural name of this DataObject
817
	 */
818
	public function i18n_plural_name()
819
	{
820
		$name = $this->plural_name();
821
		return _t($this->class.'.PLURALNAME', $name);
822
	}
823
824
	/**
825
	 * Standard implementation of a title/label for a specific
826
	 * record. Tries to find properties 'Title' or 'Name',
827
	 * and falls back to the 'ID'. Useful to provide
828
	 * user-friendly identification of a record, e.g. in errormessages
829
	 * or UI-selections.
830
	 *
831
	 * Overload this method to have a more specialized implementation,
832
	 * e.g. for an Address record this could be:
833
	 * <code>
834
	 * function getTitle() {
835
	 *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
836
	 * }
837
	 * </code>
838
	 *
839
	 * @return string
840
	 */
841
	public function getTitle() {
842
		if($this->hasDatabaseField('Title')) return $this->getField('Title');
843
		if($this->hasDatabaseField('Name')) return $this->getField('Name');
844
845
		return "#{$this->ID}";
846
	}
847
848
	/**
849
	 * Returns the associated database record - in this case, the object itself.
850
	 * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
851
	 *
852
	 * @return DataObject Associated database record
853
	 */
854
	public function data() {
855
		return $this;
856
	}
857
858
	/**
859
	 * Convert this object to a map.
860
	 *
861
	 * @return array The data as a map.
862
	 */
863
	public function toMap() {
864
		$this->loadLazyFields();
865
		return $this->record;
866
	}
867
868
	/**
869
	 * Return all currently fetched database fields.
870
	 *
871
	 * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
872
	 * Obviously, this makes it a lot faster.
873
	 *
874
	 * @return array The data as a map.
875
	 */
876
	public function getQueriedDatabaseFields() {
877
		return $this->record;
878
	}
879
880
	/**
881
	 * Update a number of fields on this object, given a map of the desired changes.
882
	 *
883
	 * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
884
	 * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
885
	 *
886
	 * update() doesn't write the main object, but if you use the dot syntax, it will write()
887
	 * the related objects that it alters.
888
	 *
889
	 * @param array $data A map of field name to data values to update.
890
	 * @return DataObject $this
891
	 */
892
	public function update($data) {
893
		foreach($data as $k => $v) {
894
			// Implement dot syntax for updates
895
			if(strpos($k,'.') !== false) {
896
				$relations = explode('.', $k);
897
				$fieldName = array_pop($relations);
898
				$relObj = $this;
899
				foreach($relations as $i=>$relation) {
900
					// no support for has_many or many_many relationships,
901
					// as the updater wouldn't know which object to write to (or create)
902
					if($relObj->$relation() instanceof DataObject) {
903
						$parentObj = $relObj;
904
						$relObj = $relObj->$relation();
905
						// If the intermediate relationship objects have been created, then write them
906
						if($i<sizeof($relations)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
907
							$relObj->write();
908
							$relatedFieldName = $relation."ID";
909
							$parentObj->$relatedFieldName = $relObj->ID;
910
							$parentObj->write();
911
						}
912
					} else {
913
						user_error(
914
							"DataObject::update(): Can't traverse relationship '$relation'," .
915
							"it has to be a has_one relationship or return a single DataObject",
916
							E_USER_NOTICE
917
						);
918
						// unset relation object so we don't write properties to the wrong object
919
						unset($relObj);
920
						break;
921
					}
922
				}
923
924
				if($relObj) {
925
					$relObj->$fieldName = $v;
926
					$relObj->write();
927
					$relatedFieldName = $relation."ID";
0 ignored issues
show
Bug introduced by
The variable $relation seems to be defined by a foreach iteration on line 899. Are you sure the iterator is never empty, otherwise this variable is not defined?

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

foreach ($a as $b) {
}

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


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

// $b is now guaranteed to be defined here.
Loading history...
928
					$this->$relatedFieldName = $relObj->ID;
929
					$relObj->flushCache();
930
				} else {
931
					user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
932
				}
933
			} else {
934
				$this->$k = $v;
935
			}
936
		}
937
		return $this;
938
	}
939
940
	/**
941
	 * Pass changes as a map, and try to
942
	 * get automatic casting for these fields.
943
	 * Doesn't write to the database. To write the data,
944
	 * use the write() method.
945
	 *
946
	 * @param array $data A map of field name to data values to update.
947
	 * @return DataObject $this
948
	 */
949
	public function castedUpdate($data) {
950
		foreach($data as $k => $v) {
951
			$this->setCastedField($k,$v);
952
		}
953
		return $this;
954
	}
955
956
	/**
957
	 * Merges data and relations from another object of same class,
958
	 * without conflict resolution. Allows to specify which
959
	 * dataset takes priority in case its not empty.
960
	 * has_one-relations are just transferred with priority 'right'.
961
	 * has_many and many_many-relations are added regardless of priority.
962
	 *
963
	 * Caution: has_many/many_many relations are moved rather than duplicated,
964
	 * meaning they are not connected to the merged object any longer.
965
	 * Caution: Just saves updated has_many/many_many relations to the database,
966
	 * doesn't write the updated object itself (just writes the object-properties).
967
	 * Caution: Does not delete the merged object.
968
	 * Caution: Does now overwrite Created date on the original object.
969
	 *
970
	 * @param $obj DataObject
971
	 * @param $priority String left|right Determines who wins in case of a conflict (optional)
972
	 * @param $includeRelations Boolean Merge any existing relations (optional)
973
	 * @param $overwriteWithEmpty Boolean Overwrite existing left values with empty right values.
974
	 *                            Only applicable with $priority='right'. (optional)
975
	 * @return Boolean
976
	 */
977
	public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) {
978
		$leftObj = $this;
979
980
		if($leftObj->ClassName != $rightObj->ClassName) {
981
			// we can't merge similiar subclasses because they might have additional relations
982
			user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
983
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
984
			return false;
985
		}
986
987
		if(!$rightObj->ID) {
988
			user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
989
				to make sure all relations are transferred properly.').", E_USER_WARNING);
990
			return false;
991
		}
992
993
		// makes sure we don't merge data like ID or ClassName
994
		$leftData = $leftObj->inheritedDatabaseFields();
995
		$rightData = $rightObj->inheritedDatabaseFields();
996
997
		foreach($rightData as $key=>$rightVal) {
998
			// don't merge conflicting values if priority is 'left'
999
			if($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) continue;
1000
1001
			// don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
1002
			if($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) continue;
1003
1004
			// TODO remove redundant merge of has_one fields
1005
			$leftObj->{$key} = $rightObj->{$key};
1006
		}
1007
1008
		// merge relations
1009
		if($includeRelations) {
1010
			if($manyMany = $this->manyMany()) {
1011
				foreach($manyMany as $relationship => $class) {
1012
					$leftComponents = $leftObj->getManyManyComponents($relationship);
1013
					$rightComponents = $rightObj->getManyManyComponents($relationship);
1014
					if($rightComponents && $rightComponents->exists()) {
1015
						$leftComponents->addMany($rightComponents->column('ID'));
1016
					}
1017
					$leftComponents->write();
1018
				}
1019
			}
1020
1021
			if($hasMany = $this->hasMany()) {
1022
				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...
1023
					$leftComponents = $leftObj->getComponents($relationship);
1024
					$rightComponents = $rightObj->getComponents($relationship);
1025
					if($rightComponents && $rightComponents->exists()) {
1026
						$leftComponents->addMany($rightComponents->column('ID'));
1027
					}
1028
					$leftComponents->write();
1029
				}
1030
1031
			}
1032
1033
			if($hasOne = $this->hasOne()) {
1034
				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...
1035
					$leftComponent = $leftObj->getComponent($relationship);
1036
					$rightComponent = $rightObj->getComponent($relationship);
1037
					if($leftComponent->exists() && $rightComponent->exists() && $priority == 'right') {
1038
						$leftObj->{$relationship . 'ID'} = $rightObj->{$relationship . 'ID'};
1039
					}
1040
				}
1041
			}
1042
		}
1043
1044
		return true;
1045
	}
1046
1047
	/**
1048
	 * Forces the record to think that all its data has changed.
1049
	 * Doesn't write to the database. Only sets fields as changed
1050
	 * if they are not already marked as changed.
1051
	 *
1052
	 * @return $this
1053
	 */
1054
	public function forceChange() {
1055
		// Ensure lazy fields loaded
1056
		$this->loadLazyFields();
1057
1058
		// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1059
		$fieldNames = array_unique(array_merge(
1060
			array_keys($this->record),
1061
			array_keys($this->inheritedDatabaseFields())
1062
		));
1063
1064
		foreach($fieldNames as $fieldName) {
1065
			if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
1066
			// Populate the null values in record so that they actually get written
1067
			if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
1068
		}
1069
1070
		// @todo Find better way to allow versioned to write a new version after forceChange
1071
		if($this->isChanged('Version')) unset($this->changed['Version']);
1072
		return $this;
1073
	}
1074
1075
	/**
1076
	 * Validate the current object.
1077
	 *
1078
	 * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1079
	 * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1080
	 *
1081
	 * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1082
	 * and onAfterWrite() won't get called either.
1083
	 *
1084
	 * It is expected that you call validate() in your own application to test that an object is valid before
1085
	 * attempting a write, and respond appropriately if it isn't.
1086
	 *
1087
	 * @see {@link ValidationResult}
1088
	 * @return ValidationResult
1089
	 */
1090
	protected function validate() {
1091
		$result = ValidationResult::create();
1092
		$this->extend('validate', $result);
1093
		return $result;
1094
	}
1095
1096
	/**
1097
	 * Public accessor for {@see DataObject::validate()}
1098
	 *
1099
	 * @return ValidationResult
1100
	 */
1101
	public function doValidate() {
1102
		// validate will be public in 4.0
1103
		return $this->validate();
1104
	}
1105
1106
	/**
1107
	 * Event handler called before writing to the database.
1108
	 * You can overload this to clean up or otherwise process data before writing it to the
1109
	 * database.  Don't forget to call parent::onBeforeWrite(), though!
1110
	 *
1111
	 * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1112
	 *
1113
	 * @uses DataExtension->onBeforeWrite()
1114
	 */
1115
	protected function onBeforeWrite() {
1116
		$this->brokenOnWrite = false;
1117
1118
		$dummy = null;
1119
		$this->extend('onBeforeWrite', $dummy);
1120
	}
1121
1122
	/**
1123
	 * Event handler called after writing to the database.
1124
	 * You can overload this to act upon changes made to the data after it is written.
1125
	 * $this->changed will have a record
1126
	 * database.  Don't forget to call parent::onAfterWrite(), though!
1127
	 *
1128
	 * @uses DataExtension->onAfterWrite()
1129
	 */
1130
	protected function onAfterWrite() {
1131
		$dummy = null;
1132
		$this->extend('onAfterWrite', $dummy);
1133
	}
1134
1135
	/**
1136
	 * Event handler called before deleting from the database.
1137
	 * You can overload this to clean up or otherwise process data before delete this
1138
	 * record.  Don't forget to call parent::onBeforeDelete(), though!
1139
	 *
1140
	 * @uses DataExtension->onBeforeDelete()
1141
	 */
1142
	protected function onBeforeDelete() {
1143
		$this->brokenOnDelete = false;
1144
1145
		$dummy = null;
1146
		$this->extend('onBeforeDelete', $dummy);
1147
	}
1148
1149
	protected function onAfterDelete() {
1150
		$this->extend('onAfterDelete');
1151
	}
1152
1153
	/**
1154
	 * Load the default values in from the self::$defaults array.
1155
	 * Will traverse the defaults of the current class and all its parent classes.
1156
	 * Called by the constructor when creating new records.
1157
	 *
1158
	 * @uses DataExtension->populateDefaults()
1159
	 * @return DataObject $this
1160
	 */
1161
	public function populateDefaults() {
1162
		$classes = array_reverse(ClassInfo::ancestry($this));
1163
1164
		foreach($classes as $class) {
1165
			$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1166
1167
			if($defaults && !is_array($defaults)) {
1168
				user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
1169
					E_USER_WARNING);
1170
				$defaults = null;
1171
			}
1172
1173
			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...
1174
				// SRM 2007-03-06: Stricter check
1175
				if(!isset($this->$fieldName) || $this->$fieldName === null) {
1176
					$this->$fieldName = $fieldValue;
1177
				}
1178
				// Set many-many defaults with an array of ids
1179
				if(is_array($fieldValue) && $this->manyManyComponent($fieldName)) {
1180
					$manyManyJoin = $this->$fieldName();
1181
					$manyManyJoin->setByIdList($fieldValue);
1182
				}
1183
			}
1184
			if($class == 'DataObject') {
1185
				break;
1186
			}
1187
		}
1188
1189
		$this->extend('populateDefaults');
1190
		return $this;
1191
	}
1192
1193
	/**
1194
	 * Determine validation of this object prior to write
1195
	 *
1196
	 * @return ValidationException Exception generated by this write, or null if valid
1197
	 */
1198
	protected function validateWrite() {
1199
		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...
1200
			return new ValidationException(
1201
				"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...
1202
				"you need to change the ClassName before you can write it",
1203
				E_USER_WARNING
1204
			);
1205
		}
1206
1207
		if(Config::inst()->get('DataObject', 'validation_enabled')) {
1208
			$result = $this->validate();
1209
			if (!$result->valid()) {
1210
				return new ValidationException(
1211
					$result,
1212
					$result->message(),
1213
					E_USER_WARNING
1214
				);
1215
			}
1216
		}
1217
	}
1218
1219
	/**
1220
	 * Prepare an object prior to write
1221
	 *
1222
	 * @throws ValidationException
1223
	 */
1224
	protected function preWrite() {
1225
		// Validate this object
1226
		if($writeException = $this->validateWrite()) {
1227
			// Used by DODs to clean up after themselves, eg, Versioned
1228
			$this->invokeWithExtensions('onAfterSkippedWrite');
1229
			throw $writeException;
1230
		}
1231
1232
		// Check onBeforeWrite
1233
		$this->brokenOnWrite = true;
1234
		$this->onBeforeWrite();
1235
		if($this->brokenOnWrite) {
1236
			user_error("$this->class has a broken onBeforeWrite() function."
1237
				. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1238
		}
1239
	}
1240
1241
	/**
1242
	 * Detects and updates all changes made to this object
1243
	 *
1244
	 * @param bool $forceChanges If set to true, force all fields to be treated as changed
1245
	 * @return bool True if any changes are detected
1246
	 */
1247
	protected function updateChanges($forceChanges = false)
1248
	{
1249
		if($forceChanges) {
1250
			// Force changes, but only for loaded fields
1251
			foreach($this->record as $field => $value) {
1252
				$this->changed[$field] = static::CHANGE_VALUE;
1253
			}
1254
			return true;
1255
		}
1256
		return $this->isChanged();
1257
	}
1258
1259
	/**
1260
	 * Writes a subset of changes for a specific table to the given manipulation
1261
	 *
1262
	 * @param string $baseTable Base table
1263
	 * @param string $now Timestamp to use for the current time
1264
	 * @param bool $isNewRecord Whether this should be treated as a new record write
1265
	 * @param array $manipulation Manipulation to write to
1266
	 * @param string $class Table and Class to select and write to
1267
	 */
1268
	protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
1269
		$manipulation[$class] = array();
1270
1271
		// Extract records for this table
1272
		foreach($this->record as $fieldName => $fieldValue) {
1273
1274
			// Check if this record pertains to this table, and
1275
			// we're not attempting to reset the BaseTable->ID
1276
			if(	empty($this->changed[$fieldName])
1277
				|| ($class === $baseTable && $fieldName === 'ID')
1278
				|| (!self::has_own_table_database_field($class, $fieldName)
1279
					&& !self::is_composite_field($class, $fieldName, false))
1280
			) {
1281
				continue;
1282
			}
1283
1284
1285
			// if database column doesn't correlate to a DBField instance...
1286
			$fieldObj = $this->dbObject($fieldName);
1287
			if(!$fieldObj) {
1288
				$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1289
			}
1290
1291
			// Ensure DBField is repopulated and written to the manipulation
1292
			$fieldObj->setValue($fieldValue, $this->record);
1293
			$fieldObj->writeToManipulation($manipulation[$class]);
1294
		}
1295
1296
		// Ensure update of Created and LastEdited columns
1297
		if($baseTable === $class) {
1298
			$manipulation[$class]['fields']['LastEdited'] = $now;
1299
			if($isNewRecord) {
1300
				$manipulation[$class]['fields']['Created']
1301
					= empty($this->record['Created'])
1302
						? $now
1303
						: $this->record['Created'];
1304
				$manipulation[$class]['fields']['ClassName'] = $this->class;
1305
			}
1306
		}
1307
1308
		// Inserts done one the base table are performed in another step, so the manipulation should instead
1309
		// attempt an update, as though it were a normal update.
1310
		$manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update';
1311
		$manipulation[$class]['id'] = $this->record['ID'];
1312
	}
1313
1314
	/**
1315
	 * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1316
	 *
1317
	 * Does nothing if an ID is already assigned for this record
1318
	 *
1319
	 * @param string $baseTable Base table
1320
	 * @param string $now Timestamp to use for the current time
1321
	 */
1322
	protected function writeBaseRecord($baseTable, $now) {
1323
		// Generate new ID if not specified
1324
		if($this->isInDB()) return;
1325
1326
		// Perform an insert on the base table
1327
		$insert = new SQLInsert('"'.$baseTable.'"');
1328
		$insert
1329
			->assign('"Created"', $now)
1330
			->execute();
1331
		$this->changed['ID'] = self::CHANGE_VALUE;
1332
		$this->record['ID'] = DB::get_generated_id($baseTable);
1333
	}
1334
1335
	/**
1336
	 * Generate and write the database manipulation for all changed fields
1337
	 *
1338
	 * @param string $baseTable Base table
1339
	 * @param string $now Timestamp to use for the current time
1340
	 * @param bool $isNewRecord If this is a new record
1341
	 */
1342
	protected function writeManipulation($baseTable, $now, $isNewRecord) {
1343
		// Generate database manipulations for each class
1344
		$manipulation = array();
1345
		foreach($this->getClassAncestry() as $class) {
1346
			if(self::has_own_table($class)) {
1347
				$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1348
			}
1349
		}
1350
1351
		// Allow extensions to extend this manipulation
1352
		$this->extend('augmentWrite', $manipulation);
1353
1354
		// New records have their insert into the base data table done first, so that they can pass the
1355
		// generated ID on to the rest of the manipulation
1356
		if($isNewRecord) {
1357
			$manipulation[$baseTable]['command'] = 'update';
1358
		}
1359
1360
		// Make sure none of our field assignment are arrays
1361
		foreach ($manipulation as $tableManipulation) {
1362
			if (!isset($tableManipulation['fields'])) {
1363
				continue;
1364
			}
1365
			foreach ($tableManipulation['fields'] as $fieldValue) {
1366
				if (is_array($fieldValue)) {
1367
					user_error(
1368
						'DataObject::writeManipulation: parameterised field assignments are disallowed',
1369
						E_USER_ERROR
1370
					);
1371
				}
1372
			}
1373
		}
1374
1375
		// Perform the manipulation
1376
		DB::manipulate($manipulation);
1377
	}
1378
1379
	/**
1380
	 * Writes all changes to this object to the database.
1381
	 *  - It will insert a record whenever ID isn't set, otherwise update.
1382
	 *  - All relevant tables will be updated.
1383
	 *  - $this->onBeforeWrite() gets called beforehand.
1384
	 *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1385
	 *
1386
	 *  @uses DataExtension->augmentWrite()
1387
	 *
1388
	 * @param boolean $showDebug Show debugging information
1389
	 * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1390
	 * @param boolean $forceWrite Write to database even if there are no changes
1391
	 * @param boolean $writeComponents Call write() on all associated component instances which were previously
1392
	 *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1393
	 *                                 {@link getManyManyComponents()} (Default: false)
1394
	 * @return int The ID of the record
1395
	 * @throws ValidationException Exception that can be caught and handled by the calling function
1396
	 */
1397
	public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1398
		$now = SS_Datetime::now()->Rfc2822();
1399
1400
		// Execute pre-write tasks
1401
		$this->preWrite();
1402
1403
		// Check if we are doing an update or an insert
1404
		$isNewRecord = !$this->isInDB() || $forceInsert;
1405
1406
		// Check changes exist, abort if there are none
1407
		$hasChanges = $this->updateChanges($isNewRecord);
1408
		if($hasChanges || $forceWrite || $isNewRecord) {
1409
1410
			// Ensure Created and LastEdited are populated
1411
			if(!isset($this->record['Created'])) {
1412
				$this->record['Created'] = $now;
1413
			}
1414
			$this->record['LastEdited'] = $now;
1415
			// New records have their insert into the base data table done first, so that they can pass the
1416
			// generated primary key on to the rest of the manipulation
1417
			$baseTable = ClassInfo::baseDataClass($this->class);
1418
			$this->writeBaseRecord($baseTable, $now);
1419
1420
			// Write the DB manipulation for all changed fields
1421
			$this->writeManipulation($baseTable, $now, $isNewRecord);
1422
1423
			// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1424
			$this->writeRelations();
1425
			$this->onAfterWrite();
1426
			$this->changed = array();
1427
		} else {
1428
			if($showDebug) Debug::message("no changes for DataObject");
1429
1430
			// Used by DODs to clean up after themselves, eg, Versioned
1431
			$this->invokeWithExtensions('onAfterSkippedWrite');
1432
		}
1433
1434
		// Write relations as necessary
1435
		if($writeComponents) $this->writeComponents(true);
1436
1437
		// Clears the cache for this object so get_one returns the correct object.
1438
		$this->flushCache();
1439
1440
		return $this->record['ID'];
1441
	}
1442
1443
	/**
1444
	 * Writes cached relation lists to the database, if possible
1445
	 */
1446
	public function writeRelations() {
1447
		if(!$this->isInDB()) return;
1448
1449
		// If there's any relations that couldn't be saved before, save them now (we have an ID here)
1450
		if($this->unsavedRelations) {
1451
			foreach($this->unsavedRelations as $name => $list) {
1452
				$list->changeToList($this->$name());
1453
			}
1454
			$this->unsavedRelations = array();
1455
		}
1456
	}
1457
1458
	/**
1459
	 * Write the cached components to the database. Cached components could refer to two different instances of the
1460
	 * same record.
1461
	 *
1462
	 * @param $recursive Recursively write components
1463
	 * @return DataObject $this
1464
	 */
1465
	public function writeComponents($recursive = false) {
1466
		if(!$this->components) return $this;
1467
1468
		foreach($this->components as $component) {
1469
			$component->write(false, false, false, $recursive);
1470
		}
1471
		return $this;
1472
	}
1473
1474
	/**
1475
	 * Delete this data object.
1476
	 * $this->onBeforeDelete() gets called.
1477
	 * Note that in Versioned objects, both Stage and Live will be deleted.
1478
	 *  @uses DataExtension->augmentSQL()
1479
	 */
1480
	public function delete() {
1481
		$this->brokenOnDelete = true;
1482
		$this->onBeforeDelete();
1483
		if($this->brokenOnDelete) {
1484
			user_error("$this->class has a broken onBeforeDelete() function."
1485
				. " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1486
		}
1487
1488
		// Deleting a record without an ID shouldn't do anything
1489
		if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1490
1491
		// TODO: This is quite ugly.  To improve:
1492
		//  - move the details of the delete code in the DataQuery system
1493
		//  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1494
		//    obviously, that means getting requireTable() to configure cascading deletes ;-)
1495
		$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1496
		foreach($srcQuery->queriedTables() as $table) {
1497
			$delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1498
			$delete->execute();
1499
		}
1500
		// Remove this item out of any caches
1501
		$this->flushCache();
1502
1503
		$this->onAfterDelete();
1504
1505
		$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...
1506
		$this->ID = 0;
1507
	}
1508
1509
	/**
1510
	 * Delete the record with the given ID.
1511
	 *
1512
	 * @param string $className The class name of the record to be deleted
1513
	 * @param int $id ID of record to be deleted
1514
	 */
1515
	public static function delete_by_id($className, $id) {
1516
		$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...
1517
		if($obj) {
1518
			$obj->delete();
1519
		} else {
1520
			user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1521
		}
1522
	}
1523
1524
	/**
1525
	 * Get the class ancestry, including the current class name.
1526
	 * The ancestry will be returned as an array of class names, where the 0th element
1527
	 * will be the class that inherits directly from DataObject, and the last element
1528
	 * will be the current class.
1529
	 *
1530
	 * @return array Class ancestry
1531
	 */
1532
	public function getClassAncestry() {
1533
		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...
1534
			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...
1535
			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...
1536
				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...
1537
			}
1538
		}
1539
		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...
1540
	}
1541
1542
	/**
1543
	 * Return a component object from a one to one relationship, as a DataObject.
1544
	 * If no component is available, an 'empty component' will be returned for
1545
	 * non-polymorphic relations, or for polymorphic relations with a class set.
1546
	 *
1547
	 * @param string $componentName Name of the component
1548
	 *
1549
	 * @return DataObject The component object. It's exact type will be that of the component.
1550
	 */
1551
	public function getComponent($componentName) {
1552
		if(isset($this->components[$componentName])) {
1553
			return $this->components[$componentName];
1554
		}
1555
1556
		if($class = $this->hasOneComponent($componentName)) {
1557
			$joinField = $componentName . 'ID';
1558
			$joinID    = $this->getField($joinField);
1559
1560
			// Extract class name for polymorphic relations
1561
			if($class === 'DataObject') {
1562
				$class = $this->getField($componentName . 'Class');
1563
				if(empty($class)) return null;
1564
			}
1565
1566
			if($joinID) {
1567
				$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...
1568
			}
1569
1570
			if(empty($component)) {
1571
				$component = $this->model->$class->newObject();
1572
			}
1573
		} elseif($class = $this->belongsToComponent($componentName)) {
1574
1575
			$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
1576
			$joinID    = $this->ID;
1577
1578
			if($joinID) {
1579
1580
				$filter = $polymorphic
1581
					? array(
1582
						"{$joinField}ID" => $joinID,
1583
						"{$joinField}Class" => $this->class
1584
					)
1585
					: array(
1586
						$joinField => $joinID
1587
					);
1588
				$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...
1589
			}
1590
1591
			if(empty($component)) {
1592
				$component = $this->model->$class->newObject();
1593
				if($polymorphic) {
1594
					$component->{$joinField.'ID'} = $this->ID;
1595
					$component->{$joinField.'Class'} = $this->class;
1596
				} else {
1597
					$component->$joinField = $this->ID;
1598
				}
1599
			}
1600
		} else {
1601
			throw new Exception("DataObject->getComponent(): Could not find component '$componentName'.");
1602
		}
1603
1604
		$this->components[$componentName] = $component;
1605
		return $component;
1606
	}
1607
1608
	/**
1609
	 * Returns a one-to-many relation as a HasManyList
1610
	 *
1611
	 * @param string $componentName Name of the component
1612
	 * @param string|null $filter Deprecated. A filter to be inserted into the WHERE clause
1613
	 * @param string|null|array $sort Deprecated. A sort expression to be inserted into the ORDER BY clause. If omitted,
1614
	 *                                the static field $default_sort on the component class will be used.
1615
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1616
	 * @param string|null|array $limit Deprecated. A limit expression to be inserted into the LIMIT clause
1617
	 *
1618
	 * @return HasManyList The components of the one-to-many relationship.
1619
	 */
1620
	public function getComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
1621
		$result = null;
1622
1623
		if(!$componentClass = $this->hasManyComponent($componentName)) {
1624
			user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'"
1625
				. " on class '$this->class'", E_USER_ERROR);
1626
		}
1627
1628
		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...
1629
			throw new \InvalidArgumentException(
1630
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
1631
			);
1632
		}
1633
1634
		if($filter !== null || $sort !== null || $limit !== null) {
1635
			Deprecation::notice('4.0', 'The $filter, $sort and $limit parameters for DataObject::getComponents()
1636
				have been deprecated. Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
1637
		}
1638
1639
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1640
		if(!$this->ID) {
1641
			if(!isset($this->unsavedRelations[$componentName])) {
1642
				$this->unsavedRelations[$componentName] =
1643
					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 1623 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...
1644
			}
1645
			return $this->unsavedRelations[$componentName];
1646
		}
1647
1648
		// Determine type and nature of foreign relation
1649
		$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
1650
		if($polymorphic) {
1651
			$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1652
		} else {
1653
			$result = HasManyList::create($componentClass, $joinField);
1654
		}
1655
1656
		if($this->model) $result->setDataModel($this->model);
1657
		
1658
		$this->extend('updateComponents', $result);
1659
1660
		return $result
1661
			->forForeignID($this->ID)
1662
			->where($filter)
1663
			->limit($limit)
1664
			->sort($sort);
1665
	}
1666
1667
	/**
1668
	 * @deprecated
1669
	 */
1670
	public function getComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") {
1671
		Deprecation::notice('4.0', "Use getComponents to get a filtered DataList for an object's relation");
1672
		return $this->getComponents($componentName, $filter, $sort, $join, $limit);
1673
	}
1674
1675
	/**
1676
	 * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1677
	 *
1678
	 * @param $relationName Relation name.
1679
	 * @return string Class name, or null if not found.
1680
	 */
1681
	public function getRelationClass($relationName) {
1682
		// Go through all relationship configuration fields.
1683
		$candidates = array_merge(
1684
			($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1685
			($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1686
			($relations = Config::inst()->get($this->class, 'many_many')) ? $relations : array(),
1687
			($relations = Config::inst()->get($this->class, 'belongs_many_many')) ? $relations : array(),
1688
			($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1689
		);
1690
1691
		if (isset($candidates[$relationName])) {
1692
			$remoteClass = $candidates[$relationName];
1693
1694
			// If dot notation is present, extract just the first part that contains the class.
1695
			if(($fieldPos = strpos($remoteClass, '.'))!==false) {
1696
				return substr($remoteClass, 0, $fieldPos);
1697
			}
1698
1699
			// Otherwise just return the class
1700
			return $remoteClass;
1701
		}
1702
1703
		return null;
1704
	}
1705
1706
	/**
1707
	 * Tries to find the database key on another object that is used to store a
1708
	 * relationship to this class. If no join field can be found it defaults to 'ParentID'.
1709
	 *
1710
	 * If the remote field is polymorphic then $polymorphic is set to true, and the return value
1711
	 * is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
1712
	 *
1713
	 * @param string $component Name of the relation on the current object pointing to the
1714
	 * remote object.
1715
	 * @param string $type the join type - either 'has_many' or 'belongs_to'
1716
	 * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic.
1717
	 * @return string
1718
	 */
1719
	public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
1720
		// Extract relation from current object
1721
		if($type === 'has_many') {
1722
			$remoteClass = $this->hasManyComponent($component, false);
1723
		} else {
1724
			$remoteClass = $this->belongsToComponent($component, false);
1725
		}
1726
1727
		if(empty($remoteClass)) {
1728
			throw new Exception("Unknown $type component '$component' on class '$this->class'");
1729
		}
1730
		if(!ClassInfo::exists(strtok($remoteClass, '.'))) {
1731
			throw new Exception(
1732
				"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
1733
			);
1734
		}
1735
1736
		// If presented with an explicit field name (using dot notation) then extract field name
1737
		$remoteField = null;
1738
		if(strpos($remoteClass, '.') !== false) {
1739
			list($remoteClass, $remoteField) = explode('.', $remoteClass);
1740
		}
1741
1742
		// Reference remote has_one to check against
1743
		$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
1744
1745
		// Without an explicit field name, attempt to match the first remote field
1746
		// with the same type as the current class
1747
		if(empty($remoteField)) {
1748
			// look for remote has_one joins on this class or any parent classes
1749
			$remoteRelationsMap = array_flip($remoteRelations);
1750
			foreach(array_reverse(ClassInfo::ancestry($this)) as $class) {
1751
				if(array_key_exists($class, $remoteRelationsMap)) {
1752
					$remoteField = $remoteRelationsMap[$class];
1753
					break;
1754
				}
1755
			}
1756
		}
1757
1758
		// In case of an indeterminate remote field show an error
1759
		if(empty($remoteField)) {
1760
			$polymorphic = false;
1761
			$message = "No has_one found on class '$remoteClass'";
1762
			if($type == 'has_many') {
1763
				// include a hint for has_many that is missing a has_one
1764
				$message .= ", the has_many relation from '$this->class' to '$remoteClass'";
1765
				$message .= " requires a has_one on '$remoteClass'";
1766
			}
1767
			throw new Exception($message);
1768
		}
1769
1770
		// If given an explicit field name ensure the related class specifies this
1771
		if(empty($remoteRelations[$remoteField])) {
1772
			throw new Exception("Missing expected has_one named '$remoteField'
1773
				on class '$remoteClass' referenced by $type named '$component'
1774
				on class {$this->class}"
1775
			);
1776
		}
1777
1778
		// Inspect resulting found relation
1779
		if($remoteRelations[$remoteField] === 'DataObject') {
1780
			$polymorphic = true;
1781
			return $remoteField; // Composite polymorphic field does not include 'ID' suffix
1782
		} else {
1783
			$polymorphic = false;
1784
			return $remoteField . 'ID';
1785
		}
1786
	}
1787
1788
	/**
1789
	 * Returns a many-to-many component, as a ManyManyList.
1790
	 * @param string $componentName Name of the many-many component
1791
	 * @return ManyManyList The set of components
1792
	 *
1793
	 * @todo Implement query-params
1794
	 */
1795
	public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) {
1796
		list($parentClass, $componentClass, $parentField, $componentField, $table)
1797
			= $this->manyManyComponent($componentName);
1798
1799
		if($filter !== null || $sort !== null || $join !== null || $limit !== null) {
1800
			Deprecation::notice('4.0', 'The $filter, $sort, $join and $limit parameters for
1801
				DataObject::getManyManyComponents() have been deprecated.
1802
				Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL);
1803
		}
1804
1805
		// If we haven't been written yet, we can't save these relations, so use a list that handles this case
1806
		if(!$this->ID) {
1807
			if(!isset($this->unsavedRelations[$componentName])) {
1808
				$this->unsavedRelations[$componentName] =
1809
					new UnsavedRelationList($parentClass, $componentName, $componentClass);
1810
			}
1811
			return $this->unsavedRelations[$componentName];
1812
		}
1813
1814
		$extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array();
1815
		$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
1816
1817
1818
		// Store component data in query meta-data
1819
		$result = $result->alterDataQuery(function($query) use ($extraFields) {
1820
			$query->setQueryParam('Component.ExtraFields', $extraFields);
1821
		});
1822
1823
		if($this->model) $result->setDataModel($this->model);
1824
1825
		$this->extend('updateManyManyComponents', $result);
1826
1827
		// If this is called on a singleton, then we return an 'orphaned relation' that can have the
1828
		// foreignID set elsewhere.
1829
		return $result
1830
			->forForeignID($this->ID)
1831
			->where($filter)
1832
			->sort($sort)
1833
			->limit($limit);
1834
	}
1835
1836
	/**
1837
	 * @deprecated 4.0 Method has been replaced by hasOne() and hasOneComponent()
1838
	 * @param string $component
1839
	 * @return array|null
1840
	 */
1841
	public function has_one($component = null) {
1842
		if($component) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $component of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1843
			Deprecation::notice('4.0', 'Please use hasOneComponent() instead');
1844
			return $this->hasOneComponent($component);
1845
		}
1846
1847
		Deprecation::notice('4.0', 'Please use hasOne() instead');
1848
		return $this->hasOne();
1849
	}
1850
1851
	/**
1852
	 * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1853
	 * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1854
	 *
1855
	 * @param string $component Deprecated - Name of component
1856
	 * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1857
	 * 							their classes.
1858
	 */
1859
	public function hasOne($component = null) {
1860
		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...
1861
			Deprecation::notice(
1862
				'4.0',
1863
				'Please use DataObject::hasOneComponent() instead of passing a component name to hasOne()',
1864
				Deprecation::SCOPE_GLOBAL
1865
			);
1866
			return $this->hasOneComponent($component);
1867
		}
1868
1869
		return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1870
	}
1871
1872
	/**
1873
	 * Return data for a specific has_one component.
1874
	 * @param string $component
1875
	 * @return string|null
1876
	 */
1877
	public function hasOneComponent($component) {
1878
		$hasOnes = (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1879
1880
		if(isset($hasOnes[$component])) {
1881
			return $hasOnes[$component];
1882
		}
1883
	}
1884
1885
	/**
1886
	 * @deprecated 4.0 Method has been replaced by belongsTo() and belongsToComponent()
1887
	 * @param string $component
1888
	 * @param bool $classOnly
1889
	 * @return array|null
1890
	 */
1891
	public function belongs_to($component = null, $classOnly = true) {
1892
		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...
1893
			Deprecation::notice('4.0', 'Please use belongsToComponent() instead');
1894
			return $this->belongsToComponent($component, $classOnly);
1895
		}
1896
1897
		Deprecation::notice('4.0', 'Please use belongsTo() instead');
1898
		return $this->belongsTo(null, $classOnly);
1899
	}
1900
1901
	/**
1902
	 * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1903
	 * their class name will be returned.
1904
	 *
1905
	 * @param string $component - Name of component
1906
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1907
	 *        the field data stripped off. It defaults to TRUE.
1908
	 * @return string|array
1909
	 */
1910
	public function belongsTo($component = null, $classOnly = true) {
1911
		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...
1912
			Deprecation::notice(
1913
				'4.0',
1914
				'Please use DataObject::belongsToComponent() instead of passing a component name to belongsTo()',
1915
				Deprecation::SCOPE_GLOBAL
1916
			);
1917
			return $this->belongsToComponent($component, $classOnly);
1918
		}
1919
1920
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1921
		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...
1922
			return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1923
		} else {
1924
			return $belongsTo ? $belongsTo : array();
1925
		}
1926
	}
1927
1928
	/**
1929
	 * Return data for a specific belongs_to component.
1930
	 * @param string $component
1931
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1932
	 *        the field data stripped off. It defaults to TRUE.
1933
	 * @return string|false
1934
	 */
1935
	public function belongsToComponent($component, $classOnly = true) {
1936
		$belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1937
1938
		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...
1939
			$belongsTo = $belongsTo[$component];
1940
		} else {
1941
			return false;
1942
		}
1943
1944
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $belongsTo) : $belongsTo;
1945
	}
1946
1947
	/**
1948
	 * Return all of the database fields defined in self::$db and all the parent classes.
1949
	 * Doesn't include any fields specified by self::$has_one.  Use $this->hasOne() to get these fields
1950
	 *
1951
	 * @param string $fieldName Limit the output to a specific field name
1952
	 * @return array The database fields
1953
	 */
1954
	public function db($fieldName = null) {
1955
		$classes = ClassInfo::ancestry($this, true);
1956
1957
		// If we're looking for a specific field, we want to hit subclasses first as they may override field types
1958
		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...
1959
			$classes = array_reverse($classes);
1960
		}
1961
1962
		$items = array();
1963
		foreach($classes as $class) {
1964
			if(isset(self::$_cache_db[$class])) {
1965
				$dbItems = self::$_cache_db[$class];
1966
			} else {
1967
				$dbItems = (array) Config::inst()->get($class, 'db', Config::UNINHERITED);
1968
				self::$_cache_db[$class] = $dbItems;
1969
			}
1970
1971
			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...
1972
				if(isset($dbItems[$fieldName])) {
1973
					return $dbItems[$fieldName];
1974
				}
1975
			} else {
1976
				$items = isset($items) ? array_merge((array) $items, $dbItems) : $dbItems;
1977
			}
1978
		}
1979
		// If we requested a non-existant named field return null instead of all fields
1980
		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...
1981
			return null;
1982
		}
1983
		return $items;
1984
	}
1985
1986
	/**
1987
	 * @deprecated 4.0 Method has been replaced by hasMany() and hasManyComponent()
1988
	 * @param string $component
1989
	 * @param bool $classOnly
1990
	 * @return array|null
1991
	 */
1992
	public function has_many($component = null, $classOnly = true) {
1993
		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...
1994
			Deprecation::notice('4.0', 'Please use hasManyComponent() instead');
1995
			return $this->hasManyComponent($component, $classOnly);
1996
		}
1997
1998
		Deprecation::notice('4.0', 'Please use hasMany() instead');
1999
		return $this->hasMany(null, $classOnly);
2000
	}
2001
2002
	/**
2003
	 * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2004
	 * relationships and their classes will be returned.
2005
	 *
2006
	 * @param string $component Deprecated - Name of component
2007
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2008
	 *        the field data stripped off. It defaults to TRUE.
2009
	 * @return string|array|false
2010
	 */
2011
	public function hasMany($component = null, $classOnly = true) {
2012
		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...
2013
			Deprecation::notice(
2014
				'4.0',
2015
				'Please use DataObject::hasManyComponent() instead of passing a component name to hasMany()',
2016
				Deprecation::SCOPE_GLOBAL
2017
			);
2018
			return $this->hasManyComponent($component, $classOnly);
2019
		}
2020
2021
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2022
		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...
2023
			return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2024
		} else {
2025
			return $hasMany ? $hasMany : array();
2026
		}
2027
	}
2028
2029
	/**
2030
	 * Return data for a specific has_many component.
2031
	 * @param string $component
2032
	 * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2033
	 *        the field data stripped off. It defaults to TRUE.
2034
	 * @return string|false
2035
	 */
2036
	public function hasManyComponent($component, $classOnly = true) {
2037
		$hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
2038
2039
		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...
2040
			$hasMany = $hasMany[$component];
2041
		} else {
2042
			return false;
2043
		}
2044
2045
		return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany;
2046
	}
2047
2048
	/**
2049
	 * @deprecated 4.0 Method has been replaced by manyManyExtraFields() and
2050
	 *                 manyManyExtraFieldsForComponent()
2051
	 * @param string $component
2052
	 * @return array
2053
	 */
2054
	public function many_many_extraFields($component = null) {
2055
		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...
2056
			Deprecation::notice('4.0', 'Please use manyManyExtraFieldsForComponent() instead');
2057
			return $this->manyManyExtraFieldsForComponent($component);
2058
		}
2059
2060
		Deprecation::notice('4.0', 'Please use manyManyExtraFields() instead');
2061
		return $this->manyManyExtraFields();
2062
	}
2063
2064
	/**
2065
	 * Return the many-to-many extra fields specification.
2066
	 *
2067
	 * If you don't specify a component name, it returns all
2068
	 * extra fields for all components available.
2069
	 *
2070
	 * @param string $component Deprecated - Name of component
2071
	 * @return array|null
2072
	 */
2073
	public function manyManyExtraFields($component = null) {
2074
		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...
2075
			Deprecation::notice(
2076
				'4.0',
2077
				'Please use DataObject::manyManyExtraFieldsForComponent() instead of passing a component name
2078
					to manyManyExtraFields()',
2079
				Deprecation::SCOPE_GLOBAL
2080
			);
2081
			return $this->manyManyExtraFieldsForComponent($component);
2082
		}
2083
2084
		return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2085
	}
2086
2087
	/**
2088
	 * Return the many-to-many extra fields specification for a specific component.
2089
	 * @param string $component
2090
	 * @return array|null
2091
	 */
2092
	public function manyManyExtraFieldsForComponent($component) {
2093
		// Get all many_many_extraFields defined in this class or parent classes
2094
		$extraFields = (array)Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
2095
		// Extra fields are immediately available
2096
		if(isset($extraFields[$component])) {
2097
			return $extraFields[$component];
2098
		}
2099
2100
		// Check this class' belongs_many_manys to see if any of their reverse associations contain extra fields
2101
		$manyMany = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2102
		$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2103
		if($candidate) {
2104
			$relationName = null;
2105
			// Extract class and relation name from dot-notation
2106
			if(strpos($candidate, '.') !== false) {
2107
				list($candidate, $relationName) = explode('.', $candidate, 2);
2108
			}
2109
2110
			// If we've not already found the relation name from dot notation, we need to find a relation that points
2111
			// back to this class. As there's no dot-notation, there can only be one relation pointing to this class,
2112
			// so it's safe to assume that it's the correct one
2113
			if(!$relationName) {
2114
				$candidateManyManys = (array)Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2115
2116
				foreach($candidateManyManys as $relation => $relatedClass) {
2117
					if (is_a($this, $relatedClass)) {
2118
						$relationName = $relation;
2119
					}
2120
				}
2121
			}
2122
2123
			// If we've found a matching relation on the target class, see if we can find extra fields for it
2124
			$extraFields = (array)Config::inst()->get($candidate, 'many_many_extraFields', Config::UNINHERITED);
2125
			if(isset($extraFields[$relationName])) {
2126
				return $extraFields[$relationName];
2127
			}
2128
		}
2129
2130
		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...
2131
	}
2132
2133
	/**
2134
	 * @deprecated 4.0 Method has been renamed to manyMany()
2135
	 * @param string $component
2136
	 * @return array|null
2137
	 */
2138
	public function many_many($component = null) {
2139
		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...
2140
			Deprecation::notice('4.0', 'Please use manyManyComponent() instead');
2141
			return $this->manyManyComponent($component);
2142
		}
2143
2144
		Deprecation::notice('4.0', 'Please use manyMany() instead');
2145
		return $this->manyMany();
2146
	}
2147
2148
	/**
2149
	 * Return information about a many-to-many component.
2150
	 * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2151
	 * components are returned.
2152
	 *
2153
	 * @see DataObject::manyManyComponent()
2154
	 * @param string $component Deprecated - Name of component
2155
	 * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2156
	 */
2157
	public function manyMany($component = null) {
2158
		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...
2159
			Deprecation::notice(
2160
				'4.0',
2161
				'Please use DataObject::manyManyComponent() instead of passing a component name to manyMany()',
2162
				Deprecation::SCOPE_GLOBAL
2163
			);
2164
			return $this->manyManyComponent($component);
2165
		}
2166
2167
		$manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
2168
		$belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
2169
2170
		$items = array_merge($manyManys, $belongsManyManys);
2171
		return $items;
2172
	}
2173
2174
	/**
2175
	 * Return information about a specific many_many component. Returns a numeric array of:
2176
	 * array(
2177
	 * 	<classname>,		The class that relation is defined in e.g. "Product"
2178
	 * 	<candidateName>,	The target class of the relation e.g. "Category"
2179
	 * 	<parentField>,		The field name pointing to <classname>'s table e.g. "ProductID"
2180
	 * 	<childField>,		The field name pointing to <candidatename>'s table e.g. "CategoryID"
2181
	 * 	<joinTable>			The join table between the two classes e.g. "Product_Categories"
2182
	 * )
2183
	 * @param string $component The component name
2184
	 * @return array|null
2185
	 */
2186
	public function manyManyComponent($component) {
2187
		$classes = $this->getClassAncestry();
2188
		foreach($classes as $class) {
2189
			$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
2190
			// Check if the component is defined in many_many on this class
2191
			$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
2192
			if($candidate) {
2193
				$parentField = $class . "ID";
2194
				$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
2195
				return array($class, $candidate, $parentField, $childField, "{$class}_$component");
2196
			}
2197
2198
			// Check if the component is defined in belongs_many_many on this class
2199
			$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
2200
			$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
2201
			if($candidate) {
2202
				// Extract class and relation name from dot-notation
2203
				if(strpos($candidate, '.') !== false) {
2204
					list($candidate, $relationName) = explode('.', $candidate, 2);
2205
				}
2206
2207
				$childField = $candidate . "ID";
2208
2209
				// We need to find the inverse component name
2210
				$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
2211
				if(!$otherManyMany) {
2212
					throw new LogicException("Inverse component of $candidate not found ({$this->class})");
2213
				}
2214
2215
				// If we've got a relation name (extracted from dot-notation), we can already work out
2216
				// the join table and candidate class name...
2217
				if(isset($relationName) && isset($otherManyMany[$relationName])) {
2218
					$candidateClass = $otherManyMany[$relationName];
2219
					$joinTable = "{$candidate}_{$relationName}";
2220
				} else {
2221
					// ... otherwise, we need to loop over the many_manys and find a relation that
2222
					// matches up to this class
2223
					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...
2224
						if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
2225
							$joinTable = "{$candidate}_{$inverseComponentName}";
2226
							break;
2227
						}
2228
					}
2229
				}
2230
2231
				// If we could work out the join table, we've got all the info we need
2232
				if(isset($joinTable)) {
2233
					$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...
2234
					return array($class, $candidate, $parentField, $childField, $joinTable);
2235
				}
2236
2237
				throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
2238
			}
2239
		}
2240
	}
2241
2242
	/**
2243
	 * This returns an array (if it exists) describing the database extensions that are required, or false if none
2244
	 *
2245
	 * This is experimental, and is currently only a Postgres-specific enhancement.
2246
	 *
2247
	 * @return array or false
2248
	 */
2249
	public function database_extensions($class){
2250
		$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2251
2252
		if($extensions)
2253
			return $extensions;
2254
		else
2255
			return false;
2256
	}
2257
2258
	/**
2259
	 * Generates a SearchContext to be used for building and processing
2260
	 * a generic search form for properties on this object.
2261
	 *
2262
	 * @return SearchContext
2263
	 */
2264
	public function getDefaultSearchContext() {
2265
		return new SearchContext(
2266
			$this->class,
2267
			$this->scaffoldSearchFields(),
2268
			$this->defaultSearchFilters()
2269
		);
2270
	}
2271
2272
	/**
2273
	 * Determine which properties on the DataObject are
2274
	 * searchable, and map them to their default {@link FormField}
2275
	 * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2276
	 *
2277
	 * Some additional logic is included for switching field labels, based on
2278
	 * how generic or specific the field type is.
2279
	 *
2280
	 * Used by {@link SearchContext}.
2281
	 *
2282
	 * @param array $_params
2283
	 *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2284
	 *   'restrictFields': Numeric array of a field name whitelist
2285
	 * @return FieldList
2286
	 */
2287
	public function scaffoldSearchFields($_params = null) {
2288
		$params = array_merge(
2289
			array(
2290
				'fieldClasses' => false,
2291
				'restrictFields' => false
2292
			),
2293
			(array)$_params
2294
		);
2295
		$fields = new FieldList();
2296
		foreach($this->searchableFields() as $fieldName => $spec) {
2297
			if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
2298
2299
			// If a custom fieldclass is provided as a string, use it
2300
			if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2301
				$fieldClass = $params['fieldClasses'][$fieldName];
2302
				$field = new $fieldClass($fieldName);
2303
			// If we explicitly set a field, then construct that
2304
			} else if(isset($spec['field'])) {
2305
				// If it's a string, use it as a class name and construct
2306
				if(is_string($spec['field'])) {
2307
					$fieldClass = $spec['field'];
2308
					$field = new $fieldClass($fieldName);
2309
2310
				// If it's a FormField object, then just use that object directly.
2311
				} else if($spec['field'] instanceof FormField) {
2312
					$field = $spec['field'];
2313
2314
				// Otherwise we have a bug
2315
				} else {
2316
					user_error("Bad value for searchable_fields, 'field' value: "
2317
						. var_export($spec['field'], true), E_USER_WARNING);
2318
				}
2319
2320
			// Otherwise, use the database field's scaffolder
2321
			} else {
2322
				$field = $this->relObject($fieldName)->scaffoldSearchField();
2323
			}
2324
2325
			if (strstr($fieldName, '.')) {
2326
				$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...
2327
			}
2328
			$field->setTitle($spec['title']);
2329
2330
			$fields->push($field);
2331
		}
2332
		return $fields;
2333
	}
2334
2335
	/**
2336
	 * Scaffold a simple edit form for all properties on this dataobject,
2337
	 * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2338
	 * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2339
	 *
2340
	 * @uses FormScaffolder
2341
	 *
2342
	 * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2343
	 * @return FieldList
2344
	 */
2345
	public function scaffoldFormFields($_params = null) {
2346
		$params = array_merge(
2347
			array(
2348
				'tabbed' => false,
2349
				'includeRelations' => false,
2350
				'restrictFields' => false,
2351
				'fieldClasses' => false,
2352
				'ajaxSafe' => false
2353
			),
2354
			(array)$_params
2355
		);
2356
2357
		$fs = new FormScaffolder($this);
2358
		$fs->tabbed = $params['tabbed'];
2359
		$fs->includeRelations = $params['includeRelations'];
2360
		$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...
2361
		$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...
2362
		$fs->ajaxSafe = $params['ajaxSafe'];
2363
2364
		return $fs->getFieldList();
2365
	}
2366
2367
	/**
2368
	 * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2369
	 * being called on extensions
2370
	 *
2371
	 * @param callable $callback The callback to execute
2372
	 */
2373
	protected function beforeUpdateCMSFields($callback) {
2374
		$this->beforeExtending('updateCMSFields', $callback);
2375
	}
2376
2377
	/**
2378
	 * Centerpiece of every data administration interface in Silverstripe,
2379
	 * which returns a {@link FieldList} suitable for a {@link Form} object.
2380
	 * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2381
	 * generate this set. To customize, overload this method in a subclass
2382
	 * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2383
	 *
2384
	 * <code>
2385
	 * class MyCustomClass extends DataObject {
2386
	 *  static $db = array('CustomProperty'=>'Boolean');
2387
	 *
2388
	 *  function getCMSFields() {
2389
	 *    $fields = parent::getCMSFields();
2390
	 *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2391
	 *    return $fields;
2392
	 *  }
2393
	 * }
2394
	 * </code>
2395
	 *
2396
	 * @see Good example of complex FormField building: SiteTree::getCMSFields()
2397
	 *
2398
	 * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2399
	 */
2400
	public function getCMSFields() {
2401
		$tabbedFields = $this->scaffoldFormFields(array(
2402
			// Don't allow has_many/many_many relationship editing before the record is first saved
2403
			'includeRelations' => ($this->ID > 0),
2404
			'tabbed' => true,
2405
			'ajaxSafe' => true
2406
		));
2407
2408
		$this->extend('updateCMSFields', $tabbedFields);
2409
2410
		return $tabbedFields;
2411
	}
2412
2413
	/**
2414
	 * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2415
	 * including that dataobject's extensions customised actions could be added to the EditForm.
2416
	 *
2417
	 * @return an Empty FieldList(); need to be overload by solid subclass
2418
	 */
2419
	public function getCMSActions() {
2420
		$actions = new FieldList();
2421
		$this->extend('updateCMSActions', $actions);
2422
		return $actions;
2423
	}
2424
2425
2426
	/**
2427
	 * Used for simple frontend forms without relation editing
2428
	 * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2429
	 * by default. To customize, either overload this method in your
2430
	 * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2431
	 *
2432
	 * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2433
	 *
2434
	 * @param array $params See {@link scaffoldFormFields()}
2435
	 * @return FieldList Always returns a simple field collection without TabSet.
2436
	 */
2437
	public function getFrontEndFields($params = null) {
2438
		$untabbedFields = $this->scaffoldFormFields($params);
2439
		$this->extend('updateFrontEndFields', $untabbedFields);
2440
2441
		return $untabbedFields;
2442
	}
2443
2444
	/**
2445
	 * Gets the value of a field.
2446
	 * Called by {@link __get()} and any getFieldName() methods you might create.
2447
	 *
2448
	 * @param string $field The name of the field
2449
	 *
2450
	 * @return mixed The field value
2451
	 */
2452
	public function getField($field) {
2453
		// If we already have an object in $this->record, then we should just return that
2454
		if(isset($this->record[$field]) && is_object($this->record[$field]))  return $this->record[$field];
2455
2456
		// Do we have a field that needs to be lazy loaded?
2457
		if(isset($this->record[$field.'_Lazy'])) {
2458
			$tableClass = $this->record[$field.'_Lazy'];
2459
			$this->loadLazyFields($tableClass);
2460
		}
2461
2462
		// Otherwise, we need to determine if this is a complex field
2463
		if(self::is_composite_field($this->class, $field)) {
2464
			$helper = $this->castingHelper($field);
2465
			$fieldObj = SS_Object::create_from_string($helper, $field);
2466
2467
			$compositeFields = $fieldObj->compositeDatabaseFields();
2468
			foreach ($compositeFields as $compositeName => $compositeType) {
2469
				if(isset($this->record[$field.$compositeName.'_Lazy'])) {
2470
					$tableClass = $this->record[$field.$compositeName.'_Lazy'];
2471
					$this->loadLazyFields($tableClass);
2472
				}
2473
			}
2474
2475
			// write value only if either the field value exists,
2476
			// or a valid record has been loaded from the database
2477
			$value = (isset($this->record[$field])) ? $this->record[$field] : null;
2478
			if($value || $this->exists()) $fieldObj->setValue($value, $this->record, false);
2479
2480
			$this->record[$field] = $fieldObj;
2481
2482
			return $this->record[$field];
2483
		}
2484
2485
		return isset($this->record[$field]) ? $this->record[$field] : null;
2486
	}
2487
2488
	/**
2489
	 * Loads all the stub fields that an initial lazy load didn't load fully.
2490
	 *
2491
	 * @param string $tableClass Base table to load the values from. Others are joined as required.
2492
	 * Not specifying a tableClass will load all lazy fields from all tables.
2493
	 * @return bool Flag if lazy loading succeeded
2494
	 */
2495
	protected function loadLazyFields($tableClass = null) {
2496
		if(!$this->isInDB() || !is_numeric($this->ID)) {
2497
			return false;
2498
		}
2499
2500
		if (!$tableClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tableClass of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2501
			$loaded = array();
2502
2503
			foreach ($this->record as $key => $value) {
2504
				if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2505
					$this->loadLazyFields($value);
2506
					$loaded[$value] = $value;
2507
				}
2508
			}
2509
2510
			return false;
2511
		}
2512
2513
		$dataQuery = new DataQuery($tableClass);
2514
2515
		// Reset query parameter context to that of this DataObject
2516
		if($params = $this->getSourceQueryParams()) {
2517
			foreach($params as $key => $value) $dataQuery->setQueryParam($key, $value);
2518
		}
2519
2520
		// Limit query to the current record, unless it has the Versioned extension,
2521
		// in which case it requires special handling through augmentLoadLazyFields()
2522
		if(!$this->hasExtension('Versioned')) {
2523
			$dataQuery->where("\"$tableClass\".\"ID\" = {$this->record['ID']}")->limit(1);
2524
		}
2525
2526
		$columns = array();
2527
2528
		// Add SQL for fields, both simple & multi-value
2529
		// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2530
		$databaseFields = self::database_fields($tableClass, false);
2531
		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...
2532
			if(!isset($this->record[$k]) || $this->record[$k] === null) {
2533
				$columns[] = $k;
2534
			}
2535
		}
2536
2537
		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...
2538
			$query = $dataQuery->query();
2539
			$this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2540
			$this->extend('augmentSQL', $query, $dataQuery);
2541
2542
			$dataQuery->setQueriedColumns($columns);
2543
			$newData = $dataQuery->execute()->record();
2544
2545
			// Load the data into record
2546
			if($newData) {
2547
				foreach($newData as $k => $v) {
2548
					if (in_array($k, $columns)) {
2549
						$this->record[$k] = $v;
2550
						$this->original[$k] = $v;
2551
						unset($this->record[$k . '_Lazy']);
2552
					}
2553
				}
2554
2555
			// No data means that the query returned nothing; assign 'null' to all the requested fields
2556
			} else {
2557
				foreach($columns as $k) {
2558
					$this->record[$k] = null;
2559
					$this->original[$k] = null;
2560
					unset($this->record[$k . '_Lazy']);
2561
				}
2562
			}
2563
		}
2564
		return true;
2565
	}
2566
2567
	/**
2568
	 * Return the fields that have changed.
2569
	 *
2570
	 * The change level affects what the functions defines as "changed":
2571
	 * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2572
	 * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2573
	 *   for example a change from 0 to null would not be included.
2574
	 *
2575
	 * Example return:
2576
	 * <code>
2577
	 * array(
2578
	 *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2579
	 * )
2580
	 * </code>
2581
	 *
2582
	 * @param boolean $databaseFieldsOnly Get only database fields that have changed
2583
	 * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2584
	 * @return array
2585
	 */
2586
	public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
2587
		$changedFields = array();
2588
2589
		// Update the changed array with references to changed obj-fields
2590
		foreach($this->record as $k => $v) {
2591
			if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2592
				$this->changed[$k] = self::CHANGE_VALUE;
2593
			}
2594
		}
2595
2596
		if($databaseFieldsOnly) {
2597
			// Merge all DB fields together
2598
			$inheritedFields = $this->inheritedDatabaseFields();
2599
			$compositeFields = static::composite_fields(get_class($this));
2600
			$fixedFields = $this->config()->fixed_fields;
0 ignored issues
show
Documentation introduced by
The property fixed_fields does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2601
			$databaseFields = array_merge(
2602
				$inheritedFields,
2603
				$fixedFields,
2604
				$compositeFields
2605
			);
2606
			$fields = array_intersect_key((array)$this->changed, $databaseFields);
2607
		} else {
2608
			$fields = $this->changed;
2609
		}
2610
2611
		// Filter the list to those of a certain change level
2612
		if($changeLevel > self::CHANGE_STRICT) {
2613
			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...
2614
				if($level < $changeLevel) {
2615
					unset($fields[$name]);
2616
				}
2617
			}
2618
		}
2619
2620
		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...
2621
			$changedFields[$name] = array(
2622
				'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2623
				'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2624
				'level' => $level
2625
			);
2626
		}
2627
2628
		return $changedFields;
2629
	}
2630
2631
	/**
2632
	 * Uses {@link getChangedFields()} to determine if fields have been changed
2633
	 * since loading them from the database.
2634
	 *
2635
	 * @param string $fieldName Name of the database field to check, will check for any if not given
2636
	 * @param int $changeLevel See {@link getChangedFields()}
2637
	 * @return boolean
2638
	 */
2639
	public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT) {
2640
		if (!$fieldName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldName of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2641
			// Limit "any changes" to db fields only
2642
			$changed = $this->getChangedFields(true, $changeLevel);
2643
			return !empty($changed);
2644
		} else {
2645
			// Given a field name, check all fields
2646
			$changed = $this->getChangedFields(false, $changeLevel);
2647
			return array_key_exists($fieldName, $changed);
2648
		}
2649
	}
2650
2651
	/**
2652
	 * Set the value of the field
2653
	 * Called by {@link __set()} and any setFieldName() methods you might create.
2654
	 *
2655
	 * @param string $fieldName Name of the field
2656
	 * @param mixed $val New field value
2657
	 * @return DataObject $this
2658
	 */
2659
	public function setField($fieldName, $val) {
2660
		//if it's a has_one component, destroy the cache
2661
		if (substr($fieldName, -2) == 'ID') {
2662
			unset($this->components[substr($fieldName, 0, -2)]);
2663
		}
2664
		// Situation 1: Passing an DBField
2665
		if($val instanceof DBField) {
2666
			$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...
2667
2668
			// If we've just lazy-loaded the column, then we need to populate the $original array by
2669
			// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
2670
			// on a call to getChanged()?
2671
			$this->getField($fieldName);
2672
2673
			$this->record[$fieldName] = $val;
2674
		// Situation 2: Passing a literal or non-DBField object
2675
		} else {
2676
			// If this is a proper database field, we shouldn't be getting non-DBField objects
2677
			if(is_object($val) && $this->db($fieldName)) {
2678
				user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
2679
			}
2680
2681
			$dbField = $this->dbObject($fieldName);
2682
			if ($dbField && $dbField->scalarValueOnly() && !empty($val) && !is_scalar($val)){
2683
			    $val = null;
2684
                user_error(
2685
                    sprintf(
2686
                        'DataObject::setField: %s only accepts scalars',
2687
                        $fieldName
2688
                    ),
2689
                    E_USER_WARNING
2690
                );
2691
            }
2692
2693
			// if a field is not existing or has strictly changed
2694
			if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2695
				// TODO Add check for php-level defaults which are not set in the db
2696
				// TODO Add check for hidden input-fields (readonly) which are not set in the db
2697
				// At the very least, the type has changed
2698
				$this->changed[$fieldName] = self::CHANGE_STRICT;
2699
2700
				if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
2701
						&& $this->record[$fieldName] != $val)) {
2702
2703
					// Value has changed as well, not just the type
2704
					$this->changed[$fieldName] = self::CHANGE_VALUE;
2705
				}
2706
2707
				// If we've just lazy-loaded the column, then we need to populate the $original array by
2708
				// called getField(). Too much overhead? Could this be done by a quicker method? Maybe only
2709
				// on a call to getChanged()?
2710
				$this->getField($fieldName);
2711
2712
				// Value is always saved back when strict check succeeds.
2713
				$this->record[$fieldName] = $val;
2714
			}
2715
		}
2716
		return $this;
2717
	}
2718
2719
	/**
2720
	 * Set the value of the field, using a casting object.
2721
	 * This is useful when you aren't sure that a date is in SQL format, for example.
2722
	 * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2723
	 * can be saved into the Image table.
2724
	 *
2725
	 * @param string $fieldName Name of the field
2726
	 * @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...
2727
	 * @return DataObject $this
2728
	 */
2729
	public function setCastedField($fieldName, $val) {
2730
		if(!$fieldName) {
2731
			user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2732
		}
2733
		$castingHelper = $this->castingHelper($fieldName);
2734
		if($castingHelper) {
2735
			$fieldObj = SS_Object::create_from_string($castingHelper, $fieldName);
2736
			$fieldObj->setValue($val);
2737
			$fieldObj->saveInto($this);
2738
		} else {
2739
			$this->$fieldName = $val;
2740
		}
2741
		return $this;
2742
	}
2743
2744
	/**
2745
	 * {@inheritdoc}
2746
	 */
2747
	public function castingHelper($field) {
2748
		if ($fieldSpec = $this->db($field)) {
2749
			return $fieldSpec;
2750
		}
2751
2752
		// many_many_extraFields aren't presented by db(), so we check if the source query params
2753
		// provide us with meta-data for a many_many relation we can inspect for extra fields.
2754
		$queryParams = $this->getSourceQueryParams();
2755
		if (!empty($queryParams['Component.ExtraFields'])) {
2756
			$extraFields = $queryParams['Component.ExtraFields'];
2757
2758
			if (isset($extraFields[$field])) {
2759
				return $extraFields[$field];
2760
			}
2761
		}
2762
2763
		return parent::castingHelper($field);
2764
	}
2765
2766
	/**
2767
	 * Returns true if the given field exists in a database column on any of
2768
	 * the objects tables and optionally look up a dynamic getter with
2769
	 * get<fieldName>().
2770
	 *
2771
	 * @param string $field Name of the field
2772
	 * @return boolean True if the given field exists
2773
	 */
2774
	public function hasField($field) {
2775
		return (
2776
			array_key_exists($field, $this->record)
2777
			|| $this->db($field)
2778
			|| (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...
2779
			|| $this->hasMethod("get{$field}")
2780
		);
2781
	}
2782
2783
	/**
2784
	 * Returns true if the given field exists as a database column
2785
	 *
2786
	 * @param string $field Name of the field
2787
	 *
2788
	 * @return boolean
2789
	 */
2790
	public function hasDatabaseField($field) {
2791
		if(isset(self::$fixed_fields[$field])) return true;
2792
2793
		return array_key_exists($field, $this->inheritedDatabaseFields());
2794
	}
2795
2796
	/**
2797
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2798
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2799
	 *
2800
	 * @param string $field Name of the field
2801
	 * @return string The field type of the given field
2802
	 */
2803
	public function hasOwnTableDatabaseField($field) {
2804
		return self::has_own_table_database_field($this->class, $field);
2805
	}
2806
2807
	/**
2808
	 * Returns the field type of the given field, if it belongs to this class, and not a parent.
2809
	 * Note that the field type will not include constructor arguments in round brackets, only the classname.
2810
	 *
2811
	 * @param string $class Class name to check
2812
	 * @param string $field Name of the field
2813
	 * @return string The field type of the given field
2814
	 */
2815
	public static function has_own_table_database_field($class, $field) {
2816
		// Since database_fields omits 'ID'
2817
		if($field == "ID") return "Int";
2818
2819
		$fieldMap = self::database_fields($class, false);
2820
2821
		// Remove string-based "constructor-arguments" from the DBField definition
2822
		if(isset($fieldMap[$field])) {
2823
			$spec = $fieldMap[$field];
2824
			if(is_string($spec)) return strtok($spec,'(');
2825
			else return $spec['type'];
2826
		}
2827
	}
2828
2829
	/**
2830
	 * Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
2831
	 * actually looking in the database.
2832
	 *
2833
	 * @param string $dataClass
2834
	 * @return bool
2835
	 */
2836
	public static function has_own_table($dataClass) {
2837
		if(!is_subclass_of($dataClass,'DataObject')) return false;
2838
2839
		$dataClass = ClassInfo::class_name($dataClass);
2840
		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...
2841
			if(get_parent_class($dataClass) == 'DataObject') {
2842
				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...
2843
			} else {
2844
				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...
2845
					= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
2846
					|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
2847
			}
2848
		}
2849
		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...
2850
	}
2851
2852
	/**
2853
	 * Returns true if the member is allowed to do the given action.
2854
	 * See {@link extendedCan()} for a more versatile tri-state permission control.
2855
	 *
2856
	 * @param string $perm The permission to be checked, such as 'View'.
2857
	 * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2858
	 * in user.
2859
	 *
2860
	 * @return boolean True if the the member is allowed to do the given action
2861
	 */
2862
	public function can($perm, $member = null) {
2863
		if(!isset($member)) {
2864
			$member = Member::currentUser();
2865
		}
2866
		if(Permission::checkMember($member, "ADMIN")) return true;
2867
2868
		if($this->manyManyComponent('Can' . $perm)) {
2869
			if($this->ParentID && $this->SecurityType == 'Inherit') {
2870
				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...
2871
					return false;
2872
				}
2873
				return $this->Parent->can($perm, $member);
2874
2875
			} else {
2876
				$permissionCache = $this->uninherited('permissionCache');
2877
				$memberID = $member ? $member->ID : 'none';
2878
2879
				if(!isset($permissionCache[$memberID][$perm])) {
2880
					if($member->ID) {
2881
						$groups = $member->Groups();
2882
					}
2883
2884
					$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...
2885
2886
					// TODO Fix relation table hardcoding
2887
					$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...
2888
						"\"Page_Can$perm\".PageID",
2889
					array("\"Page_Can$perm\""),
2890
						"GroupID IN ($groupList)");
2891
2892
					$permissionCache[$memberID][$perm] = $query->execute()->column();
2893
2894
					if($perm == "View") {
2895
						// TODO Fix relation table hardcoding
2896
						$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...
2897
							"\"SiteTree\"",
2898
							"LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2899
							), "\"Page_CanView\".\"PageID\" IS NULL");
2900
2901
							$unsecuredPages = $query->execute()->column();
2902
							if($permissionCache[$memberID][$perm]) {
2903
								$permissionCache[$memberID][$perm]
2904
									= array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2905
							} else {
2906
								$permissionCache[$memberID][$perm] = $unsecuredPages;
2907
							}
2908
					}
2909
2910
					Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2911
				}
2912
2913
				if($permissionCache[$memberID][$perm]) {
2914
					return in_array($this->ID, $permissionCache[$memberID][$perm]);
2915
				}
2916
			}
2917
		} else {
2918
			return parent::can($perm, $member);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ViewableData as the method can() does only exist in the following sub-classes of ViewableData: AdminRootController, AggregateTest_Bar, AggregateTest_Baz, AggregateTest_Fab, AggregateTest_Fac, AggregateTest_Foo, BasicAuthTest_ControllerSecuredWithPermission, BasicAuthTest_ControllerSecuredWithoutPermission, BulkLoaderTestPlayer, CMSFormTest_Controller, CMSMenuTest_LeftAndMainController, CMSProfileController, CMSSecurity, CheckboxFieldTest_Article, CheckboxSetFieldTest_Article, CheckboxSetFieldTest_Tag, ClassInfoTest_BaseClass, ClassInfoTest_BaseDataClass, ClassInfoTest_ChildClass, ClassInfoTest_GrandChildClass, ClassInfoTest_HasFields, ClassInfoTest_NoFields, ClassInfoTest_WithRelation, CliController, ComponentSetTest_Player, ComponentSetTest_Team, CompositeDBFieldTest_DataObject, Controller, ControllerTest_AccessBaseController, ControllerTest_AccessSecuredController, ControllerTest_AccessWildcardSecuredController, ControllerTest_ContainerController, ControllerTest_Controller, ControllerTest_HasAction, ControllerTest_HasAction_Unsecured, ControllerTest_IndexSecuredController, ControllerTest_SubController, ControllerTest_UnsecuredController, CsvBulkLoaderTest_Player, CsvBulkLoaderTest_PlayerContract, CsvBulkLoaderTest_Team, DailyTask, DataDifferencerTest_HasOneRelationObject, DataDifferencerTest_MockImage, DataDifferencerTest_Object, DataExtensionTest_CMSFieldsBase, DataExtensionTest_CMSFieldsChild, DataExtensionTest_CMSFieldsGrandchild, DataExtensionTest_Member, DataExtensionTest_MyObject, DataExtensionTest_Player, DataExtensionTest_RelatedObject, DataObject, DataObjectDuplicateTestClass1, DataObjectDuplicateTestClass2, DataObjectDuplicateTestClass3, DataObjectSchemaGenerationTest_DO, DataObjectSchemaGenerationTest_IndexDO, DataObjectSchemaGenerationTest_Sorted, DataObjectSchemaGenerationTest_SortedByText, DataObjectTest\NamespacedClass, DataObjectTest\RelationClass, DataObjectTest_Bogey, DataObjectTest_CEO, DataObjectTest_Company, DataObjectTest_CompositeDBField, DataObjectTest_EquipmentCompany, DataObjectTest_ExtendedTeamComment, DataObjectTest_Fan, DataObjectTest_FieldlessSubTable, DataObjectTest_FieldlessTable, DataObjectTest_Fixture, DataObjectTest_Play, DataObjectTest_Player, DataObjectTest_Ploy, DataObjectTest_Sortable, DataObjectTest_Staff, DataObjectTest_SubEquipmentCompany, DataObjectTest_SubTeam, DataObjectTest_Team, DataObjectTest_TeamComment, DataObjectTest_ValidatedObject, DataQueryTest_A, DataQueryTest_B, DataQueryTest_C, DataQueryTest_D, DataQueryTest_E, DataQueryTest_F, DataQueryTest_G, DatabaseAdmin, DatabaseTest_MyObject, DatetimeFieldTest_Model, DbDateTimeTest_Team, DecimalTest_DataObject, DevAdminControllerTest_Controller1, DevBuildController, DevelopmentAdmin, DirectorTestRequest_Controller, EmailFieldTest_Controller, FakeController, File, FileTest_MyCustomFile, FixtureBlueprintTest_Article, FixtureBlueprintTest_Page, FixtureBlueprintTest_SiteTree, FixtureFactoryTest_DataObject, FixtureFactoryTest_DataObjectRelation, Folder, FormScaffolderTest_Article, FormScaffolderTest_Author, FormScaffolderTest_Tag, FormTest_Controller, FormTest_ControllerWithSecurityToken, FormTest_ControllerWithStrictPostCheck, FormTest_Player, FormTest_Team, FulltextFilterTest_DataObject, GridFieldAction_Delete_Team, GridFieldAction_Edit_Team, GridFieldAddExistingAutocompleterTest_Controller, GridFieldDetailFormTest_Category, GridFieldDetailFormTest_CategoryController, GridFieldDetailFormTest_Controller, GridFieldDetailFormTest_GroupController, GridFieldDetailFormTest_PeopleGroup, GridFieldDetailFormTest_Person, GridFieldExportButtonTest_NoView, GridFieldExportButtonTest_Team, GridFieldFilterHeaderTest_DataObject, GridFieldPrintButtonTest_DO, GridFieldSortableHeaderTest_Cheerleader, GridFieldSortableHeaderTest_CheerleaderHat, GridFieldSortableHeaderTest_Team, GridFieldTest_Cheerleader, GridFieldTest_Permissions, GridFieldTest_Player, GridFieldTest_Team, GridField_URLHandlerTest_Controller, Group, GroupTest_Member, HTTPCacheControlIntegrationTest_RuleController, HTTPCacheControlIntegrationTest_SessionController, HasManyListTest_Company, HasManyListTest_CompanyCar, HasManyListTest_Employee, HierarchyHideTest_Object, HierarchyHideTest_SubObject, HierarchyTest_Object, HourlyTask, HtmlEditorFieldTest_Object, Image, Image_Cached, InstallerTest, JSTestRunner, LeftAndMain, LeftAndMainTest_Controller, LeftAndMainTest_Object, ListboxFieldTest_Article, ListboxFieldTest_DataObject, ListboxFieldTest_Tag, LoginAttempt, ManyManyListTest_Category, ManyManyListTest_ExtraFields, ManyManyListTest_IndirectPrimary, ManyManyListTest_Product, ManyManyListTest_Secondary, ManyManyListTest_SecondarySub, Member, MemberDatetimeOptionsetFieldTest_Controller, MemberPassword, ModelAdmin, ModelAdminTest_Admin, ModelAdminTest_Contact, ModelAdminTest_Player, ModelAdminTest_PlayerAdmin, MoneyFieldTest_CustomSetter_Object, MoneyFieldTest_Object, MoneyTest_DataObject, MoneyTest_SubClass, MonthlyTask, MySQLDatabaseTest_Data, NumericFieldTest_Object, OtherSubclassWithSameField, Permission, PermissionRole, PermissionRoleCode, QuarterHourlyTask, RequestHandlingFieldTest_Controller, RequestHandlingTest_AllowedController, RequestHandlingTest_Controller, RequestHandlingTest_Cont...rFormWithAllowedActions, RequestHandlingTest_FormActionController, RestfulServiceTest_Controller, SQLInsertTestBase, SQLQueryTestBase, SQLQueryTestChild, SQLQueryTest_DO, SQLUpdateChild, SQLUpdateTestBase, SSViewerCacheBlockTest_Model, SSViewerCacheBlockTest_VersionedModel, SSViewerTest_Controller, SSViewerTest_Object, SapphireInfo, SapphireREPL, ScheduledTask, SearchContextTest_Action, SearchContextTest_AllFilterTypes, SearchContextTest_Book, SearchContextTest_Company, SearchContextTest_Deadline, SearchContextTest_Person, SearchContextTest_Project, SearchFilterApplyRelationTest_DO, SearchFilterApplyRelationTest_HasManyChild, SearchFilterApplyRelationTest_HasManyGrantChild, SearchFilterApplyRelationTest_HasManyParent, SearchFilterApplyRelationTest_HasOneChild, SearchFilterApplyRelationTest_HasOneGrantChild, SearchFilterApplyRelationTest_HasOneParent, SearchFilterApplyRelationTest_ManyManyChild, SearchFilterApplyRelationTest_ManyManyGrantChild, SearchFilterApplyRelationTest_ManyManyParent, Security, SecurityAdmin, SecurityTest_NullController, SecurityTest_SecuredController, SilverStripe\Framework\Tests\ClassI, SubclassedDBFieldObject, TaskRunner, TestRunner, TransactionTest_Object, UnsavedRelationListTest_DataObject, Upload, UploadFieldTest_Controller, UploadFieldTest_ExtendedFile, UploadFieldTest_Record, VersionableExtensionsTest_DataObject, VersionedLazySub_DataObject, VersionedLazy_DataObject, VersionedTest_AnotherSubclass, VersionedTest_DataObject, VersionedTest_PublicStage, VersionedTest_PublicViaExtension, VersionedTest_RelatedWithoutVersion, VersionedTest_SingleStage, VersionedTest_Subclass, VersionedTest_UnversionedWithField, VersionedTest_WithIndexes, WeeklyTask, XMLDataFormatterTest_DataObject, YamlFixtureTest_DataObject, YamlFixtureTest_DataObjectRelation, YearlyTask, i18nTestModule, i18nTest_DataObject, i18nTextCollectorTestMyObject, i18nTextCollectorTestMySubObject. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

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

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

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

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

Available Fixes

  1. Change the type-hint for the parameter:

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

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

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
2919
		}
2920
	}
2921
2922
	/**
2923
	 * Process tri-state responses from permission-alterting extensions.  The extensions are
2924
	 * expected to return one of three values:
2925
	 *
2926
	 *  - false: Disallow this permission, regardless of what other extensions say
2927
	 *  - true: Allow this permission, as long as no other extensions return false
2928
	 *  - NULL: Don't affect the outcome
2929
	 *
2930
	 * This method itself returns a tri-state value, and is designed to be used like this:
2931
	 *
2932
	 * <code>
2933
	 * $extended = $this->extendedCan('canDoSomething', $member);
2934
	 * if($extended !== null) return $extended;
2935
	 * else return $normalValue;
2936
	 * </code>
2937
	 *
2938
	 * @param String $methodName Method on the same object, e.g. {@link canEdit()}
2939
	 * @param Member|int $member
2940
	 * @return boolean|null
2941
	 */
2942
	public function extendedCan($methodName, $member) {
2943
		$results = $this->extend($methodName, $member);
2944
		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...
2945
			// Remove NULLs
2946
			$results = array_filter($results, function($v) {return !is_null($v);});
2947
			// If there are any non-NULL responses, then return the lowest one of them.
2948
			// If any explicitly deny the permission, then we don't get access
2949
			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...
2950
		}
2951
		return null;
2952
	}
2953
2954
	/**
2955
	 * @param Member $member
2956
	 * @return boolean
2957
	 */
2958
	public function canView($member = null) {
2959
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2958 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2960
		if($extended !== null) {
2961
			return $extended;
2962
		}
2963
		return Permission::check('ADMIN', 'any', $member);
2964
	}
2965
2966
	/**
2967
	 * @param Member $member
2968
	 * @return boolean
2969
	 */
2970
	public function canEdit($member = null) {
2971
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2970 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...
2972
		if($extended !== null) {
2973
			return $extended;
2974
		}
2975
		return Permission::check('ADMIN', 'any', $member);
2976
	}
2977
2978
	/**
2979
	 * @param Member $member
2980
	 * @return boolean
2981
	 */
2982
	public function canDelete($member = null) {
2983
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2982 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...
2984
		if($extended !== null) {
2985
			return $extended;
2986
		}
2987
		return Permission::check('ADMIN', 'any', $member);
2988
	}
2989
2990
	/**
2991
	 * @todo Should canCreate be a static method?
2992
	 *
2993
	 * @param Member $member
2994
	 * @return boolean
2995
	 */
2996
	public function canCreate($member = null) {
2997
		$extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2996 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...
2998
		if($extended !== null) {
2999
			return $extended;
3000
		}
3001
		return Permission::check('ADMIN', 'any', $member);
3002
	}
3003
3004
	/**
3005
	 * Debugging used by Debug::show()
3006
	 *
3007
	 * @return string HTML data representing this object
3008
	 */
3009
	public function debug() {
3010
		$val = "<h3>Database record: $this->class</h3>\n<ul>\n";
3011
		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...
3012
			$val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
3013
		}
3014
		$val .= "</ul>\n";
3015
		return $val;
3016
	}
3017
3018
	/**
3019
	 * Return the DBField object that represents the given field.
3020
	 * This works similarly to obj() with 2 key differences:
3021
	 *   - it still returns an object even when the field has no value.
3022
	 *   - it only matches fields and not methods
3023
	 *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
3024
	 *
3025
	 * @param string $fieldName Name of the field
3026
	 * @return DBField The field as a DBField object
3027
	 */
3028
	public function dbObject($fieldName) {
3029
		// If we have a CompositeDBField object in $this->record, then return that
3030
		if(isset($this->record[$fieldName]) && is_object($this->record[$fieldName])) {
3031
			return $this->record[$fieldName];
3032
3033
		// Special case for ID field
3034
		} else if($fieldName == 'ID') {
3035
			return new PrimaryKey($fieldName, $this);
3036
3037
		// Special case for ClassName
3038
		} else if($fieldName == 'ClassName') {
3039
			$val = get_class($this);
3040
			return DBField::create_field('Varchar', $val, $fieldName);
3041
3042
		} else if(array_key_exists($fieldName, self::$fixed_fields)) {
3043
			return DBField::create_field(self::$fixed_fields[$fieldName], $this->$fieldName, $fieldName);
3044
3045
		// General casting information for items in $db
3046
		} else if($helper = $this->db($fieldName)) {
3047
			$obj = SS_Object::create_from_string($helper, $fieldName);
3048
			$obj->setValue($this->$fieldName, $this->record, false);
3049
			return $obj;
3050
3051
		// Special case for has_one relationships
3052
		} 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...
3053
			$val = $this->$fieldName;
3054
			return DBField::create_field('ForeignKey', $val, $fieldName, $this);
3055
3056
		// has_one for polymorphic relations do not end in ID
3057
		} else if(($type = $this->hasOneComponent($fieldName)) && ($type === 'DataObject')) {
3058
			$val = $this->$fieldName();
3059
			return DBField::create_field('PolymorphicForeignKey', $val, $fieldName, $this);
3060
3061
		}
3062
	}
3063
3064
	/**
3065
	 * Traverses to a DBField referenced by relationships between data objects.
3066
	 *
3067
	 * The path to the related field is specified with dot separated syntax
3068
	 * (eg: Parent.Child.Child.FieldName).
3069
	 *
3070
	 * @param string $fieldPath
3071
	 *
3072
	 * @return mixed DBField of the field on the object or a DataList instance.
3073
	 */
3074
	public function relObject($fieldPath) {
3075
		$object = null;
3076
3077
		if(strpos($fieldPath, '.') !== false) {
3078
			$parts = explode('.', $fieldPath);
3079
			$fieldName = array_pop($parts);
3080
3081
			// Traverse dot syntax
3082
			$component = $this;
3083
3084
			foreach($parts as $relation) {
3085
				if($component instanceof SS_List) {
3086
					if(method_exists($component,$relation)) {
3087
						$component = $component->$relation();
3088
					} else {
3089
						$component = $component->relation($relation);
3090
					}
3091
				} else {
3092
					$component = $component->$relation();
3093
				}
3094
			}
3095
3096
			$object = $component->dbObject($fieldName);
3097
3098
		} else {
3099
			$object = $this->dbObject($fieldPath);
3100
		}
3101
3102
		return $object;
3103
	}
3104
3105
	/**
3106
	 * Traverses to a field referenced by relationships between data objects, returning the value
3107
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3108
	 *
3109
	 * @param $fieldPath string
3110
	 * @return string | null - will return null on a missing value
3111
	 */
3112
	public function relField($fieldName) {
3113
		$component = $this;
3114
3115
		// We're dealing with relations here so we traverse the dot syntax
3116
		if(strpos($fieldName, '.') !== false) {
3117
			$relations = explode('.', $fieldName);
3118
			$fieldName = array_pop($relations);
3119
			foreach($relations as $relation) {
3120
				// Bail if the component is null
3121
				if(!$component) {
3122
					return null;
3123
				// Inspect $component for element $relation
3124
				} elseif($component->hasMethod($relation)) {
3125
					// Check nested method
3126
					$component = $component->$relation();
3127
				} elseif($component instanceof SS_List) {
3128
					// Select adjacent relation from DataList
3129
					$component = $component->relation($relation);
3130
				} elseif($component instanceof DataObject
3131
					&& ($dbObject = $component->dbObject($relation))
3132
				) {
3133
					// Select db object
3134
					$component = $dbObject;
3135
				} else {
3136
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
3137
				}
3138
			}
3139
		}
3140
3141
		// Bail if the component is null
3142
		if(!$component) {
3143
			return null;
3144
		}
3145
		if($component->hasMethod($fieldName)) {
3146
			return $component->$fieldName();
3147
		}
3148
		return $component->$fieldName;
3149
	}
3150
3151
	/**
3152
	 * Temporary hack to return an association name, based on class, to get around the mangle
3153
	 * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3154
	 *
3155
	 * @return String
3156
	 */
3157
	public function getReverseAssociation($className) {
3158
		if (is_array($this->manyMany())) {
3159
			$many_many = array_flip($this->manyMany());
3160
			if (array_key_exists($className, $many_many)) return $many_many[$className];
3161
		}
3162
		if (is_array($this->hasMany())) {
3163
			$has_many = array_flip($this->hasMany());
3164
			if (array_key_exists($className, $has_many)) return $has_many[$className];
3165
		}
3166
		if (is_array($this->hasOne())) {
3167
			$has_one = array_flip($this->hasOne());
3168
			if (array_key_exists($className, $has_one)) return $has_one[$className];
3169
		}
3170
3171
		return false;
3172
	}
3173
3174
	/**
3175
	 * Return all objects matching the filter
3176
	 * sub-classes are automatically selected and included
3177
	 *
3178
	 * @param string $callerClass The class of objects to be returned
3179
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3180
	 * Supports parameterised queries. See SQLQuery::addWhere() for syntax examples.
3181
	 * @param string|array $sort A sort expression to be inserted into the ORDER
3182
	 * BY clause.  If omitted, self::$default_sort will be used.
3183
	 * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3184
	 * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3185
	 * @param string $containerClass The container class to return the results in.
3186
	 *
3187
	 * @todo $containerClass is Ignored, why?
3188
	 *
3189
	 * @return DataList The objects matching the filter, in the class specified by $containerClass
3190
	 */
3191
	public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
3192
			$containerClass = 'DataList') {
3193
3194
		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...
3195
			$callerClass = get_called_class();
3196
			if($callerClass == 'DataObject') {
3197
				throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3198
			}
3199
3200
			if($filter || $sort || $join || $limit || ($containerClass != 'DataList')) {
3201
				throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3202
					. ' arguments');
3203
			}
3204
3205
			$result = DataList::create(get_called_class());
3206
			$result->setDataModel(DataModel::inst());
3207
			return $result;
3208
		}
3209
3210
		if($join) {
3211
			throw new \InvalidArgumentException(
3212
				'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3213
			);
3214
		}
3215
3216
		$result = DataList::create($callerClass)->where($filter)->sort($sort);
3217
3218
		if($limit && strpos($limit, ',') !== false) {
3219
			$limitArguments = explode(',', $limit);
3220
			$result = $result->limit($limitArguments[1],$limitArguments[0]);
3221
		} elseif($limit) {
3222
			$result = $result->limit($limit);
3223
		}
3224
3225
		$result->setDataModel(DataModel::inst());
3226
		return $result;
3227
	}
3228
3229
3230
	/**
3231
	 * @deprecated
3232
	 */
3233
	public function Aggregate($class = null) {
3234
		Deprecation::notice('4.0', 'Call aggregate methods on a DataList directly instead. In templates'
3235
			. ' an example of the new syntax is &lt% cached List(Member).max(LastEdited) %&gt instead'
3236
			. ' (check partial-caching.md documentation for more details.)');
3237
3238
		if($class) {
3239
			$list = new DataList($class);
3240
			$list->setDataModel(DataModel::inst());
3241
		} else if(isset($this)) {
3242
			$list = new DataList(get_class($this));
3243
			$list->setDataModel($this->model);
3244
		} else {
3245
			throw new \InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed"
3246
				. " a classname");
3247
		}
3248
		return $list;
3249
	}
3250
3251
	/**
3252
	 * @deprecated
3253
	 */
3254
	public function RelationshipAggregate($relationship) {
3255
		Deprecation::notice('4.0', 'Call aggregate methods on a relationship directly instead.');
3256
3257
		return $this->$relationship();
3258
	}
3259
3260
	/**
3261
	 * Return the first item matching the given query.
3262
	 * All calls to get_one() are cached.
3263
	 *
3264
	 * @param string $callerClass The class of objects to be returned
3265
	 * @param string|array $filter A filter to be inserted into the WHERE clause.
3266
	 * Supports parameterised queries. See SQLQuery::addWhere() for syntax examples.
3267
	 * @param boolean $cache Use caching
3268
	 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3269
	 *
3270
	 * @return DataObject The first item matching the query
3271
	 */
3272
	public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
3273
		$SNG = singleton($callerClass);
3274
3275
		$cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3276
		$cacheKey = md5(serialize($cacheComponents));
3277
3278
		// Flush destroyed items out of the cache
3279
		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...
3280
				&& 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...
3281
				&& 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...
3282
3283
			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...
3284
		}
3285
		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...
3286
			$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...
3287
			$item = $dl->First();
3288
3289
			if($cache) {
3290
				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...
3291
				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...
3292
					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...
3293
				}
3294
			}
3295
		}
3296
		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...
3297
	}
3298
3299
	/**
3300
	 * Flush the cached results for all relations (has_one, has_many, many_many)
3301
	 * Also clears any cached aggregate data.
3302
	 *
3303
	 * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3304
	 *                            When false will just clear session-local cached data
3305
	 * @return DataObject $this
3306
	 */
3307
	public function flushCache($persistent = true) {
3308
		if($persistent) Aggregate::flushCache($this->class);
3309
3310
		if($this->class == 'DataObject') {
3311
			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...
3312
			return $this;
3313
		}
3314
3315
		$classes = ClassInfo::ancestry($this->class);
3316
		foreach($classes as $class) {
3317
			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...
3318
		}
3319
3320
		$this->extend('flushCache');
3321
3322
		$this->components = array();
3323
		return $this;
3324
	}
3325
3326
	/**
3327
	 * Flush the get_one global cache and destroy associated objects.
3328
	 */
3329
	public static function flush_and_destroy_cache() {
3330
		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...
3331
			if(is_array($items)) foreach($items as $item) {
3332
				if($item) $item->destroy();
3333
			}
3334
		}
3335
		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...
3336
	}
3337
3338
	/**
3339
	 * Reset all global caches associated with DataObject.
3340
	 */
3341
	public static function reset() {
3342
		self::clear_classname_spec_cache();
3343
		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...
3344
		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...
3345
		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...
3346
		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...
3347
		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...
3348
		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...
3349
		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...
3350
		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...
3351
	}
3352
3353
	/**
3354
	 * Return the given element, searching by ID
3355
	 *
3356
	 * @param string $callerClass The class of the object to be returned
3357
	 * @param int $id The id of the element
3358
	 * @param boolean $cache See {@link get_one()}
3359
	 *
3360
	 * @return DataObject The element
3361
	 */
3362
	public static function get_by_id($callerClass, $id, $cache = true) {
3363
		if(!is_numeric($id)) {
3364
			user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3365
		}
3366
3367
		// Check filter column
3368
		if(is_subclass_of($callerClass, 'DataObject')) {
3369
			$baseClass = ClassInfo::baseDataClass($callerClass);
3370
			$column = "\"$baseClass\".\"ID\"";
3371
		} else{
3372
			// This simpler code will be used by non-DataObject classes that implement DataObjectInterface
3373
			$column = '"ID"';
3374
		}
3375
3376
		// Relegate to get_one
3377
		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...
3378
	}
3379
3380
	/**
3381
	 * Get the name of the base table for this object
3382
	 */
3383
	public function baseTable() {
3384
		$tableClasses = ClassInfo::dataClassesFor($this->class);
3385
		return array_shift($tableClasses);
3386
	}
3387
3388
	/**
3389
	 * @var Array Parameters used in the query that built this object.
3390
	 * This can be used by decorators (e.g. lazy loading) to
3391
	 * run additional queries using the same context.
3392
	 */
3393
	protected $sourceQueryParams;
3394
3395
	/**
3396
	 * @see $sourceQueryParams
3397
	 * @return array
3398
	 */
3399
	public function getSourceQueryParams() {
3400
		return $this->sourceQueryParams;
3401
	}
3402
3403
	/**
3404
	 * @see $sourceQueryParams
3405
	 * @param array
3406
	 */
3407
	public function setSourceQueryParams($array) {
3408
		$this->sourceQueryParams = $array;
3409
	}
3410
3411
	/**
3412
	 * @see $sourceQueryParams
3413
	 * @param array
3414
	 */
3415
	public function setSourceQueryParam($key, $value) {
3416
		$this->sourceQueryParams[$key] = $value;
3417
	}
3418
3419
	/**
3420
	 * @see $sourceQueryParams
3421
	 * @return Mixed
3422
	 */
3423
	public function getSourceQueryParam($key) {
3424
		if(isset($this->sourceQueryParams[$key])) return $this->sourceQueryParams[$key];
3425
		else return null;
3426
	}
3427
3428
	//-------------------------------------------------------------------------------------------//
3429
3430
	/**
3431
	 * Return the database indexes on this table.
3432
	 * This array is indexed by the name of the field with the index, and
3433
	 * the value is the type of index.
3434
	 */
3435
	public function databaseIndexes() {
3436
		$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...
3437
		$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...
3438
		$sort = $this->uninherited('default_sort',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...
3439
		//$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...
3440
3441
		$indexes = array();
3442
3443
		if($has_one) {
3444
			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...
3445
				$indexes[$relationshipName . 'ID'] = true;
3446
			}
3447
		}
3448
3449
		if($classIndexes) {
3450
			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...
3451
				$indexes[$indexName] = $indexType;
3452
			}
3453
		}
3454
3455
		if ($sort && is_string($sort)) {
3456
			$sort = preg_split('/,(?![^()]*+\\))/', $sort);
3457
3458
			foreach ($sort as $value) {
3459
				try {
3460
					list ($table, $column) = $this->parseSortColumn(trim($value));
3461
3462
					$table = trim($table, '"');
3463
					$column = trim($column, '"');
3464
3465
					if ($table && strtolower($table) !== strtolower($this->class)) {
3466
						continue;
3467
					}
3468
					// Skip already indexed columns
3469
					if (array_key_exists($column, $indexes)) {
3470
						continue;
3471
					}
3472
					// Get field type (including fixed fields) on this table, if it exists
3473
					$fieldType = $this->hasOwnTableDatabaseField($column);
3474
					if (!$fieldType) {
3475
						continue;
3476
					}
3477
					$isAutoIndexable = Config::inst()->get($fieldType, 'auto_indexable')
3478
						|| Config::inst()->get("DB{$fieldType}", 'auto_indexable');
3479
					if ($isAutoIndexable) {
3480
						$indexes[$column] = true;
3481
					}
3482
				} catch (InvalidArgumentException $e) { }
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
3483
			}
3484
		}
3485
3486
		if(get_parent_class($this) == "DataObject") {
3487
			$indexes['ClassName'] = true;
3488
		}
3489
3490
		return $indexes;
3491
	}
3492
3493
	/**
3494
	 * Parses a specified column into a sort field and direction
3495
	 *
3496
	 * @param string $column String to parse containing the column name
3497
	 * @return array Resolved table and column.
3498
	 */
3499
	protected function parseSortColumn($column) {
3500
		// Parse column specification, considering possible ansi sql quoting
3501
		// Note that table prefix is allowed, but discarded
3502
		if(preg_match('/^("?(?<table>[^"\s]+)"?\\.)?"?(?<column>[^"\s]+)"?(\s+(?<direction>((asc)|(desc))(ending)?))?$/i', $column, $match)) {
3503
			$table = $match['table'];
3504
			$column = $match['column'];
3505
		} else {
3506
			throw new InvalidArgumentException("Invalid sort() column");
3507
		}
3508
3509
		return array($table, $column);
3510
	}
3511
3512
	/**
3513
	 * Check the database schema and update it as necessary.
3514
	 *
3515
	 * @uses DataExtension->augmentDatabase()
3516
	 */
3517
	public function requireTable() {
3518
		// Only build the table if we've actually got fields
3519
		$fields = self::database_fields($this->class);
3520
		$extensions = self::database_extensions($this->class);
3521
3522
		$indexes = $this->databaseIndexes();
3523
3524
		// Validate relationship configuration
3525
		$this->validateModelDefinitions();
3526
3527
		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...
3528
			$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
3529
			DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
3530
				$extensions);
3531
		} else {
3532
			DB::dont_require_table($this->class);
3533
		}
3534
3535
		// Build any child tables for many_many items
3536
		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...
3537
			$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...
3538
			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...
3539
				// Build field list
3540
				$manymanyFields = array(
3541
					"{$this->class}ID" => "Int",
3542
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => "Int",
3543
				);
3544
				if(isset($extras[$relationship])) {
3545
					$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
3546
				}
3547
3548
				// Build index list
3549
				$manymanyIndexes = array(
3550
					"{$this->class}ID" => true,
3551
				(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true,
3552
				);
3553
3554
				DB::require_table("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null,
3555
					$extensions);
3556
			}
3557
		}
3558
3559
		// Let any extentions make their own database fields
3560
		$this->extend('augmentDatabase', $dummy);
3561
	}
3562
3563
	/**
3564
	 * Validate that the configured relations for this class use the correct syntaxes
3565
	 * @throws LogicException
3566
	 */
3567
	protected function validateModelDefinitions() {
3568
		$modelDefinitions = array(
3569
			'db' => Config::inst()->get($this->class, 'db', Config::UNINHERITED),
3570
			'has_one' => Config::inst()->get($this->class, 'has_one', Config::UNINHERITED),
3571
			'has_many' => Config::inst()->get($this->class, 'has_many', Config::UNINHERITED),
3572
			'belongs_to' => Config::inst()->get($this->class, 'belongs_to', Config::UNINHERITED),
3573
			'many_many' => Config::inst()->get($this->class, 'many_many', Config::UNINHERITED),
3574
			'belongs_many_many' => Config::inst()->get($this->class, 'belongs_many_many', Config::UNINHERITED),
3575
			'many_many_extraFields' => Config::inst()->get($this->class, 'many_many_extraFields', Config::UNINHERITED)
3576
		);
3577
3578
		foreach($modelDefinitions as $defType => $relations) {
3579
			if( ! $relations) continue;
3580
3581
			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...
3582
				if($defType === 'many_many_extraFields') {
3583
					if(!is_array($v)) {
3584
						throw new LogicException("$this->class::\$many_many_extraFields has a bad entry: "
3585
							. var_export($k, true) . " => " . var_export($v, true)
3586
							. ". Each many_many_extraFields entry should map to a field specification array.");
3587
					}
3588
				} else {
3589
					if(!is_string($k) || is_numeric($k) || !is_string($v)) {
3590
						throw new LogicException("$this->class::$defType has a bad entry: "
3591
							. var_export($k, true). " => " . var_export($v, true) . ".  Each map key should be a
3592
							 relationship name, and the map value should be the data class to join to.");
3593
					}
3594
				}
3595
			}
3596
		}
3597
	}
3598
3599
	/**
3600
	 * Add default records to database. This function is called whenever the
3601
	 * database is built, after the database tables have all been created. Overload
3602
	 * this to add default records when the database is built, but make sure you
3603
	 * call parent::requireDefaultRecords().
3604
	 *
3605
	 * @uses DataExtension->requireDefaultRecords()
3606
	 */
3607
	public function requireDefaultRecords() {
3608
		$defaultRecords = $this->config()->get('default_records', Config::UNINHERITED);
3609
3610
		if(!empty($defaultRecords)) {
3611
			$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...
3612
			if(!$hasData) {
3613
				$className = $this->class;
3614
				foreach($defaultRecords as $record) {
3615
					$obj = $this->model->$className->newObject($record);
3616
					$obj->write();
3617
				}
3618
				DB::alteration_message("Added default records to $className table","created");
3619
			}
3620
		}
3621
3622
		// Let any extentions make their own database default data
3623
		$this->extend('requireDefaultRecords', $dummy);
3624
	}
3625
3626
	/**
3627
	 * Returns fields bu traversing the class heirachy in a bottom-up direction.
3628
	 *
3629
	 * Needed to avoid getCMSFields being empty when customDatabaseFields overlooks
3630
	 * the inheritance chain of the $db array, where a child data object has no $db array,
3631
	 * but still needs to know the properties of its parent. This should be merged into databaseFields or
3632
	 * customDatabaseFields.
3633
	 *
3634
	 * @todo review whether this is still needed after recent API changes
3635
	 */
3636
	public function inheritedDatabaseFields() {
3637
		$fields     = array();
3638
		$currentObj = $this->class;
3639
3640
		while($currentObj != 'DataObject') {
3641
			$fields     = array_merge($fields, self::custom_database_fields($currentObj));
3642
			$currentObj = get_parent_class($currentObj);
3643
		}
3644
3645
		return (array) $fields;
3646
	}
3647
3648
	/**
3649
	 * Get the default searchable fields for this object, as defined in the
3650
	 * $searchable_fields list. If searchable fields are not defined on the
3651
	 * data object, uses a default selection of summary fields.
3652
	 *
3653
	 * @return array
3654
	 */
3655
	public function searchableFields() {
3656
		// can have mixed format, need to make consistent in most verbose form
3657
		$fields = $this->stat('searchable_fields');
3658
		$labels = $this->fieldLabels();
3659
3660
		// fallback to summary fields (unless empty array is explicitly specified)
3661
		if( ! $fields && ! is_array($fields)) {
3662
			$summaryFields = array_keys($this->summaryFields());
3663
			$fields = array();
3664
3665
			// remove the custom getters as the search should not include them
3666
			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...
3667
				foreach($summaryFields as $key => $name) {
3668
					$spec = $name;
3669
3670
					// Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3671
					if(($fieldPos = strpos($name, '.')) !== false) {
3672
						$name = substr($name, 0, $fieldPos);
3673
					}
3674
3675
					if($this->hasDatabaseField($name)) {
3676
						$fields[] = $name;
3677
					} elseif($this->relObject($spec)) {
3678
						$fields[] = $spec;
3679
					}
3680
				}
3681
			}
3682
		}
3683
3684
		// we need to make sure the format is unified before
3685
		// augmenting fields, so extensions can apply consistent checks
3686
		// but also after augmenting fields, because the extension
3687
		// might use the shorthand notation as well
3688
3689
		// rewrite array, if it is using shorthand syntax
3690
		$rewrite = array();
3691
		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...
3692
			$identifer = (is_int($name)) ? $specOrName : $name;
3693
3694
			if(is_int($name)) {
3695
				// 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...
3696
				$rewrite[$identifer] = array();
3697
			} elseif(is_array($specOrName)) {
3698
				// 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...
3699
				//   'filter => 'ExactMatchFilter',
3700
				//   '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...
3701
				//   'title' => 'My Title', // optional
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3702
				// ))
3703
				$rewrite[$identifer] = array_merge(
3704
					array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3705
					(array)$specOrName
3706
				);
3707
			} else {
3708
				// 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...
3709
				$rewrite[$identifer] = array(
3710
					'filter' => $specOrName,
3711
				);
3712
			}
3713
			if(!isset($rewrite[$identifer]['title'])) {
3714
				$rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3715
					? $labels[$identifer] : FormField::name_to_label($identifer);
3716
			}
3717
			if(!isset($rewrite[$identifer]['filter'])) {
3718
				$rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3719
			}
3720
		}
3721
3722
		$fields = $rewrite;
3723
3724
		// apply DataExtensions if present
3725
		$this->extend('updateSearchableFields', $fields);
3726
3727
		return $fields;
3728
	}
3729
3730
	/**
3731
	 * Get any user defined searchable fields labels that
3732
	 * exist. Allows overriding of default field names in the form
3733
	 * interface actually presented to the user.
3734
	 *
3735
	 * The reason for keeping this separate from searchable_fields,
3736
	 * which would be a logical place for this functionality, is to
3737
	 * avoid bloating and complicating the configuration array. Currently
3738
	 * much of this system is based on sensible defaults, and this property
3739
	 * would generally only be set in the case of more complex relationships
3740
	 * between data object being required in the search interface.
3741
	 *
3742
	 * Generates labels based on name of the field itself, if no static property
3743
	 * {@link self::field_labels} exists.
3744
	 *
3745
	 * @uses $field_labels
3746
	 * @uses FormField::name_to_label()
3747
	 *
3748
	 * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3749
	 *
3750
	 * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3751
	 */
3752
	public function fieldLabels($includerelations = true) {
3753
		$cacheKey = $this->class . '_' . $includerelations;
3754
3755
		if(!isset(self::$_cache_field_labels[$cacheKey])) {
3756
			$customLabels = $this->stat('field_labels');
3757
			$autoLabels = array();
3758
3759
			// get all translated static properties as defined in i18nCollectStatics()
3760
			$ancestry = ClassInfo::ancestry($this->class);
3761
			$ancestry = array_reverse($ancestry);
3762
			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...
3763
				if($ancestorClass == 'ViewableData') break;
3764
				$types = array(
3765
					'db'        => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3766
				);
3767
				if($includerelations){
3768
					$types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3769
					$types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3770
					$types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3771
					$types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3772
				}
3773
				foreach($types as $type => $attrs) {
3774
					foreach($attrs as $name => $spec) {
3775
						$autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}",FormField::name_to_label($name));
3776
					}
3777
				}
3778
			}
3779
3780
			$labels = array_merge((array)$autoLabels, (array)$customLabels);
3781
			$this->extend('updateFieldLabels', $labels);
3782
			self::$_cache_field_labels[$cacheKey] = $labels;
3783
		}
3784
3785
		return self::$_cache_field_labels[$cacheKey];
3786
	}
3787
3788
	/**
3789
	 * Get a human-readable label for a single field,
3790
	 * see {@link fieldLabels()} for more details.
3791
	 *
3792
	 * @uses fieldLabels()
3793
	 * @uses FormField::name_to_label()
3794
	 *
3795
	 * @param string $name Name of the field
3796
	 * @return string Label of the field
3797
	 */
3798
	public function fieldLabel($name) {
3799
		$labels = $this->fieldLabels();
3800
		return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3801
	}
3802
3803
	/**
3804
	 * Get the default summary fields for this object.
3805
	 *
3806
	 * @todo use the translation apparatus to return a default field selection for the language
3807
	 *
3808
	 * @return array
3809
	 */
3810
	public function summaryFields() {
3811
		$rawFields = $this->stat('summary_fields');
3812
3813
		$fields = array();
3814
		// Merge associative / numeric keys
3815
		if (is_array($rawFields)) {
3816
			foreach ($rawFields as $key => $value) {
3817
				if (is_int($key)) {
3818
					$key = $value;
3819
				}
3820
				$fields[$key] = $value;
3821
			}
3822
		}
3823
3824
		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...
3825
			$fields = array();
3826
			// try to scaffold a couple of usual suspects
3827
			if ($this->hasField('Name')) $fields['Name'] = 'Name';
3828
			if ($this->hasDataBaseField('Title')) $fields['Title'] = 'Title';
3829
			if ($this->hasField('Description')) $fields['Description'] = 'Description';
3830
			if ($this->hasField('FirstName')) $fields['FirstName'] = 'First Name';
3831
		}
3832
		$this->extend("updateSummaryFields", $fields);
3833
3834
		// Final fail-over, just list ID field
3835
		if(!$fields) $fields['ID'] = 'ID';
3836
3837
		// Localize fields (if possible)
3838
		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...
3839
			// only attempt to localize if the label definition is the same as the field name.
3840
			// this will preserve any custom labels set in the summary_fields configuration
3841
			if(isset($fields[$name]) && $name === $fields[$name]) {
3842
				$fields[$name] = $label;
3843
			}
3844
		}
3845
3846
		return $fields;
3847
	}
3848
3849
	/**
3850
	 * Defines a default list of filters for the search context.
3851
	 *
3852
	 * If a filter class mapping is defined on the data object,
3853
	 * it is constructed here. Otherwise, the default filter specified in
3854
	 * {@link DBField} is used.
3855
	 *
3856
	 * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3857
	 *
3858
	 * @return array
3859
	 */
3860
	public function defaultSearchFilters() {
3861
		$filters = array();
3862
3863
		foreach($this->searchableFields() as $name => $spec) {
3864
			$filterClass = $spec['filter'];
3865
3866
			if($spec['filter'] instanceof SearchFilter) {
3867
				$filters[$name] = $spec['filter'];
3868
			} else {
3869
				$class = $spec['filter'];
3870
3871
				if(!is_subclass_of($spec['filter'], 'SearchFilter')) {
3872
					$class = 'PartialMatchFilter';
3873
				}
3874
3875
				$filters[$name] = new $class($name);
3876
			}
3877
		}
3878
3879
		return $filters;
3880
	}
3881
3882
	/**
3883
	 * @return boolean True if the object is in the database
3884
	 */
3885
	public function isInDB() {
3886
		return is_numeric( $this->ID ) && $this->ID > 0;
3887
	}
3888
3889
	/*
3890
	 * @ignore
3891
	 */
3892
	private static $subclass_access = true;
3893
3894
	/**
3895
	 * Temporarily disable subclass access in data object qeur
3896
	 */
3897
	public static function disable_subclass_access() {
3898
		self::$subclass_access = false;
3899
	}
3900
	public static function enable_subclass_access() {
3901
		self::$subclass_access = true;
3902
	}
3903
3904
	//-------------------------------------------------------------------------------------------//
3905
3906
	/**
3907
	 * Database field definitions.
3908
	 * This is a map from field names to field type. The field
3909
	 * type should be a class that extends .
3910
	 * @var array
3911
	 * @config
3912
	 */
3913
	private static $db = null;
3914
3915
	/**
3916
	 * Use a casting object for a field. This is a map from
3917
	 * field name to class name of the casting object.
3918
	 * @var array
3919
	 */
3920
	private static $casting = array(
3921
		"ID" => 'Int',
3922
		"ClassName" => 'Varchar',
3923
		"LastEdited" => "SS_Datetime",
3924
		"Created" => "SS_Datetime",
3925
		"Title" => 'Text',
3926
	);
3927
3928
	/**
3929
	 * Specify custom options for a CREATE TABLE call.
3930
	 * Can be used to specify a custom storage engine for specific database table.
3931
	 * All options have to be keyed for a specific database implementation,
3932
	 * identified by their class name (extending from {@link SS_Database}).
3933
	 *
3934
	 * <code>
3935
	 * array(
3936
	 *  'MySQLDatabase' => 'ENGINE=MyISAM'
3937
	 * )
3938
	 * </code>
3939
	 *
3940
	 * Caution: This API is experimental, and might not be
3941
	 * included in the next major release. Please use with care.
3942
	 *
3943
	 * @var array
3944
	 * @config
3945
	 */
3946
	private static $create_table_options = array(
3947
		'MySQLDatabase' => 'ENGINE=InnoDB'
3948
	);
3949
3950
	/**
3951
	 * If a field is in this array, then create a database index
3952
	 * on that field. This is a map from fieldname to index type.
3953
	 * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3954
	 *
3955
	 * @var array
3956
	 * @config
3957
	 */
3958
	private static $indexes = null;
3959
3960
	/**
3961
	 * Inserts standard column-values when a DataObject
3962
	 * is instanciated. Does not insert default records {@see $default_records}.
3963
	 * This is a map from fieldname to default value.
3964
	 *
3965
	 *  - If you would like to change a default value in a sub-class, just specify it.
3966
	 *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3967
	 *    or false in your subclass.  Setting it to null won't work.
3968
	 *
3969
	 * @var array
3970
	 * @config
3971
	 */
3972
	private static $defaults = null;
3973
3974
	/**
3975
	 * Multidimensional array which inserts default data into the database
3976
	 * on a db/build-call as long as the database-table is empty. Please use this only
3977
	 * for simple constructs, not for SiteTree-Objects etc. which need special
3978
	 * behaviour such as publishing and ParentNodes.
3979
	 *
3980
	 * Example:
3981
	 * array(
3982
	 *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3983
	 *  array('Title' => "DefaultPage2")
3984
	 * ).
3985
	 *
3986
	 * @var array
3987
	 * @config
3988
	 */
3989
	private static $default_records = null;
3990
3991
	/**
3992
	 * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3993
	 * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3994
	 *
3995
	 * Note that you cannot have a has_one and belongs_to relationship with the same name.
3996
	 *
3997
	 *	@var array
3998
	 * @config
3999
	 */
4000
	private static $has_one = null;
4001
4002
	/**
4003
	 * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
4004
	 *
4005
	 * This does not actually create any data structures, but allows you to query the other object in a one-to-one
4006
	 * relationship from the child object. If you have multiple belongs_to links to another object you can use the
4007
	 * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
4008
	 *
4009
	 * Note that you cannot have a has_one and belongs_to relationship with the same name.
4010
	 *
4011
	 * @var array
4012
	 * @config
4013
	 */
4014
	private static $belongs_to;
4015
4016
	/**
4017
	 * This defines a one-to-many relationship. It is a map of component name to the remote data class.
4018
	 *
4019
	 * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
4020
	 * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
4021
	 * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
4022
	 * which foreign key to use.
4023
	 *
4024
	 * @var array
4025
	 * @config
4026
	 */
4027
	private static $has_many = null;
4028
4029
	/**
4030
	 * many-many relationship definitions.
4031
	 * This is a map from component name to data type.
4032
	 * @var array
4033
	 * @config
4034
	 */
4035
	private static $many_many = null;
4036
4037
	/**
4038
	 * Extra fields to include on the connecting many-many table.
4039
	 * This is a map from field name to field type.
4040
	 *
4041
	 * Example code:
4042
	 * <code>
4043
	 * public static $many_many_extraFields = array(
4044
	 *  'Members' => array(
4045
	 *			'Role' => 'Varchar(100)'
4046
	 *		)
4047
	 * );
4048
	 * </code>
4049
	 *
4050
	 * @var array
4051
	 * @config
4052
	 */
4053
	private static $many_many_extraFields = null;
4054
4055
	/**
4056
	 * The inverse side of a many-many relationship.
4057
	 * This is a map from component name to data type.
4058
	 * @var array
4059
	 * @config
4060
	 */
4061
	private static $belongs_many_many = null;
4062
4063
	/**
4064
	 * The default sort expression. This will be inserted in the ORDER BY
4065
	 * clause of a SQL query if no other sort expression is provided.
4066
	 * @var string
4067
	 * @config
4068
	 */
4069
	private static $default_sort = null;
4070
4071
	/**
4072
	 * Default list of fields that can be scaffolded by the ModelAdmin
4073
	 * search interface.
4074
	 *
4075
	 * Overriding the default filter, with a custom defined filter:
4076
	 * <code>
4077
	 *  static $searchable_fields = array(
4078
	 *     "Name" => "PartialMatchFilter"
4079
	 *  );
4080
	 * </code>
4081
	 *
4082
	 * Overriding the default form fields, with a custom defined field.
4083
	 * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
4084
	 * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
4085
	 * <code>
4086
	 *  static $searchable_fields = array(
4087
	 *    "Name" => array(
4088
	 *      "field" => "TextField"
4089
	 *    )
4090
	 *  );
4091
	 * </code>
4092
	 *
4093
	 * Overriding the default form field, filter and title:
4094
	 * <code>
4095
	 *  static $searchable_fields = array(
4096
	 *    "Organisation.ZipCode" => array(
4097
	 *      "field" => "TextField",
4098
	 *      "filter" => "PartialMatchFilter",
4099
	 *      "title" => 'Organisation ZIP'
4100
	 *    )
4101
	 *  );
4102
	 * </code>
4103
	 * @config
4104
	 */
4105
	private static $searchable_fields = null;
4106
4107
	/**
4108
	 * User defined labels for searchable_fields, used to override
4109
	 * default display in the search form.
4110
	 * @config
4111
	 */
4112
	private static $field_labels = null;
4113
4114
	/**
4115
	 * Provides a default list of fields to be used by a 'summary'
4116
	 * view of this object.
4117
	 * @config
4118
	 */
4119
	private static $summary_fields = null;
4120
4121
	/**
4122
	 * Provides a list of allowed methods that can be called via RESTful api.
4123
	 */
4124
	public static $allowed_actions = null;
4125
4126
	/**
4127
	 * Collect all static properties on the object
4128
	 * which contain natural language, and need to be translated.
4129
	 * The full entity name is composed from the class name and a custom identifier.
4130
	 *
4131
	 * @return array A numerical array which contains one or more entities in array-form.
4132
	 * Each numeric entity array contains the "arguments" for a _t() call as array values:
4133
	 * $entity, $string, $priority, $context.
4134
	 */
4135
	public function provideI18nEntities() {
4136
		$entities = array();
4137
4138
		$entities["{$this->class}.SINGULARNAME"] = array(
4139
			$this->singular_name(),
4140
4141
			'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
4142
		);
4143
4144
		$entities["{$this->class}.PLURALNAME"] = array(
4145
			$this->plural_name(),
4146
4147
			'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the'
4148
			. ' interface'
4149
		);
4150
4151
		return $entities;
4152
	}
4153
4154
	/**
4155
	 * Returns true if the given method/parameter has a value
4156
	 * (Uses the DBField::hasValue if the parameter is a database field)
4157
	 *
4158
	 * @param string $field The field name
4159
	 * @param array $arguments
4160
	 * @param bool $cache
4161
	 * @return boolean
4162
	 */
4163
	public function hasValue($field, $arguments = null, $cache = true) {
4164
		// has_one fields should not use dbObject to check if a value is given
4165
		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...
4166
			return $obj->exists();
4167
		} else {
4168
			return parent::hasValue($field, $arguments, $cache);
4169
		}
4170
	}
4171
4172
}
4173