Passed
Push — 4.0 ( a104b5...3f3a18 )
by
unknown
14:17 queued 05:58
created

DataObject   F

Complexity

Total Complexity 505

Size/Duplication

Total Lines 3754
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 1239
dl 0
loc 3754
rs 0.8
c 0
b 0
f 0
wmc 505

116 Methods

Rating   Name   Duplication   Size   Complexity  
A destroy() 0 3 1
A getSchema() 0 3 1
A findCascadeDeletes() 0 4 1
A toMap() 0 4 1
A i18n_singular_name() 0 3 1
A onAfterDelete() 0 3 1
A onAfterWrite() 0 4 1
A getQueriedDatabaseFields() 0 3 1
A i18n_plural_name() 0 3 1
A doValidate() 0 4 1
A exists() 0 3 2
A data() 0 3 1
A duplicateManyManyRelation() 0 24 5
A setClassName() 0 10 3
B defineMethods() 0 27 10
A updateChanges() 0 10 3
B update() 0 50 10
A writeBaseRecord() 0 14 2
A newClassInstance() 0 19 3
A getTitle() 0 11 3
A validateWrite() 0 17 4
A duplicate() 0 18 3
B prepareManipulationTable() 0 47 11
A onBeforeDelete() 0 11 2
A getObsoleteClassName() 0 7 2
A singular_name() 0 10 2
F __construct() 0 80 16
B populateDefaults() 0 36 11
A onBeforeWrite() 0 6 1
A preWrite() 0 15 3
D merge() 0 71 21
A duplicateManyManyRelations() 0 12 5
A castedUpdate() 0 6 2
A isEmpty() 0 18 5
A i18n_pluralise() 0 7 1
A validate() 0 5 1
A forceChange() 0 27 5
A getClassName() 0 7 2
A plural_name() 0 11 3
D loadLazyFields() 0 75 18
B findRelatedObjects() 0 39 8
A getCMSActions() 0 5 1
A flush_and_destroy_cache() 0 14 6
A flushCache() 0 18 4
A getSourceQueryParams() 0 3 1
A getFrontEndFields() 0 6 1
A canDelete() 0 7 2
B getRelationClass() 0 29 7
A hasField() 0 8 5
A provideI18nEntities() 0 13 2
A isChanged() 0 8 3
A setJoin() 0 12 3
A database_extensions() 0 7 2
A baseClass() 0 3 1
A getViewerTemplates() 0 3 1
A hasMany() 0 7 4
A mergeRelatedObject() 0 15 4
B fieldLabels() 0 39 8
A canEdit() 0 7 2
A reset() 0 8 1
A setCastedField() 0 13 3
A hasDatabaseField() 0 4 1
A delete() 0 36 4
D summaryFields() 0 46 12
B writeManipulation() 0 33 7
C get() 0 39 12
A getManyManyComponents() 0 46 5
C setField() 0 63 17
A getField() 0 19 6
A beforeUpdateCMSFields() 0 3 1
A getRelationType() 0 11 4
A relObject() 0 30 5
C searchableFields() 0 76 15
A get_by_id() 0 9 2
A disable_subclass_access() 0 3 1
A setSourceQueryParam() 0 3 1
A getInheritableQueryParams() 0 5 1
C inferReciprocalComponent() 0 97 13
A getJoin() 0 3 1
A dbObject() 0 30 6
A manyManyExtraFields() 0 3 1
A manyMany() 0 7 1
A canCreate() 0 7 2
B requireTable() 0 72 7
A hasValue() 0 8 3
A fieldLabel() 0 4 2
A defaultSearchFilters() 0 16 4
A enable_subclass_access() 0 3 1
B getReverseAssociation() 0 22 7
A getDefaultSearchContext() 0 6 1
B write() 0 49 8
B relField() 0 38 10
A baseTable() 0 3 1
A canView() 0 7 2
A debug() 0 11 3
A delete_by_id() 0 7 2
A getComponents() 0 35 5
A hasOne() 0 3 1
A getSourceQueryParam() 0 6 2
A scaffoldFormFields() 0 21 1
A writeComponents() 0 13 4
A castingHelper() 0 19 4
A writeRelations() 0 12 4
A requireDefaultRecords() 0 18 4
B scaffoldSearchFields() 0 55 11
B get_one() 0 24 7
B can() 0 21 8
A isInDB() 0 3 2
A extendedCan() 0 15 4
A belongsTo() 0 7 4
A mergeRelatedObjects() 0 15 4
A setSourceQueryParams() 0 3 1
C getComponent() 0 71 12
C getChangedFields() 0 46 17
A getCMSFields() 0 12 1
A getClassAncestry() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DataObject often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataObject, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use BadMethodCallException;
6
use Exception;
7
use InvalidArgumentException;
8
use LogicException;
9
use SilverStripe\Control\HTTP;
10
use SilverStripe\Core\ClassInfo;
11
use SilverStripe\Core\Config\Config;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\Resettable;
14
use SilverStripe\Dev\Debug;
15
use SilverStripe\Dev\Deprecation;
16
use SilverStripe\Forms\FieldList;
17
use SilverStripe\Forms\FormField;
18
use SilverStripe\Forms\FormScaffolder;
19
use SilverStripe\i18n\i18n;
20
use SilverStripe\i18n\i18nEntityProvider;
21
use SilverStripe\ORM\Connect\MySQLSchemaManager;
22
use SilverStripe\ORM\FieldType\DBClassName;
23
use SilverStripe\ORM\FieldType\DBComposite;
24
use SilverStripe\ORM\FieldType\DBDatetime;
25
use SilverStripe\ORM\FieldType\DBField;
26
use SilverStripe\ORM\Filters\SearchFilter;
27
use SilverStripe\ORM\Queries\SQLDelete;
28
use SilverStripe\ORM\Queries\SQLInsert;
29
use SilverStripe\ORM\Search\SearchContext;
30
use SilverStripe\Security\Member;
31
use SilverStripe\Security\Permission;
32
use SilverStripe\Security\Security;
33
use SilverStripe\View\SSViewer;
34
use SilverStripe\View\ViewableData;
35
use stdClass;
36
37
/**
38
 * A single database record & abstract class for the data-access-model.
39
 *
40
 * <h2>Extensions</h2>
41
 *
42
 * See {@link Extension} and {@link DataExtension}.
43
 *
44
 * <h2>Permission Control</h2>
45
 *
46
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
47
 * strings which can be selected on a group-by-group basis.
48
 *
49
 * <code>
50
 * class Article extends DataObject implements PermissionProvider {
51
 *  static $api_access = true;
52
 *
53
 *  function canView($member = false) {
54
 *    return Permission::check('ARTICLE_VIEW');
55
 *  }
56
 *  function canEdit($member = false) {
57
 *    return Permission::check('ARTICLE_EDIT');
58
 *  }
59
 *  function canDelete() {
60
 *    return Permission::check('ARTICLE_DELETE');
61
 *  }
62
 *  function canCreate() {
63
 *    return Permission::check('ARTICLE_CREATE');
64
 *  }
65
 *  function providePermissions() {
66
 *    return array(
67
 *      'ARTICLE_VIEW' => 'Read an article object',
68
 *      'ARTICLE_EDIT' => 'Edit an article object',
69
 *      'ARTICLE_DELETE' => 'Delete an article object',
70
 *      'ARTICLE_CREATE' => 'Create an article object',
71
 *    );
72
 *  }
73
 * }
74
 * </code>
75
 *
76
 * Object-level access control by {@link Group} membership:
77
 * <code>
78
 * class Article extends DataObject {
79
 *   static $api_access = true;
80
 *
81
 *   function canView($member = false) {
82
 *     if(!$member) $member = Security::getCurrentUser();
83
 *     return $member->inGroup('Subscribers');
84
 *   }
85
 *   function canEdit($member = false) {
86
 *     if(!$member) $member = Security::getCurrentUser();
87
 *     return $member->inGroup('Editors');
88
 *   }
89
 *
90
 *   // ...
91
 * }
92
 * </code>
93
 *
94
 * If any public method on this class is prefixed with an underscore,
95
 * the results are cached in memory through {@link cachedCall()}.
96
 *
97
 *
98
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
99
 *  and defineMethods()
100
 *
101
 * @property int $ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
102
 * @property int $OldID ID of object, if deleted
103
 * @property string $ClassName Class name of the DataObject
104
 * @property string $LastEdited Date and time of DataObject's last modification.
105
 * @property string $Created Date and time of DataObject creation.
106
 */
107
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable
108
{
109
110
    /**
111
     * Human-readable singular name.
112
     * @var string
113
     * @config
114
     */
115
    private static $singular_name = null;
116
117
    /**
118
     * Human-readable plural name
119
     * @var string
120
     * @config
121
     */
122
    private static $plural_name = null;
123
124
    /**
125
     * Allow API access to this object?
126
     * @todo Define the options that can be set here
127
     * @config
128
     */
129
    private static $api_access = false;
130
131
    /**
132
     * Allows specification of a default value for the ClassName field.
133
     * Configure this value only in subclasses of DataObject.
134
     *
135
     * @config
136
     * @var string
137
     */
138
    private static $default_classname = null;
139
140
    /**
141
     * @deprecated 4.0..5.0
142
     * @var bool
143
     */
144
    public $destroyed = false;
145
146
    /**
147
     * Data stored in this objects database record. An array indexed by fieldname.
148
     *
149
     * Use {@link toMap()} if you want an array representation
150
     * of this object, as the $record array might contain lazy loaded field aliases.
151
     *
152
     * @var array
153
     */
154
    protected $record;
155
156
    /**
157
     * If selected through a many_many through relation, this is the instance of the through record
158
     *
159
     * @var DataObject
160
     */
161
    protected $joinRecord;
162
163
    /**
164
     * Represents a field that hasn't changed (before === after, thus before == after)
165
     */
166
    const CHANGE_NONE = 0;
167
168
    /**
169
     * Represents a field that has changed type, although not the loosely defined value.
170
     * (before !== after && before == after)
171
     * E.g. change 1 to true or "true" to true, but not true to 0.
172
     * Value changes are by nature also considered strict changes.
173
     */
174
    const CHANGE_STRICT = 1;
175
176
    /**
177
     * Represents a field that has changed the loosely defined value
178
     * (before != after, thus, before !== after))
179
     * E.g. change false to true, but not false to 0
180
     */
181
    const CHANGE_VALUE = 2;
182
183
    /**
184
     * An array indexed by fieldname, true if the field has been changed.
185
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
186
     * the changed state.
187
     *
188
     * @var array
189
     */
190
    private $changed;
191
192
    /**
193
     * The database record (in the same format as $record), before
194
     * any changes.
195
     * @var array
196
     */
197
    protected $original;
198
199
    /**
200
     * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
201
     * @var boolean
202
     */
203
    protected $brokenOnDelete = false;
204
205
    /**
206
     * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
207
     * @var boolean
208
     */
209
    protected $brokenOnWrite = false;
210
211
    /**
212
     * @config
213
     * @var boolean Should dataobjects be validated before they are written?
214
     * Caution: Validation can contain safeguards against invalid/malicious data,
215
     * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
216
     * to only disable validation for very specific use cases.
217
     */
218
    private static $validation_enabled = true;
219
220
    /**
221
     * Static caches used by relevant functions.
222
     *
223
     * @var array
224
     */
225
    protected static $_cache_get_one;
226
227
    /**
228
     * Cache of field labels
229
     *
230
     * @var array
231
     */
232
    protected static $_cache_field_labels = array();
233
234
    /**
235
     * Base fields which are not defined in static $db
236
     *
237
     * @config
238
     * @var array
239
     */
240
    private static $fixed_fields = array(
241
        'ID' => 'PrimaryKey',
242
        'ClassName' => 'DBClassName',
243
        'LastEdited' => 'DBDatetime',
244
        'Created' => 'DBDatetime',
245
    );
246
247
    /**
248
     * Override table name for this class. If ignored will default to FQN of class.
249
     * This option is not inheritable, and must be set on each class.
250
     * If left blank naming will default to the legacy (3.x) behaviour.
251
     *
252
     * @var string
253
     */
254
    private static $table_name = null;
255
256
    /**
257
     * Non-static relationship cache, indexed by component name.
258
     *
259
     * @var DataObject[]
260
     */
261
    protected $components;
262
263
    /**
264
     * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
265
     *
266
     * @var UnsavedRelationList[]
267
     */
268
    protected $unsavedRelations;
269
270
    /**
271
     * List of relations that should be cascade deleted, similar to `owns`
272
     * Note: This will trigger delete on many_many objects, not only the mapping table.
273
     * For many_many through you can specify the components you want to delete separately
274
     * (many_many or has_many sub-component)
275
     *
276
     * @config
277
     * @var array
278
     */
279
    private static $cascade_deletes = [];
280
281
    /**
282
     * Get schema object
283
     *
284
     * @return DataObjectSchema
285
     */
286
    public static function getSchema()
287
    {
288
        return Injector::inst()->get(DataObjectSchema::class);
289
    }
290
291
    /**
292
     * Construct a new DataObject.
293
     *
294
295
     * @param array|null $record Used internally for rehydrating an object from database content.
296
     *                           Bypasses setters on this class, and hence should not be used
297
     *                           for populating data on new records.
298
     * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
299
     *                             Singletons don't have their defaults set.
300
     * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
301
     */
302
    public function __construct($record = null, $isSingleton = false, $queryParams = array())
303
    {
304
        parent::__construct();
305
306
        // Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
307
        $this->setSourceQueryParams($queryParams);
308
309
        // Set the fields data.
310
        if (!$record) {
311
            $record = array(
312
                'ID' => 0,
313
                'ClassName' => static::class,
314
                'RecordClassName' => static::class
315
            );
316
        }
317
318
        if ($record instanceof stdClass) {
0 ignored issues
show
introduced by
$record is never a sub-type of stdClass.
Loading history...
319
            $record = (array)$record;
320
        }
321
322
        if (!is_array($record)) {
0 ignored issues
show
introduced by
The condition is_array($record) is always true.
Loading history...
323
            if (is_object($record)) {
324
                $passed = "an object of type '" . get_class($record) . "'";
325
            } else {
326
                $passed = "The value '$record'";
327
            }
328
329
            user_error(
330
                "DataObject::__construct passed $passed.  It's supposed to be passed an array,"
331
                . " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
332
                E_USER_WARNING
333
            );
334
            $record = null;
335
        }
336
337
        // Set $this->record to $record, but ignore NULLs
338
        $this->record = array();
339
        foreach ($record as $k => $v) {
340
            // Ensure that ID is stored as a number and not a string
341
            // To do: this kind of clean-up should be done on all numeric fields, in some relatively
342
            // performant manner
343
            if ($v !== null) {
344
                if ($k == 'ID' && is_numeric($v)) {
345
                    $this->record[$k] = (int)$v;
346
                } else {
347
                    $this->record[$k] = $v;
348
                }
349
            }
350
        }
351
352
        // Identify fields that should be lazy loaded, but only on existing records
353
        if (!empty($record['ID'])) {
354
            // Get all field specs scoped to class for later lazy loading
355
            $fields = static::getSchema()->fieldSpecs(
356
                static::class,
357
                DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
358
            );
359
            foreach ($fields as $field => $fieldSpec) {
360
                $fieldClass = strtok($fieldSpec, ".");
361
                if (!array_key_exists($field, $record)) {
362
                    $this->record[$field . '_Lazy'] = $fieldClass;
363
                }
364
            }
365
        }
366
367
        $this->original = $this->record;
368
369
        // Keep track of the modification date of all the data sourced to make this page
370
        // From this we create a Last-Modified HTTP header
371
        if (isset($record['LastEdited'])) {
372
            HTTP::register_modification_date($record['LastEdited']);
373
        }
374
375
        // Must be called after parent constructor
376
        if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
377
            $this->populateDefaults();
378
        }
379
380
        // prevent populateDefaults() and setField() from marking overwritten defaults as changed
381
        $this->changed = array();
382
    }
383
384
    /**
385
     * Destroy all of this objects dependant objects and local caches.
386
     * You'll need to call this to get the memory of an object that has components or extensions freed.
387
     */
388
    public function destroy()
389
    {
390
        $this->flushCache(false);
391
    }
392
393
    /**
394
     * Create a duplicate of this node. Can duplicate many_many relations
395
     *
396
     * @param bool $doWrite Perform a write() operation before returning the object.
397
     * If this is true, it will create the duplicate in the database.
398
     * @param bool|string $manyMany Which many-many to duplicate. Set to true to duplicate all, false to duplicate none.
399
     * Alternatively set to the string of the relation config to duplicate
400
     * (supports 'many_many', or 'belongs_many_many')
401
     * @return static A duplicate of this node. The exact type will be the type of this node.
402
     */
403
    public function duplicate($doWrite = true, $manyMany = 'many_many')
404
    {
405
        $map = $this->toMap();
406
        unset($map['Created']);
407
        /** @var static $clone */
408
        $clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams());
409
        $clone->ID = 0;
410
411
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $manyMany);
412
        if ($manyMany) {
413
            $this->duplicateManyManyRelations($this, $clone, $manyMany);
414
        }
415
        if ($doWrite) {
416
            $clone->write();
417
        }
418
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $manyMany);
419
420
        return $clone;
421
    }
422
423
    /**
424
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
425
     *
426
     * @param DataObject $sourceObject the source object to duplicate from
427
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
428
     * @param bool|string $filter
429
     */
430
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
431
    {
432
        // Get list of relations to duplicate
433
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
434
            $relations = $sourceObject->config()->get($filter);
435
        } elseif ($filter === true) {
436
            $relations = $sourceObject->manyMany();
437
        } else {
438
            throw new InvalidArgumentException("Invalid many_many duplication filter");
439
        }
440
        foreach ($relations as $manyManyName => $type) {
441
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
442
        }
443
    }
444
445
    /**
446
     * Duplicates a single many_many relation from one object to another
447
     *
448
     * @param DataObject $sourceObject
449
     * @param DataObject $destinationObject
450
     * @param string $manyManyName
451
     */
452
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName)
453
    {
454
        // Ensure this component exists on the destination side as well
455
        if (!static::getSchema()->manyManyComponent(get_class($destinationObject), $manyManyName)) {
456
            return;
457
        }
458
459
        // Copy all components from source to destination
460
        $source = $sourceObject->getManyManyComponents($manyManyName);
461
        $dest = $destinationObject->getManyManyComponents($manyManyName);
462
463
        if ($source instanceof ManyManyList) {
464
            $extraFieldNames = $source->getExtraFields();
465
        } else {
466
            $extraFieldNames = array();
467
        }
468
469
        foreach ($source as $item) {
470
            // Merge extra fields
471
            $extraFields = array();
472
            foreach ($extraFieldNames as $fieldName => $fieldType) {
473
                $extraFields[$fieldName] = $item->getField($fieldName);
474
            }
475
            $dest->add($item, $extraFields);
0 ignored issues
show
Unused Code introduced by
The call to SilverStripe\ORM\DataList::add() has too many arguments starting with $extraFields. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

475
            $dest->/** @scrutinizer ignore-call */ 
476
                   add($item, $extraFields);

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. Please note the @ignore annotation hint above.

Loading history...
476
        }
477
    }
478
479
    /**
480
     * Return obsolete class name, if this is no longer a valid class
481
     *
482
     * @return string
483
     */
484
    public function getObsoleteClassName()
485
    {
486
        $className = $this->getField("ClassName");
487
        if (!ClassInfo::exists($className)) {
488
            return $className;
489
        }
490
        return null;
491
    }
492
493
    /**
494
     * Gets name of this class
495
     *
496
     * @return string
497
     */
498
    public function getClassName()
499
    {
500
        $className = $this->getField("ClassName");
501
        if (!ClassInfo::exists($className)) {
502
            return static::class;
503
        }
504
        return $className;
505
    }
506
507
    /**
508
     * Set the ClassName attribute. {@link $class} is also updated.
509
     * Warning: This will produce an inconsistent record, as the object
510
     * instance will not automatically switch to the new subclass.
511
     * Please use {@link newClassInstance()} for this purpose,
512
     * or destroy and reinstanciate the record.
513
     *
514
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
515
     * @return $this
516
     */
517
    public function setClassName($className)
518
    {
519
        $className = trim($className);
520
        if (!$className || !is_subclass_of($className, self::class)) {
521
            return $this;
522
        }
523
524
        $this->setField("ClassName", $className);
525
        $this->setField('RecordClassName', $className);
526
        return $this;
527
    }
528
529
    /**
530
     * Create a new instance of a different class from this object's record.
531
     * This is useful when dynamically changing the type of an instance. Specifically,
532
     * it ensures that the instance of the class is a match for the className of the
533
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
534
     * property manually before calling this method, as it will confuse change detection.
535
     *
536
     * If the new class is different to the original class, defaults are populated again
537
     * because this will only occur automatically on instantiation of a DataObject if
538
     * there is no record, or the record has no ID. In this case, we do have an ID but
539
     * we still need to repopulate the defaults.
540
     *
541
     * @param string $newClassName The name of the new class
542
     *
543
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
544
     */
545
    public function newClassInstance($newClassName)
546
    {
547
        if (!is_subclass_of($newClassName, self::class)) {
548
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
549
        }
550
551
        $originalClass = $this->ClassName;
552
553
        /** @var DataObject $newInstance */
554
        $newInstance = Injector::inst()->create($newClassName, $this->record, false);
555
556
        // Modify ClassName
557
        if ($newClassName != $originalClass) {
558
            $newInstance->setClassName($newClassName);
559
            $newInstance->populateDefaults();
560
            $newInstance->forceChange();
561
        }
562
563
        return $newInstance;
564
    }
565
566
    /**
567
     * Adds methods from the extensions.
568
     * Called by Object::__construct() once per class.
569
     */
570
    public function defineMethods()
571
    {
572
        parent::defineMethods();
573
574
        if (static::class === self::class) {
0 ignored issues
show
introduced by
The condition static::class === self::class is always true.
Loading history...
575
             return;
576
        }
577
578
        // Set up accessors for joined items
579
        if ($manyMany = $this->manyMany()) {
580
            foreach ($manyMany as $relationship => $class) {
581
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
582
            }
583
        }
584
        if ($hasMany = $this->hasMany()) {
585
            foreach ($hasMany as $relationship => $class) {
586
                $this->addWrapperMethod($relationship, 'getComponents');
587
            }
588
        }
589
        if ($hasOne = $this->hasOne()) {
590
            foreach ($hasOne as $relationship => $class) {
591
                $this->addWrapperMethod($relationship, 'getComponent');
592
            }
593
        }
594
        if ($belongsTo = $this->belongsTo()) {
595
            foreach (array_keys($belongsTo) as $relationship) {
596
                $this->addWrapperMethod($relationship, 'getComponent');
597
            }
598
        }
599
    }
600
601
    /**
602
     * Returns true if this object "exists", i.e., has a sensible value.
603
     * The default behaviour for a DataObject is to return true if
604
     * the object exists in the database, you can override this in subclasses.
605
     *
606
     * @return boolean true if this object exists
607
     */
608
    public function exists()
609
    {
610
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
611
    }
612
613
    /**
614
     * Returns TRUE if all values (other than "ID") are
615
     * considered empty (by weak boolean comparison).
616
     *
617
     * @return boolean
618
     */
619
    public function isEmpty()
620
    {
621
        $fixed = DataObject::config()->uninherited('fixed_fields');
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...
622
        foreach ($this->toMap() as $field => $value) {
623
            // only look at custom fields
624
            if (isset($fixed[$field])) {
625
                continue;
626
            }
627
628
            $dbObject = $this->dbObject($field);
629
            if (!$dbObject) {
630
                continue;
631
            }
632
            if ($dbObject->exists()) {
633
                return false;
634
            }
635
        }
636
        return true;
637
    }
638
639
    /**
640
     * Pluralise this item given a specific count.
641
     *
642
     * E.g. "0 Pages", "1 File", "3 Images"
643
     *
644
     * @param string $count
645
     * @return string
646
     */
647
    public function i18n_pluralise($count)
648
    {
649
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
650
        return i18n::_t(
651
            static::class . '.PLURALS',
652
            $default,
653
            [ 'count' => $count ]
654
        );
655
    }
656
657
    /**
658
     * Get the user friendly singular name of this DataObject.
659
     * If the name is not defined (by redefining $singular_name in the subclass),
660
     * this returns the class name.
661
     *
662
     * @return string User friendly singular name of this DataObject
663
     */
664
    public function singular_name()
665
    {
666
        $name = $this->config()->get('singular_name');
667
        if ($name) {
668
            return $name;
669
        }
670
        return ucwords(trim(strtolower(preg_replace(
671
            '/_?([A-Z])/',
672
            ' $1',
673
            ClassInfo::shortName($this)
674
        ))));
675
    }
676
677
    /**
678
     * Get the translated user friendly singular name of this DataObject
679
     * same as singular_name() but runs it through the translating function
680
     *
681
     * Translating string is in the form:
682
     *     $this->class.SINGULARNAME
683
     * Example:
684
     *     Page.SINGULARNAME
685
     *
686
     * @return string User friendly translated singular name of this DataObject
687
     */
688
    public function i18n_singular_name()
689
    {
690
        return _t(static::class . '.SINGULARNAME', $this->singular_name());
691
    }
692
693
    /**
694
     * Get the user friendly plural name of this DataObject
695
     * If the name is not defined (by renaming $plural_name in the subclass),
696
     * this returns a pluralised version of the class name.
697
     *
698
     * @return string User friendly plural name of this DataObject
699
     */
700
    public function plural_name()
701
    {
702
        if ($name = $this->config()->get('plural_name')) {
703
            return $name;
704
        }
705
        $name = $this->singular_name();
706
        //if the penultimate character is not a vowel, replace "y" with "ies"
707
        if (preg_match('/[^aeiou]y$/i', $name)) {
708
            $name = substr($name, 0, -1) . 'ie';
709
        }
710
        return ucfirst($name . 's');
711
    }
712
713
    /**
714
     * Get the translated user friendly plural name of this DataObject
715
     * Same as plural_name but runs it through the translation function
716
     * Translation string is in the form:
717
     *      $this->class.PLURALNAME
718
     * Example:
719
     *      Page.PLURALNAME
720
     *
721
     * @return string User friendly translated plural name of this DataObject
722
     */
723
    public function i18n_plural_name()
724
    {
725
        return _t(static::class . '.PLURALNAME', $this->plural_name());
726
    }
727
728
    /**
729
     * Standard implementation of a title/label for a specific
730
     * record. Tries to find properties 'Title' or 'Name',
731
     * and falls back to the 'ID'. Useful to provide
732
     * user-friendly identification of a record, e.g. in errormessages
733
     * or UI-selections.
734
     *
735
     * Overload this method to have a more specialized implementation,
736
     * e.g. for an Address record this could be:
737
     * <code>
738
     * function getTitle() {
739
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
740
     * }
741
     * </code>
742
     *
743
     * @return string
744
     */
745
    public function getTitle()
746
    {
747
        $schema = static::getSchema();
748
        if ($schema->fieldSpec($this, 'Title')) {
749
            return $this->getField('Title');
750
        }
751
        if ($schema->fieldSpec($this, 'Name')) {
752
            return $this->getField('Name');
753
        }
754
755
        return "#{$this->ID}";
756
    }
757
758
    /**
759
     * Returns the associated database record - in this case, the object itself.
760
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
761
     *
762
     * @return DataObject Associated database record
763
     */
764
    public function data()
765
    {
766
        return $this;
767
    }
768
769
    /**
770
     * Convert this object to a map.
771
     *
772
     * @return array The data as a map.
773
     */
774
    public function toMap()
775
    {
776
        $this->loadLazyFields();
777
        return $this->record;
778
    }
779
780
    /**
781
     * Return all currently fetched database fields.
782
     *
783
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
784
     * Obviously, this makes it a lot faster.
785
     *
786
     * @return array The data as a map.
787
     */
788
    public function getQueriedDatabaseFields()
789
    {
790
        return $this->record;
791
    }
792
793
    /**
794
     * Update a number of fields on this object, given a map of the desired changes.
795
     *
796
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
797
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
798
     *
799
     * update() doesn't write the main object, but if you use the dot syntax, it will write()
800
     * the related objects that it alters.
801
     *
802
     * @param array $data A map of field name to data values to update.
803
     * @return DataObject $this
804
     */
805
    public function update($data)
806
    {
807
        foreach ($data as $key => $value) {
808
            // Implement dot syntax for updates
809
            if (strpos($key, '.') !== false) {
810
                $relations = explode('.', $key);
811
                $fieldName = array_pop($relations);
812
                /** @var static $relObj */
813
                $relObj = $this;
814
                $relation = null;
815
                foreach ($relations as $i => $relation) {
816
                    // no support for has_many or many_many relationships,
817
                    // as the updater wouldn't know which object to write to (or create)
818
                    if ($relObj->$relation() instanceof DataObject) {
819
                        $parentObj = $relObj;
820
                        $relObj = $relObj->$relation();
821
                        // If the intermediate relationship objects haven't been created, then write them
822
                        if ($i<sizeof($relations)-1 && !$relObj->ID || (!$relObj->ID && $parentObj !== $this)) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($i < sizeof($relations)...&& $parentObj !== $this, Probably Intended Meaning: $i < sizeof($relations) ...& $parentObj !== $this)
Loading history...
823
                            $relObj->write();
824
                            $relatedFieldName = $relation . "ID";
825
                            $parentObj->$relatedFieldName = $relObj->ID;
826
                            $parentObj->write();
827
                        }
828
                    } else {
829
                        user_error(
830
                            "DataObject::update(): Can't traverse relationship '$relation'," .
831
                            "it has to be a has_one relationship or return a single DataObject",
832
                            E_USER_NOTICE
833
                        );
834
                        // unset relation object so we don't write properties to the wrong object
835
                        $relObj = null;
836
                        break;
837
                    }
838
                }
839
840
                if ($relObj) {
841
                    $relObj->$fieldName = $value;
842
                    $relObj->write();
843
                    $relatedFieldName = $relation . "ID";
844
                    $this->$relatedFieldName = $relObj->ID;
845
                    $relObj->flushCache();
846
                } else {
847
                    $class = static::class;
848
                    user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
849
                }
850
            } else {
851
                $this->$key = $value;
852
            }
853
        }
854
        return $this;
855
    }
856
857
    /**
858
     * Pass changes as a map, and try to
859
     * get automatic casting for these fields.
860
     * Doesn't write to the database. To write the data,
861
     * use the write() method.
862
     *
863
     * @param array $data A map of field name to data values to update.
864
     * @return DataObject $this
865
     */
866
    public function castedUpdate($data)
867
    {
868
        foreach ($data as $k => $v) {
869
            $this->setCastedField($k, $v);
870
        }
871
        return $this;
872
    }
873
874
    /**
875
     * Merges data and relations from another object of same class,
876
     * without conflict resolution. Allows to specify which
877
     * dataset takes priority in case its not empty.
878
     * has_one-relations are just transferred with priority 'right'.
879
     * has_many and many_many-relations are added regardless of priority.
880
     *
881
     * Caution: has_many/many_many relations are moved rather than duplicated,
882
     * meaning they are not connected to the merged object any longer.
883
     * Caution: Just saves updated has_many/many_many relations to the database,
884
     * doesn't write the updated object itself (just writes the object-properties).
885
     * Caution: Does not delete the merged object.
886
     * Caution: Does now overwrite Created date on the original object.
887
     *
888
     * @param DataObject $rightObj
889
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
890
     * @param bool $includeRelations Merge any existing relations (optional)
891
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
892
     *                            Only applicable with $priority='right'. (optional)
893
     * @return Boolean
894
     */
895
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
896
    {
897
        $leftObj = $this;
898
899
        if ($leftObj->ClassName != $rightObj->ClassName) {
900
            // we can't merge similiar subclasses because they might have additional relations
901
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
902
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
903
            return false;
904
        }
905
906
        if (!$rightObj->ID) {
907
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
908
				to make sure all relations are transferred properly.').", E_USER_WARNING);
909
            return false;
910
        }
911
912
        // makes sure we don't merge data like ID or ClassName
913
        $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
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...
914
        foreach ($rightData as $key => $rightSpec) {
915
            // Don't merge ID
916
            if ($key === 'ID') {
917
                continue;
918
            }
919
920
            // Only merge relations if allowed
921
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
922
                continue;
923
            }
924
925
            // don't merge conflicting values if priority is 'left'
926
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
927
                continue;
928
            }
929
930
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
931
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
932
                continue;
933
            }
934
935
            // TODO remove redundant merge of has_one fields
936
            $leftObj->{$key} = $rightObj->{$key};
937
        }
938
939
        // merge relations
940
        if ($includeRelations) {
941
            if ($manyMany = $this->manyMany()) {
942
                foreach ($manyMany as $relationship => $class) {
943
                    /** @var DataObject $leftComponents */
944
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
945
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
946
                    if ($rightComponents && $rightComponents->exists()) {
947
                        $leftComponents->addMany($rightComponents->column('ID'));
0 ignored issues
show
Bug introduced by
The method addMany() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

947
                        $leftComponents->/** @scrutinizer ignore-call */ 
948
                                         addMany($rightComponents->column('ID'));
Loading history...
948
                    }
949
                    $leftComponents->write();
950
                }
951
            }
952
953
            if ($hasMany = $this->hasMany()) {
954
                foreach ($hasMany as $relationship => $class) {
955
                    $leftComponents = $leftObj->getComponents($relationship);
956
                    $rightComponents = $rightObj->getComponents($relationship);
957
                    if ($rightComponents && $rightComponents->exists()) {
958
                        $leftComponents->addMany($rightComponents->column('ID'));
959
                    }
960
                    $leftComponents->write();
0 ignored issues
show
Bug introduced by
The method write() does not exist on SilverStripe\ORM\UnsavedRelationList. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

960
                    $leftComponents->/** @scrutinizer ignore-call */ 
961
                                     write();
Loading history...
Bug introduced by
The method write() does not exist on SilverStripe\ORM\HasManyList. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

960
                    $leftComponents->/** @scrutinizer ignore-call */ 
961
                                     write();
Loading history...
961
                }
962
            }
963
        }
964
965
        return true;
966
    }
967
968
    /**
969
     * Forces the record to think that all its data has changed.
970
     * Doesn't write to the database. Only sets fields as changed
971
     * if they are not already marked as changed.
972
     *
973
     * @return $this
974
     */
975
    public function forceChange()
976
    {
977
        // Ensure lazy fields loaded
978
        $this->loadLazyFields();
979
        $fields = static::getSchema()->fieldSpecs(static::class);
980
981
        // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
982
        $fieldNames = array_unique(array_merge(
983
            array_keys($this->record),
984
            array_keys($fields)
985
        ));
986
987
        foreach ($fieldNames as $fieldName) {
988
            if (!isset($this->changed[$fieldName])) {
989
                $this->changed[$fieldName] = self::CHANGE_STRICT;
990
            }
991
            // Populate the null values in record so that they actually get written
992
            if (!isset($this->record[$fieldName])) {
993
                $this->record[$fieldName] = null;
994
            }
995
        }
996
997
        // @todo Find better way to allow versioned to write a new version after forceChange
998
        if ($this->isChanged('Version')) {
999
            unset($this->changed['Version']);
1000
        }
1001
        return $this;
1002
    }
1003
1004
    /**
1005
     * Validate the current object.
1006
     *
1007
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1008
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1009
     *
1010
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1011
     * and onAfterWrite() won't get called either.
1012
     *
1013
     * It is expected that you call validate() in your own application to test that an object is valid before
1014
     * attempting a write, and respond appropriately if it isn't.
1015
     *
1016
     * @see {@link ValidationResult}
1017
     * @return ValidationResult
1018
     */
1019
    public function validate()
1020
    {
1021
        $result = ValidationResult::create();
1022
        $this->extend('validate', $result);
1023
        return $result;
1024
    }
1025
1026
    /**
1027
     * Public accessor for {@see DataObject::validate()}
1028
     *
1029
     * @return ValidationResult
1030
     */
1031
    public function doValidate()
1032
    {
1033
        Deprecation::notice('5.0', 'Use validate');
1034
        return $this->validate();
1035
    }
1036
1037
    /**
1038
     * Event handler called before writing to the database.
1039
     * You can overload this to clean up or otherwise process data before writing it to the
1040
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1041
     *
1042
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1043
     *
1044
     * @uses DataExtension->onBeforeWrite()
1045
     */
1046
    protected function onBeforeWrite()
1047
    {
1048
        $this->brokenOnWrite = false;
1049
1050
        $dummy = null;
1051
        $this->extend('onBeforeWrite', $dummy);
1052
    }
1053
1054
    /**
1055
     * Event handler called after writing to the database.
1056
     * You can overload this to act upon changes made to the data after it is written.
1057
     * $this->changed will have a record
1058
     * database.  Don't forget to call parent::onAfterWrite(), though!
1059
     *
1060
     * @uses DataExtension->onAfterWrite()
1061
     */
1062
    protected function onAfterWrite()
1063
    {
1064
        $dummy = null;
1065
        $this->extend('onAfterWrite', $dummy);
1066
    }
1067
1068
    /**
1069
     * Find all objects that will be cascade deleted if this object is deleted
1070
     *
1071
     * Notes:
1072
     *   - If this object is versioned, objects will only be searched in the same stage as the given record.
1073
     *   - This will only be useful prior to deletion, as post-deletion this record will no longer exist.
1074
     *
1075
     * @param bool $recursive True if recursive
1076
     * @param ArrayList $list Optional list to add items to
1077
     * @return ArrayList list of objects
1078
     */
1079
    public function findCascadeDeletes($recursive = true, $list = null)
1080
    {
1081
        // Find objects in these relationships
1082
        return $this->findRelatedObjects('cascade_deletes', $recursive, $list);
1083
    }
1084
1085
    /**
1086
     * Event handler called before deleting from the database.
1087
     * You can overload this to clean up or otherwise process data before delete this
1088
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1089
     *
1090
     * @uses DataExtension->onBeforeDelete()
1091
     */
1092
    protected function onBeforeDelete()
1093
    {
1094
        $this->brokenOnDelete = false;
1095
1096
        $dummy = null;
1097
        $this->extend('onBeforeDelete', $dummy);
1098
1099
        // Cascade deletes
1100
        $deletes = $this->findCascadeDeletes(false);
1101
        foreach ($deletes as $delete) {
1102
            $delete->delete();
1103
        }
1104
    }
1105
1106
    protected function onAfterDelete()
1107
    {
1108
        $this->extend('onAfterDelete');
1109
    }
1110
1111
    /**
1112
     * Load the default values in from the self::$defaults array.
1113
     * Will traverse the defaults of the current class and all its parent classes.
1114
     * Called by the constructor when creating new records.
1115
     *
1116
     * @uses DataExtension->populateDefaults()
1117
     * @return DataObject $this
1118
     */
1119
    public function populateDefaults()
1120
    {
1121
        $classes = array_reverse(ClassInfo::ancestry($this));
1122
1123
        foreach ($classes as $class) {
1124
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1125
1126
            if ($defaults && !is_array($defaults)) {
1127
                user_error(
1128
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1129
                    E_USER_WARNING
1130
                );
1131
                $defaults = null;
1132
            }
1133
1134
            if ($defaults) {
1135
                foreach ($defaults as $fieldName => $fieldValue) {
1136
                // SRM 2007-03-06: Stricter check
1137
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1138
                        $this->$fieldName = $fieldValue;
1139
                    }
1140
                // Set many-many defaults with an array of ids
1141
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1142
                        /** @var ManyManyList $manyManyJoin */
1143
                        $manyManyJoin = $this->$fieldName();
1144
                        $manyManyJoin->setByIDList($fieldValue);
1145
                    }
1146
                }
1147
            }
1148
            if ($class == self::class) {
1149
                break;
1150
            }
1151
        }
1152
1153
        $this->extend('populateDefaults');
1154
        return $this;
1155
    }
1156
1157
    /**
1158
     * Determine validation of this object prior to write
1159
     *
1160
     * @return ValidationException Exception generated by this write, or null if valid
1161
     */
1162
    protected function validateWrite()
1163
    {
1164
        if ($this->ObsoleteClassName) {
0 ignored issues
show
Bug Best Practice introduced by
The property ObsoleteClassName does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
1165
            return new ValidationException(
1166
                "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - " .
1167
                "you need to change the ClassName before you can write it"
1168
            );
1169
        }
1170
1171
        // Note: Validation can only be disabled at the global level, not per-model
1172
        if (DataObject::config()->uninherited('validation_enabled')) {
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...
1173
            $result = $this->validate();
1174
            if (!$result->isValid()) {
1175
                return new ValidationException($result);
1176
            }
1177
        }
1178
        return null;
1179
    }
1180
1181
    /**
1182
     * Prepare an object prior to write
1183
     *
1184
     * @throws ValidationException
1185
     */
1186
    protected function preWrite()
1187
    {
1188
        // Validate this object
1189
        if ($writeException = $this->validateWrite()) {
1190
            // Used by DODs to clean up after themselves, eg, Versioned
1191
            $this->invokeWithExtensions('onAfterSkippedWrite');
1192
            throw $writeException;
1193
        }
1194
1195
        // Check onBeforeWrite
1196
        $this->brokenOnWrite = true;
1197
        $this->onBeforeWrite();
1198
        if ($this->brokenOnWrite) {
1199
            user_error(static::class . " has a broken onBeforeWrite() function."
1200
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1201
        }
1202
    }
1203
1204
    /**
1205
     * Detects and updates all changes made to this object
1206
     *
1207
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1208
     * @return bool True if any changes are detected
1209
     */
1210
    protected function updateChanges($forceChanges = false)
1211
    {
1212
        if ($forceChanges) {
1213
            // Force changes, but only for loaded fields
1214
            foreach ($this->record as $field => $value) {
1215
                $this->changed[$field] = static::CHANGE_VALUE;
1216
            }
1217
            return true;
1218
        }
1219
        return $this->isChanged();
1220
    }
1221
1222
    /**
1223
     * Writes a subset of changes for a specific table to the given manipulation
1224
     *
1225
     * @param string $baseTable Base table
1226
     * @param string $now Timestamp to use for the current time
1227
     * @param bool $isNewRecord Whether this should be treated as a new record write
1228
     * @param array $manipulation Manipulation to write to
1229
     * @param string $class Class of table to manipulate
1230
     */
1231
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1232
    {
1233
        $schema = $this->getSchema();
1234
        $table = $schema->tableName($class);
1235
        $manipulation[$table] = array();
1236
1237
        // Extract records for this table
1238
        foreach ($this->record as $fieldName => $fieldValue) {
1239
            // we're not attempting to reset the BaseTable->ID
1240
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1241
            if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1242
                continue;
1243
            }
1244
1245
            // Ensure this field pertains to this table
1246
            $specification = $schema->fieldSpec($class, $fieldName, DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED);
1247
            if (!$specification) {
1248
                continue;
1249
            }
1250
1251
            // if database column doesn't correlate to a DBField instance...
1252
            $fieldObj = $this->dbObject($fieldName);
1253
            if (!$fieldObj) {
1254
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1255
            }
1256
1257
            // Write to manipulation
1258
            $fieldObj->writeToManipulation($manipulation[$table]);
1259
        }
1260
1261
        // Ensure update of Created and LastEdited columns
1262
        if ($baseTable === $table) {
1263
            $manipulation[$table]['fields']['LastEdited'] = $now;
1264
            if ($isNewRecord) {
1265
                $manipulation[$table]['fields']['Created']
1266
                    = empty($this->record['Created'])
1267
                        ? $now
1268
                        : $this->record['Created'];
1269
                $manipulation[$table]['fields']['ClassName'] = static::class;
1270
            }
1271
        }
1272
1273
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1274
        // attempt an update, as though it were a normal update.
1275
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1276
        $manipulation[$table]['id'] = $this->record['ID'];
1277
        $manipulation[$table]['class'] = $class;
1278
    }
1279
1280
    /**
1281
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1282
     *
1283
     * Does nothing if an ID is already assigned for this record
1284
     *
1285
     * @param string $baseTable Base table
1286
     * @param string $now Timestamp to use for the current time
1287
     */
1288
    protected function writeBaseRecord($baseTable, $now)
1289
    {
1290
        // Generate new ID if not specified
1291
        if ($this->isInDB()) {
1292
            return;
1293
        }
1294
1295
        // Perform an insert on the base table
1296
        $insert = new SQLInsert('"' . $baseTable . '"');
1297
        $insert
1298
            ->assign('"Created"', $now)
1299
            ->execute();
1300
        $this->changed['ID'] = self::CHANGE_VALUE;
1301
        $this->record['ID'] = DB::get_generated_id($baseTable);
1302
    }
1303
1304
    /**
1305
     * Generate and write the database manipulation for all changed fields
1306
     *
1307
     * @param string $baseTable Base table
1308
     * @param string $now Timestamp to use for the current time
1309
     * @param bool $isNewRecord If this is a new record
1310
     * @throws InvalidArgumentException
1311
     */
1312
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1313
    {
1314
        // Generate database manipulations for each class
1315
        $manipulation = array();
1316
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1317
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1318
        }
1319
1320
        // Allow extensions to extend this manipulation
1321
        $this->extend('augmentWrite', $manipulation);
1322
1323
        // New records have their insert into the base data table done first, so that they can pass the
1324
        // generated ID on to the rest of the manipulation
1325
        if ($isNewRecord) {
1326
            $manipulation[$baseTable]['command'] = 'update';
1327
        }
1328
1329
        // Make sure none of our field assignment are arrays
1330
        foreach ($manipulation as $tableManipulation) {
1331
            if (!isset($tableManipulation['fields'])) {
1332
                continue;
1333
            }
1334
            foreach ($tableManipulation['fields'] as $fieldValue) {
1335
                if (is_array($fieldValue)) {
1336
                    throw new InvalidArgumentException(
1337
                        'DataObject::writeManipulation: parameterised field assignments are disallowed'
1338
                    );
1339
                }
1340
            }
1341
        }
1342
1343
        // Perform the manipulation
1344
        DB::manipulate($manipulation);
1345
    }
1346
1347
    /**
1348
     * Writes all changes to this object to the database.
1349
     *  - It will insert a record whenever ID isn't set, otherwise update.
1350
     *  - All relevant tables will be updated.
1351
     *  - $this->onBeforeWrite() gets called beforehand.
1352
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1353
     *
1354
     *  @uses DataExtension->augmentWrite()
1355
     *
1356
     * @param boolean $showDebug Show debugging information
1357
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1358
     * @param boolean $forceWrite Write to database even if there are no changes
1359
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1360
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1361
     *                                 {@link getManyManyComponents()} (Default: false)
1362
     * @return int The ID of the record
1363
     * @throws ValidationException Exception that can be caught and handled by the calling function
1364
     */
1365
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1366
    {
1367
        $now = DBDatetime::now()->Rfc2822();
1368
1369
        // Execute pre-write tasks
1370
        $this->preWrite();
1371
1372
        // Check if we are doing an update or an insert
1373
        $isNewRecord = !$this->isInDB() || $forceInsert;
1374
1375
        // Check changes exist, abort if there are none
1376
        $hasChanges = $this->updateChanges($isNewRecord);
1377
        if ($hasChanges || $forceWrite || $isNewRecord) {
1378
            // Ensure Created and LastEdited are populated
1379
            if (!isset($this->record['Created'])) {
1380
                $this->record['Created'] = $now;
1381
            }
1382
            $this->record['LastEdited'] = $now;
1383
1384
            // New records have their insert into the base data table done first, so that they can pass the
1385
            // generated primary key on to the rest of the manipulation
1386
            $baseTable = $this->baseTable();
1387
            $this->writeBaseRecord($baseTable, $now);
1388
1389
            // Write the DB manipulation for all changed fields
1390
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1391
1392
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1393
            $this->writeRelations();
1394
            $this->onAfterWrite();
1395
            $this->changed = array();
1396
        } else {
1397
            if ($showDebug) {
1398
                Debug::message("no changes for DataObject");
1399
            }
1400
1401
            // Used by DODs to clean up after themselves, eg, Versioned
1402
            $this->invokeWithExtensions('onAfterSkippedWrite');
1403
        }
1404
1405
        // Write relations as necessary
1406
        if ($writeComponents) {
1407
            $this->writeComponents(true);
1408
        }
1409
1410
        // Clears the cache for this object so get_one returns the correct object.
1411
        $this->flushCache();
1412
1413
        return $this->record['ID'];
1414
    }
1415
1416
    /**
1417
     * Writes cached relation lists to the database, if possible
1418
     */
1419
    public function writeRelations()
1420
    {
1421
        if (!$this->isInDB()) {
1422
            return;
1423
        }
1424
1425
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1426
        if ($this->unsavedRelations) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->unsavedRelations of type SilverStripe\ORM\UnsavedRelationList[] 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...
1427
            foreach ($this->unsavedRelations as $name => $list) {
1428
                $list->changeToList($this->$name());
1429
            }
1430
            $this->unsavedRelations = array();
1431
        }
1432
    }
1433
1434
    /**
1435
     * Write the cached components to the database. Cached components could refer to two different instances of the
1436
     * same record.
1437
     *
1438
     * @param bool $recursive Recursively write components
1439
     * @return DataObject $this
1440
     */
1441
    public function writeComponents($recursive = false)
1442
    {
1443
        if ($this->components) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->components of type SilverStripe\ORM\DataObject[] 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...
1444
            foreach ($this->components as $component) {
1445
                $component->write(false, false, false, $recursive);
1446
            }
1447
        }
1448
1449
        if ($join = $this->getJoin()) {
1450
            $join->write(false, false, false, $recursive);
1451
        }
1452
1453
        return $this;
1454
    }
1455
1456
    /**
1457
     * Delete this data object.
1458
     * $this->onBeforeDelete() gets called.
1459
     * Note that in Versioned objects, both Stage and Live will be deleted.
1460
     *  @uses DataExtension->augmentSQL()
1461
     */
1462
    public function delete()
1463
    {
1464
        $this->brokenOnDelete = true;
1465
        $this->onBeforeDelete();
1466
        if ($this->brokenOnDelete) {
1467
            user_error(static::class . " has a broken onBeforeDelete() function."
1468
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1469
        }
1470
1471
        // Deleting a record without an ID shouldn't do anything
1472
        if (!$this->ID) {
1473
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1474
        }
1475
1476
        // TODO: This is quite ugly.  To improve:
1477
        //  - move the details of the delete code in the DataQuery system
1478
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1479
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1480
        $srcQuery = DataList::create(static::class)
1481
            ->filter('ID', $this->ID)
1482
            ->dataQuery()
1483
            ->query();
1484
        $queriedTables = $srcQuery->queriedTables();
1485
        $this->extend('updateDeleteTables', $queriedTables, $srcQuery);
1486
        foreach ($queriedTables as $table) {
1487
            $delete = SQLDelete::create("\"$table\"", array('"ID"' => $this->ID));
1488
            $this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
1489
            $delete->execute();
1490
        }
1491
        // Remove this item out of any caches
1492
        $this->flushCache();
1493
1494
        $this->onAfterDelete();
1495
1496
        $this->OldID = $this->ID;
1497
        $this->ID = 0;
1498
    }
1499
1500
    /**
1501
     * Delete the record with the given ID.
1502
     *
1503
     * @param string $className The class name of the record to be deleted
1504
     * @param int $id ID of record to be deleted
1505
     */
1506
    public static function delete_by_id($className, $id)
1507
    {
1508
        $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...
1509
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1510
            $obj->delete();
1511
        } else {
1512
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1513
        }
1514
    }
1515
1516
    /**
1517
     * Get the class ancestry, including the current class name.
1518
     * The ancestry will be returned as an array of class names, where the 0th element
1519
     * will be the class that inherits directly from DataObject, and the last element
1520
     * will be the current class.
1521
     *
1522
     * @return array Class ancestry
1523
     */
1524
    public function getClassAncestry()
1525
    {
1526
        return ClassInfo::ancestry(static::class);
1527
    }
1528
1529
    /**
1530
     * Return a component object from a one to one relationship, as a DataObject.
1531
     * If no component is available, an 'empty component' will be returned for
1532
     * non-polymorphic relations, or for polymorphic relations with a class set.
1533
     *
1534
     * @param string $componentName Name of the component
1535
     * @return DataObject The component object. It's exact type will be that of the component.
1536
     * @throws Exception
1537
     */
1538
    public function getComponent($componentName)
1539
    {
1540
        if (isset($this->components[$componentName])) {
1541
            return $this->components[$componentName];
1542
        }
1543
1544
        $schema = static::getSchema();
1545
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1546
            $joinField = $componentName . 'ID';
1547
            $joinID    = $this->getField($joinField);
1548
1549
            // Extract class name for polymorphic relations
1550
            if ($class === self::class) {
1551
                $class = $this->getField($componentName . 'Class');
1552
                if (empty($class)) {
1553
                    return null;
1554
                }
1555
            }
1556
1557
            if ($joinID) {
1558
                // Ensure that the selected object originates from the same stage, subsite, etc
1559
                $component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1560
                    ->filter('ID', $joinID)
1561
                    ->setDataQueryParam($this->getInheritableQueryParams())
1562
                    ->first();
1563
            }
1564
1565
            if (empty($component)) {
1566
                $component = Injector::inst()->create($class);
1567
            }
1568
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1569
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1570
            $joinID = $this->ID;
1571
1572
            if ($joinID) {
1573
                // Prepare filter for appropriate join type
1574
                if ($polymorphic) {
1575
                    $filter = array(
1576
                        "{$joinField}ID" => $joinID,
1577
                        "{$joinField}Class" => static::class,
1578
                    );
1579
                } else {
1580
                    $filter = array(
1581
                        $joinField => $joinID
1582
                    );
1583
                }
1584
1585
                // Ensure that the selected object originates from the same stage, subsite, etc
1586
                $component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

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

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1729
        $class = $remote->getRelationClass($remoteRelation);
1730
        $schema = static::getSchema();
1731
1732
        // Validate arguments
1733
        if (!$this->isInDB()) {
1734
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1735
        }
1736
        if (empty($class)) {
1737
            throw new InvalidArgumentException(sprintf(
1738
                "%s invoked with invalid relation %s.%s",
1739
                __METHOD__,
1740
                $remoteClass,
1741
                $remoteRelation
1742
            ));
1743
        }
1744
        if ($class === self::class) {
1745
            throw new InvalidArgumentException(sprintf(
1746
                "%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1747
                "This method does not support polymorphic relationships",
1748
                __METHOD__,
1749
                $remoteClass,
1750
                $remoteRelation
1751
            ));
1752
        }
1753
        if (!is_a($this, $class, true)) {
1754
            throw new InvalidArgumentException(sprintf(
1755
                "Relation %s on %s does not refer to objects of type %s",
1756
                $remoteRelation,
1757
                $remoteClass,
1758
                static::class
1759
            ));
1760
        }
1761
1762
        // Check the relation type to mock
1763
        $relationType = $remote->getRelationType($remoteRelation);
1764
        switch ($relationType) {
1765
            case 'has_one': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

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

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

Loading history...
1766
                // Mock has_many
1767
                $joinField = "{$remoteRelation}ID";
1768
                $componentClass = $schema->classForField($remoteClass, $joinField);
1769
                $result = HasManyList::create($componentClass, $joinField);
1770
                return $result
1771
                    ->setDataQueryParam($this->getInheritableQueryParams())
1772
                    ->forForeignID($this->ID);
1773
            }
1774
            case 'belongs_to':
1775
            case 'has_many': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

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

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

Loading history...
1776
                // These relations must have a has_one on the other end, so find it
1777
                $joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic);
1778
                if ($polymorphic) {
1779
                    throw new InvalidArgumentException(sprintf(
1780
                        "%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1781
                        "to be a has_one polymorphic. This method does not support polymorphic relationships",
1782
                        __METHOD__,
1783
                        $remoteClass,
1784
                        $remoteRelation
1785
                    ));
1786
                }
1787
                $joinID = $this->getField($joinField);
1788
                if (empty($joinID)) {
1789
                    return null;
1790
                }
1791
                // Get object by joined ID
1792
                return DataObject::get($remoteClass)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1793
                    ->filter('ID', $joinID)
1794
                    ->setDataQueryParam($this->getInheritableQueryParams())
1795
                    ->first();
1796
            }
1797
            case 'many_many':
1798
            case 'belongs_many_many': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

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

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

Loading history...
1799
                // Get components and extra fields from parent
1800
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1801
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1802
1803
                // Reverse parent and component fields and create an inverse ManyManyList
1804
                /** @var RelationList $result */
1805
                $result = Injector::inst()->create(
1806
                    $manyMany['relationClass'],
1807
                    $manyMany['parentClass'], // Substitute parent class for dataClass
1808
                    $manyMany['join'],
1809
                    $manyMany['parentField'], // Reversed parent / child field
1810
                    $manyMany['childField'], // Reversed parent / child field
1811
                    $extraFields
1812
                );
1813
                $this->extend('updateManyManyComponents', $result);
1814
1815
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1816
                // foreignID set elsewhere.
1817
                return $result
1818
                    ->setDataQueryParam($this->getInheritableQueryParams())
1819
                    ->forForeignID($this->ID);
1820
            }
1821
            default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

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

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

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

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

Loading history...
1822
                return null;
1823
            }
1824
        }
1825
    }
1826
1827
    /**
1828
     * Returns a many-to-many component, as a ManyManyList.
1829
     * @param string $componentName Name of the many-many component
1830
     * @return RelationList|UnsavedRelationList The set of components
1831
     */
1832
    public function getManyManyComponents($componentName)
1833
    {
1834
        $schema = static::getSchema();
1835
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
1836
        if (!$manyManyComponent) {
1837
            throw new InvalidArgumentException(sprintf(
1838
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1839
                $componentName,
1840
                static::class
1841
            ));
1842
        }
1843
1844
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1845
        if (!$this->ID) {
1846
            if (!isset($this->unsavedRelations[$componentName])) {
1847
                $this->unsavedRelations[$componentName] =
1848
                    new UnsavedRelationList($manyManyComponent['parentClass'], $componentName, $manyManyComponent['childClass']);
1849
            }
1850
            return $this->unsavedRelations[$componentName];
1851
        }
1852
1853
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
1854
        /** @var RelationList $result */
1855
        $result = Injector::inst()->create(
1856
            $manyManyComponent['relationClass'],
1857
            $manyManyComponent['childClass'],
1858
            $manyManyComponent['join'],
1859
            $manyManyComponent['childField'],
1860
            $manyManyComponent['parentField'],
1861
            $extraFields
1862
        );
1863
1864
1865
        // Store component data in query meta-data
1866
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
1867
            /** @var DataQuery $query */
1868
            $query->setQueryParam('Component.ExtraFields', $extraFields);
1869
        });
1870
1871
        $this->extend('updateManyManyComponents', $result);
1872
1873
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1874
        // foreignID set elsewhere.
1875
        return $result
1876
            ->setDataQueryParam($this->getInheritableQueryParams())
1877
            ->forForeignID($this->ID);
1878
    }
1879
1880
    /**
1881
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1882
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1883
     *
1884
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1885
     *                          their classes.
1886
     */
1887
    public function hasOne()
1888
    {
1889
        return (array)$this->config()->get('has_one');
1890
    }
1891
1892
    /**
1893
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1894
     * their class name will be returned.
1895
     *
1896
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1897
     *        the field data stripped off. It defaults to TRUE.
1898
     * @return string|array
1899
     */
1900
    public function belongsTo($classOnly = true)
1901
    {
1902
        $belongsTo = (array)$this->config()->get('belongs_to');
1903
        if ($belongsTo && $classOnly) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $belongsTo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1904
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1905
        } else {
1906
            return $belongsTo ? $belongsTo : array();
1907
        }
1908
    }
1909
1910
    /**
1911
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1912
     * relationships and their classes will be returned.
1913
     *
1914
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1915
     *        the field data stripped off. It defaults to TRUE.
1916
     * @return string|array|false
1917
     */
1918
    public function hasMany($classOnly = true)
1919
    {
1920
        $hasMany = (array)$this->config()->get('has_many');
1921
        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...
1922
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1923
        } else {
1924
            return $hasMany ? $hasMany : array();
1925
        }
1926
    }
1927
1928
    /**
1929
     * Return the many-to-many extra fields specification.
1930
     *
1931
     * If you don't specify a component name, it returns all
1932
     * extra fields for all components available.
1933
     *
1934
     * @return array|null
1935
     */
1936
    public function manyManyExtraFields()
1937
    {
1938
        return $this->config()->get('many_many_extraFields');
1939
    }
1940
1941
    /**
1942
     * Return information about a many-to-many component.
1943
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
1944
     * components are returned.
1945
     *
1946
     * @see DataObjectSchema::manyManyComponent()
1947
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
1948
     */
1949
    public function manyMany()
1950
    {
1951
        $config = $this->config();
1952
        $manyManys = (array)$config->get('many_many');
1953
        $belongsManyManys = (array)$config->get('belongs_many_many');
1954
        $items = array_merge($manyManys, $belongsManyManys);
1955
        return $items;
1956
    }
1957
1958
    /**
1959
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
1960
     *
1961
     * This is experimental, and is currently only a Postgres-specific enhancement.
1962
     *
1963
     * @param string $class
1964
     * @return array|false
1965
     */
1966
    public function database_extensions($class)
1967
    {
1968
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
1969
        if ($extensions) {
1970
            return $extensions;
1971
        } else {
1972
            return false;
1973
        }
1974
    }
1975
1976
    /**
1977
     * Generates a SearchContext to be used for building and processing
1978
     * a generic search form for properties on this object.
1979
     *
1980
     * @return SearchContext
1981
     */
1982
    public function getDefaultSearchContext()
1983
    {
1984
        return new SearchContext(
1985
            static::class,
1986
            $this->scaffoldSearchFields(),
1987
            $this->defaultSearchFilters()
1988
        );
1989
    }
1990
1991
    /**
1992
     * Determine which properties on the DataObject are
1993
     * searchable, and map them to their default {@link FormField}
1994
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
1995
     *
1996
     * Some additional logic is included for switching field labels, based on
1997
     * how generic or specific the field type is.
1998
     *
1999
     * Used by {@link SearchContext}.
2000
     *
2001
     * @param array $_params
2002
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2003
     *   'restrictFields': Numeric array of a field name whitelist
2004
     * @return FieldList
2005
     */
2006
    public function scaffoldSearchFields($_params = null)
2007
    {
2008
        $params = array_merge(
2009
            array(
2010
                'fieldClasses' => false,
2011
                'restrictFields' => false
2012
            ),
2013
            (array)$_params
2014
        );
2015
        $fields = new FieldList();
2016
        foreach ($this->searchableFields() as $fieldName => $spec) {
2017
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
2018
                continue;
2019
            }
2020
2021
            // If a custom fieldclass is provided as a string, use it
2022
            $field = null;
2023
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2024
                $fieldClass = $params['fieldClasses'][$fieldName];
2025
                $field = new $fieldClass($fieldName);
2026
            // If we explicitly set a field, then construct that
2027
            } elseif (isset($spec['field'])) {
2028
                // If it's a string, use it as a class name and construct
2029
                if (is_string($spec['field'])) {
2030
                    $fieldClass = $spec['field'];
2031
                    $field = new $fieldClass($fieldName);
2032
2033
                // If it's a FormField object, then just use that object directly.
2034
                } elseif ($spec['field'] instanceof FormField) {
2035
                    $field = $spec['field'];
2036
2037
                // Otherwise we have a bug
2038
                } else {
2039
                    user_error("Bad value for searchable_fields, 'field' value: "
2040
                        . var_export($spec['field'], true), E_USER_WARNING);
2041
                }
2042
2043
            // Otherwise, use the database field's scaffolder
2044
            } else {
2045
                $field = $this->relObject($fieldName)->scaffoldSearchField();
2046
            }
2047
2048
            // Allow fields to opt out of search
2049
            if (!$field) {
2050
                continue;
2051
            }
2052
2053
            if (strstr($fieldName, '.')) {
2054
                $field->setName(str_replace('.', '__', $fieldName));
2055
            }
2056
            $field->setTitle($spec['title']);
2057
2058
            $fields->push($field);
2059
        }
2060
        return $fields;
2061
    }
2062
2063
    /**
2064
     * Scaffold a simple edit form for all properties on this dataobject,
2065
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2066
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2067
     *
2068
     * @uses FormScaffolder
2069
     *
2070
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2071
     * @return FieldList
2072
     */
2073
    public function scaffoldFormFields($_params = null)
2074
    {
2075
        $params = array_merge(
2076
            array(
2077
                'tabbed' => false,
2078
                'includeRelations' => false,
2079
                'restrictFields' => false,
2080
                'fieldClasses' => false,
2081
                'ajaxSafe' => false
2082
            ),
2083
            (array)$_params
2084
        );
2085
2086
        $fs = FormScaffolder::create($this);
2087
        $fs->tabbed = $params['tabbed'];
2088
        $fs->includeRelations = $params['includeRelations'];
2089
        $fs->restrictFields = $params['restrictFields'];
2090
        $fs->fieldClasses = $params['fieldClasses'];
2091
        $fs->ajaxSafe = $params['ajaxSafe'];
2092
2093
        return $fs->getFieldList();
2094
    }
2095
2096
    /**
2097
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2098
     * being called on extensions
2099
     *
2100
     * @param callable $callback The callback to execute
2101
     */
2102
    protected function beforeUpdateCMSFields($callback)
2103
    {
2104
        $this->beforeExtending('updateCMSFields', $callback);
2105
    }
2106
2107
    /**
2108
     * Centerpiece of every data administration interface in Silverstripe,
2109
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2110
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2111
     * generate this set. To customize, overload this method in a subclass
2112
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2113
     *
2114
     * <code>
2115
     * class MyCustomClass extends DataObject {
2116
     *  static $db = array('CustomProperty'=>'Boolean');
2117
     *
2118
     *  function getCMSFields() {
2119
     *    $fields = parent::getCMSFields();
2120
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2121
     *    return $fields;
2122
     *  }
2123
     * }
2124
     * </code>
2125
     *
2126
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2127
     *
2128
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2129
     */
2130
    public function getCMSFields()
2131
    {
2132
        $tabbedFields = $this->scaffoldFormFields(array(
2133
            // Don't allow has_many/many_many relationship editing before the record is first saved
2134
            'includeRelations' => ($this->ID > 0),
2135
            'tabbed' => true,
2136
            'ajaxSafe' => true
2137
        ));
2138
2139
        $this->extend('updateCMSFields', $tabbedFields);
2140
2141
        return $tabbedFields;
2142
    }
2143
2144
    /**
2145
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2146
     * including that dataobject's extensions customised actions could be added to the EditForm.
2147
     *
2148
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2149
     */
2150
    public function getCMSActions()
2151
    {
2152
        $actions = new FieldList();
2153
        $this->extend('updateCMSActions', $actions);
2154
        return $actions;
2155
    }
2156
2157
2158
    /**
2159
     * Used for simple frontend forms without relation editing
2160
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2161
     * by default. To customize, either overload this method in your
2162
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2163
     *
2164
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2165
     *
2166
     * @param array $params See {@link scaffoldFormFields()}
2167
     * @return FieldList Always returns a simple field collection without TabSet.
2168
     */
2169
    public function getFrontEndFields($params = null)
2170
    {
2171
        $untabbedFields = $this->scaffoldFormFields($params);
2172
        $this->extend('updateFrontEndFields', $untabbedFields);
2173
2174
        return $untabbedFields;
2175
    }
2176
2177
    public function getViewerTemplates($suffix = '')
2178
    {
2179
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2180
    }
2181
2182
    /**
2183
     * Gets the value of a field.
2184
     * Called by {@link __get()} and any getFieldName() methods you might create.
2185
     *
2186
     * @param string $field The name of the field
2187
     * @return mixed The field value
2188
     */
2189
    public function getField($field)
2190
    {
2191
        // If we already have an object in $this->record, then we should just return that
2192
        if (isset($this->record[$field]) && is_object($this->record[$field])) {
2193
            return $this->record[$field];
2194
        }
2195
2196
        // Do we have a field that needs to be lazy loaded?
2197
        if (isset($this->record[$field . '_Lazy'])) {
2198
            $tableClass = $this->record[$field . '_Lazy'];
2199
            $this->loadLazyFields($tableClass);
2200
        }
2201
2202
        // In case of complex fields, return the DBField object
2203
        if (static::getSchema()->compositeField(static::class, $field)) {
2204
            $this->record[$field] = $this->dbObject($field);
2205
        }
2206
2207
        return isset($this->record[$field]) ? $this->record[$field] : null;
2208
    }
2209
2210
    /**
2211
     * Loads all the stub fields that an initial lazy load didn't load fully.
2212
     *
2213
     * @param string $class Class to load the values from. Others are joined as required.
2214
     * Not specifying a tableClass will load all lazy fields from all tables.
2215
     * @return bool Flag if lazy loading succeeded
2216
     */
2217
    protected function loadLazyFields($class = null)
2218
    {
2219
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2220
            return false;
2221
        }
2222
2223
        if (!$class) {
2224
            $loaded = array();
2225
2226
            foreach ($this->record as $key => $value) {
2227
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2228
                    $this->loadLazyFields($value);
2229
                    $loaded[$value] = $value;
2230
                }
2231
            }
2232
2233
            return false;
2234
        }
2235
2236
        $dataQuery = new DataQuery($class);
2237
2238
        // Reset query parameter context to that of this DataObject
2239
        if ($params = $this->getSourceQueryParams()) {
2240
            foreach ($params as $key => $value) {
2241
                $dataQuery->setQueryParam($key, $value);
2242
            }
2243
        }
2244
2245
        // Limit query to the current record, unless it has the Versioned extension,
2246
        // in which case it requires special handling through augmentLoadLazyFields()
2247
        $schema = static::getSchema();
2248
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2249
        $dataQuery->where([
2250
            $baseIDColumn => $this->record['ID']
2251
        ])->limit(1);
2252
2253
        $columns = array();
2254
2255
        // Add SQL for fields, both simple & multi-value
2256
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2257
        $databaseFields = $schema->databaseFields($class, false);
2258
        foreach ($databaseFields as $k => $v) {
2259
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2260
                $columns[] = $k;
2261
            }
2262
        }
2263
2264
        if ($columns) {
2265
            $query = $dataQuery->query();
2266
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2267
            $this->extend('augmentSQL', $query, $dataQuery);
2268
2269
            $dataQuery->setQueriedColumns($columns);
2270
            $newData = $dataQuery->execute()->record();
2271
2272
            // Load the data into record
2273
            if ($newData) {
2274
                foreach ($newData as $k => $v) {
2275
                    if (in_array($k, $columns)) {
2276
                        $this->record[$k] = $v;
2277
                        $this->original[$k] = $v;
2278
                        unset($this->record[$k . '_Lazy']);
2279
                    }
2280
                }
2281
2282
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2283
            } else {
2284
                foreach ($columns as $k) {
2285
                    $this->record[$k] = null;
2286
                    $this->original[$k] = null;
2287
                    unset($this->record[$k . '_Lazy']);
2288
                }
2289
            }
2290
        }
2291
        return true;
2292
    }
2293
2294
    /**
2295
     * Return the fields that have changed.
2296
     *
2297
     * The change level affects what the functions defines as "changed":
2298
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2299
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2300
     *   for example a change from 0 to null would not be included.
2301
     *
2302
     * Example return:
2303
     * <code>
2304
     * array(
2305
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2306
     * )
2307
     * </code>
2308
     *
2309
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2310
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2311
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2312
     * @return array
2313
     */
2314
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2315
    {
2316
        $changedFields = array();
2317
2318
        // Update the changed array with references to changed obj-fields
2319
        foreach ($this->record as $k => $v) {
2320
            // Prevents DBComposite infinite looping on isChanged
2321
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2322
                continue;
2323
            }
2324
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2325
                $this->changed[$k] = self::CHANGE_VALUE;
2326
            }
2327
        }
2328
2329
        if (is_array($databaseFieldsOnly)) {
2330
            $fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2331
        } elseif ($databaseFieldsOnly) {
2332
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2333
            $fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
2334
        } else {
2335
            $fields = $this->changed;
2336
        }
2337
2338
        // Filter the list to those of a certain change level
2339
        if ($changeLevel > self::CHANGE_STRICT) {
2340
            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...
2341
                foreach ($fields as $name => $level) {
2342
                    if ($level < $changeLevel) {
2343
                        unset($fields[$name]);
2344
                    }
2345
                }
2346
            }
2347
        }
2348
2349
        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...
2350
            foreach ($fields as $name => $level) {
2351
                $changedFields[$name] = array(
2352
                'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2353
                'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2354
                'level' => $level
2355
                );
2356
            }
2357
        }
2358
2359
        return $changedFields;
2360
    }
2361
2362
    /**
2363
     * Uses {@link getChangedFields()} to determine if fields have been changed
2364
     * since loading them from the database.
2365
     *
2366
     * @param string $fieldName Name of the database field to check, will check for any if not given
2367
     * @param int $changeLevel See {@link getChangedFields()}
2368
     * @return boolean
2369
     */
2370
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2371
    {
2372
        $fields = $fieldName ? array($fieldName) : true;
2373
        $changed = $this->getChangedFields($fields, $changeLevel);
2374
        if (!isset($fieldName)) {
2375
            return !empty($changed);
2376
        } else {
2377
            return array_key_exists($fieldName, $changed);
2378
        }
2379
    }
2380
2381
    /**
2382
     * Set the value of the field
2383
     * Called by {@link __set()} and any setFieldName() methods you might create.
2384
     *
2385
     * @param string $fieldName Name of the field
2386
     * @param mixed $val New field value
2387
     * @return $this
2388
     */
2389
    public function setField($fieldName, $val)
2390
    {
2391
        $this->objCacheClear();
2392
        //if it's a has_one component, destroy the cache
2393
        if (substr($fieldName, -2) == 'ID') {
2394
            unset($this->components[substr($fieldName, 0, -2)]);
2395
        }
2396
2397
        // If we've just lazy-loaded the column, then we need to populate the $original array
2398
        if (isset($this->record[$fieldName . '_Lazy'])) {
2399
            $tableClass = $this->record[$fieldName . '_Lazy'];
2400
            $this->loadLazyFields($tableClass);
2401
        }
2402
2403
        // Situation 1: Passing an DBField
2404
        if ($val instanceof DBField) {
2405
            $val->setName($fieldName);
2406
            $val->saveInto($this);
2407
2408
            // Situation 1a: Composite fields should remain bound in case they are
2409
            // later referenced to update the parent dataobject
2410
            if ($val instanceof DBComposite) {
2411
                $val->bindTo($this);
2412
                $this->record[$fieldName] = $val;
2413
            }
2414
        // Situation 2: Passing a literal or non-DBField object
2415
        } else {
2416
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2417
            if (is_object($val) && static::getSchema()->fieldSpec(static::class, $fieldName)) {
2418
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2419
            }
2420
2421
            if (!empty($val) && !is_scalar($val)) {
2422
                $dbField = $this->dbObject($fieldName);
2423
                if ($dbField && $dbField->scalarValueOnly()) {
2424
                    throw new InvalidArgumentException(
2425
                        sprintf(
2426
                            'DataObject::setField: %s only accepts scalars',
2427
                            $fieldName
2428
                        )
2429
                    );
2430
                }
2431
            }
2432
2433
            // if a field is not existing or has strictly changed
2434
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2435
                // TODO Add check for php-level defaults which are not set in the db
2436
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2437
                // At the very least, the type has changed
2438
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2439
2440
                if ((!isset($this->record[$fieldName]) && $val)
2441
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2442
                ) {
2443
                    // Value has changed as well, not just the type
2444
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2445
                }
2446
2447
                // Value is always saved back when strict check succeeds.
2448
                $this->record[$fieldName] = $val;
2449
            }
2450
        }
2451
        return $this;
2452
    }
2453
2454
    /**
2455
     * Set the value of the field, using a casting object.
2456
     * This is useful when you aren't sure that a date is in SQL format, for example.
2457
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2458
     * can be saved into the Image table.
2459
     *
2460
     * @param string $fieldName Name of the field
2461
     * @param mixed $value New field value
2462
     * @return $this
2463
     */
2464
    public function setCastedField($fieldName, $value)
2465
    {
2466
        if (!$fieldName) {
2467
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2468
        }
2469
        $fieldObj = $this->dbObject($fieldName);
2470
        if ($fieldObj) {
0 ignored issues
show
introduced by
$fieldObj is of type SilverStripe\ORM\FieldType\DBField, thus it always evaluated to true.
Loading history...
2471
            $fieldObj->setValue($value);
2472
            $fieldObj->saveInto($this);
2473
        } else {
2474
            $this->$fieldName = $value;
2475
        }
2476
        return $this;
2477
    }
2478
2479
    /**
2480
     * {@inheritdoc}
2481
     */
2482
    public function castingHelper($field)
2483
    {
2484
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2485
        if ($fieldSpec) {
2486
            return $fieldSpec;
2487
        }
2488
2489
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2490
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2491
        $queryParams = $this->getSourceQueryParams();
2492
        if (!empty($queryParams['Component.ExtraFields'])) {
2493
            $extraFields = $queryParams['Component.ExtraFields'];
2494
2495
            if (isset($extraFields[$field])) {
2496
                return $extraFields[$field];
2497
            }
2498
        }
2499
2500
        return parent::castingHelper($field);
2501
    }
2502
2503
    /**
2504
     * Returns true if the given field exists in a database column on any of
2505
     * the objects tables and optionally look up a dynamic getter with
2506
     * get<fieldName>().
2507
     *
2508
     * @param string $field Name of the field
2509
     * @return boolean True if the given field exists
2510
     */
2511
    public function hasField($field)
2512
    {
2513
        $schema = static::getSchema();
2514
        return (
2515
            array_key_exists($field, $this->record)
2516
            || $schema->fieldSpec(static::class, $field)
2517
            || (substr($field, -2) == 'ID') && $schema->hasOneComponent(static::class, substr($field, 0, -2))
2518
            || $this->hasMethod("get{$field}")
2519
        );
2520
    }
2521
2522
    /**
2523
     * Returns true if the given field exists as a database column
2524
     *
2525
     * @param string $field Name of the field
2526
     *
2527
     * @return boolean
2528
     */
2529
    public function hasDatabaseField($field)
2530
    {
2531
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2532
        return !empty($spec);
2533
    }
2534
2535
    /**
2536
     * Returns true if the member is allowed to do the given action.
2537
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2538
     *
2539
     * @param string $perm The permission to be checked, such as 'View'.
2540
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2541
     * in user.
2542
     * @param array $context Additional $context to pass to extendedCan()
2543
     *
2544
     * @return boolean True if the the member is allowed to do the given action
2545
     */
2546
    public function can($perm, $member = null, $context = array())
2547
    {
2548
        if (!$member) {
2549
            $member = Security::getCurrentUser();
2550
        }
2551
2552
        if ($member && Permission::checkMember($member, "ADMIN")) {
2553
            return true;
2554
        }
2555
2556
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2557
            $method = 'can' . ucfirst($perm);
2558
            return $this->$method($member);
2559
        }
2560
2561
        $results = $this->extendedCan('can', $member);
2562
        if (isset($results)) {
2563
            return $results;
2564
        }
2565
2566
        return ($member && Permission::checkMember($member, $perm));
2567
    }
2568
2569
    /**
2570
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2571
     * expected to return one of three values:
2572
     *
2573
     *  - false: Disallow this permission, regardless of what other extensions say
2574
     *  - true: Allow this permission, as long as no other extensions return false
2575
     *  - NULL: Don't affect the outcome
2576
     *
2577
     * This method itself returns a tri-state value, and is designed to be used like this:
2578
     *
2579
     * <code>
2580
     * $extended = $this->extendedCan('canDoSomething', $member);
2581
     * if($extended !== null) return $extended;
2582
     * else return $normalValue;
2583
     * </code>
2584
     *
2585
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2586
     * @param Member|int $member
2587
     * @param array $context Optional context
2588
     * @return boolean|null
2589
     */
2590
    public function extendedCan($methodName, $member, $context = array())
2591
    {
2592
        $results = $this->extend($methodName, $member, $context);
2593
        if ($results && is_array($results)) {
2594
            // Remove NULLs
2595
            $results = array_filter($results, function ($v) {
2596
                return !is_null($v);
2597
            });
2598
            // If there are any non-NULL responses, then return the lowest one of them.
2599
            // If any explicitly deny the permission, then we don't get access
2600
            if ($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...
2601
                return min($results);
2602
            }
2603
        }
2604
        return null;
2605
    }
2606
2607
    /**
2608
     * @param Member $member
2609
     * @return boolean
2610
     */
2611
    public function canView($member = null)
2612
    {
2613
        $extended = $this->extendedCan(__FUNCTION__, $member);
2614
        if ($extended !== null) {
2615
            return $extended;
2616
        }
2617
        return Permission::check('ADMIN', 'any', $member);
2618
    }
2619
2620
    /**
2621
     * @param Member $member
2622
     * @return boolean
2623
     */
2624
    public function canEdit($member = null)
2625
    {
2626
        $extended = $this->extendedCan(__FUNCTION__, $member);
2627
        if ($extended !== null) {
2628
            return $extended;
2629
        }
2630
        return Permission::check('ADMIN', 'any', $member);
2631
    }
2632
2633
    /**
2634
     * @param Member $member
2635
     * @return boolean
2636
     */
2637
    public function canDelete($member = null)
2638
    {
2639
        $extended = $this->extendedCan(__FUNCTION__, $member);
2640
        if ($extended !== null) {
2641
            return $extended;
2642
        }
2643
        return Permission::check('ADMIN', 'any', $member);
2644
    }
2645
2646
    /**
2647
     * @param Member $member
2648
     * @param array $context Additional context-specific data which might
2649
     * affect whether (or where) this object could be created.
2650
     * @return boolean
2651
     */
2652
    public function canCreate($member = null, $context = array())
2653
    {
2654
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2655
        if ($extended !== null) {
2656
            return $extended;
2657
        }
2658
        return Permission::check('ADMIN', 'any', $member);
2659
    }
2660
2661
    /**
2662
     * Debugging used by Debug::show()
2663
     *
2664
     * @return string HTML data representing this object
2665
     */
2666
    public function debug()
2667
    {
2668
        $class = static::class;
2669
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2670
        if ($this->record) {
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...
2671
            foreach ($this->record as $fieldName => $fieldVal) {
2672
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2673
            }
2674
        }
2675
        $val .= "</ul>\n";
2676
        return $val;
2677
    }
2678
2679
    /**
2680
     * Return the DBField object that represents the given field.
2681
     * This works similarly to obj() with 2 key differences:
2682
     *   - it still returns an object even when the field has no value.
2683
     *   - it only matches fields and not methods
2684
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2685
     *
2686
     * @param string $fieldName Name of the field
2687
     * @return DBField The field as a DBField object
2688
     */
2689
    public function dbObject($fieldName)
2690
    {
2691
        // Check for field in DB
2692
        $schema = static::getSchema();
2693
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2694
        if (!$helper) {
2695
            return null;
2696
        }
2697
2698
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2699
            $tableClass = $this->record[$fieldName . '_Lazy'];
2700
            $this->loadLazyFields($tableClass);
2701
        }
2702
2703
        $value = isset($this->record[$fieldName])
2704
            ? $this->record[$fieldName]
2705
            : null;
2706
2707
        // If we have a DBField object in $this->record, then return that
2708
        if ($value instanceof DBField) {
2709
            return $value;
2710
        }
2711
2712
        list($class, $spec) = explode('.', $helper);
2713
        /** @var DBField $obj */
2714
        $table = $schema->tableName($class);
2715
        $obj = Injector::inst()->create($spec, $fieldName);
2716
        $obj->setTable($table);
2717
        $obj->setValue($value, $this, false);
2718
        return $obj;
2719
    }
2720
2721
    /**
2722
     * Traverses to a DBField referenced by relationships between data objects.
2723
     *
2724
     * The path to the related field is specified with dot separated syntax
2725
     * (eg: Parent.Child.Child.FieldName).
2726
     *
2727
     * @param string $fieldPath
2728
     *
2729
     * @return mixed DBField of the field on the object or a DataList instance.
2730
     */
2731
    public function relObject($fieldPath)
2732
    {
2733
        $object = null;
2734
2735
        if (strpos($fieldPath, '.') !== false) {
2736
            $parts = explode('.', $fieldPath);
2737
            $fieldName = array_pop($parts);
2738
2739
            // Traverse dot syntax
2740
            $component = $this;
2741
2742
            foreach ($parts as $relation) {
2743
                if ($component instanceof SS_List) {
2744
                    if (method_exists($component, $relation)) {
2745
                        $component = $component->$relation();
2746
                    } else {
2747
                        /** @var DataList $component */
2748
                        $component = $component->relation($relation);
2749
                    }
2750
                } else {
2751
                    $component = $component->$relation();
2752
                }
2753
            }
2754
2755
            $object = $component->dbObject($fieldName);
2756
        } else {
2757
            $object = $this->dbObject($fieldPath);
2758
        }
2759
2760
        return $object;
2761
    }
2762
2763
    /**
2764
     * Traverses to a field referenced by relationships between data objects, returning the value
2765
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2766
     *
2767
     * @param $fieldName string
2768
     * @return string | null - will return null on a missing value
2769
     */
2770
    public function relField($fieldName)
2771
    {
2772
        $component = $this;
2773
2774
        // We're dealing with relations here so we traverse the dot syntax
2775
        if (strpos($fieldName, '.') !== false) {
2776
            $relations = explode('.', $fieldName);
2777
            $fieldName = array_pop($relations);
2778
            foreach ($relations as $relation) {
2779
                if (!$component) {
2780
                    return null;
2781
                // Inspect $component for element $relation
2782
                } elseif ($component->hasMethod($relation)) {
2783
                    // Check nested method
2784
                    $component = $component->$relation();
2785
                } elseif ($component instanceof SS_List) {
2786
                    // Select adjacent relation from DataList
2787
                    /** @var DataList $component */
2788
                    $component = $component->relation($relation);
2789
                } elseif ($component instanceof DataObject
2790
                    && ($dbObject = $component->dbObject($relation))
2791
                ) {
2792
                    // Select db object
2793
                    $component = $dbObject;
2794
                } else {
2795
                    user_error("$relation is not a relation/field on " . get_class($component), E_USER_ERROR);
2796
                }
2797
            }
2798
        }
2799
2800
        // Bail if the component is null
2801
        if (!$component) {
2802
            return null;
2803
        }
2804
        if ($component->hasMethod($fieldName)) {
2805
            return $component->$fieldName();
2806
        }
2807
        return $component->$fieldName;
2808
    }
2809
2810
    /**
2811
     * Temporary hack to return an association name, based on class, to get around the mangle
2812
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2813
     *
2814
     * @param string $className
2815
     * @return string
2816
     */
2817
    public function getReverseAssociation($className)
2818
    {
2819
        if (is_array($this->manyMany())) {
0 ignored issues
show
introduced by
The condition is_array($this->manyMany()) is always true.
Loading history...
2820
            $many_many = array_flip($this->manyMany());
2821
            if (array_key_exists($className, $many_many)) {
2822
                return $many_many[$className];
2823
            }
2824
        }
2825
        if (is_array($this->hasMany())) {
0 ignored issues
show
introduced by
The condition is_array($this->hasMany()) is always true.
Loading history...
2826
            $has_many = array_flip($this->hasMany());
2827
            if (array_key_exists($className, $has_many)) {
2828
                return $has_many[$className];
2829
            }
2830
        }
2831
        if (is_array($this->hasOne())) {
0 ignored issues
show
introduced by
The condition is_array($this->hasOne()) is always true.
Loading history...
2832
            $has_one = array_flip($this->hasOne());
2833
            if (array_key_exists($className, $has_one)) {
2834
                return $has_one[$className];
2835
            }
2836
        }
2837
2838
        return false;
2839
    }
2840
2841
    /**
2842
     * Return all objects matching the filter
2843
     * sub-classes are automatically selected and included
2844
     *
2845
     * @param string $callerClass The class of objects to be returned
2846
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2847
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2848
     * @param string|array $sort A sort expression to be inserted into the ORDER
2849
     * BY clause.  If omitted, self::$default_sort will be used.
2850
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
2851
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2852
     * @param string $containerClass The container class to return the results in.
2853
     *
2854
     * @todo $containerClass is Ignored, why?
2855
     *
2856
     * @return DataList The objects matching the filter, in the class specified by $containerClass
2857
     */
2858
    public static function get(
2859
        $callerClass = null,
2860
        $filter = "",
2861
        $sort = "",
2862
        $join = "",
2863
        $limit = null,
2864
        $containerClass = DataList::class
2865
    ) {
2866
2867
        if ($callerClass == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $callerClass of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2868
            $callerClass = get_called_class();
2869
            if ($callerClass == self::class) {
2870
                throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
2871
            }
2872
2873
            if ($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
2874
                throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
2875
                    . ' arguments');
2876
            }
2877
2878
            return DataList::create(get_called_class());
2879
        }
2880
2881
        if ($join) {
2882
            throw new \InvalidArgumentException(
2883
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
2884
            );
2885
        }
2886
2887
        $result = DataList::create($callerClass)->where($filter)->sort($sort);
2888
2889
        if ($limit && strpos($limit, ',') !== false) {
0 ignored issues
show
Bug introduced by
It seems like $limit can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2889
        if ($limit && strpos(/** @scrutinizer ignore-type */ $limit, ',') !== false) {
Loading history...
2890
            $limitArguments = explode(',', $limit);
0 ignored issues
show
Bug introduced by
It seems like $limit can also be of type array; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2890
            $limitArguments = explode(',', /** @scrutinizer ignore-type */ $limit);
Loading history...
2891
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
2892
        } elseif ($limit) {
2893
            $result = $result->limit($limit);
2894
        }
2895
2896
        return $result;
2897
    }
2898
2899
2900
    /**
2901
     * Return the first item matching the given query.
2902
     * All calls to get_one() are cached.
2903
     *
2904
     * @param string $callerClass The class of objects to be returned
2905
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2906
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2907
     * @param boolean $cache Use caching
2908
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
2909
     *
2910
     * @return DataObject|null The first item matching the query
2911
     */
2912
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
2913
    {
2914
        $SNG = singleton($callerClass);
2915
2916
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
2917
        $cacheKey = md5(serialize($cacheComponents));
2918
2919
        $item = null;
2920
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
2921
            $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...
2922
            $item = $dl->first();
2923
2924
            if ($cache) {
2925
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
2926
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
2927
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
2928
                }
2929
            }
2930
        }
2931
2932
        if ($cache) {
2933
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
2934
        } else {
2935
            return $item;
2936
        }
2937
    }
2938
2939
    /**
2940
     * Flush the cached results for all relations (has_one, has_many, many_many)
2941
     * Also clears any cached aggregate data.
2942
     *
2943
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
2944
     *                            When false will just clear session-local cached data
2945
     * @return DataObject $this
2946
     */
2947
    public function flushCache($persistent = true)
2948
    {
2949
        if (static::class == self::class) {
0 ignored issues
show
introduced by
The condition static::class == self::class is always true.
Loading history...
2950
            self::$_cache_get_one = array();
2951
            return $this;
2952
        }
2953
2954
        $classes = ClassInfo::ancestry(static::class);
2955
        foreach ($classes as $class) {
2956
            if (isset(self::$_cache_get_one[$class])) {
2957
                unset(self::$_cache_get_one[$class]);
2958
            }
2959
        }
2960
2961
        $this->extend('flushCache');
2962
2963
        $this->components = array();
2964
        return $this;
2965
    }
2966
2967
    /**
2968
     * Flush the get_one global cache and destroy associated objects.
2969
     */
2970
    public static function flush_and_destroy_cache()
2971
    {
2972
        if (self::$_cache_get_one) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::_cache_get_one of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2973
            foreach (self::$_cache_get_one as $class => $items) {
2974
                if (is_array($items)) {
2975
                    foreach ($items as $item) {
2976
                        if ($item) {
2977
                            $item->destroy();
2978
                        }
2979
                    }
2980
                }
2981
            }
2982
        }
2983
        self::$_cache_get_one = array();
2984
    }
2985
2986
    /**
2987
     * Reset all global caches associated with DataObject.
2988
     */
2989
    public static function reset()
2990
    {
2991
        // @todo Decouple these
2992
        DBClassName::clear_classname_cache();
2993
        ClassInfo::reset_db_cache();
2994
        static::getSchema()->reset();
2995
        self::$_cache_get_one = array();
2996
        self::$_cache_field_labels = array();
2997
    }
2998
2999
    /**
3000
     * Return the given element, searching by ID
3001
     *
3002
     * @param string $callerClass The class of the object to be returned
3003
     * @param int $id The id of the element
3004
     * @param boolean $cache See {@link get_one()}
3005
     *
3006
     * @return DataObject The element
3007
     */
3008
    public static function get_by_id($callerClass, $id, $cache = true)
3009
    {
3010
        if (!is_numeric($id)) {
0 ignored issues
show
introduced by
The condition is_numeric($id) is always true.
Loading history...
3011
            user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3012
        }
3013
3014
        // Pass to get_one
3015
        $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
3016
        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...
3017
    }
3018
3019
    /**
3020
     * Get the name of the base table for this object
3021
     *
3022
     * @return string
3023
     */
3024
    public function baseTable()
3025
    {
3026
        return static::getSchema()->baseDataTable($this);
3027
    }
3028
3029
    /**
3030
     * Get the base class for this object
3031
     *
3032
     * @return string
3033
     */
3034
    public function baseClass()
3035
    {
3036
        return static::getSchema()->baseDataClass($this);
3037
    }
3038
3039
    /**
3040
     * @var array Parameters used in the query that built this object.
3041
     * This can be used by decorators (e.g. lazy loading) to
3042
     * run additional queries using the same context.
3043
     */
3044
    protected $sourceQueryParams;
3045
3046
    /**
3047
     * @see $sourceQueryParams
3048
     * @return array
3049
     */
3050
    public function getSourceQueryParams()
3051
    {
3052
        return $this->sourceQueryParams;
3053
    }
3054
3055
    /**
3056
     * Get list of parameters that should be inherited to relations on this object
3057
     *
3058
     * @return array
3059
     */
3060
    public function getInheritableQueryParams()
3061
    {
3062
        $params = $this->getSourceQueryParams();
3063
        $this->extend('updateInheritableQueryParams', $params);
3064
        return $params;
3065
    }
3066
3067
    /**
3068
     * @see $sourceQueryParams
3069
     * @param array
3070
     */
3071
    public function setSourceQueryParams($array)
3072
    {
3073
        $this->sourceQueryParams = $array;
3074
    }
3075
3076
    /**
3077
     * @see $sourceQueryParams
3078
     * @param string $key
3079
     * @param string $value
3080
     */
3081
    public function setSourceQueryParam($key, $value)
3082
    {
3083
        $this->sourceQueryParams[$key] = $value;
3084
    }
3085
3086
    /**
3087
     * @see $sourceQueryParams
3088
     * @param string $key
3089
     * @return string
3090
     */
3091
    public function getSourceQueryParam($key)
3092
    {
3093
        if (isset($this->sourceQueryParams[$key])) {
3094
            return $this->sourceQueryParams[$key];
3095
        }
3096
        return null;
3097
    }
3098
3099
    //-------------------------------------------------------------------------------------------//
3100
3101
    /**
3102
     * Check the database schema and update it as necessary.
3103
     *
3104
     * @uses DataExtension->augmentDatabase()
3105
     */
3106
    public function requireTable()
3107
    {
3108
        // Only build the table if we've actually got fields
3109
        $schema = static::getSchema();
3110
        $table = $schema->tableName(static::class);
3111
        $fields = $schema->databaseFields(static::class, false);
3112
        $indexes = $schema->databaseIndexes(static::class, false);
3113
        $extensions = self::database_extensions(static::class);
0 ignored issues
show
Bug Best Practice introduced by
The method SilverStripe\ORM\DataObject::database_extensions() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3113
        /** @scrutinizer ignore-call */ 
3114
        $extensions = self::database_extensions(static::class);
Loading history...
3114
3115
        if (empty($table)) {
3116
            throw new LogicException(
3117
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3118
            );
3119
        }
3120
3121
        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...
3122
            $hasAutoIncPK = get_parent_class($this) === self::class;
3123
            DB::require_table(
3124
                $table,
3125
                $fields,
0 ignored issues
show
Bug introduced by
$fields of type array is incompatible with the type string expected by parameter $fieldSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3125
                /** @scrutinizer ignore-type */ $fields,
Loading history...
3126
                $indexes,
0 ignored issues
show
Bug introduced by
$indexes of type array is incompatible with the type string expected by parameter $indexSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3126
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3127
                $hasAutoIncPK,
3128
                $this->config()->get('create_table_options'),
3129
                $extensions
0 ignored issues
show
Bug introduced by
It seems like $extensions can also be of type false; however, parameter $extensions of SilverStripe\ORM\DB::require_table() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3129
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3130
            );
3131
        } else {
3132
            DB::dont_require_table($table);
3133
        }
3134
3135
        // Build any child tables for many_many items
3136
        if ($manyMany = $this->uninherited('many_many')) {
3137
            $extras = $this->uninherited('many_many_extraFields');
3138
            foreach ($manyMany as $component => $spec) {
3139
                // Get many_many spec
3140
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3141
                $parentField = $manyManyComponent['parentField'];
3142
                $childField = $manyManyComponent['childField'];
3143
                $tableOrClass = $manyManyComponent['join'];
3144
3145
                // Skip if backed by actual class
3146
                if (class_exists($tableOrClass)) {
3147
                    continue;
3148
                }
3149
3150
                // Build fields
3151
                $manymanyFields = array(
3152
                    $parentField => "Int",
3153
                    $childField => "Int",
3154
                );
3155
                if (isset($extras[$component])) {
3156
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3157
                }
3158
3159
                // Build index list
3160
                $manymanyIndexes = [
3161
                    $parentField => [
3162
                        'type' => 'index',
3163
                        'name' => $parentField,
3164
                        'columns' => [$parentField],
3165
                    ],
3166
                    $childField => [
3167
                        'type' => 'index',
3168
                        'name' =>$childField,
3169
                        'columns' => [$childField],
3170
                    ],
3171
                ];
3172
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Bug introduced by
$manymanyIndexes of type array<mixed,array<string,array|mixed|string>> is incompatible with the type string expected by parameter $indexSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3172
                DB::require_table($tableOrClass, $manymanyFields, /** @scrutinizer ignore-type */ $manymanyIndexes, true, null, $extensions);
Loading history...
Bug introduced by
$manymanyFields of type array|string[] is incompatible with the type string expected by parameter $fieldSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3172
                DB::require_table($tableOrClass, /** @scrutinizer ignore-type */ $manymanyFields, $manymanyIndexes, true, null, $extensions);
Loading history...
3173
            }
3174
        }
3175
3176
        // Let any extentions make their own database fields
3177
        $this->extend('augmentDatabase', $dummy);
3178
    }
3179
3180
    /**
3181
     * Add default records to database. This function is called whenever the
3182
     * database is built, after the database tables have all been created. Overload
3183
     * this to add default records when the database is built, but make sure you
3184
     * call parent::requireDefaultRecords().
3185
     *
3186
     * @uses DataExtension->requireDefaultRecords()
3187
     */
3188
    public function requireDefaultRecords()
3189
    {
3190
        $defaultRecords = $this->config()->uninherited('default_records');
3191
3192
        if (!empty($defaultRecords)) {
3193
            $hasData = DataObject::get_one(static::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...
3194
            if (!$hasData) {
3195
                $className = static::class;
3196
                foreach ($defaultRecords as $record) {
3197
                    $obj = Injector::inst()->create($className, $record);
3198
                    $obj->write();
3199
                }
3200
                DB::alteration_message("Added default records to $className table", "created");
3201
            }
3202
        }
3203
3204
        // Let any extentions make their own database default data
3205
        $this->extend('requireDefaultRecords', $dummy);
3206
    }
3207
3208
    /**
3209
     * Get the default searchable fields for this object, as defined in the
3210
     * $searchable_fields list. If searchable fields are not defined on the
3211
     * data object, uses a default selection of summary fields.
3212
     *
3213
     * @return array
3214
     */
3215
    public function searchableFields()
3216
    {
3217
        // can have mixed format, need to make consistent in most verbose form
3218
        $fields = $this->config()->get('searchable_fields');
3219
        $labels = $this->fieldLabels();
3220
3221
        // fallback to summary fields (unless empty array is explicitly specified)
3222
        if (! $fields && ! is_array($fields)) {
3223
            $summaryFields = array_keys($this->summaryFields());
3224
            $fields = array();
3225
3226
            // remove the custom getters as the search should not include them
3227
            $schema = static::getSchema();
3228
            if ($summaryFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $summaryFields 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...
3229
                foreach ($summaryFields as $key => $name) {
3230
                    $spec = $name;
3231
3232
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3233
                    if (($fieldPos = strpos($name, '.')) !== false) {
3234
                        $name = substr($name, 0, $fieldPos);
3235
                    }
3236
3237
                    if ($schema->fieldSpec($this, $name)) {
3238
                        $fields[] = $name;
3239
                    } elseif ($this->relObject($spec)) {
3240
                        $fields[] = $spec;
3241
                    }
3242
                }
3243
            }
3244
        }
3245
3246
        // we need to make sure the format is unified before
3247
        // augmenting fields, so extensions can apply consistent checks
3248
        // but also after augmenting fields, because the extension
3249
        // might use the shorthand notation as well
3250
3251
        // rewrite array, if it is using shorthand syntax
3252
        $rewrite = array();
3253
        foreach ($fields as $name => $specOrName) {
3254
            $identifer = (is_int($name)) ? $specOrName : $name;
3255
3256
            if (is_int($name)) {
3257
                // 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...
3258
                $rewrite[$identifer] = array();
3259
            } elseif (is_array($specOrName)) {
3260
                // 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...
3261
                //   'filter => 'ExactMatchFilter',
3262
                //   '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...
3263
                //   '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...
3264
                // ))
3265
                $rewrite[$identifer] = array_merge(
3266
                    array('filter' => $this->relObject($identifer)->config()->get('default_search_filter_class')),
3267
                    (array)$specOrName
3268
                );
3269
            } else {
3270
                // 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...
3271
                $rewrite[$identifer] = array(
3272
                    'filter' => $specOrName,
3273
                );
3274
            }
3275
            if (!isset($rewrite[$identifer]['title'])) {
3276
                $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3277
                    ? $labels[$identifer] : FormField::name_to_label($identifer);
3278
            }
3279
            if (!isset($rewrite[$identifer]['filter'])) {
3280
                /** @skipUpgrade */
3281
                $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3282
            }
3283
        }
3284
3285
        $fields = $rewrite;
3286
3287
        // apply DataExtensions if present
3288
        $this->extend('updateSearchableFields', $fields);
3289
3290
        return $fields;
3291
    }
3292
3293
    /**
3294
     * Get any user defined searchable fields labels that
3295
     * exist. Allows overriding of default field names in the form
3296
     * interface actually presented to the user.
3297
     *
3298
     * The reason for keeping this separate from searchable_fields,
3299
     * which would be a logical place for this functionality, is to
3300
     * avoid bloating and complicating the configuration array. Currently
3301
     * much of this system is based on sensible defaults, and this property
3302
     * would generally only be set in the case of more complex relationships
3303
     * between data object being required in the search interface.
3304
     *
3305
     * Generates labels based on name of the field itself, if no static property
3306
     * {@link self::field_labels} exists.
3307
     *
3308
     * @uses $field_labels
3309
     * @uses FormField::name_to_label()
3310
     *
3311
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3312
     *
3313
     * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3314
     */
3315
    public function fieldLabels($includerelations = true)
3316
    {
3317
        $cacheKey = static::class . '_' . $includerelations;
3318
3319
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
3320
            $customLabels = $this->config()->get('field_labels');
3321
            $autoLabels = array();
3322
3323
            // get all translated static properties as defined in i18nCollectStatics()
3324
            $ancestry = ClassInfo::ancestry(static::class);
3325
            $ancestry = array_reverse($ancestry);
3326
            if ($ancestry) {
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...
3327
                foreach ($ancestry as $ancestorClass) {
3328
                    if ($ancestorClass === ViewableData::class) {
3329
                        break;
3330
                    }
3331
                    $types = [
3332
                        'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3333
                    ];
3334
                    if ($includerelations) {
3335
                        $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3336
                        $types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3337
                        $types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3338
                        $types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3339
                    }
3340
                    foreach ($types as $type => $attrs) {
3341
                        foreach ($attrs as $name => $spec) {
3342
                            $autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}", FormField::name_to_label($name));
3343
                        }
3344
                    }
3345
                }
3346
            }
3347
3348
            $labels = array_merge((array)$autoLabels, (array)$customLabels);
3349
            $this->extend('updateFieldLabels', $labels);
3350
            self::$_cache_field_labels[$cacheKey] = $labels;
3351
        }
3352
3353
        return self::$_cache_field_labels[$cacheKey];
3354
    }
3355
3356
    /**
3357
     * Get a human-readable label for a single field,
3358
     * see {@link fieldLabels()} for more details.
3359
     *
3360
     * @uses fieldLabels()
3361
     * @uses FormField::name_to_label()
3362
     *
3363
     * @param string $name Name of the field
3364
     * @return string Label of the field
3365
     */
3366
    public function fieldLabel($name)
3367
    {
3368
        $labels = $this->fieldLabels();
3369
        return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3370
    }
3371
3372
    /**
3373
     * Get the default summary fields for this object.
3374
     *
3375
     * @todo use the translation apparatus to return a default field selection for the language
3376
     *
3377
     * @return array
3378
     */
3379
    public function summaryFields()
3380
    {
3381
        $rawFields = $this->config()->get('summary_fields');
3382
3383
        // Merge associative / numeric keys
3384
        $fields = [];
3385
        foreach ($rawFields as $key => $value) {
3386
            if (is_int($key)) {
3387
                $key = $value;
3388
            }
3389
            $fields[$key] = $value;
3390
        }
3391
3392
        if (!$fields) {
3393
            $fields = array();
3394
            // try to scaffold a couple of usual suspects
3395
            if ($this->hasField('Name')) {
3396
                $fields['Name'] = 'Name';
3397
            }
3398
            if (static::getSchema()->fieldSpec($this, 'Title')) {
3399
                $fields['Title'] = 'Title';
3400
            }
3401
            if ($this->hasField('Description')) {
3402
                $fields['Description'] = 'Description';
3403
            }
3404
            if ($this->hasField('FirstName')) {
3405
                $fields['FirstName'] = 'First Name';
3406
            }
3407
        }
3408
        $this->extend("updateSummaryFields", $fields);
3409
3410
        // Final fail-over, just list ID field
3411
        if (!$fields) {
3412
            $fields['ID'] = 'ID';
3413
        }
3414
3415
        // Localize fields (if possible)
3416
        foreach ($this->fieldLabels(false) as $name => $label) {
3417
            // only attempt to localize if the label definition is the same as the field name.
3418
            // this will preserve any custom labels set in the summary_fields configuration
3419
            if (isset($fields[$name]) && $name === $fields[$name]) {
3420
                $fields[$name] = $label;
3421
            }
3422
        }
3423
3424
        return $fields;
3425
    }
3426
3427
    /**
3428
     * Defines a default list of filters for the search context.
3429
     *
3430
     * If a filter class mapping is defined on the data object,
3431
     * it is constructed here. Otherwise, the default filter specified in
3432
     * {@link DBField} is used.
3433
     *
3434
     * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3435
     *
3436
     * @return array
3437
     */
3438
    public function defaultSearchFilters()
3439
    {
3440
        $filters = array();
3441
3442
        foreach ($this->searchableFields() as $name => $spec) {
3443
            if (empty($spec['filter'])) {
3444
                /** @skipUpgrade */
3445
                $filters[$name] = 'PartialMatchFilter';
3446
            } elseif ($spec['filter'] instanceof SearchFilter) {
3447
                $filters[$name] = $spec['filter'];
3448
            } else {
3449
                $filters[$name] = Injector::inst()->create($spec['filter'], $name);
3450
            }
3451
        }
3452
3453
        return $filters;
3454
    }
3455
3456
    /**
3457
     * @return boolean True if the object is in the database
3458
     */
3459
    public function isInDB()
3460
    {
3461
        return is_numeric($this->ID) && $this->ID > 0;
3462
    }
3463
3464
    /*
3465
     * @ignore
3466
     */
3467
    private static $subclass_access = true;
3468
3469
    /**
3470
     * Temporarily disable subclass access in data object qeur
3471
     */
3472
    public static function disable_subclass_access()
3473
    {
3474
        self::$subclass_access = false;
3475
    }
3476
    public static function enable_subclass_access()
3477
    {
3478
        self::$subclass_access = true;
3479
    }
3480
3481
    //-------------------------------------------------------------------------------------------//
3482
3483
    /**
3484
     * Database field definitions.
3485
     * This is a map from field names to field type. The field
3486
     * type should be a class that extends .
3487
     * @var array
3488
     * @config
3489
     */
3490
    private static $db = [];
3491
3492
    /**
3493
     * Use a casting object for a field. This is a map from
3494
     * field name to class name of the casting object.
3495
     *
3496
     * @var array
3497
     */
3498
    private static $casting = array(
3499
        "Title" => 'Text',
3500
    );
3501
3502
    /**
3503
     * Specify custom options for a CREATE TABLE call.
3504
     * Can be used to specify a custom storage engine for specific database table.
3505
     * All options have to be keyed for a specific database implementation,
3506
     * identified by their class name (extending from {@link SS_Database}).
3507
     *
3508
     * <code>
3509
     * array(
3510
     *  'MySQLDatabase' => 'ENGINE=MyISAM'
3511
     * )
3512
     * </code>
3513
     *
3514
     * Caution: This API is experimental, and might not be
3515
     * included in the next major release. Please use with care.
3516
     *
3517
     * @var array
3518
     * @config
3519
     */
3520
    private static $create_table_options = array(
3521
        MySQLSchemaManager::ID => 'ENGINE=InnoDB'
3522
    );
3523
3524
    /**
3525
     * If a field is in this array, then create a database index
3526
     * on that field. This is a map from fieldname to index type.
3527
     * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3528
     *
3529
     * @var array
3530
     * @config
3531
     */
3532
    private static $indexes = null;
3533
3534
    /**
3535
     * Inserts standard column-values when a DataObject
3536
     * is instanciated. Does not insert default records {@see $default_records}.
3537
     * This is a map from fieldname to default value.
3538
     *
3539
     *  - If you would like to change a default value in a sub-class, just specify it.
3540
     *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3541
     *    or false in your subclass.  Setting it to null won't work.
3542
     *
3543
     * @var array
3544
     * @config
3545
     */
3546
    private static $defaults = [];
3547
3548
    /**
3549
     * Multidimensional array which inserts default data into the database
3550
     * on a db/build-call as long as the database-table is empty. Please use this only
3551
     * for simple constructs, not for SiteTree-Objects etc. which need special
3552
     * behaviour such as publishing and ParentNodes.
3553
     *
3554
     * Example:
3555
     * array(
3556
     *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3557
     *  array('Title' => "DefaultPage2")
3558
     * ).
3559
     *
3560
     * @var array
3561
     * @config
3562
     */
3563
    private static $default_records = null;
3564
3565
    /**
3566
     * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3567
     * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3568
     *
3569
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3570
     *
3571
     *  @var array
3572
     * @config
3573
     */
3574
    private static $has_one = [];
3575
3576
    /**
3577
     * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3578
     *
3579
     * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3580
     * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3581
     * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3582
     *
3583
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3584
     *
3585
     * @var array
3586
     * @config
3587
     */
3588
    private static $belongs_to = [];
3589
3590
    /**
3591
     * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3592
     *
3593
     * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3594
     * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3595
     * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3596
     * which foreign key to use.
3597
     *
3598
     * @var array
3599
     * @config
3600
     */
3601
    private static $has_many = [];
3602
3603
    /**
3604
     * many-many relationship definitions.
3605
     * This is a map from component name to data type.
3606
     * @var array
3607
     * @config
3608
     */
3609
    private static $many_many = [];
3610
3611
    /**
3612
     * Extra fields to include on the connecting many-many table.
3613
     * This is a map from field name to field type.
3614
     *
3615
     * Example code:
3616
     * <code>
3617
     * public static $many_many_extraFields = array(
3618
     *  'Members' => array(
3619
     *          'Role' => 'Varchar(100)'
3620
     *      )
3621
     * );
3622
     * </code>
3623
     *
3624
     * @var array
3625
     * @config
3626
     */
3627
    private static $many_many_extraFields = [];
3628
3629
    /**
3630
     * The inverse side of a many-many relationship.
3631
     * This is a map from component name to data type.
3632
     * @var array
3633
     * @config
3634
     */
3635
    private static $belongs_many_many = [];
3636
3637
    /**
3638
     * The default sort expression. This will be inserted in the ORDER BY
3639
     * clause of a SQL query if no other sort expression is provided.
3640
     * @var string
3641
     * @config
3642
     */
3643
    private static $default_sort = null;
3644
3645
    /**
3646
     * Default list of fields that can be scaffolded by the ModelAdmin
3647
     * search interface.
3648
     *
3649
     * Overriding the default filter, with a custom defined filter:
3650
     * <code>
3651
     *  static $searchable_fields = array(
3652
     *     "Name" => "PartialMatchFilter"
3653
     *  );
3654
     * </code>
3655
     *
3656
     * Overriding the default form fields, with a custom defined field.
3657
     * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3658
     * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3659
     * <code>
3660
     *  static $searchable_fields = array(
3661
     *    "Name" => array(
3662
     *      "field" => "TextField"
3663
     *    )
3664
     *  );
3665
     * </code>
3666
     *
3667
     * Overriding the default form field, filter and title:
3668
     * <code>
3669
     *  static $searchable_fields = array(
3670
     *    "Organisation.ZipCode" => array(
3671
     *      "field" => "TextField",
3672
     *      "filter" => "PartialMatchFilter",
3673
     *      "title" => 'Organisation ZIP'
3674
     *    )
3675
     *  );
3676
     * </code>
3677
     * @config
3678
     */
3679
    private static $searchable_fields = null;
3680
3681
    /**
3682
     * User defined labels for searchable_fields, used to override
3683
     * default display in the search form.
3684
     * @config
3685
     */
3686
    private static $field_labels = [];
3687
3688
    /**
3689
     * Provides a default list of fields to be used by a 'summary'
3690
     * view of this object.
3691
     * @config
3692
     */
3693
    private static $summary_fields = [];
3694
3695
    public function provideI18nEntities()
3696
    {
3697
        // Note: see http://guides.rubyonrails.org/i18n.html#pluralization for rules
3698
        // Best guess for a/an rule. Better guesses require overriding in subclasses
3699
        $pluralName = $this->plural_name();
3700
        $singularName = $this->singular_name();
3701
        $conjunction = preg_match('/^[aeiou]/i', $singularName) ? 'An ' : 'A ';
3702
        return [
3703
            static::class . '.SINGULARNAME' => $this->singular_name(),
3704
            static::class . '.PLURALNAME' => $pluralName,
3705
            static::class . '.PLURALS' => [
3706
                'one' => $conjunction . $singularName,
3707
                'other' => '{count} ' . $pluralName
3708
            ]
3709
        ];
3710
    }
3711
3712
    /**
3713
     * Returns true if the given method/parameter has a value
3714
     * (Uses the DBField::hasValue if the parameter is a database field)
3715
     *
3716
     * @param string $field The field name
3717
     * @param array $arguments
3718
     * @param bool $cache
3719
     * @return boolean
3720
     */
3721
    public function hasValue($field, $arguments = null, $cache = true)
3722
    {
3723
        // has_one fields should not use dbObject to check if a value is given
3724
        $hasOne = static::getSchema()->hasOneComponent(static::class, $field);
3725
        if (!$hasOne && ($obj = $this->dbObject($field))) {
3726
            return $obj->exists();
3727
        } else {
3728
            return parent::hasValue($field, $arguments, $cache);
3729
        }
3730
    }
3731
3732
    /**
3733
     * If selected through a many_many through relation, this is the instance of the joined record
3734
     *
3735
     * @return DataObject
3736
     */
3737
    public function getJoin()
3738
    {
3739
        return $this->joinRecord;
3740
    }
3741
3742
    /**
3743
     * Set joining object
3744
     *
3745
     * @param DataObject $object
3746
     * @param string $alias Alias
3747
     * @return $this
3748
     */
3749
    public function setJoin(DataObject $object, $alias = null)
3750
    {
3751
        $this->joinRecord = $object;
3752
        if ($alias) {
3753
            if (static::getSchema()->fieldSpec(static::class, $alias)) {
3754
                throw new InvalidArgumentException(
3755
                    "Joined record $alias cannot also be a db field"
3756
                );
3757
            }
3758
            $this->record[$alias] = $object;
3759
        }
3760
        return $this;
3761
    }
3762
3763
    /**
3764
     * Find objects in the given relationships, merging them into the given list
3765
     *
3766
     * @param string $source Config property to extract relationships from
3767
     * @param bool $recursive True if recursive
3768
     * @param ArrayList $list If specified, items will be added to this list. If not, a new
3769
     * instance of ArrayList will be constructed and returned
3770
     * @return ArrayList The list of related objects
3771
     */
3772
    public function findRelatedObjects($source, $recursive = true, $list = null)
3773
    {
3774
        if (!$list) {
3775
            $list = new ArrayList();
3776
        }
3777
3778
        // Skip search for unsaved records
3779
        if (!$this->isInDB()) {
3780
            return $list;
3781
        }
3782
3783
        $relationships = $this->config()->get($source) ?: [];
3784
        foreach ($relationships as $relationship) {
3785
            // Warn if invalid config
3786
            if (!$this->hasMethod($relationship)) {
3787
                trigger_error(sprintf(
3788
                    "Invalid %s config value \"%s\" on object on class \"%s\"",
3789
                    $source,
3790
                    $relationship,
3791
                    get_class($this)
3792
                ), E_USER_WARNING);
3793
                continue;
3794
            }
3795
3796
            // Inspect value of this relationship
3797
            $items = $this->{$relationship}();
3798
3799
            // Merge any new item
3800
            $newItems = $this->mergeRelatedObjects($list, $items);
3801
3802
            // Recurse if necessary
3803
            if ($recursive) {
3804
                foreach ($newItems as $item) {
3805
                    /** @var DataObject $item */
3806
                    $item->findRelatedObjects($source, true, $list);
3807
                }
3808
            }
3809
        }
3810
        return $list;
3811
    }
3812
3813
    /**
3814
     * Helper method to merge owned/owning items into a list.
3815
     * Items already present in the list will be skipped.
3816
     *
3817
     * @param ArrayList $list Items to merge into
3818
     * @param mixed $items List of new items to merge
3819
     * @return ArrayList List of all newly added items that did not already exist in $list
3820
     */
3821
    public function mergeRelatedObjects($list, $items)
3822
    {
3823
        $added = new ArrayList();
3824
        if (!$items) {
3825
            return $added;
3826
        }
3827
        if ($items instanceof DataObject) {
3828
            $items = [$items];
3829
        }
3830
3831
        /** @var DataObject $item */
3832
        foreach ($items as $item) {
3833
            $this->mergeRelatedObject($list, $added, $item);
3834
        }
3835
        return $added;
3836
    }
3837
3838
    /**
3839
     * Merge single object into a list, but ensures that existing objects are not
3840
     * re-added.
3841
     *
3842
     * @param ArrayList $list Global list
3843
     * @param ArrayList $added Additional list to insert into
3844
     * @param DataObject $item Item to add
3845
     */
3846
    protected function mergeRelatedObject($list, $added, $item)
3847
    {
3848
        // Identify item
3849
        $itemKey = get_class($item) . '/' . $item->ID;
3850
3851
        // Write if saved, versioned, and not already added
3852
        if ($item->isInDB() && !isset($list[$itemKey])) {
3853
            $list[$itemKey] = $item;
3854
            $added[$itemKey] = $item;
3855
        }
3856
3857
        // Add joined record (from many_many through) automatically
3858
        $joined = $item->getJoin();
3859
        if ($joined) {
0 ignored issues
show
introduced by
$joined is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
3860
            $this->mergeRelatedObject($list, $added, $joined);
3861
        }
3862
    }
3863
}
3864