DataObject   F
last analyzed

Complexity

Total Complexity 568

Size/Duplication

Total Lines 4126
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1391
c 1
b 0
f 0
dl 0
loc 4126
rs 0.8
wmc 568

122 Methods

Rating   Name   Duplication   Size   Complexity  
A toMap() 0 4 1
A i18n_singular_name() 0 3 1
A getQueriedDatabaseFields() 0 3 1
A i18n_plural_name() 0 3 1
A exists() 0 3 2
A data() 0 3 1
A findCascadeDeletes() 0 4 1
A onAfterDelete() 0 3 1
A onAfterWrite() 0 4 1
A doValidate() 0 4 1
A updateChanges() 0 10 3
A validateWrite() 0 17 4
A onBeforeDelete() 0 11 2
B populateDefaults() 0 36 11
A onBeforeWrite() 0 6 1
A preWrite() 0 15 3
A validate() 0 5 1
B update() 0 50 10
D merge() 0 71 21
A castedUpdate() 0 6 2
A forceChange() 0 15 3
B duplicate() 0 38 9
C __construct() 0 75 15
A destroy() 0 3 1
A getSchema() 0 3 1
A duplicateManyManyRelation() 0 19 4
A setClassName() 0 10 3
B defineMethods() 0 27 10
A duplicateHasManyRelation() 0 13 2
A newClassInstance() 0 19 3
A getTitle() 0 11 3
A duplicateBelongsToRelation() 0 14 2
A getObsoleteClassName() 0 7 2
A singular_name() 0 10 2
A duplicateHasOneRelation() 0 10 2
A duplicateManyManyRelations() 0 14 5
B duplicateRelations() 0 29 6
A isEmpty() 0 18 5
A i18n_pluralise() 0 7 1
A getClassName() 0 7 2
A plural_name() 0 11 3
B writeManipulation() 0 37 9
A writeBaseRecord() 0 14 2
C prepareManipulationTable() 0 53 12
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
A skipWriteComponents() 0 31 5
B getRelationClass() 0 29 7
A hasField() 0 9 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 54 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
C get() 0 43 15
B getManyManyComponents() 0 59 7
A beforeUpdateCMSFields() 0 3 1
F setField() 0 80 22
A getField() 0 25 6
A getRelationType() 0 11 4
B relObject() 0 29 11
B setComponent() 0 37 10
C searchableFields() 0 76 16
A get_by_id() 0 15 4
A disable_subclass_access() 0 3 1
A setSourceQueryParam() 0 3 1
A getInheritableQueryParams() 0 5 1
C inferReciprocalComponent() 0 91 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
C requireTable() 0 93 11
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
C write() 0 62 12
A relField() 0 18 4
A baseTable() 0 3 1
A canView() 0 7 2
A debug() 0 11 3
A delete_by_id() 0 7 2
B getComponents() 0 38 6
A hasOne() 0 3 1
A getSourceQueryParam() 0 6 2
A scaffoldFormFields() 0 23 1
A writeComponents() 0 24 6
A castingHelper() 0 19 4
A writeRelations() 0 12 4
A requireDefaultRecords() 0 18 4
C scaffoldSearchFields() 0 63 14
B get_one() 0 33 9
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
D getChangedFields() 0 58 19
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\Core\ClassInfo;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Core\Resettable;
13
use SilverStripe\Dev\Debug;
14
use SilverStripe\Dev\Deprecation;
15
use SilverStripe\Forms\FieldList;
16
use SilverStripe\Forms\FormField;
17
use SilverStripe\Forms\FormScaffolder;
18
use SilverStripe\i18n\i18n;
19
use SilverStripe\i18n\i18nEntityProvider;
20
use SilverStripe\ORM\Connect\MySQLSchemaManager;
21
use SilverStripe\ORM\FieldType\DBClassName;
22
use SilverStripe\ORM\FieldType\DBEnum;
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 $Title
104
 * @property string $ClassName Class name of the DataObject
105
 * @property string $LastEdited Date and time of DataObject's last modification.
106
 * @property string $Created Date and time of DataObject creation.
107
 * @property string $ObsoleteClassName If ClassName no longer exists this will be set to the legacy value
108
 */
109
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable
110
{
111
112
    /**
113
     * Human-readable singular name.
114
     * @var string
115
     * @config
116
     */
117
    private static $singular_name = null;
118
119
    /**
120
     * Human-readable plural name
121
     * @var string
122
     * @config
123
     */
124
    private static $plural_name = null;
125
126
    /**
127
     * Allow API access to this object?
128
     * @todo Define the options that can be set here
129
     * @config
130
     */
131
    private static $api_access = false;
132
133
    /**
134
     * Allows specification of a default value for the ClassName field.
135
     * Configure this value only in subclasses of DataObject.
136
     *
137
     * @config
138
     * @var string
139
     */
140
    private static $default_classname = null;
141
142
    /**
143
     * @deprecated 4.0.0:5.0.0
144
     * @var bool
145
     */
146
    public $destroyed = false;
147
148
    /**
149
     * Data stored in this objects database record. An array indexed by fieldname.
150
     *
151
     * Use {@link toMap()} if you want an array representation
152
     * of this object, as the $record array might contain lazy loaded field aliases.
153
     *
154
     * @var array
155
     */
156
    protected $record;
157
158
    /**
159
     * If selected through a many_many through relation, this is the instance of the through record
160
     *
161
     * @var DataObject
162
     */
163
    protected $joinRecord;
164
165
    /**
166
     * Represents a field that hasn't changed (before === after, thus before == after)
167
     */
168
    const CHANGE_NONE = 0;
169
170
    /**
171
     * Represents a field that has changed type, although not the loosely defined value.
172
     * (before !== after && before == after)
173
     * E.g. change 1 to true or "true" to true, but not true to 0.
174
     * Value changes are by nature also considered strict changes.
175
     */
176
    const CHANGE_STRICT = 1;
177
178
    /**
179
     * Represents a field that has changed the loosely defined value
180
     * (before != after, thus, before !== after))
181
     * E.g. change false to true, but not false to 0
182
     */
183
    const CHANGE_VALUE = 2;
184
185
    /**
186
     * An array indexed by fieldname, true if the field has been changed.
187
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
188
     * the changed state.
189
     *
190
     * @var array
191
     */
192
    private $changed = [];
193
194
    /**
195
     * A flag to indicate that a "strict" change of the entire record been forced
196
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
197
     * the changed state.
198
     *
199
     * @var boolean
200
     */
201
    private $changeForced = false;
202
203
    /**
204
     * The database record (in the same format as $record), before
205
     * any changes.
206
     * @var array
207
     */
208
    protected $original = [];
209
210
    /**
211
     * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
212
     * @var boolean
213
     */
214
    protected $brokenOnDelete = false;
215
216
    /**
217
     * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
218
     * @var boolean
219
     */
220
    protected $brokenOnWrite = false;
221
222
    /**
223
     * Should dataobjects be validated before they are written?
224
     *
225
     * Caution: Validation can contain safeguards against invalid/malicious data,
226
     * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
227
     * to only disable validation for very specific use cases.
228
     *
229
     * @config
230
     * @var boolean
231
     */
232
    private static $validation_enabled = true;
233
234
    /**
235
     * Static caches used by relevant functions.
236
     *
237
     * @var array
238
     */
239
    protected static $_cache_get_one;
240
241
    /**
242
     * Cache of field labels
243
     *
244
     * @var array
245
     */
246
    protected static $_cache_field_labels = array();
247
248
    /**
249
     * Base fields which are not defined in static $db
250
     *
251
     * @config
252
     * @var array
253
     */
254
    private static $fixed_fields = array(
255
        'ID' => 'PrimaryKey',
256
        'ClassName' => 'DBClassName',
257
        'LastEdited' => 'DBDatetime',
258
        'Created' => 'DBDatetime',
259
    );
260
261
    /**
262
     * Override table name for this class. If ignored will default to FQN of class.
263
     * This option is not inheritable, and must be set on each class.
264
     * If left blank naming will default to the legacy (3.x) behaviour.
265
     *
266
     * @var string
267
     */
268
    private static $table_name = null;
269
270
    /**
271
     * Non-static relationship cache, indexed by component name.
272
     *
273
     * @var DataObject[]
274
     */
275
    protected $components = [];
276
277
    /**
278
     * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
279
     *
280
     * @var UnsavedRelationList[]
281
     */
282
    protected $unsavedRelations;
283
284
    /**
285
     * List of relations that should be cascade deleted, similar to `owns`
286
     * Note: This will trigger delete on many_many objects, not only the mapping table.
287
     * For many_many through you can specify the components you want to delete separately
288
     * (many_many or has_many sub-component)
289
     *
290
     * @config
291
     * @var array
292
     */
293
    private static $cascade_deletes = [];
294
295
    /**
296
     * List of relations that should be cascade duplicate.
297
     * many_many duplications are shallow only.
298
     *
299
     * Note: If duplicating a many_many through you should refer to the
300
     * has_many intermediary relation instead, otherwise extra fields
301
     * will be omitted from the duplicated relation.
302
     *
303
     * @var array
304
     */
305
    private static $cascade_duplicates = [];
306
307
    /**
308
     * Get schema object
309
     *
310
     * @return DataObjectSchema
311
     */
312
    public static function getSchema()
313
    {
314
        return Injector::inst()->get(DataObjectSchema::class);
315
    }
316
317
    /**
318
     * Construct a new DataObject.
319
     *
320
     * @param array|null $record Used internally for rehydrating an object from database content.
321
     *                           Bypasses setters on this class, and hence should not be used
322
     *                           for populating data on new records.
323
     * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
324
     *                             Singletons don't have their defaults set.
325
     * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
326
     */
327
    public function __construct($record = null, $isSingleton = false, $queryParams = array())
328
    {
329
        parent::__construct();
330
331
        // Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
332
        $this->setSourceQueryParams($queryParams);
333
334
        // Set the fields data.
335
        if (!$record) {
336
            $record = array(
337
                'ID' => 0,
338
                'ClassName' => static::class,
339
                'RecordClassName' => static::class
340
            );
341
        }
342
343
        if ($record instanceof stdClass) {
0 ignored issues
show
introduced by
$record is never a sub-type of stdClass.
Loading history...
344
            $record = (array)$record;
345
        }
346
347
        if (!is_array($record)) {
0 ignored issues
show
introduced by
The condition is_array($record) is always true.
Loading history...
348
            if (is_object($record)) {
349
                $passed = "an object of type '" . get_class($record) . "'";
350
            } else {
351
                $passed = "The value '$record'";
352
            }
353
354
            user_error(
355
                "DataObject::__construct passed $passed.  It's supposed to be passed an array,"
356
                . " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
357
                E_USER_WARNING
358
            );
359
            $record = null;
360
        }
361
362
        // Set $this->record to $record, but ignore NULLs
363
        $this->record = array();
364
        foreach ($record as $k => $v) {
365
            // Ensure that ID is stored as a number and not a string
366
            // To do: this kind of clean-up should be done on all numeric fields, in some relatively
367
            // performant manner
368
            if ($v !== null) {
369
                if ($k == 'ID' && is_numeric($v)) {
370
                    $this->record[$k] = (int)$v;
371
                } else {
372
                    $this->record[$k] = $v;
373
                }
374
            }
375
        }
376
377
        // Identify fields that should be lazy loaded, but only on existing records
378
        if (!empty($record['ID'])) {
379
            // Get all field specs scoped to class for later lazy loading
380
            $fields = static::getSchema()->fieldSpecs(
381
                static::class,
382
                DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
383
            );
384
            foreach ($fields as $field => $fieldSpec) {
385
                $fieldClass = strtok($fieldSpec, ".");
386
                if (!array_key_exists($field, $record)) {
387
                    $this->record[$field . '_Lazy'] = $fieldClass;
388
                }
389
            }
390
        }
391
392
        $this->original = $this->record;
393
394
        // Must be called after parent constructor
395
        if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
396
            $this->populateDefaults();
397
        }
398
399
        // prevent populateDefaults() and setField() from marking overwritten defaults as changed
400
        $this->changed = [];
401
        $this->changeForced = false;
402
    }
403
404
    /**
405
     * Destroy all of this objects dependant objects and local caches.
406
     * You'll need to call this to get the memory of an object that has components or extensions freed.
407
     */
408
    public function destroy()
409
    {
410
        $this->flushCache(false);
411
    }
412
413
    /**
414
     * Create a duplicate of this node. Can duplicate many_many relations
415
     *
416
     * @param bool $doWrite Perform a write() operation before returning the object.
417
     * If this is true, it will create the duplicate in the database.
418
     * @param array|null|false $relations List of relations to duplicate.
419
     * Will default to `cascade_duplicates` if null.
420
     * Set to 'false' to force none.
421
     * Set to specific array of names to duplicate to override these.
422
     * Note: If using versioned, this will additionally failover to `owns` config.
423
     * @return static A duplicate of this node. The exact type will be the type of this node.
424
     */
425
    public function duplicate($doWrite = true, $relations = null)
426
    {
427
        // Handle legacy behaviour
428
        if (is_string($relations) || $relations === true) {
0 ignored issues
show
introduced by
The condition $relations === true is always false.
Loading history...
429
            if ($relations === true) {
430
                $relations = 'many_many';
431
            }
432
            Deprecation::notice('5.0', 'Use cascade_duplicates config instead of providing a string to duplicate()');
433
            $relations = array_keys($this->config()->get($relations)) ?: [];
434
        }
435
436
        // Get duplicates
437
        if ($relations === null) {
438
            $relations = $this->config()->get('cascade_duplicates');
439
            // Remove any duplicate entries before duplicating them
440
            if (is_array($relations)) {
441
                $relations = array_unique($relations);
442
            }
443
        }
444
445
        // Create unsaved raw duplicate
446
        $map = $this->toMap();
447
        unset($map['Created']);
448
        /** @var static $clone */
449
        $clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams());
450
        $clone->ID = 0;
451
452
        // Note: Extensions such as versioned may update $relations here
453
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $relations);
454
        if ($relations) {
455
            $this->duplicateRelations($this, $clone, $relations);
456
        }
457
        if ($doWrite) {
458
            $clone->write();
459
        }
460
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $relations);
461
462
        return $clone;
463
    }
464
465
    /**
466
     * Copies the given relations from this object to the destination
467
     *
468
     * @param DataObject $sourceObject the source object to duplicate from
469
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
470
     * @param array $relations List of relations
471
     */
472
    protected function duplicateRelations($sourceObject, $destinationObject, $relations)
473
    {
474
        // Get list of duplicable relation types
475
        $manyMany = $sourceObject->manyMany();
476
        $hasMany = $sourceObject->hasMany();
477
        $hasOne = $sourceObject->hasOne();
478
        $belongsTo = $sourceObject->belongsTo();
479
480
        // Duplicate each relation based on type
481
        foreach ($relations as $relation) {
482
            switch (true) {
483
                case array_key_exists($relation, $manyMany):
484
                    $this->duplicateManyManyRelation($sourceObject, $destinationObject, $relation);
485
                    break;
486
                case array_key_exists($relation, $hasMany):
487
                    $this->duplicateHasManyRelation($sourceObject, $destinationObject, $relation);
488
                    break;
489
                case array_key_exists($relation, $hasOne):
490
                    $this->duplicateHasOneRelation($sourceObject, $destinationObject, $relation);
491
                    break;
492
                case array_key_exists($relation, $belongsTo):
493
                    $this->duplicateBelongsToRelation($sourceObject, $destinationObject, $relation);
494
                    break;
495
                default:
496
                    $sourceType = get_class($sourceObject);
497
                    throw new InvalidArgumentException(
498
                        "Cannot duplicate unknown relation {$relation} on parent type {$sourceType}"
499
                    );
500
                    break;
501
            }
502
        }
503
    }
504
505
    /**
506
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
507
     *
508
     * @deprecated 4.1.0:5.0.0 Use duplicateRelations() instead
509
     * @param DataObject $sourceObject the source object to duplicate from
510
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
511
     * @param bool|string $filter
512
     */
513
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
514
    {
515
        Deprecation::notice('5.0', 'Use duplicateRelations() instead');
516
517
        // Get list of relations to duplicate
518
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
519
            $relations = $sourceObject->config()->get($filter);
520
        } elseif ($filter === true) {
521
            $relations = $sourceObject->manyMany();
522
        } else {
523
            throw new InvalidArgumentException("Invalid many_many duplication filter");
524
        }
525
        foreach ($relations as $manyManyName => $type) {
526
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
527
        }
528
    }
529
530
    /**
531
     * Duplicates a single many_many relation from one object to another.
532
     *
533
     * @param DataObject $sourceObject
534
     * @param DataObject $destinationObject
535
     * @param string $relation
536
     */
537
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $relation)
538
    {
539
        // Copy all components from source to destination
540
        $source = $sourceObject->getManyManyComponents($relation);
541
        $dest = $destinationObject->getManyManyComponents($relation);
542
543
        if ($source instanceof ManyManyList) {
544
            $extraFieldNames = $source->getExtraFields();
545
        } else {
546
            $extraFieldNames = [];
547
        }
548
549
        foreach ($source as $item) {
550
            // Merge extra fields
551
            $extraFields = [];
552
            foreach ($extraFieldNames as $fieldName => $fieldType) {
553
                $extraFields[$fieldName] = $item->getField($fieldName);
554
            }
555
            $dest->add($item, $extraFields);
556
        }
557
    }
558
559
    /**
560
     * Duplicates a single many_many relation from one object to another.
561
     *
562
     * @param DataObject $sourceObject
563
     * @param DataObject $destinationObject
564
     * @param string $relation
565
     */
566
    protected function duplicateHasManyRelation($sourceObject, $destinationObject, $relation)
567
    {
568
        // Copy all components from source to destination
569
        $source = $sourceObject->getComponents($relation);
570
        $dest = $destinationObject->getComponents($relation);
571
572
        /** @var DataObject $item */
573
        foreach ($source as $item) {
574
            // Don't write on duplicate; Wait until ParentID is available later.
575
            // writeRelations() will eventually write these records when converting
576
            // from UnsavedRelationList
577
            $clonedItem = $item->duplicate(false);
578
            $dest->add($clonedItem);
579
        }
580
    }
581
582
    /**
583
     * Duplicates a single has_one relation from one object to another.
584
     * Note: Child object will be force written.
585
     *
586
     * @param DataObject $sourceObject
587
     * @param DataObject $destinationObject
588
     * @param string $relation
589
     */
590
    protected function duplicateHasOneRelation($sourceObject, $destinationObject, $relation)
591
    {
592
        // Check if original object exists
593
        $item = $sourceObject->getComponent($relation);
594
        if (!$item->isInDB()) {
595
            return;
596
        }
597
598
        $clonedItem = $item->duplicate(false);
599
        $destinationObject->setComponent($relation, $clonedItem);
600
    }
601
602
    /**
603
     * Duplicates a single belongs_to relation from one object to another.
604
     * Note: This will force a write on both parent / child objects.
605
     *
606
     * @param DataObject $sourceObject
607
     * @param DataObject $destinationObject
608
     * @param string $relation
609
     */
610
    protected function duplicateBelongsToRelation($sourceObject, $destinationObject, $relation)
611
    {
612
        // Check if original object exists
613
        $item = $sourceObject->getComponent($relation);
614
        if (!$item->isInDB()) {
615
            return;
616
        }
617
618
        $clonedItem = $item->duplicate(false);
619
        $destinationObject->setComponent($relation, $clonedItem);
620
        // After $clonedItem is assigned the appropriate FieldID / FieldClass, force write
621
        // @todo Write this component in onAfterWrite instead, assigning the FieldID then
622
        // https://github.com/silverstripe/silverstripe-framework/issues/7818
623
        $clonedItem->write();
624
    }
625
626
    /**
627
     * Return obsolete class name, if this is no longer a valid class
628
     *
629
     * @return string
630
     */
631
    public function getObsoleteClassName()
632
    {
633
        $className = $this->getField("ClassName");
634
        if (!ClassInfo::exists($className)) {
635
            return $className;
636
        }
637
        return null;
638
    }
639
640
    /**
641
     * Gets name of this class
642
     *
643
     * @return string
644
     */
645
    public function getClassName()
646
    {
647
        $className = $this->getField("ClassName");
648
        if (!ClassInfo::exists($className)) {
649
            return static::class;
650
        }
651
        return $className;
652
    }
653
654
    /**
655
     * Set the ClassName attribute. {@link $class} is also updated.
656
     * Warning: This will produce an inconsistent record, as the object
657
     * instance will not automatically switch to the new subclass.
658
     * Please use {@link newClassInstance()} for this purpose,
659
     * or destroy and reinstanciate the record.
660
     *
661
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
662
     * @return $this
663
     */
664
    public function setClassName($className)
665
    {
666
        $className = trim($className);
667
        if (!$className || !is_subclass_of($className, self::class)) {
668
            return $this;
669
        }
670
671
        $this->setField("ClassName", $className);
672
        $this->setField('RecordClassName', $className);
673
        return $this;
674
    }
675
676
    /**
677
     * Create a new instance of a different class from this object's record.
678
     * This is useful when dynamically changing the type of an instance. Specifically,
679
     * it ensures that the instance of the class is a match for the className of the
680
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
681
     * property manually before calling this method, as it will confuse change detection.
682
     *
683
     * If the new class is different to the original class, defaults are populated again
684
     * because this will only occur automatically on instantiation of a DataObject if
685
     * there is no record, or the record has no ID. In this case, we do have an ID but
686
     * we still need to repopulate the defaults.
687
     *
688
     * @param string $newClassName The name of the new class
689
     *
690
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
691
     */
692
    public function newClassInstance($newClassName)
693
    {
694
        if (!is_subclass_of($newClassName, self::class)) {
695
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
696
        }
697
698
        $originalClass = $this->ClassName;
699
700
        /** @var DataObject $newInstance */
701
        $newInstance = Injector::inst()->create($newClassName, $this->record, false);
702
703
        // Modify ClassName
704
        if ($newClassName != $originalClass) {
705
            $newInstance->setClassName($newClassName);
706
            $newInstance->populateDefaults();
707
            $newInstance->forceChange();
708
        }
709
710
        return $newInstance;
711
    }
712
713
    /**
714
     * Adds methods from the extensions.
715
     * Called by Object::__construct() once per class.
716
     */
717
    public function defineMethods()
718
    {
719
        parent::defineMethods();
720
721
        if (static::class === self::class) {
0 ignored issues
show
introduced by
The condition static::class === self::class is always true.
Loading history...
722
            return;
723
        }
724
725
        // Set up accessors for joined items
726
        if ($manyMany = $this->manyMany()) {
727
            foreach ($manyMany as $relationship => $class) {
728
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
729
            }
730
        }
731
        if ($hasMany = $this->hasMany()) {
732
            foreach ($hasMany as $relationship => $class) {
733
                $this->addWrapperMethod($relationship, 'getComponents');
734
            }
735
        }
736
        if ($hasOne = $this->hasOne()) {
737
            foreach ($hasOne as $relationship => $class) {
738
                $this->addWrapperMethod($relationship, 'getComponent');
739
            }
740
        }
741
        if ($belongsTo = $this->belongsTo()) {
742
            foreach (array_keys($belongsTo) as $relationship) {
743
                $this->addWrapperMethod($relationship, 'getComponent');
744
            }
745
        }
746
    }
747
748
    /**
749
     * Returns true if this object "exists", i.e., has a sensible value.
750
     * The default behaviour for a DataObject is to return true if
751
     * the object exists in the database, you can override this in subclasses.
752
     *
753
     * @return boolean true if this object exists
754
     */
755
    public function exists()
756
    {
757
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
758
    }
759
760
    /**
761
     * Returns TRUE if all values (other than "ID") are
762
     * considered empty (by weak boolean comparison).
763
     *
764
     * @return boolean
765
     */
766
    public function isEmpty()
767
    {
768
        $fixed = DataObject::config()->uninherited('fixed_fields');
769
        foreach ($this->toMap() as $field => $value) {
770
            // only look at custom fields
771
            if (isset($fixed[$field])) {
772
                continue;
773
            }
774
775
            $dbObject = $this->dbObject($field);
776
            if (!$dbObject) {
777
                continue;
778
            }
779
            if ($dbObject->exists()) {
780
                return false;
781
            }
782
        }
783
        return true;
784
    }
785
786
    /**
787
     * Pluralise this item given a specific count.
788
     *
789
     * E.g. "0 Pages", "1 File", "3 Images"
790
     *
791
     * @param string $count
792
     * @return string
793
     */
794
    public function i18n_pluralise($count)
795
    {
796
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
797
        return i18n::_t(
798
            static::class . '.PLURALS',
799
            $default,
800
            ['count' => $count]
801
        );
802
    }
803
804
    /**
805
     * Get the user friendly singular name of this DataObject.
806
     * If the name is not defined (by redefining $singular_name in the subclass),
807
     * this returns the class name.
808
     *
809
     * @return string User friendly singular name of this DataObject
810
     */
811
    public function singular_name()
812
    {
813
        $name = $this->config()->get('singular_name');
814
        if ($name) {
815
            return $name;
816
        }
817
        return ucwords(trim(strtolower(preg_replace(
818
            '/_?([A-Z])/',
819
            ' $1',
820
            ClassInfo::shortName($this)
821
        ))));
822
    }
823
824
    /**
825
     * Get the translated user friendly singular name of this DataObject
826
     * same as singular_name() but runs it through the translating function
827
     *
828
     * Translating string is in the form:
829
     *     $this->class.SINGULARNAME
830
     * Example:
831
     *     Page.SINGULARNAME
832
     *
833
     * @return string User friendly translated singular name of this DataObject
834
     */
835
    public function i18n_singular_name()
836
    {
837
        return _t(static::class . '.SINGULARNAME', $this->singular_name());
838
    }
839
840
    /**
841
     * Get the user friendly plural name of this DataObject
842
     * If the name is not defined (by renaming $plural_name in the subclass),
843
     * this returns a pluralised version of the class name.
844
     *
845
     * @return string User friendly plural name of this DataObject
846
     */
847
    public function plural_name()
848
    {
849
        if ($name = $this->config()->get('plural_name')) {
850
            return $name;
851
        }
852
        $name = $this->singular_name();
853
        //if the penultimate character is not a vowel, replace "y" with "ies"
854
        if (preg_match('/[^aeiou]y$/i', $name)) {
855
            $name = substr($name, 0, -1) . 'ie';
856
        }
857
        return ucfirst($name . 's');
858
    }
859
860
    /**
861
     * Get the translated user friendly plural name of this DataObject
862
     * Same as plural_name but runs it through the translation function
863
     * Translation string is in the form:
864
     *      $this->class.PLURALNAME
865
     * Example:
866
     *      Page.PLURALNAME
867
     *
868
     * @return string User friendly translated plural name of this DataObject
869
     */
870
    public function i18n_plural_name()
871
    {
872
        return _t(static::class . '.PLURALNAME', $this->plural_name());
873
    }
874
875
    /**
876
     * Standard implementation of a title/label for a specific
877
     * record. Tries to find properties 'Title' or 'Name',
878
     * and falls back to the 'ID'. Useful to provide
879
     * user-friendly identification of a record, e.g. in errormessages
880
     * or UI-selections.
881
     *
882
     * Overload this method to have a more specialized implementation,
883
     * e.g. for an Address record this could be:
884
     * <code>
885
     * function getTitle() {
886
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
887
     * }
888
     * </code>
889
     *
890
     * @return string
891
     */
892
    public function getTitle()
893
    {
894
        $schema = static::getSchema();
895
        if ($schema->fieldSpec($this, 'Title')) {
896
            return $this->getField('Title');
897
        }
898
        if ($schema->fieldSpec($this, 'Name')) {
899
            return $this->getField('Name');
900
        }
901
902
        return "#{$this->ID}";
903
    }
904
905
    /**
906
     * Returns the associated database record - in this case, the object itself.
907
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
908
     *
909
     * @return DataObject Associated database record
910
     */
911
    public function data()
912
    {
913
        return $this;
914
    }
915
916
    /**
917
     * Convert this object to a map.
918
     *
919
     * @return array The data as a map.
920
     */
921
    public function toMap()
922
    {
923
        $this->loadLazyFields();
924
        return $this->record;
925
    }
926
927
    /**
928
     * Return all currently fetched database fields.
929
     *
930
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
931
     * Obviously, this makes it a lot faster.
932
     *
933
     * @return array The data as a map.
934
     */
935
    public function getQueriedDatabaseFields()
936
    {
937
        return $this->record;
938
    }
939
940
    /**
941
     * Update a number of fields on this object, given a map of the desired changes.
942
     *
943
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
944
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
945
     *
946
     * Doesn't write the main object, but if you use the dot syntax, it will write()
947
     * the related objects that it alters.
948
     *
949
     * When using this method with user supplied data, it's very important to
950
     * whitelist the allowed keys.
951
     *
952
     * @param array $data A map of field name to data values to update.
953
     * @return DataObject $this
954
     */
955
    public function update($data)
956
    {
957
        foreach ($data as $key => $value) {
958
            // Implement dot syntax for updates
959
            if (strpos($key, '.') !== false) {
960
                $relations = explode('.', $key);
961
                $fieldName = array_pop($relations);
962
                /** @var static $relObj */
963
                $relObj = $this;
964
                $relation = null;
965
                foreach ($relations as $i => $relation) {
966
                    // no support for has_many or many_many relationships,
967
                    // as the updater wouldn't know which object to write to (or create)
968
                    if ($relObj->$relation() instanceof DataObject) {
969
                        $parentObj = $relObj;
970
                        $relObj = $relObj->$relation();
971
                        // If the intermediate relationship objects haven't been created, then write them
972
                        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...
973
                            $relObj->write();
974
                            $relatedFieldName = $relation . "ID";
975
                            $parentObj->$relatedFieldName = $relObj->ID;
976
                            $parentObj->write();
977
                        }
978
                    } else {
979
                        user_error(
980
                            "DataObject::update(): Can't traverse relationship '$relation'," .
981
                            "it has to be a has_one relationship or return a single DataObject",
982
                            E_USER_NOTICE
983
                        );
984
                        // unset relation object so we don't write properties to the wrong object
985
                        $relObj = null;
986
                        break;
987
                    }
988
                }
989
990
                if ($relObj) {
991
                    $relObj->$fieldName = $value;
992
                    $relObj->write();
993
                    $relatedFieldName = $relation . "ID";
994
                    $this->$relatedFieldName = $relObj->ID;
995
                    $relObj->flushCache();
996
                } else {
997
                    $class = static::class;
998
                    user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
999
                }
1000
            } else {
1001
                $this->$key = $value;
1002
            }
1003
        }
1004
        return $this;
1005
    }
1006
1007
    /**
1008
     * Pass changes as a map, and try to
1009
     * get automatic casting for these fields.
1010
     * Doesn't write to the database. To write the data,
1011
     * use the write() method.
1012
     *
1013
     * @param array $data A map of field name to data values to update.
1014
     * @return DataObject $this
1015
     */
1016
    public function castedUpdate($data)
1017
    {
1018
        foreach ($data as $k => $v) {
1019
            $this->setCastedField($k, $v);
1020
        }
1021
        return $this;
1022
    }
1023
1024
    /**
1025
     * Merges data and relations from another object of same class,
1026
     * without conflict resolution. Allows to specify which
1027
     * dataset takes priority in case its not empty.
1028
     * has_one-relations are just transferred with priority 'right'.
1029
     * has_many and many_many-relations are added regardless of priority.
1030
     *
1031
     * Caution: has_many/many_many relations are moved rather than duplicated,
1032
     * meaning they are not connected to the merged object any longer.
1033
     * Caution: Just saves updated has_many/many_many relations to the database,
1034
     * doesn't write the updated object itself (just writes the object-properties).
1035
     * Caution: Does not delete the merged object.
1036
     * Caution: Does now overwrite Created date on the original object.
1037
     *
1038
     * @param DataObject $rightObj
1039
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
1040
     * @param bool $includeRelations Merge any existing relations (optional)
1041
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
1042
     *                            Only applicable with $priority='right'. (optional)
1043
     * @return Boolean
1044
     */
1045
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
1046
    {
1047
        $leftObj = $this;
1048
1049
        if ($leftObj->ClassName != $rightObj->ClassName) {
1050
            // we can't merge similiar subclasses because they might have additional relations
1051
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
1052
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
1053
            return false;
1054
        }
1055
1056
        if (!$rightObj->ID) {
1057
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
1058
				to make sure all relations are transferred properly.').", E_USER_WARNING);
1059
            return false;
1060
        }
1061
1062
        // makes sure we don't merge data like ID or ClassName
1063
        $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
1064
        foreach ($rightData as $key => $rightSpec) {
1065
            // Don't merge ID
1066
            if ($key === 'ID') {
1067
                continue;
1068
            }
1069
1070
            // Only merge relations if allowed
1071
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
1072
                continue;
1073
            }
1074
1075
            // don't merge conflicting values if priority is 'left'
1076
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
1077
                continue;
1078
            }
1079
1080
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
1081
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
1082
                continue;
1083
            }
1084
1085
            // TODO remove redundant merge of has_one fields
1086
            $leftObj->{$key} = $rightObj->{$key};
1087
        }
1088
1089
        // merge relations
1090
        if ($includeRelations) {
1091
            if ($manyMany = $this->manyMany()) {
1092
                foreach ($manyMany as $relationship => $class) {
1093
                    /** @var DataObject $leftComponents */
1094
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
1095
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
1096
                    if ($rightComponents && $rightComponents->exists()) {
1097
                        $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

1097
                        $leftComponents->/** @scrutinizer ignore-call */ 
1098
                                         addMany($rightComponents->column('ID'));
Loading history...
1098
                    }
1099
                    $leftComponents->write();
1100
                }
1101
            }
1102
1103
            if ($hasMany = $this->hasMany()) {
1104
                foreach ($hasMany as $relationship => $class) {
1105
                    $leftComponents = $leftObj->getComponents($relationship);
1106
                    $rightComponents = $rightObj->getComponents($relationship);
1107
                    if ($rightComponents && $rightComponents->exists()) {
1108
                        $leftComponents->addMany($rightComponents->column('ID'));
1109
                    }
1110
                    $leftComponents->write();
0 ignored issues
show
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

1110
                    $leftComponents->/** @scrutinizer ignore-call */ 
1111
                                     write();
Loading history...
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

1110
                    $leftComponents->/** @scrutinizer ignore-call */ 
1111
                                     write();
Loading history...
1111
                }
1112
            }
1113
        }
1114
1115
        return true;
1116
    }
1117
1118
    /**
1119
     * Forces the record to think that all its data has changed.
1120
     * Doesn't write to the database. Force-change preseved until
1121
     * next write. Existing CHANGE_VALUE or CHANGE_STRICT values
1122
     * are preserved.
1123
     *
1124
     * @return $this
1125
     */
1126
    public function forceChange()
1127
    {
1128
        // Ensure lazy fields loaded
1129
        $this->loadLazyFields();
1130
1131
        // Populate the null values in record so that they actually get written
1132
        foreach (array_keys(static::getSchema()->fieldSpecs(static::class)) as $fieldName) {
1133
            if (!isset($this->record[$fieldName])) {
1134
                $this->record[$fieldName] = null;
1135
            }
1136
        }
1137
1138
        $this->changeForced = true;
1139
1140
        return $this;
1141
    }
1142
1143
    /**
1144
     * Validate the current object.
1145
     *
1146
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1147
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1148
     *
1149
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1150
     * and onAfterWrite() won't get called either.
1151
     *
1152
     * It is expected that you call validate() in your own application to test that an object is valid before
1153
     * attempting a write, and respond appropriately if it isn't.
1154
     *
1155
     * @see {@link ValidationResult}
1156
     * @return ValidationResult
1157
     */
1158
    public function validate()
1159
    {
1160
        $result = ValidationResult::create();
1161
        $this->extend('validate', $result);
1162
        return $result;
1163
    }
1164
1165
    /**
1166
     * Public accessor for {@see DataObject::validate()}
1167
     *
1168
     * @return ValidationResult
1169
     */
1170
    public function doValidate()
1171
    {
1172
        Deprecation::notice('5.0', 'Use validate');
1173
        return $this->validate();
1174
    }
1175
1176
    /**
1177
     * Event handler called before writing to the database.
1178
     * You can overload this to clean up or otherwise process data before writing it to the
1179
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1180
     *
1181
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1182
     *
1183
     * @uses DataExtension->onBeforeWrite()
1184
     */
1185
    protected function onBeforeWrite()
1186
    {
1187
        $this->brokenOnWrite = false;
1188
1189
        $dummy = null;
1190
        $this->extend('onBeforeWrite', $dummy);
1191
    }
1192
1193
    /**
1194
     * Event handler called after writing to the database.
1195
     * You can overload this to act upon changes made to the data after it is written.
1196
     * $this->changed will have a record
1197
     * database.  Don't forget to call parent::onAfterWrite(), though!
1198
     *
1199
     * @uses DataExtension->onAfterWrite()
1200
     */
1201
    protected function onAfterWrite()
1202
    {
1203
        $dummy = null;
1204
        $this->extend('onAfterWrite', $dummy);
1205
    }
1206
1207
    /**
1208
     * Find all objects that will be cascade deleted if this object is deleted
1209
     *
1210
     * Notes:
1211
     *   - If this object is versioned, objects will only be searched in the same stage as the given record.
1212
     *   - This will only be useful prior to deletion, as post-deletion this record will no longer exist.
1213
     *
1214
     * @param bool $recursive True if recursive
1215
     * @param ArrayList $list Optional list to add items to
1216
     * @return ArrayList list of objects
1217
     */
1218
    public function findCascadeDeletes($recursive = true, $list = null)
1219
    {
1220
        // Find objects in these relationships
1221
        return $this->findRelatedObjects('cascade_deletes', $recursive, $list);
1222
    }
1223
1224
    /**
1225
     * Event handler called before deleting from the database.
1226
     * You can overload this to clean up or otherwise process data before delete this
1227
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1228
     *
1229
     * @uses DataExtension->onBeforeDelete()
1230
     */
1231
    protected function onBeforeDelete()
1232
    {
1233
        $this->brokenOnDelete = false;
1234
1235
        $dummy = null;
1236
        $this->extend('onBeforeDelete', $dummy);
1237
1238
        // Cascade deletes
1239
        $deletes = $this->findCascadeDeletes(false);
1240
        foreach ($deletes as $delete) {
1241
            $delete->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on SilverStripe\View\ArrayData. 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

1241
            $delete->/** @scrutinizer ignore-call */ 
1242
                     delete();
Loading history...
1242
        }
1243
    }
1244
1245
    protected function onAfterDelete()
1246
    {
1247
        $this->extend('onAfterDelete');
1248
    }
1249
1250
    /**
1251
     * Load the default values in from the self::$defaults array.
1252
     * Will traverse the defaults of the current class and all its parent classes.
1253
     * Called by the constructor when creating new records.
1254
     *
1255
     * @uses DataExtension->populateDefaults()
1256
     * @return DataObject $this
1257
     */
1258
    public function populateDefaults()
1259
    {
1260
        $classes = array_reverse(ClassInfo::ancestry($this));
1261
1262
        foreach ($classes as $class) {
1263
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1264
1265
            if ($defaults && !is_array($defaults)) {
1266
                user_error(
1267
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1268
                    E_USER_WARNING
1269
                );
1270
                $defaults = null;
1271
            }
1272
1273
            if ($defaults) {
1274
                foreach ($defaults as $fieldName => $fieldValue) {
1275
                    // SRM 2007-03-06: Stricter check
1276
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1277
                        $this->$fieldName = $fieldValue;
1278
                    }
1279
                    // Set many-many defaults with an array of ids
1280
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1281
                        /** @var ManyManyList $manyManyJoin */
1282
                        $manyManyJoin = $this->$fieldName();
1283
                        $manyManyJoin->setByIDList($fieldValue);
1284
                    }
1285
                }
1286
            }
1287
            if ($class == self::class) {
1288
                break;
1289
            }
1290
        }
1291
1292
        $this->extend('populateDefaults');
1293
        return $this;
1294
    }
1295
1296
    /**
1297
     * Determine validation of this object prior to write
1298
     *
1299
     * @return ValidationException Exception generated by this write, or null if valid
1300
     */
1301
    protected function validateWrite()
1302
    {
1303
        if ($this->ObsoleteClassName) {
1304
            return new ValidationException(
1305
                "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - " .
1306
                "you need to change the ClassName before you can write it"
1307
            );
1308
        }
1309
1310
        // Note: Validation can only be disabled at the global level, not per-model
1311
        if (DataObject::config()->uninherited('validation_enabled')) {
1312
            $result = $this->validate();
1313
            if (!$result->isValid()) {
1314
                return new ValidationException($result);
1315
            }
1316
        }
1317
        return null;
1318
    }
1319
1320
    /**
1321
     * Prepare an object prior to write
1322
     *
1323
     * @throws ValidationException
1324
     */
1325
    protected function preWrite()
1326
    {
1327
        // Validate this object
1328
        if ($writeException = $this->validateWrite()) {
1329
            // Used by DODs to clean up after themselves, eg, Versioned
1330
            $this->invokeWithExtensions('onAfterSkippedWrite');
1331
            throw $writeException;
1332
        }
1333
1334
        // Check onBeforeWrite
1335
        $this->brokenOnWrite = true;
1336
        $this->onBeforeWrite();
1337
        if ($this->brokenOnWrite) {
1338
            user_error(static::class . " has a broken onBeforeWrite() function."
1339
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1340
        }
1341
    }
1342
1343
    /**
1344
     * Detects and updates all changes made to this object
1345
     *
1346
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1347
     * @return bool True if any changes are detected
1348
     */
1349
    protected function updateChanges($forceChanges = false)
1350
    {
1351
        if ($forceChanges) {
1352
            // Force changes, but only for loaded fields
1353
            foreach ($this->record as $field => $value) {
1354
                $this->changed[$field] = static::CHANGE_VALUE;
1355
            }
1356
            return true;
1357
        }
1358
        return $this->isChanged();
1359
    }
1360
1361
    /**
1362
     * Writes a subset of changes for a specific table to the given manipulation
1363
     *
1364
     * @param string $baseTable Base table
1365
     * @param string $now Timestamp to use for the current time
1366
     * @param bool $isNewRecord Whether this should be treated as a new record write
1367
     * @param array $manipulation Manipulation to write to
1368
     * @param string $class Class of table to manipulate
1369
     */
1370
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1371
    {
1372
        $schema = $this->getSchema();
1373
        $table = $schema->tableName($class);
1374
        $manipulation[$table] = array();
1375
1376
        $changed = $this->getChangedFields();
1377
1378
        // Extract records for this table
1379
        foreach ($this->record as $fieldName => $fieldValue) {
1380
            // we're not attempting to reset the BaseTable->ID
1381
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1382
            if (empty($changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1383
                continue;
1384
            }
1385
1386
            // Ensure this field pertains to this table
1387
            $specification = $schema->fieldSpec(
1388
                $class,
1389
                $fieldName,
1390
                DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
1391
            );
1392
            if (!$specification) {
1393
                continue;
1394
            }
1395
1396
            // if database column doesn't correlate to a DBField instance...
1397
            $fieldObj = $this->dbObject($fieldName);
1398
            if (!$fieldObj) {
1399
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1400
            }
1401
1402
            // Write to manipulation
1403
            $fieldObj->writeToManipulation($manipulation[$table]);
1404
        }
1405
1406
        // Ensure update of Created and LastEdited columns
1407
        if ($baseTable === $table) {
1408
            $manipulation[$table]['fields']['LastEdited'] = $now;
1409
            if ($isNewRecord) {
1410
                $manipulation[$table]['fields']['Created'] = empty($this->record['Created'])
1411
                    ? $now
1412
                    : $this->record['Created'];
1413
                $manipulation[$table]['fields']['ClassName'] = static::class;
1414
            }
1415
        }
1416
1417
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1418
        // attempt an update, as though it were a normal update.
1419
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1420
        $manipulation[$table]['class'] = $class;
1421
        if ($this->isInDB()) {
1422
            $manipulation[$table]['id'] = $this->record['ID'];
1423
        }
1424
    }
1425
1426
    /**
1427
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1428
     *
1429
     * Does nothing if an ID is already assigned for this record
1430
     *
1431
     * @param string $baseTable Base table
1432
     * @param string $now Timestamp to use for the current time
1433
     */
1434
    protected function writeBaseRecord($baseTable, $now)
1435
    {
1436
        // Generate new ID if not specified
1437
        if ($this->isInDB()) {
1438
            return;
1439
        }
1440
1441
        // Perform an insert on the base table
1442
        $manipulation = [];
1443
        $this->prepareManipulationTable($baseTable, $now, true, $manipulation, $this->baseClass());
1444
        DB::manipulate($manipulation);
1445
1446
        $this->changed['ID'] = self::CHANGE_VALUE;
1447
        $this->record['ID'] = DB::get_generated_id($baseTable);
1448
    }
1449
1450
    /**
1451
     * Generate and write the database manipulation for all changed fields
1452
     *
1453
     * @param string $baseTable Base table
1454
     * @param string $now Timestamp to use for the current time
1455
     * @param bool $isNewRecord If this is a new record
1456
     * @throws InvalidArgumentException
1457
     */
1458
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1459
    {
1460
        // Generate database manipulations for each class
1461
        $manipulation = array();
1462
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1463
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1464
        }
1465
1466
        // Allow extensions to extend this manipulation
1467
        $this->extend('augmentWrite', $manipulation);
1468
1469
        // New records have their insert into the base data table done first, so that they can pass the
1470
        // generated ID on to the rest of the manipulation
1471
        if ($isNewRecord) {
1472
            $manipulation[$baseTable]['command'] = 'update';
1473
        }
1474
1475
        // Make sure none of our field assignment are arrays
1476
        foreach ($manipulation as $tableManipulation) {
1477
            if (!isset($tableManipulation['fields'])) {
1478
                continue;
1479
            }
1480
            foreach ($tableManipulation['fields'] as $fieldName => $fieldValue) {
1481
                if (is_array($fieldValue)) {
1482
                    $dbObject = $this->dbObject($fieldName);
1483
                    // If the field allows non-scalar values we'll let it do dynamic assignments
1484
                    if ($dbObject && $dbObject->scalarValueOnly()) {
1485
                        throw new InvalidArgumentException(
1486
                            'DataObject::writeManipulation: parameterised field assignments are disallowed'
1487
                        );
1488
                    }
1489
                }
1490
            }
1491
        }
1492
1493
        // Perform the manipulation
1494
        DB::manipulate($manipulation);
1495
    }
1496
1497
    /**
1498
     * Writes all changes to this object to the database.
1499
     *  - It will insert a record whenever ID isn't set, otherwise update.
1500
     *  - All relevant tables will be updated.
1501
     *  - $this->onBeforeWrite() gets called beforehand.
1502
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1503
     *
1504
     * @uses DataExtension->augmentWrite()
1505
     *
1506
     * @param boolean       $showDebug Show debugging information
1507
     * @param boolean       $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1508
     * @param boolean       $forceWrite Write to database even if there are no changes
1509
     * @param boolean|array $writeComponents Call write() on all associated component instances which were previously
1510
     *                      retrieved through {@link getComponent()}, {@link getComponents()} or
1511
     *                      {@link getManyManyComponents()}. Default to `false`. The parameter can also be provided in
1512
     *                      the form of an array: `['recursive' => true, skip => ['Page'=>[1,2,3]]`. This avoid infinite
1513
     *                      loops when one DataObject are components of each other.
1514
     * @return int The ID of the record
1515
     * @throws ValidationException Exception that can be caught and handled by the calling function
1516
     */
1517
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1518
    {
1519
        $now = DBDatetime::now()->Rfc2822();
1520
1521
        // Execute pre-write tasks
1522
        $this->preWrite();
1523
1524
        // Check if we are doing an update or an insert
1525
        $isNewRecord = !$this->isInDB() || $forceInsert;
1526
1527
        // Check changes exist, abort if there are none
1528
        $hasChanges = $this->updateChanges($isNewRecord);
1529
        if ($hasChanges || $forceWrite || $isNewRecord) {
1530
            // Ensure Created and LastEdited are populated
1531
            if (!isset($this->record['Created'])) {
1532
                $this->record['Created'] = $now;
1533
            }
1534
            $this->record['LastEdited'] = $now;
1535
1536
            // New records have their insert into the base data table done first, so that they can pass the
1537
            // generated primary key on to the rest of the manipulation
1538
            $baseTable = $this->baseTable();
1539
            $this->writeBaseRecord($baseTable, $now);
1540
1541
            // Write the DB manipulation for all changed fields
1542
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1543
1544
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1545
            $this->writeRelations();
1546
            $this->onAfterWrite();
1547
1548
            // Reset isChanged data
1549
            // DBComposites properly bound to the parent record will also have their isChanged value reset
1550
            $this->changed = [];
1551
            $this->changeForced = false;
1552
            $this->original = $this->record;
1553
        } else {
1554
            if ($showDebug) {
1555
                Debug::message("no changes for DataObject");
1556
            }
1557
1558
            // Used by DODs to clean up after themselves, eg, Versioned
1559
            $this->invokeWithExtensions('onAfterSkippedWrite');
1560
        }
1561
1562
        // Write relations as necessary
1563
        if ($writeComponents) {
1564
            $recursive = true;
1565
            $skip = [];
1566
            if (is_array($writeComponents)) {
1567
                $recursive = isset($writeComponents['recursive']) && $writeComponents['recursive'];
1568
                $skip = isset($writeComponents['skip']) && is_array($writeComponents['skip'])
1569
                    ? $writeComponents['skip']
1570
                    : [];
1571
            }
1572
            $this->writeComponents($recursive, $skip);
1573
        }
1574
1575
        // Clears the cache for this object so get_one returns the correct object.
1576
        $this->flushCache();
1577
1578
        return $this->record['ID'];
1579
    }
1580
1581
    /**
1582
     * Writes cached relation lists to the database, if possible
1583
     */
1584
    public function writeRelations()
1585
    {
1586
        if (!$this->isInDB()) {
1587
            return;
1588
        }
1589
1590
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1591
        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...
1592
            foreach ($this->unsavedRelations as $name => $list) {
1593
                $list->changeToList($this->$name());
1594
            }
1595
            $this->unsavedRelations = array();
1596
        }
1597
    }
1598
1599
    /**
1600
     * Write the cached components to the database. Cached components could refer to two different instances of the
1601
     * same record.
1602
     *
1603
     * @param bool $recursive Recursively write components
1604
     * @param array $skip List of DataObject references to skip
1605
     * @return DataObject $this
1606
     */
1607
    public function writeComponents($recursive = false, $skip = [])
1608
    {
1609
        // Make sure we add our current object to the skip list
1610
        $this->skipWriteComponents($recursive, $this, $skip);
1611
1612
        // All our write calls have the same arguments ... just need make sure the skip list is pass by reference
1613
        $args = [
1614
            false, false, false,
1615
            $recursive ? ["recursive" => $recursive, "skip" => &$skip] : false
1616
        ];
1617
1618
        foreach ($this->components as $component) {
1619
            if (!$this->skipWriteComponents($recursive, $component, $skip)) {
1620
                $component->write(...$args);
0 ignored issues
show
Bug introduced by
It seems like $args can also be of type array<string,mixed|true>; however, parameter $showDebug of SilverStripe\ORM\DataObject::write() does only seem to accept boolean, 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

1620
                $component->write(/** @scrutinizer ignore-type */ ...$args);
Loading history...
1621
            }
1622
        }
1623
1624
        if ($join = $this->getJoin()) {
1625
            if (!$this->skipWriteComponents($recursive, $join, $skip)) {
1626
                $join->write(...$args);
1627
            }
1628
        }
1629
1630
        return $this;
1631
    }
1632
1633
    /**
1634
     * Check if target is in the skip list and add it if it isn't.
1635
     * @param bool $recursive
1636
     * @param DataObject $target
1637
     * @param array $skip
1638
     * @return bool Whether the target is already in the list
1639
     */
1640
    private function skipWriteComponents($recursive, DataObject $target, array &$skip)
1641
    {
1642
        // skip writing component if it doesn't exist
1643
        if (!$target->exists()) {
1644
            return true;
1645
        }
1646
1647
        // We only care about the skip list if our call is meant to be recursive
1648
        if (!$recursive) {
1649
            return false;
1650
        }
1651
1652
        // Get our Skip array keys
1653
        $classname = get_class($target);
1654
        $id = $target->ID;
1655
1656
        // Check if the target is in the skip list
1657
        if (isset($skip[$classname])) {
1658
            if (in_array($id, $skip[$classname])) {
1659
                // Skip the object
1660
                return true;
1661
            }
1662
        } else {
1663
            // This is the first object of this class
1664
            $skip[$classname] = [];
1665
        }
1666
1667
        // Add the target to our skip list
1668
        $skip[$classname][] = $id;
1669
1670
        return false;
1671
    }
1672
1673
    /**
1674
     * Delete this data object.
1675
     * $this->onBeforeDelete() gets called.
1676
     * Note that in Versioned objects, both Stage and Live will be deleted.
1677
     * @uses DataExtension->augmentSQL()
1678
     */
1679
    public function delete()
1680
    {
1681
        $this->brokenOnDelete = true;
1682
        $this->onBeforeDelete();
1683
        if ($this->brokenOnDelete) {
1684
            user_error(static::class . " has a broken onBeforeDelete() function."
1685
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1686
        }
1687
1688
        // Deleting a record without an ID shouldn't do anything
1689
        if (!$this->ID) {
1690
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1691
        }
1692
1693
        // TODO: This is quite ugly.  To improve:
1694
        //  - move the details of the delete code in the DataQuery system
1695
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1696
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1697
        $srcQuery = DataList::create(static::class)
1698
            ->filter('ID', $this->ID)
1699
            ->dataQuery()
1700
            ->query();
1701
        $queriedTables = $srcQuery->queriedTables();
1702
        $this->extend('updateDeleteTables', $queriedTables, $srcQuery);
1703
        foreach ($queriedTables as $table) {
1704
            $delete = SQLDelete::create("\"$table\"", array('"ID"' => $this->ID));
1705
            $this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
1706
            $delete->execute();
1707
        }
1708
        // Remove this item out of any caches
1709
        $this->flushCache();
1710
1711
        $this->onAfterDelete();
1712
1713
        $this->OldID = $this->ID;
1714
        $this->ID = 0;
1715
    }
1716
1717
    /**
1718
     * Delete the record with the given ID.
1719
     *
1720
     * @param string $className The class name of the record to be deleted
1721
     * @param int $id ID of record to be deleted
1722
     */
1723
    public static function delete_by_id($className, $id)
1724
    {
1725
        $obj = DataObject::get_by_id($className, $id);
1726
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1727
            $obj->delete();
1728
        } else {
1729
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1730
        }
1731
    }
1732
1733
    /**
1734
     * Get the class ancestry, including the current class name.
1735
     * The ancestry will be returned as an array of class names, where the 0th element
1736
     * will be the class that inherits directly from DataObject, and the last element
1737
     * will be the current class.
1738
     *
1739
     * @return array Class ancestry
1740
     */
1741
    public function getClassAncestry()
1742
    {
1743
        return ClassInfo::ancestry(static::class);
1744
    }
1745
1746
    /**
1747
     * Return a unary component object from a one to one relationship, as a DataObject.
1748
     * If no component is available, an 'empty component' will be returned for
1749
     * non-polymorphic relations, or for polymorphic relations with a class set.
1750
     *
1751
     * @param string $componentName Name of the component
1752
     * @return DataObject The component object. It's exact type will be that of the component.
1753
     * @throws Exception
1754
     */
1755
    public function getComponent($componentName)
1756
    {
1757
        if (isset($this->components[$componentName])) {
1758
            return $this->components[$componentName];
1759
        }
1760
1761
        $schema = static::getSchema();
1762
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1763
            $joinField = $componentName . 'ID';
1764
            $joinID = $this->getField($joinField);
1765
1766
            // Extract class name for polymorphic relations
1767
            if ($class === self::class) {
1768
                $class = $this->getField($componentName . 'Class');
1769
                if (empty($class)) {
1770
                    return null;
1771
                }
1772
            }
1773
1774
            if ($joinID) {
1775
                // Ensure that the selected object originates from the same stage, subsite, etc
1776
                $component = DataObject::get($class)
1777
                    ->filter('ID', $joinID)
1778
                    ->setDataQueryParam($this->getInheritableQueryParams())
1779
                    ->first();
1780
            }
1781
1782
            if (empty($component)) {
1783
                $component = Injector::inst()->create($class);
1784
            }
1785
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1786
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1787
            $joinID = $this->ID;
1788
1789
            if ($joinID) {
1790
                // Prepare filter for appropriate join type
1791
                if ($polymorphic) {
1792
                    $filter = array(
1793
                        "{$joinField}ID" => $joinID,
1794
                        "{$joinField}Class" => static::class,
1795
                    );
1796
                } else {
1797
                    $filter = array(
1798
                        $joinField => $joinID
1799
                    );
1800
                }
1801
1802
                // Ensure that the selected object originates from the same stage, subsite, etc
1803
                $component = DataObject::get($class)
1804
                    ->filter($filter)
1805
                    ->setDataQueryParam($this->getInheritableQueryParams())
1806
                    ->first();
1807
            }
1808
1809
            if (empty($component)) {
1810
                $component = Injector::inst()->create($class);
1811
                if ($polymorphic) {
1812
                    $component->{$joinField . 'ID'} = $this->ID;
1813
                    $component->{$joinField . 'Class'} = static::class;
1814
                } else {
1815
                    $component->$joinField = $this->ID;
1816
                }
1817
            }
1818
        } else {
1819
            throw new InvalidArgumentException(
1820
                "DataObject->getComponent(): Could not find component '$componentName'."
1821
            );
1822
        }
1823
1824
        $this->components[$componentName] = $component;
1825
        return $component;
1826
    }
1827
1828
    /**
1829
     * Assign an item to the given component
1830
     *
1831
     * @param string $componentName
1832
     * @param DataObject|null $item
1833
     * @return $this
1834
     */
1835
    public function setComponent($componentName, $item)
1836
    {
1837
        // Validate component
1838
        $schema = static::getSchema();
1839
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1840
            // Force item to be written if not by this point
1841
            // @todo This could be lazy-written in a beforeWrite hook, but force write here for simplicity
1842
            // https://github.com/silverstripe/silverstripe-framework/issues/7818
1843
            if ($item && !$item->isInDB()) {
1844
                $item->write();
1845
            }
1846
1847
            // Update local ID
1848
            $joinField = $componentName . 'ID';
1849
            $this->setField($joinField, $item ? $item->ID : null);
1850
            // Update Class (Polymorphic has_one)
1851
            // Extract class name for polymorphic relations
1852
            if ($class === self::class) {
1853
                $this->setField($componentName . 'Class', $item ? get_class($item) : null);
1854
            }
1855
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
1856
            if ($item) {
1857
                // For belongs_to, add to has_one on other component
1858
                $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1859
                if (!$polymorphic) {
1860
                    $joinField = substr($joinField, 0, -2);
1861
                }
1862
                $item->setComponent($joinField, $this);
1863
            }
1864
        } else {
1865
            throw new InvalidArgumentException(
1866
                "DataObject->setComponent(): Could not find component '$componentName'."
1867
            );
1868
        }
1869
1870
        $this->components[$componentName] = $item;
1871
        return $this;
1872
    }
1873
1874
    /**
1875
     * Returns a one-to-many relation as a HasManyList
1876
     *
1877
     * @param string $componentName Name of the component
1878
     * @param int|array $id Optional ID(s) for parent of this relation, if not the current record
1879
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1880
     */
1881
    public function getComponents($componentName, $id = null)
1882
    {
1883
        if (!isset($id)) {
1884
            $id = $this->ID;
1885
        }
1886
        $result = null;
1887
1888
        $schema = $this->getSchema();
1889
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1890
        if (!$componentClass) {
1891
            throw new InvalidArgumentException(sprintf(
1892
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1893
                $componentName,
1894
                static::class
1895
            ));
1896
        }
1897
1898
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1899
        if (!$id) {
1900
            if (!isset($this->unsavedRelations[$componentName])) {
1901
                $this->unsavedRelations[$componentName] =
1902
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1903
            }
1904
            return $this->unsavedRelations[$componentName];
1905
        }
1906
1907
        // Determine type and nature of foreign relation
1908
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1909
        /** @var HasManyList $result */
1910
        if ($polymorphic) {
1911
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
1912
        } else {
1913
            $result = HasManyList::create($componentClass, $joinField);
1914
        }
1915
1916
        return $result
1917
            ->setDataQueryParam($this->getInheritableQueryParams())
1918
            ->forForeignID($id);
1919
    }
1920
1921
    /**
1922
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1923
     *
1924
     * @param string $relationName Relation name.
1925
     * @return string Class name, or null if not found.
1926
     */
1927
    public function getRelationClass($relationName)
1928
    {
1929
        // Parse many_many
1930
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1931
        if ($manyManyComponent) {
1932
            return $manyManyComponent['childClass'];
1933
        }
1934
1935
        // Go through all relationship configuration fields.
1936
        $config = $this->config();
1937
        $candidates = array_merge(
1938
            ($relations = $config->get('has_one')) ? $relations : array(),
1939
            ($relations = $config->get('has_many')) ? $relations : array(),
1940
            ($relations = $config->get('belongs_to')) ? $relations : array()
1941
        );
1942
1943
        if (isset($candidates[$relationName])) {
1944
            $remoteClass = $candidates[$relationName];
1945
1946
            // If dot notation is present, extract just the first part that contains the class.
1947
            if (($fieldPos = strpos($remoteClass, '.')) !== false) {
1948
                return substr($remoteClass, 0, $fieldPos);
1949
            }
1950
1951
            // Otherwise just return the class
1952
            return $remoteClass;
1953
        }
1954
1955
        return null;
1956
    }
1957
1958
    /**
1959
     * Given a relation name, determine the relation type
1960
     *
1961
     * @param string $component Name of component
1962
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1963
     */
1964
    public function getRelationType($component)
1965
    {
1966
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1967
        $config = $this->config();
1968
        foreach ($types as $type) {
1969
            $relations = $config->get($type);
1970
            if ($relations && isset($relations[$component])) {
1971
                return $type;
1972
            }
1973
        }
1974
        return null;
1975
    }
1976
1977
    /**
1978
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1979
     * side of the relation.
1980
     *
1981
     * Notes on behaviour:
1982
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1983
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1984
     *  - Polymorphic relationships do not have two natural endpoints (only on one side)
1985
     *   and thus attempting to infer them will return nothing.
1986
     *  - Cannot be used on unsaved objects.
1987
     *
1988
     * @param string $remoteClass
1989
     * @param string $remoteRelation
1990
     * @return DataList|DataObject The component, either as a list or single object
1991
     * @throws BadMethodCallException
1992
     * @throws InvalidArgumentException
1993
     */
1994
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1995
    {
1996
        $remote = DataObject::singleton($remoteClass);
1997
        $class = $remote->getRelationClass($remoteRelation);
1998
        $schema = static::getSchema();
1999
2000
        // Validate arguments
2001
        if (!$this->isInDB()) {
2002
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
2003
        }
2004
        if (empty($class)) {
2005
            throw new InvalidArgumentException(sprintf(
2006
                "%s invoked with invalid relation %s.%s",
2007
                __METHOD__,
2008
                $remoteClass,
2009
                $remoteRelation
2010
            ));
2011
        }
2012
        // If relation is polymorphic, do not infer recriprocal relationship
2013
        if ($class === self::class) {
2014
            return null;
2015
        }
2016
        if (!is_a($this, $class, true)) {
2017
            throw new InvalidArgumentException(sprintf(
2018
                "Relation %s on %s does not refer to objects of type %s",
2019
                $remoteRelation,
2020
                $remoteClass,
2021
                static::class
2022
            ));
2023
        }
2024
2025
        // Check the relation type to mock
2026
        $relationType = $remote->getRelationType($remoteRelation);
2027
        switch ($relationType) {
2028
            case 'has_one':
2029
                // Mock has_many
2030
                $joinField = "{$remoteRelation}ID";
2031
                $componentClass = $schema->classForField($remoteClass, $joinField);
2032
                $result = HasManyList::create($componentClass, $joinField);
2033
                return $result
2034
                    ->setDataQueryParam($this->getInheritableQueryParams())
2035
                    ->forForeignID($this->ID);
2036
            case 'belongs_to':
2037
            case 'has_many':
2038
                // These relations must have a has_one on the other end, so find it
2039
                $joinField = $schema->getRemoteJoinField(
2040
                    $remoteClass,
2041
                    $remoteRelation,
2042
                    $relationType,
2043
                    $polymorphic
2044
                );
2045
                // If relation is polymorphic, do not infer recriprocal relationship automatically
2046
                if ($polymorphic) {
2047
                    return null;
2048
                }
2049
                $joinID = $this->getField($joinField);
2050
                if (empty($joinID)) {
2051
                    return null;
2052
                }
2053
                // Get object by joined ID
2054
                return DataObject::get($remoteClass)
2055
                    ->filter('ID', $joinID)
2056
                    ->setDataQueryParam($this->getInheritableQueryParams())
2057
                    ->first();
2058
            case 'many_many':
2059
            case 'belongs_many_many':
2060
                // Get components and extra fields from parent
2061
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
2062
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
2063
2064
                // Reverse parent and component fields and create an inverse ManyManyList
2065
                /** @var RelationList $result */
2066
                $result = Injector::inst()->create(
2067
                    $manyMany['relationClass'],
2068
                    $manyMany['parentClass'], // Substitute parent class for dataClass
2069
                    $manyMany['join'],
2070
                    $manyMany['parentField'], // Reversed parent / child field
2071
                    $manyMany['childField'], // Reversed parent / child field
2072
                    $extraFields,
2073
                    $manyMany['childClass'], // substitute child class for parentClass
2074
                    $remoteClass // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2075
                );
2076
                $this->extend('updateManyManyComponents', $result);
2077
2078
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2079
                // foreignID set elsewhere.
2080
                return $result
2081
                    ->setDataQueryParam($this->getInheritableQueryParams())
2082
                    ->forForeignID($this->ID);
2083
            default:
2084
                return null;
2085
        }
2086
    }
2087
2088
    /**
2089
     * Returns a many-to-many component, as a ManyManyList.
2090
     * @param string $componentName Name of the many-many component
2091
     * @param int|array $id Optional ID for parent of this relation, if not the current record
2092
     * @return ManyManyList|UnsavedRelationList The set of components
2093
     */
2094
    public function getManyManyComponents($componentName, $id = null)
2095
    {
2096
        if (!isset($id)) {
2097
            $id = $this->ID;
2098
        }
2099
        $schema = static::getSchema();
2100
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
2101
        if (!$manyManyComponent) {
2102
            throw new InvalidArgumentException(sprintf(
2103
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
2104
                $componentName,
2105
                static::class
2106
            ));
2107
        }
2108
2109
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
2110
        if (!$id) {
2111
            if (!isset($this->unsavedRelations[$componentName])) {
2112
                $this->unsavedRelations[$componentName] = new UnsavedRelationList(
2113
                    $manyManyComponent['parentClass'],
2114
                    $componentName,
2115
                    $manyManyComponent['childClass']
2116
                );
2117
            }
2118
            return $this->unsavedRelations[$componentName];
2119
        }
2120
2121
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
2122
        /** @var RelationList $result */
2123
        $result = Injector::inst()->create(
2124
            $manyManyComponent['relationClass'],
2125
            $manyManyComponent['childClass'],
2126
            $manyManyComponent['join'],
2127
            $manyManyComponent['childField'],
2128
            $manyManyComponent['parentField'],
2129
            $extraFields,
2130
            $manyManyComponent['parentClass'],
2131
            static::class // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2132
        );
2133
2134
        // Store component data in query meta-data
2135
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
2136
            /** @var DataQuery $query */
2137
            $query->setQueryParam('Component.ExtraFields', $extraFields);
2138
        });
2139
2140
        // If we have a default sort set for our "join" then we should overwrite any default already set.
2141
        $joinSort = Config::inst()->get($manyManyComponent['join'], 'default_sort');
2142
        if (!empty($joinSort)) {
2143
            $result = $result->sort($joinSort);
2144
        }
2145
2146
        $this->extend('updateManyManyComponents', $result);
2147
2148
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2149
        // foreignID set elsewhere.
2150
        return $result
2151
            ->setDataQueryParam($this->getInheritableQueryParams())
2152
            ->forForeignID($id);
2153
    }
2154
2155
    /**
2156
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
2157
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
2158
     *
2159
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
2160
     *                          their classes.
2161
     */
2162
    public function hasOne()
2163
    {
2164
        return (array)$this->config()->get('has_one');
2165
    }
2166
2167
    /**
2168
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
2169
     * their class name will be returned.
2170
     *
2171
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2172
     *        the field data stripped off. It defaults to TRUE.
2173
     * @return string|array
2174
     */
2175
    public function belongsTo($classOnly = true)
2176
    {
2177
        $belongsTo = (array)$this->config()->get('belongs_to');
2178
        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...
2179
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
2180
        } else {
2181
            return $belongsTo ? $belongsTo : array();
2182
        }
2183
    }
2184
2185
    /**
2186
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2187
     * relationships and their classes will be returned.
2188
     *
2189
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2190
     *        the field data stripped off. It defaults to TRUE.
2191
     * @return string|array|false
2192
     */
2193
    public function hasMany($classOnly = true)
2194
    {
2195
        $hasMany = (array)$this->config()->get('has_many');
2196
        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...
2197
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2198
        } else {
2199
            return $hasMany ? $hasMany : array();
2200
        }
2201
    }
2202
2203
    /**
2204
     * Return the many-to-many extra fields specification.
2205
     *
2206
     * If you don't specify a component name, it returns all
2207
     * extra fields for all components available.
2208
     *
2209
     * @return array|null
2210
     */
2211
    public function manyManyExtraFields()
2212
    {
2213
        return $this->config()->get('many_many_extraFields');
2214
    }
2215
2216
    /**
2217
     * Return information about a many-to-many component.
2218
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2219
     * components are returned.
2220
     *
2221
     * @see DataObjectSchema::manyManyComponent()
2222
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2223
     */
2224
    public function manyMany()
2225
    {
2226
        $config = $this->config();
2227
        $manyManys = (array)$config->get('many_many');
2228
        $belongsManyManys = (array)$config->get('belongs_many_many');
2229
        $items = array_merge($manyManys, $belongsManyManys);
2230
        return $items;
2231
    }
2232
2233
    /**
2234
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
2235
     *
2236
     * This is experimental, and is currently only a Postgres-specific enhancement.
2237
     *
2238
     * @param string $class
2239
     * @return array|false
2240
     */
2241
    public function database_extensions($class)
2242
    {
2243
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2244
        if ($extensions) {
2245
            return $extensions;
2246
        } else {
2247
            return false;
2248
        }
2249
    }
2250
2251
    /**
2252
     * Generates a SearchContext to be used for building and processing
2253
     * a generic search form for properties on this object.
2254
     *
2255
     * @return SearchContext
2256
     */
2257
    public function getDefaultSearchContext()
2258
    {
2259
        return SearchContext::create(
2260
            static::class,
2261
            $this->scaffoldSearchFields(),
2262
            $this->defaultSearchFilters()
2263
        );
2264
    }
2265
2266
    /**
2267
     * Determine which properties on the DataObject are
2268
     * searchable, and map them to their default {@link FormField}
2269
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2270
     *
2271
     * Some additional logic is included for switching field labels, based on
2272
     * how generic or specific the field type is.
2273
     *
2274
     * Used by {@link SearchContext}.
2275
     *
2276
     * @param array $_params
2277
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2278
     *   'restrictFields': Numeric array of a field name whitelist
2279
     * @return FieldList
2280
     */
2281
    public function scaffoldSearchFields($_params = null)
2282
    {
2283
        $params = array_merge(
2284
            array(
2285
                'fieldClasses' => false,
2286
                'restrictFields' => false
2287
            ),
2288
            (array)$_params
2289
        );
2290
        $fields = new FieldList();
2291
        foreach ($this->searchableFields() as $fieldName => $spec) {
2292
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
2293
                continue;
2294
            }
2295
2296
            // If a custom fieldclass is provided as a string, use it
2297
            $field = null;
2298
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2299
                $fieldClass = $params['fieldClasses'][$fieldName];
2300
                $field = new $fieldClass($fieldName);
2301
            // If we explicitly set a field, then construct that
2302
            } elseif (isset($spec['field'])) {
2303
                // If it's a string, use it as a class name and construct
2304
                if (is_string($spec['field'])) {
2305
                    $fieldClass = $spec['field'];
2306
                    $field = new $fieldClass($fieldName);
2307
2308
                // If it's a FormField object, then just use that object directly.
2309
                } elseif ($spec['field'] instanceof FormField) {
2310
                    $field = $spec['field'];
2311
2312
                // Otherwise we have a bug
2313
                } else {
2314
                    user_error("Bad value for searchable_fields, 'field' value: "
2315
                        . var_export($spec['field'], true), E_USER_WARNING);
2316
                }
2317
2318
            // Otherwise, use the database field's scaffolder
2319
            } elseif ($object = $this->relObject($fieldName)) {
2320
                if (is_object($object) && $object->hasMethod('scaffoldSearchField')) {
2321
                    $field = $object->scaffoldSearchField();
2322
                } else {
2323
                    throw new Exception(sprintf(
2324
                        "SearchField '%s' on '%s' does not return a valid DBField instance.",
2325
                        $fieldName,
2326
                        get_class($this)
2327
                    ));
2328
                }
2329
            }
2330
2331
            // Allow fields to opt out of search
2332
            if (!$field) {
2333
                continue;
2334
            }
2335
2336
            if (strstr($fieldName, '.')) {
2337
                $field->setName(str_replace('.', '__', $fieldName));
2338
            }
2339
            $field->setTitle($spec['title']);
2340
2341
            $fields->push($field);
2342
        }
2343
        return $fields;
2344
    }
2345
2346
    /**
2347
     * Scaffold a simple edit form for all properties on this dataobject,
2348
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2349
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2350
     *
2351
     * @uses FormScaffolder
2352
     *
2353
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2354
     * @return FieldList
2355
     */
2356
    public function scaffoldFormFields($_params = null)
2357
    {
2358
        $params = array_merge(
2359
            array(
2360
                'tabbed' => false,
2361
                'includeRelations' => false,
2362
                'restrictFields' => false,
2363
                'fieldClasses' => false,
2364
                'ajaxSafe' => false
2365
            ),
2366
            (array)$_params
2367
        );
2368
2369
        $fs = FormScaffolder::create($this);
2370
        $fs->tabbed = $params['tabbed'];
2371
        $fs->includeRelations = $params['includeRelations'];
2372
        $fs->restrictFields = $params['restrictFields'];
2373
        $fs->fieldClasses = $params['fieldClasses'];
2374
        $fs->ajaxSafe = $params['ajaxSafe'];
2375
2376
        $this->extend('updateFormScaffolder', $fs, $this);
2377
2378
        return $fs->getFieldList();
2379
    }
2380
2381
    /**
2382
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2383
     * being called on extensions
2384
     *
2385
     * @param callable $callback The callback to execute
2386
     */
2387
    protected function beforeUpdateCMSFields($callback)
2388
    {
2389
        $this->beforeExtending('updateCMSFields', $callback);
2390
    }
2391
2392
    /**
2393
     * Centerpiece of every data administration interface in Silverstripe,
2394
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2395
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2396
     * generate this set. To customize, overload this method in a subclass
2397
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2398
     *
2399
     * <code>
2400
     * class MyCustomClass extends DataObject {
2401
     *  static $db = array('CustomProperty'=>'Boolean');
2402
     *
2403
     *  function getCMSFields() {
2404
     *    $fields = parent::getCMSFields();
2405
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2406
     *    return $fields;
2407
     *  }
2408
     * }
2409
     * </code>
2410
     *
2411
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2412
     *
2413
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2414
     */
2415
    public function getCMSFields()
2416
    {
2417
        $tabbedFields = $this->scaffoldFormFields(array(
2418
            // Don't allow has_many/many_many relationship editing before the record is first saved
2419
            'includeRelations' => ($this->ID > 0),
2420
            'tabbed' => true,
2421
            'ajaxSafe' => true
2422
        ));
2423
2424
        $this->extend('updateCMSFields', $tabbedFields);
2425
2426
        return $tabbedFields;
2427
    }
2428
2429
    /**
2430
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2431
     * including that dataobject's extensions customised actions could be added to the EditForm.
2432
     *
2433
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2434
     */
2435
    public function getCMSActions()
2436
    {
2437
        $actions = new FieldList();
2438
        $this->extend('updateCMSActions', $actions);
2439
        return $actions;
2440
    }
2441
2442
2443
    /**
2444
     * Used for simple frontend forms without relation editing
2445
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2446
     * by default. To customize, either overload this method in your
2447
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2448
     *
2449
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2450
     *
2451
     * @param array $params See {@link scaffoldFormFields()}
2452
     * @return FieldList Always returns a simple field collection without TabSet.
2453
     */
2454
    public function getFrontEndFields($params = null)
2455
    {
2456
        $untabbedFields = $this->scaffoldFormFields($params);
2457
        $this->extend('updateFrontEndFields', $untabbedFields);
2458
2459
        return $untabbedFields;
2460
    }
2461
2462
    public function getViewerTemplates($suffix = '')
2463
    {
2464
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2465
    }
2466
2467
    /**
2468
     * Gets the value of a field.
2469
     * Called by {@link __get()} and any getFieldName() methods you might create.
2470
     *
2471
     * @param string $field The name of the field
2472
     * @return mixed The field value
2473
     */
2474
    public function getField($field)
2475
    {
2476
        // If we already have a value in $this->record, then we should just return that
2477
        if (isset($this->record[$field])) {
2478
            return $this->record[$field];
2479
        }
2480
2481
        // Do we have a field that needs to be lazy loaded?
2482
        if (isset($this->record[$field . '_Lazy'])) {
2483
            $tableClass = $this->record[$field . '_Lazy'];
2484
            $this->loadLazyFields($tableClass);
2485
        }
2486
        $schema = static::getSchema();
2487
2488
        // Support unary relations as fields
2489
        if ($schema->unaryComponent(static::class, $field)) {
2490
            return $this->getComponent($field);
2491
        }
2492
2493
        // In case of complex fields, return the DBField object
2494
        if ($schema->compositeField(static::class, $field)) {
2495
            $this->record[$field] = $this->dbObject($field);
2496
        }
2497
2498
        return isset($this->record[$field]) ? $this->record[$field] : null;
2499
    }
2500
2501
    /**
2502
     * Loads all the stub fields that an initial lazy load didn't load fully.
2503
     *
2504
     * @param string $class Class to load the values from. Others are joined as required.
2505
     * Not specifying a tableClass will load all lazy fields from all tables.
2506
     * @return bool Flag if lazy loading succeeded
2507
     */
2508
    protected function loadLazyFields($class = null)
2509
    {
2510
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2511
            return false;
2512
        }
2513
2514
        if (!$class) {
2515
            $loaded = array();
2516
2517
            foreach ($this->record as $key => $value) {
2518
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2519
                    $this->loadLazyFields($value);
2520
                    $loaded[$value] = $value;
2521
                }
2522
            }
2523
2524
            return false;
2525
        }
2526
2527
        $dataQuery = new DataQuery($class);
2528
2529
        // Reset query parameter context to that of this DataObject
2530
        if ($params = $this->getSourceQueryParams()) {
2531
            foreach ($params as $key => $value) {
2532
                $dataQuery->setQueryParam($key, $value);
2533
            }
2534
        }
2535
2536
        // Limit query to the current record, unless it has the Versioned extension,
2537
        // in which case it requires special handling through augmentLoadLazyFields()
2538
        $schema = static::getSchema();
2539
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2540
        $dataQuery->where([
2541
            $baseIDColumn => $this->record['ID']
2542
        ])->limit(1);
2543
2544
        $columns = array();
2545
2546
        // Add SQL for fields, both simple & multi-value
2547
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2548
        $databaseFields = $schema->databaseFields($class, false);
2549
        foreach ($databaseFields as $k => $v) {
2550
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2551
                $columns[] = $k;
2552
            }
2553
        }
2554
2555
        if ($columns) {
2556
            $query = $dataQuery->query();
2557
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2558
            $this->extend('augmentSQL', $query, $dataQuery);
2559
2560
            $dataQuery->setQueriedColumns($columns);
2561
            $newData = $dataQuery->execute()->record();
2562
2563
            // Load the data into record
2564
            if ($newData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $newData 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...
2565
                foreach ($newData as $k => $v) {
2566
                    if (in_array($k, $columns)) {
2567
                        $this->record[$k] = $v;
2568
                        $this->original[$k] = $v;
2569
                        unset($this->record[$k . '_Lazy']);
2570
                    }
2571
                }
2572
2573
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2574
            } else {
2575
                foreach ($columns as $k) {
2576
                    $this->record[$k] = null;
2577
                    $this->original[$k] = null;
2578
                    unset($this->record[$k . '_Lazy']);
2579
                }
2580
            }
2581
        }
2582
        return true;
2583
    }
2584
2585
    /**
2586
     * Return the fields that have changed since the last write.
2587
     *
2588
     * The change level affects what the functions defines as "changed":
2589
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2590
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2591
     *   for example a change from 0 to null would not be included.
2592
     *
2593
     * Example return:
2594
     * <code>
2595
     * array(
2596
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2597
     * )
2598
     * </code>
2599
     *
2600
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2601
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2602
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2603
     * @return array
2604
     */
2605
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2606
    {
2607
        $changedFields = array();
2608
2609
        // Update the changed array with references to changed obj-fields
2610
        foreach ($this->record as $k => $v) {
2611
            // Prevents DBComposite infinite looping on isChanged
2612
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2613
                continue;
2614
            }
2615
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2616
                $this->changed[$k] = self::CHANGE_VALUE;
2617
            }
2618
        }
2619
2620
        // If change was forced, then derive change data from $this->record
2621
        if ($this->changeForced && $changeLevel <= self::CHANGE_STRICT) {
2622
            $changed = array_combine(
2623
                array_keys($this->record),
2624
                array_fill(0, count($this->record), self::CHANGE_STRICT)
2625
            );
2626
            // @todo Find better way to allow versioned to write a new version after forceChange
2627
            unset($changed['Version']);
2628
        } else {
2629
            $changed = $this->changed;
2630
        }
2631
2632
        if (is_array($databaseFieldsOnly)) {
2633
            $fields = array_intersect_key($changed, array_flip($databaseFieldsOnly));
0 ignored issues
show
Bug introduced by
It seems like $changed can also be of type false; however, parameter $array1 of array_intersect_key() 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

2633
            $fields = array_intersect_key(/** @scrutinizer ignore-type */ $changed, array_flip($databaseFieldsOnly));
Loading history...
2634
        } elseif ($databaseFieldsOnly) {
2635
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2636
            $fields = array_intersect_key($changed, $fieldsSpecs);
2637
        } else {
2638
            $fields = $changed;
2639
        }
2640
2641
        // Filter the list to those of a certain change level
2642
        if ($changeLevel > self::CHANGE_STRICT) {
2643
            if ($fields) {
2644
                foreach ($fields as $name => $level) {
2645
                    if ($level < $changeLevel) {
2646
                        unset($fields[$name]);
2647
                    }
2648
                }
2649
            }
2650
        }
2651
2652
        if ($fields) {
2653
            foreach ($fields as $name => $level) {
2654
                $changedFields[$name] = array(
2655
                    'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2656
                    'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2657
                    'level' => $level
2658
                );
2659
            }
2660
        }
2661
2662
        return $changedFields;
2663
    }
2664
2665
    /**
2666
     * Uses {@link getChangedFields()} to determine if fields have been changed
2667
     * since loading them from the database.
2668
     *
2669
     * @param string $fieldName Name of the database field to check, will check for any if not given
2670
     * @param int $changeLevel See {@link getChangedFields()}
2671
     * @return boolean
2672
     */
2673
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2674
    {
2675
        $fields = $fieldName ? array($fieldName) : true;
2676
        $changed = $this->getChangedFields($fields, $changeLevel);
2677
        if (!isset($fieldName)) {
2678
            return !empty($changed);
2679
        } else {
2680
            return array_key_exists($fieldName, $changed);
2681
        }
2682
    }
2683
2684
    /**
2685
     * Set the value of the field
2686
     * Called by {@link __set()} and any setFieldName() methods you might create.
2687
     *
2688
     * @param string $fieldName Name of the field
2689
     * @param mixed $val New field value
2690
     * @return $this
2691
     */
2692
    public function setField($fieldName, $val)
2693
    {
2694
        $this->objCacheClear();
2695
        //if it's a has_one component, destroy the cache
2696
        if (substr($fieldName, -2) == 'ID') {
2697
            unset($this->components[substr($fieldName, 0, -2)]);
2698
        }
2699
2700
        // If we've just lazy-loaded the column, then we need to populate the $original array
2701
        if (isset($this->record[$fieldName . '_Lazy'])) {
2702
            $tableClass = $this->record[$fieldName . '_Lazy'];
2703
            $this->loadLazyFields($tableClass);
2704
        }
2705
2706
        // Support component assignent via field setter
2707
        $schema = static::getSchema();
2708
        if ($schema->unaryComponent(static::class, $fieldName)) {
2709
            unset($this->components[$fieldName]);
2710
            // Assign component directly
2711
            if (is_null($val) || $val instanceof DataObject) {
2712
                return $this->setComponent($fieldName, $val);
2713
            }
2714
            // Assign by ID instead of object
2715
            if (is_numeric($val)) {
2716
                $fieldName .= 'ID';
2717
            }
2718
        }
2719
2720
        // Situation 1: Passing an DBField
2721
        if ($val instanceof DBField) {
2722
            $val->setName($fieldName);
2723
            $val->saveInto($this);
2724
2725
            // Situation 1a: Composite fields should remain bound in case they are
2726
            // later referenced to update the parent dataobject
2727
            if ($val instanceof DBComposite) {
2728
                $val->bindTo($this);
2729
                $this->record[$fieldName] = $val;
2730
            }
2731
        // Situation 2: Passing a literal or non-DBField object
2732
        } else {
2733
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2734
            if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) {
2735
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2736
            }
2737
2738
            if (!empty($val) && !is_scalar($val)) {
2739
                $dbField = $this->dbObject($fieldName);
2740
                if ($dbField && $dbField->scalarValueOnly()) {
2741
                    throw new InvalidArgumentException(
2742
                        sprintf(
2743
                            'DataObject::setField: %s only accepts scalars',
2744
                            $fieldName
2745
                        )
2746
                    );
2747
                }
2748
            }
2749
2750
            // if a field is not existing or has strictly changed
2751
            if (!array_key_exists($fieldName, $this->original) || $this->original[$fieldName] !== $val) {
2752
                // TODO Add check for php-level defaults which are not set in the db
2753
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2754
                // At the very least, the type has changed
2755
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2756
2757
                if ((!array_key_exists($fieldName, $this->original) && $val)
2758
                    || (array_key_exists($fieldName, $this->original) && $this->original[$fieldName] != $val)
2759
                ) {
2760
                    // Value has changed as well, not just the type
2761
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2762
                }
2763
            // Value has been restored to its original, remove any record of the change
2764
            } elseif (isset($this->changed[$fieldName])) {
2765
                unset($this->changed[$fieldName]);
2766
            }
2767
2768
            // Value is saved regardless, since the change detection relates to the last write
2769
            $this->record[$fieldName] = $val;
2770
        }
2771
        return $this;
2772
    }
2773
2774
    /**
2775
     * Set the value of the field, using a casting object.
2776
     * This is useful when you aren't sure that a date is in SQL format, for example.
2777
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2778
     * can be saved into the Image table.
2779
     *
2780
     * @param string $fieldName Name of the field
2781
     * @param mixed $value New field value
2782
     * @return $this
2783
     */
2784
    public function setCastedField($fieldName, $value)
2785
    {
2786
        if (!$fieldName) {
2787
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2788
        }
2789
        $fieldObj = $this->dbObject($fieldName);
2790
        if ($fieldObj) {
0 ignored issues
show
introduced by
$fieldObj is of type SilverStripe\ORM\FieldType\DBField, thus it always evaluated to true.
Loading history...
2791
            $fieldObj->setValue($value);
2792
            $fieldObj->saveInto($this);
2793
        } else {
2794
            $this->$fieldName = $value;
2795
        }
2796
        return $this;
2797
    }
2798
2799
    /**
2800
     * {@inheritdoc}
2801
     */
2802
    public function castingHelper($field)
2803
    {
2804
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2805
        if ($fieldSpec) {
2806
            return $fieldSpec;
2807
        }
2808
2809
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2810
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2811
        $queryParams = $this->getSourceQueryParams();
2812
        if (!empty($queryParams['Component.ExtraFields'])) {
2813
            $extraFields = $queryParams['Component.ExtraFields'];
2814
2815
            if (isset($extraFields[$field])) {
2816
                return $extraFields[$field];
2817
            }
2818
        }
2819
2820
        return parent::castingHelper($field);
2821
    }
2822
2823
    /**
2824
     * Returns true if the given field exists in a database column on any of
2825
     * the objects tables and optionally look up a dynamic getter with
2826
     * get<fieldName>().
2827
     *
2828
     * @param string $field Name of the field
2829
     * @return boolean True if the given field exists
2830
     */
2831
    public function hasField($field)
2832
    {
2833
        $schema = static::getSchema();
2834
        return (
2835
            array_key_exists($field, $this->record)
2836
            || array_key_exists($field, $this->components)
2837
            || $schema->fieldSpec(static::class, $field)
2838
            || $schema->unaryComponent(static::class, $field)
2839
            || $this->hasMethod("get{$field}")
2840
        );
2841
    }
2842
2843
    /**
2844
     * Returns true if the given field exists as a database column
2845
     *
2846
     * @param string $field Name of the field
2847
     *
2848
     * @return boolean
2849
     */
2850
    public function hasDatabaseField($field)
2851
    {
2852
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2853
        return !empty($spec);
2854
    }
2855
2856
    /**
2857
     * Returns true if the member is allowed to do the given action.
2858
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2859
     *
2860
     * @param string $perm The permission to be checked, such as 'View'.
2861
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2862
     * in user.
2863
     * @param array $context Additional $context to pass to extendedCan()
2864
     *
2865
     * @return boolean True if the the member is allowed to do the given action
2866
     */
2867
    public function can($perm, $member = null, $context = array())
2868
    {
2869
        if (!$member) {
2870
            $member = Security::getCurrentUser();
2871
        }
2872
2873
        if ($member && Permission::checkMember($member, "ADMIN")) {
2874
            return true;
2875
        }
2876
2877
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2878
            $method = 'can' . ucfirst($perm);
2879
            return $this->$method($member);
2880
        }
2881
2882
        $results = $this->extendedCan('can', $member);
2883
        if (isset($results)) {
2884
            return $results;
2885
        }
2886
2887
        return ($member && Permission::checkMember($member, $perm));
2888
    }
2889
2890
    /**
2891
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2892
     * expected to return one of three values:
2893
     *
2894
     *  - false: Disallow this permission, regardless of what other extensions say
2895
     *  - true: Allow this permission, as long as no other extensions return false
2896
     *  - NULL: Don't affect the outcome
2897
     *
2898
     * This method itself returns a tri-state value, and is designed to be used like this:
2899
     *
2900
     * <code>
2901
     * $extended = $this->extendedCan('canDoSomething', $member);
2902
     * if($extended !== null) return $extended;
2903
     * else return $normalValue;
2904
     * </code>
2905
     *
2906
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2907
     * @param Member|int $member
2908
     * @param array $context Optional context
2909
     * @return boolean|null
2910
     */
2911
    public function extendedCan($methodName, $member, $context = array())
2912
    {
2913
        $results = $this->extend($methodName, $member, $context);
2914
        if ($results && is_array($results)) {
2915
            // Remove NULLs
2916
            $results = array_filter($results, function ($v) {
2917
                return !is_null($v);
2918
            });
2919
            // If there are any non-NULL responses, then return the lowest one of them.
2920
            // If any explicitly deny the permission, then we don't get access
2921
            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...
2922
                return min($results);
2923
            }
2924
        }
2925
        return null;
2926
    }
2927
2928
    /**
2929
     * @param Member $member
2930
     * @return boolean
2931
     */
2932
    public function canView($member = null)
2933
    {
2934
        $extended = $this->extendedCan(__FUNCTION__, $member);
2935
        if ($extended !== null) {
2936
            return $extended;
2937
        }
2938
        return Permission::check('ADMIN', 'any', $member);
2939
    }
2940
2941
    /**
2942
     * @param Member $member
2943
     * @return boolean
2944
     */
2945
    public function canEdit($member = null)
2946
    {
2947
        $extended = $this->extendedCan(__FUNCTION__, $member);
2948
        if ($extended !== null) {
2949
            return $extended;
2950
        }
2951
        return Permission::check('ADMIN', 'any', $member);
2952
    }
2953
2954
    /**
2955
     * @param Member $member
2956
     * @return boolean
2957
     */
2958
    public function canDelete($member = null)
2959
    {
2960
        $extended = $this->extendedCan(__FUNCTION__, $member);
2961
        if ($extended !== null) {
2962
            return $extended;
2963
        }
2964
        return Permission::check('ADMIN', 'any', $member);
2965
    }
2966
2967
    /**
2968
     * @param Member $member
2969
     * @param array $context Additional context-specific data which might
2970
     * affect whether (or where) this object could be created.
2971
     * @return boolean
2972
     */
2973
    public function canCreate($member = null, $context = array())
2974
    {
2975
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2976
        if ($extended !== null) {
2977
            return $extended;
2978
        }
2979
        return Permission::check('ADMIN', 'any', $member);
2980
    }
2981
2982
    /**
2983
     * Debugging used by Debug::show()
2984
     *
2985
     * @return string HTML data representing this object
2986
     */
2987
    public function debug()
2988
    {
2989
        $class = static::class;
2990
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2991
        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...
2992
            foreach ($this->record as $fieldName => $fieldVal) {
2993
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2994
            }
2995
        }
2996
        $val .= "</ul>\n";
2997
        return $val;
2998
    }
2999
3000
    /**
3001
     * Return the DBField object that represents the given field.
3002
     * This works similarly to obj() with 2 key differences:
3003
     *   - it still returns an object even when the field has no value.
3004
     *   - it only matches fields and not methods
3005
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
3006
     *
3007
     * @param string $fieldName Name of the field
3008
     * @return DBField The field as a DBField object
3009
     */
3010
    public function dbObject($fieldName)
3011
    {
3012
        // Check for field in DB
3013
        $schema = static::getSchema();
3014
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
3015
        if (!$helper) {
3016
            return null;
3017
        }
3018
3019
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
3020
            $tableClass = $this->record[$fieldName . '_Lazy'];
3021
            $this->loadLazyFields($tableClass);
3022
        }
3023
3024
        $value = isset($this->record[$fieldName])
3025
            ? $this->record[$fieldName]
3026
            : null;
3027
3028
        // If we have a DBField object in $this->record, then return that
3029
        if ($value instanceof DBField) {
3030
            return $value;
3031
        }
3032
3033
        list($class, $spec) = explode('.', $helper);
3034
        /** @var DBField $obj */
3035
        $table = $schema->tableName($class);
3036
        $obj = Injector::inst()->create($spec, $fieldName);
3037
        $obj->setTable($table);
3038
        $obj->setValue($value, $this, false);
3039
        return $obj;
3040
    }
3041
3042
    /**
3043
     * Traverses to a DBField referenced by relationships between data objects.
3044
     *
3045
     * The path to the related field is specified with dot separated syntax
3046
     * (eg: Parent.Child.Child.FieldName).
3047
     *
3048
     * If a relation is blank, this will return null instead.
3049
     * If a relation name is invalid (e.g. non-relation on a parent) this
3050
     * can throw a LogicException.
3051
     *
3052
     * @param string $fieldPath List of paths on this object. All items in this path
3053
     * must be ViewableData implementors
3054
     *
3055
     * @return mixed DBField of the field on the object or a DataList instance.
3056
     * @throws LogicException If accessing invalid relations
3057
     */
3058
    public function relObject($fieldPath)
3059
    {
3060
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
3061
        $component = $this;
3062
3063
        // Parse all relations
3064
        foreach (explode('.', $fieldPath) as $relation) {
3065
            if (!$component) {
3066
                return null;
3067
            }
3068
3069
            // Inspect relation type
3070
            if (ClassInfo::hasMethod($component, $relation)) {
3071
                $component = $component->$relation();
3072
            } elseif ($component instanceof Relation || $component instanceof DataList) {
3073
                // $relation could either be a field (aggregate), or another relation
3074
                $singleton = DataObject::singleton($component->dataClass());
0 ignored issues
show
Bug introduced by
The method dataClass() 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

3074
                $singleton = DataObject::singleton($component->/** @scrutinizer ignore-call */ dataClass());
Loading history...
3075
                $component = $singleton->dbObject($relation) ?: $component->relation($relation);
0 ignored issues
show
Bug introduced by
The method relation() 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

3075
                $component = $singleton->dbObject($relation) ?: $component->/** @scrutinizer ignore-call */ relation($relation);
Loading history...
3076
            } elseif ($component instanceof DataObject && ($dbObject = $component->dbObject($relation))) {
3077
                $component = $dbObject;
3078
            } elseif ($component instanceof ViewableData && $component->hasField($relation)) {
3079
                $component = $component->obj($relation);
3080
            } else {
3081
                throw new LogicException(
3082
                    "$relation is not a relation/field on " . get_class($component)
3083
                );
3084
            }
3085
        }
3086
        return $component;
3087
    }
3088
3089
    /**
3090
     * Traverses to a field referenced by relationships between data objects, returning the value
3091
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3092
     *
3093
     * @param string $fieldName string
3094
     * @return mixed Will return null on a missing value
3095
     */
3096
    public function relField($fieldName)
3097
    {
3098
        // Navigate to relative parent using relObject() if needed
3099
        $component = $this;
3100
        if (($pos = strrpos($fieldName, '.')) !== false) {
3101
            $relation = substr($fieldName, 0, $pos);
3102
            $fieldName = substr($fieldName, $pos + 1);
3103
            $component = $this->relObject($relation);
3104
        }
3105
3106
        // Bail if the component is null
3107
        if (!$component) {
3108
            return null;
3109
        }
3110
        if (ClassInfo::hasMethod($component, $fieldName)) {
3111
            return $component->$fieldName();
3112
        }
3113
        return $component->$fieldName;
3114
    }
3115
3116
    /**
3117
     * Temporary hack to return an association name, based on class, to get around the mangle
3118
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3119
     *
3120
     * @param string $className
3121
     * @return string
3122
     */
3123
    public function getReverseAssociation($className)
3124
    {
3125
        if (is_array($this->manyMany())) {
0 ignored issues
show
introduced by
The condition is_array($this->manyMany()) is always true.
Loading history...
3126
            $many_many = array_flip($this->manyMany());
3127
            if (array_key_exists($className, $many_many)) {
3128
                return $many_many[$className];
3129
            }
3130
        }
3131
        if (is_array($this->hasMany())) {
0 ignored issues
show
introduced by
The condition is_array($this->hasMany()) is always true.
Loading history...
3132
            $has_many = array_flip($this->hasMany());
3133
            if (array_key_exists($className, $has_many)) {
3134
                return $has_many[$className];
3135
            }
3136
        }
3137
        if (is_array($this->hasOne())) {
0 ignored issues
show
introduced by
The condition is_array($this->hasOne()) is always true.
Loading history...
3138
            $has_one = array_flip($this->hasOne());
3139
            if (array_key_exists($className, $has_one)) {
3140
                return $has_one[$className];
3141
            }
3142
        }
3143
3144
        return false;
3145
    }
3146
3147
    /**
3148
     * Return all objects matching the filter
3149
     * sub-classes are automatically selected and included
3150
     *
3151
     * @param string $callerClass The class of objects to be returned
3152
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3153
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3154
     * @param string|array $sort A sort expression to be inserted into the ORDER
3155
     * BY clause.  If omitted, self::$default_sort will be used.
3156
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3157
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3158
     * @param string $containerClass The container class to return the results in.
3159
     *
3160
     * @todo $containerClass is Ignored, why?
3161
     *
3162
     * @return DataList The objects matching the filter, in the class specified by $containerClass
3163
     */
3164
    public static function get(
3165
        $callerClass = null,
3166
        $filter = "",
3167
        $sort = "",
3168
        $join = "",
3169
        $limit = null,
3170
        $containerClass = DataList::class
3171
    ) {
3172
        // Validate arguments
3173
        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...
3174
            $callerClass = get_called_class();
3175
            if ($callerClass === self::class) {
3176
                throw new InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3177
            }
3178
            if ($filter || $sort || $join || $limit || ($containerClass !== DataList::class)) {
3179
                throw new InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3180
                    . ' arguments');
3181
            }
3182
        } elseif ($callerClass === self::class) {
3183
            throw new InvalidArgumentException('DataObject::get() cannot query non-subclass DataObject directly');
3184
        }
3185
        if ($join) {
3186
            throw new InvalidArgumentException(
3187
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3188
            );
3189
        }
3190
3191
        // Build and decorate with args
3192
        $result = DataList::create($callerClass);
3193
        if ($filter) {
3194
            $result = $result->where($filter);
3195
        }
3196
        if ($sort) {
3197
            $result = $result->sort($sort);
3198
        }
3199
        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

3199
        if ($limit && strpos(/** @scrutinizer ignore-type */ $limit, ',') !== false) {
Loading history...
3200
            $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

3200
            $limitArguments = explode(',', /** @scrutinizer ignore-type */ $limit);
Loading history...
3201
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
3202
        } elseif ($limit) {
3203
            $result = $result->limit($limit);
3204
        }
3205
3206
        return $result;
3207
    }
3208
3209
3210
    /**
3211
     * Return the first item matching the given query.
3212
     * All calls to get_one() are cached.
3213
     *
3214
     * The filter argument supports parameterised queries (see SQLSelect::addWhere() for syntax examples). Because
3215
     * of that (and differently from e.g. DataList::filter()) you need to manually escape the field names:
3216
     * <code>
3217
     * $member = DataObject::get_one('Member', [ '"FirstName"' => 'John' ]);
3218
     * </code>
3219
     *
3220
     * @param string|null  $callerClass The class of objects to be returned. Defaults to the class that calls the method
3221
     *                                  e.g. MyObject::get_one() will return a MyObject
3222
     * @param string|array $filter      A filter to be inserted into the WHERE clause.
3223
     *                                  Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3224
     * @param boolean      $cache       Use caching
3225
     * @param string       $orderBy     A sort expression to be inserted into the ORDER BY clause.
3226
     *
3227
     * @return DataObject|null The first item matching the query
3228
     */
3229
    public static function get_one($callerClass = null, $filter = "", $cache = true, $orderBy = "")
3230
    {
3231
        if ($callerClass === null) {
3232
            $callerClass = static::class;
3233
        }
3234
3235
        // Validate class
3236
        if ($callerClass === self::class) {
3237
            throw new InvalidArgumentException('DataObject::get_one() cannot query non-subclass DataObject directly');
3238
        }
3239
        $SNG = singleton($callerClass);
3240
3241
        $cacheComponents = array($filter, $orderBy, $SNG->extend('cacheKeyComponent'));
3242
        $cacheKey = md5(serialize($cacheComponents));
3243
3244
        $item = null;
3245
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3246
            $dl = DataObject::get($callerClass)->where($filter)->sort($orderBy);
3247
            $item = $dl->first();
3248
3249
            if ($cache) {
3250
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3251
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
3252
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
3253
                }
3254
            }
3255
        }
3256
3257
        if ($cache) {
3258
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
3259
        }
3260
3261
        return $item;
3262
    }
3263
3264
    /**
3265
     * Flush the cached results for all relations (has_one, has_many, many_many)
3266
     * Also clears any cached aggregate data.
3267
     *
3268
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3269
     *                            When false will just clear session-local cached data
3270
     * @return DataObject $this
3271
     */
3272
    public function flushCache($persistent = true)
3273
    {
3274
        if (static::class == self::class) {
0 ignored issues
show
introduced by
The condition static::class == self::class is always true.
Loading history...
3275
            self::$_cache_get_one = array();
3276
            return $this;
3277
        }
3278
3279
        $classes = ClassInfo::ancestry(static::class);
3280
        foreach ($classes as $class) {
3281
            if (isset(self::$_cache_get_one[$class])) {
3282
                unset(self::$_cache_get_one[$class]);
3283
            }
3284
        }
3285
3286
        $this->extend('flushCache');
3287
3288
        $this->components = array();
3289
        return $this;
3290
    }
3291
3292
    /**
3293
     * Flush the get_one global cache and destroy associated objects.
3294
     */
3295
    public static function flush_and_destroy_cache()
3296
    {
3297
        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...
3298
            foreach (self::$_cache_get_one as $class => $items) {
3299
                if (is_array($items)) {
3300
                    foreach ($items as $item) {
3301
                        if ($item) {
3302
                            $item->destroy();
3303
                        }
3304
                    }
3305
                }
3306
            }
3307
        }
3308
        self::$_cache_get_one = array();
3309
    }
3310
3311
    /**
3312
     * Reset all global caches associated with DataObject.
3313
     */
3314
    public static function reset()
3315
    {
3316
        // @todo Decouple these
3317
        DBEnum::flushCache();
3318
        ClassInfo::reset_db_cache();
3319
        static::getSchema()->reset();
3320
        self::$_cache_get_one = array();
3321
        self::$_cache_field_labels = array();
3322
    }
3323
3324
    /**
3325
     * Return the given element, searching by ID.
3326
     *
3327
     * This can be called either via `DataObject::get_by_id(MyClass::class, $id)`
3328
     * or `MyClass::get_by_id($id)`
3329
     *
3330
     * @param string|int $classOrID The class of the object to be returned, or id if called on target class
3331
     * @param int|bool $idOrCache The id of the element, or cache if called on target class
3332
     * @param boolean $cache See {@link get_one()}
3333
     *
3334
     * @return static The element
3335
     */
3336
    public static function get_by_id($classOrID, $idOrCache = null, $cache = true)
3337
    {
3338
        // Shift arguments if passing id in first or second argument
3339
        list ($class, $id, $cached) = is_numeric($classOrID)
3340
            ? [get_called_class(), $classOrID, isset($idOrCache) ? $idOrCache : $cache]
3341
            : [$classOrID, $idOrCache, $cache];
3342
3343
        // Validate class
3344
        if ($class === self::class) {
3345
            throw new InvalidArgumentException('DataObject::get_by_id() cannot query non-subclass DataObject directly');
3346
        }
3347
3348
        // Pass to get_one
3349
        $column = static::getSchema()->sqlColumnForField($class, 'ID');
3350
        return DataObject::get_one($class, [$column => $id], $cached);
3351
    }
3352
3353
    /**
3354
     * Get the name of the base table for this object
3355
     *
3356
     * @return string
3357
     */
3358
    public function baseTable()
3359
    {
3360
        return static::getSchema()->baseDataTable($this);
3361
    }
3362
3363
    /**
3364
     * Get the base class for this object
3365
     *
3366
     * @return string
3367
     */
3368
    public function baseClass()
3369
    {
3370
        return static::getSchema()->baseDataClass($this);
3371
    }
3372
3373
    /**
3374
     * @var array Parameters used in the query that built this object.
3375
     * This can be used by decorators (e.g. lazy loading) to
3376
     * run additional queries using the same context.
3377
     */
3378
    protected $sourceQueryParams;
3379
3380
    /**
3381
     * @see $sourceQueryParams
3382
     * @return array
3383
     */
3384
    public function getSourceQueryParams()
3385
    {
3386
        return $this->sourceQueryParams;
3387
    }
3388
3389
    /**
3390
     * Get list of parameters that should be inherited to relations on this object
3391
     *
3392
     * @return array
3393
     */
3394
    public function getInheritableQueryParams()
3395
    {
3396
        $params = $this->getSourceQueryParams();
3397
        $this->extend('updateInheritableQueryParams', $params);
3398
        return $params;
3399
    }
3400
3401
    /**
3402
     * @see $sourceQueryParams
3403
     * @param array
3404
     */
3405
    public function setSourceQueryParams($array)
3406
    {
3407
        $this->sourceQueryParams = $array;
3408
    }
3409
3410
    /**
3411
     * @see $sourceQueryParams
3412
     * @param string $key
3413
     * @param string $value
3414
     */
3415
    public function setSourceQueryParam($key, $value)
3416
    {
3417
        $this->sourceQueryParams[$key] = $value;
3418
    }
3419
3420
    /**
3421
     * @see $sourceQueryParams
3422
     * @param string $key
3423
     * @return string
3424
     */
3425
    public function getSourceQueryParam($key)
3426
    {
3427
        if (isset($this->sourceQueryParams[$key])) {
3428
            return $this->sourceQueryParams[$key];
3429
        }
3430
        return null;
3431
    }
3432
3433
    //-------------------------------------------------------------------------------------------//
3434
3435
    /**
3436
     * Check the database schema and update it as necessary.
3437
     *
3438
     * @uses DataExtension->augmentDatabase()
3439
     */
3440
    public function requireTable()
3441
    {
3442
        // Only build the table if we've actually got fields
3443
        $schema = static::getSchema();
3444
        $table = $schema->tableName(static::class);
3445
        $fields = $schema->databaseFields(static::class, false);
3446
        $indexes = $schema->databaseIndexes(static::class, false);
3447
        $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

3447
        /** @scrutinizer ignore-call */ 
3448
        $extensions = self::database_extensions(static::class);
Loading history...
3448
        $legacyTables = $schema->getLegacyTableNames(static::class);
3449
3450
        if (empty($table)) {
3451
            throw new LogicException(
3452
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3453
            );
3454
        }
3455
3456
        if ($legacyTables) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $legacyTables 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...
3457
            $ignore = Config::inst()->get(static::class, 'ignored_legacy_tables') ?: [];
3458
            $renameTables = array_diff(
3459
                array_intersect($legacyTables, DB::table_list()),
3460
                $ignore
3461
            );
3462
            if (count($renameTables) > 1) {
3463
                $class = static::class;
3464
                $legacyList = implode(', ', $renameTables);
3465
                trigger_error(
3466
                    "Class $class has multiple legacy tables: $legacyList",
3467
                    E_USER_NOTICE
3468
                );
3469
            }
3470
            if (count($renameTables) === 1) {
3471
                $dbSchema = DB::get_schema();
3472
                $dbSchema->renameTable($renameTables[0], $table);
3473
            }
3474
        }
3475
3476
        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...
3477
            $hasAutoIncPK = get_parent_class($this) === self::class;
3478
            DB::require_table(
3479
                $table,
3480
                $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

3480
                /** @scrutinizer ignore-type */ $fields,
Loading history...
3481
                $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

3481
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3482
                $hasAutoIncPK,
3483
                $this->config()->get('create_table_options'),
3484
                $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

3484
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3485
            );
3486
        } else {
3487
            DB::dont_require_table($table);
3488
        }
3489
3490
        // Build any child tables for many_many items
3491
        if ($manyMany = $this->uninherited('many_many')) {
3492
            $extras = $this->uninherited('many_many_extraFields');
3493
            foreach ($manyMany as $component => $spec) {
3494
                // Get many_many spec
3495
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3496
                $parentField = $manyManyComponent['parentField'];
3497
                $childField = $manyManyComponent['childField'];
3498
                $tableOrClass = $manyManyComponent['join'];
3499
3500
                // Skip if backed by actual class
3501
                if (class_exists($tableOrClass)) {
3502
                    continue;
3503
                }
3504
3505
                // Build fields
3506
                $manymanyFields = array(
3507
                    $parentField => "Int",
3508
                    $childField => "Int",
3509
                );
3510
                if (isset($extras[$component])) {
3511
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3512
                }
3513
3514
                // Build index list
3515
                $manymanyIndexes = [
3516
                    $parentField => [
3517
                        'type' => 'index',
3518
                        'name' => $parentField,
3519
                        'columns' => [$parentField],
3520
                    ],
3521
                    $childField => [
3522
                        'type' => 'index',
3523
                        'name' => $childField,
3524
                        'columns' => [$childField],
3525
                    ],
3526
                ];
3527
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
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

3527
                DB::require_table($tableOrClass, /** @scrutinizer ignore-type */ $manymanyFields, $manymanyIndexes, true, null, $extensions);
Loading history...
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

3527
                DB::require_table($tableOrClass, $manymanyFields, /** @scrutinizer ignore-type */ $manymanyIndexes, true, null, $extensions);
Loading history...
3528
            }
3529
        }
3530
3531
        // Let any extentions make their own database fields
3532
        $this->extend('augmentDatabase', $dummy);
3533
    }
3534
3535
    /**
3536
     * Add default records to database. This function is called whenever the
3537
     * database is built, after the database tables have all been created. Overload
3538
     * this to add default records when the database is built, but make sure you
3539
     * call parent::requireDefaultRecords().
3540
     *
3541
     * @uses DataExtension->requireDefaultRecords()
3542
     */
3543
    public function requireDefaultRecords()
3544
    {
3545
        $defaultRecords = $this->config()->uninherited('default_records');
3546
3547
        if (!empty($defaultRecords)) {
3548
            $hasData = DataObject::get_one(static::class);
3549
            if (!$hasData) {
3550
                $className = static::class;
3551
                foreach ($defaultRecords as $record) {
3552
                    $obj = Injector::inst()->create($className, $record);
3553
                    $obj->write();
3554
                }
3555
                DB::alteration_message("Added default records to $className table", "created");
3556
            }
3557
        }
3558
3559
        // Let any extentions make their own database default data
3560
        $this->extend('requireDefaultRecords', $dummy);
3561
    }
3562
3563
    /**
3564
     * Get the default searchable fields for this object, as defined in the
3565
     * $searchable_fields list. If searchable fields are not defined on the
3566
     * data object, uses a default selection of summary fields.
3567
     *
3568
     * @return array
3569
     */
3570
    public function searchableFields()
3571
    {
3572
        // can have mixed format, need to make consistent in most verbose form
3573
        $fields = $this->config()->get('searchable_fields');
3574
        $labels = $this->fieldLabels();
3575
3576
        // fallback to summary fields (unless empty array is explicitly specified)
3577
        if (!$fields && !is_array($fields)) {
3578
            $summaryFields = array_keys($this->summaryFields());
3579
            $fields = array();
3580
3581
            // remove the custom getters as the search should not include them
3582
            $schema = static::getSchema();
3583
            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...
3584
                foreach ($summaryFields as $key => $name) {
3585
                    $spec = $name;
3586
3587
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3588
                    if (($fieldPos = strpos($name, '.')) !== false) {
3589
                        $name = substr($name, 0, $fieldPos);
3590
                    }
3591
3592
                    if ($schema->fieldSpec($this, $name)) {
3593
                        $fields[] = $name;
3594
                    } elseif ($this->relObject($spec)) {
3595
                        $fields[] = $spec;
3596
                    }
3597
                }
3598
            }
3599
        }
3600
3601
        // we need to make sure the format is unified before
3602
        // augmenting fields, so extensions can apply consistent checks
3603
        // but also after augmenting fields, because the extension
3604
        // might use the shorthand notation as well
3605
3606
        // rewrite array, if it is using shorthand syntax
3607
        $rewrite = array();
3608
        foreach ($fields as $name => $specOrName) {
3609
            $identifer = (is_int($name)) ? $specOrName : $name;
3610
3611
            if (is_int($name)) {
3612
                // Format: array('MyFieldName')
3613
                $rewrite[$identifer] = array();
3614
            } elseif (is_array($specOrName) && ($relObject = $this->relObject($identifer))) {
3615
                // Format: array('MyFieldName' => array(
3616
                //   'filter => 'ExactMatchFilter',
3617
                //   'field' => 'NumericField', // optional
3618
                //   'title' => 'My Title', // optional
3619
                // ))
3620
                $rewrite[$identifer] = array_merge(
3621
                    array('filter' => $relObject->config()->get('default_search_filter_class')),
3622
                    (array)$specOrName
3623
                );
3624
            } else {
3625
                // Format: array('MyFieldName' => 'ExactMatchFilter')
3626
                $rewrite[$identifer] = array(
3627
                    'filter' => $specOrName,
3628
                );
3629
            }
3630
            if (!isset($rewrite[$identifer]['title'])) {
3631
                $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3632
                    ? $labels[$identifer] : FormField::name_to_label($identifer);
3633
            }
3634
            if (!isset($rewrite[$identifer]['filter'])) {
3635
                /** @skipUpgrade */
3636
                $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3637
            }
3638
        }
3639
3640
        $fields = $rewrite;
3641
3642
        // apply DataExtensions if present
3643
        $this->extend('updateSearchableFields', $fields);
3644
3645
        return $fields;
3646
    }
3647
3648
    /**
3649
     * Get any user defined searchable fields labels that
3650
     * exist. Allows overriding of default field names in the form
3651
     * interface actually presented to the user.
3652
     *
3653
     * The reason for keeping this separate from searchable_fields,
3654
     * which would be a logical place for this functionality, is to
3655
     * avoid bloating and complicating the configuration array. Currently
3656
     * much of this system is based on sensible defaults, and this property
3657
     * would generally only be set in the case of more complex relationships
3658
     * between data object being required in the search interface.
3659
     *
3660
     * Generates labels based on name of the field itself, if no static property
3661
     * {@link self::field_labels} exists.
3662
     *
3663
     * @uses $field_labels
3664
     * @uses FormField::name_to_label()
3665
     *
3666
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3667
     *
3668
     * @return array Array of all element labels
3669
     */
3670
    public function fieldLabels($includerelations = true)
3671
    {
3672
        $cacheKey = static::class . '_' . $includerelations;
3673
3674
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
3675
            $customLabels = $this->config()->get('field_labels');
3676
            $autoLabels = array();
3677
3678
            // get all translated static properties as defined in i18nCollectStatics()
3679
            $ancestry = ClassInfo::ancestry(static::class);
3680
            $ancestry = array_reverse($ancestry);
3681
            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...
3682
                foreach ($ancestry as $ancestorClass) {
3683
                    if ($ancestorClass === ViewableData::class) {
3684
                        break;
3685
                    }
3686
                    $types = [
3687
                        'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3688
                    ];
3689
                    if ($includerelations) {
3690
                        $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3691
                        $types['has_many'] = (array)Config::inst()->get(
3692
                            $ancestorClass,
3693
                            'has_many',
3694
                            Config::UNINHERITED
3695
                        );
3696
                        $types['many_many'] = (array)Config::inst()->get(
3697
                            $ancestorClass,
3698
                            'many_many',
3699
                            Config::UNINHERITED
3700
                        );
3701
                        $types['belongs_many_many'] = (array)Config::inst()->get(
3702
                            $ancestorClass,
3703
                            'belongs_many_many',
3704
                            Config::UNINHERITED
3705
                        );
3706
                    }
3707
                    foreach ($types as $type => $attrs) {
3708
                        foreach ($attrs as $name => $spec) {
3709
                            $autoLabels[$name] = _t(
3710
                                "{$ancestorClass}.{$type}_{$name}",
3711
                                FormField::name_to_label($name)
3712
                            );
3713
                        }
3714
                    }
3715
                }
3716
            }
3717
3718
            $labels = array_merge((array)$autoLabels, (array)$customLabels);
3719
            $this->extend('updateFieldLabels', $labels);
3720
            self::$_cache_field_labels[$cacheKey] = $labels;
3721
        }
3722
3723
        return self::$_cache_field_labels[$cacheKey];
3724
    }
3725
3726
    /**
3727
     * Get a human-readable label for a single field,
3728
     * see {@link fieldLabels()} for more details.
3729
     *
3730
     * @uses fieldLabels()
3731
     * @uses FormField::name_to_label()
3732
     *
3733
     * @param string $name Name of the field
3734
     * @return string Label of the field
3735
     */
3736
    public function fieldLabel($name)
3737
    {
3738
        $labels = $this->fieldLabels();
3739
        return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3740
    }
3741
3742
    /**
3743
     * Get the default summary fields for this object.
3744
     *
3745
     * @todo use the translation apparatus to return a default field selection for the language
3746
     *
3747
     * @return array
3748
     */
3749
    public function summaryFields()
3750
    {
3751
        $rawFields = $this->config()->get('summary_fields');
3752
3753
        // Merge associative / numeric keys
3754
        $fields = [];
3755
        foreach ($rawFields as $key => $value) {
3756
            if (is_int($key)) {
3757
                $key = $value;
3758
            }
3759
            $fields[$key] = $value;
3760
        }
3761
3762
        if (!$fields) {
3763
            $fields = array();
3764
            // try to scaffold a couple of usual suspects
3765
            if ($this->hasField('Name')) {
3766
                $fields['Name'] = 'Name';
3767
            }
3768
            if (static::getSchema()->fieldSpec($this, 'Title')) {
3769
                $fields['Title'] = 'Title';
3770
            }
3771
            if ($this->hasField('Description')) {
3772
                $fields['Description'] = 'Description';
3773
            }
3774
            if ($this->hasField('FirstName')) {
3775
                $fields['FirstName'] = 'First Name';
3776
            }
3777
        }
3778
        $this->extend("updateSummaryFields", $fields);
3779
3780
        // Final fail-over, just list ID field
3781
        if (!$fields) {
3782
            $fields['ID'] = 'ID';
3783
        }
3784
3785
        // Localize fields (if possible)
3786
        foreach ($this->fieldLabels(false) as $name => $label) {
3787
            // only attempt to localize if the label definition is the same as the field name.
3788
            // this will preserve any custom labels set in the summary_fields configuration
3789
            if (isset($fields[$name]) && $name === $fields[$name]) {
3790
                $fields[$name] = $label;
3791
            }
3792
        }
3793
3794
        return $fields;
3795
    }
3796
3797
    /**
3798
     * Defines a default list of filters for the search context.
3799
     *
3800
     * If a filter class mapping is defined on the data object,
3801
     * it is constructed here. Otherwise, the default filter specified in
3802
     * {@link DBField} is used.
3803
     *
3804
     * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3805
     *
3806
     * @return array
3807
     */
3808
    public function defaultSearchFilters()
3809
    {
3810
        $filters = array();
3811
3812
        foreach ($this->searchableFields() as $name => $spec) {
3813
            if (empty($spec['filter'])) {
3814
                /** @skipUpgrade */
3815
                $filters[$name] = 'PartialMatchFilter';
3816
            } elseif ($spec['filter'] instanceof SearchFilter) {
3817
                $filters[$name] = $spec['filter'];
3818
            } else {
3819
                $filters[$name] = Injector::inst()->create($spec['filter'], $name);
3820
            }
3821
        }
3822
3823
        return $filters;
3824
    }
3825
3826
    /**
3827
     * @return boolean True if the object is in the database
3828
     */
3829
    public function isInDB()
3830
    {
3831
        return is_numeric($this->ID) && $this->ID > 0;
3832
    }
3833
3834
    /*
3835
     * @ignore
3836
     */
3837
    private static $subclass_access = true;
3838
3839
    /**
3840
     * Temporarily disable subclass access in data object qeur
3841
     */
3842
    public static function disable_subclass_access()
3843
    {
3844
        self::$subclass_access = false;
3845
    }
3846
3847
    public static function enable_subclass_access()
3848
    {
3849
        self::$subclass_access = true;
3850
    }
3851
3852
    //-------------------------------------------------------------------------------------------//
3853
3854
    /**
3855
     * Database field definitions.
3856
     * This is a map from field names to field type. The field
3857
     * type should be a class that extends .
3858
     * @var array
3859
     * @config
3860
     */
3861
    private static $db = [];
3862
3863
    /**
3864
     * Use a casting object for a field. This is a map from
3865
     * field name to class name of the casting object.
3866
     *
3867
     * @var array
3868
     */
3869
    private static $casting = array(
3870
        "Title" => 'Text',
3871
    );
3872
3873
    /**
3874
     * Specify custom options for a CREATE TABLE call.
3875
     * Can be used to specify a custom storage engine for specific database table.
3876
     * All options have to be keyed for a specific database implementation,
3877
     * identified by their class name (extending from {@link SS_Database}).
3878
     *
3879
     * <code>
3880
     * array(
3881
     *  'MySQLDatabase' => 'ENGINE=MyISAM'
3882
     * )
3883
     * </code>
3884
     *
3885
     * Caution: This API is experimental, and might not be
3886
     * included in the next major release. Please use with care.
3887
     *
3888
     * @var array
3889
     * @config
3890
     */
3891
    private static $create_table_options = array(
3892
        MySQLSchemaManager::ID => 'ENGINE=InnoDB'
3893
    );
3894
3895
    /**
3896
     * If a field is in this array, then create a database index
3897
     * on that field. This is a map from fieldname to index type.
3898
     * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3899
     *
3900
     * @var array
3901
     * @config
3902
     */
3903
    private static $indexes = null;
3904
3905
    /**
3906
     * Inserts standard column-values when a DataObject
3907
     * is instantiated. Does not insert default records {@see $default_records}.
3908
     * This is a map from fieldname to default value.
3909
     *
3910
     *  - If you would like to change a default value in a sub-class, just specify it.
3911
     *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3912
     *    or false in your subclass.  Setting it to null won't work.
3913
     *
3914
     * @var array
3915
     * @config
3916
     */
3917
    private static $defaults = [];
3918
3919
    /**
3920
     * Multidimensional array which inserts default data into the database
3921
     * on a db/build-call as long as the database-table is empty. Please use this only
3922
     * for simple constructs, not for SiteTree-Objects etc. which need special
3923
     * behaviour such as publishing and ParentNodes.
3924
     *
3925
     * Example:
3926
     * array(
3927
     *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3928
     *  array('Title' => "DefaultPage2")
3929
     * ).
3930
     *
3931
     * @var array
3932
     * @config
3933
     */
3934
    private static $default_records = null;
3935
3936
    /**
3937
     * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3938
     * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3939
     *
3940
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3941
     *
3942
     * @var array
3943
     * @config
3944
     */
3945
    private static $has_one = [];
3946
3947
    /**
3948
     * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3949
     *
3950
     * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3951
     * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3952
     * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3953
     *
3954
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3955
     *
3956
     * @var array
3957
     * @config
3958
     */
3959
    private static $belongs_to = [];
3960
3961
    /**
3962
     * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3963
     *
3964
     * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3965
     * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3966
     * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3967
     * which foreign key to use.
3968
     *
3969
     * @var array
3970
     * @config
3971
     */
3972
    private static $has_many = [];
3973
3974
    /**
3975
     * many-many relationship definitions.
3976
     * This is a map from component name to data type.
3977
     * @var array
3978
     * @config
3979
     */
3980
    private static $many_many = [];
3981
3982
    /**
3983
     * Extra fields to include on the connecting many-many table.
3984
     * This is a map from field name to field type.
3985
     *
3986
     * Example code:
3987
     * <code>
3988
     * public static $many_many_extraFields = array(
3989
     *  'Members' => array(
3990
     *          'Role' => 'Varchar(100)'
3991
     *      )
3992
     * );
3993
     * </code>
3994
     *
3995
     * @var array
3996
     * @config
3997
     */
3998
    private static $many_many_extraFields = [];
3999
4000
    /**
4001
     * The inverse side of a many-many relationship.
4002
     * This is a map from component name to data type.
4003
     * @var array
4004
     * @config
4005
     */
4006
    private static $belongs_many_many = [];
4007
4008
    /**
4009
     * The default sort expression. This will be inserted in the ORDER BY
4010
     * clause of a SQL query if no other sort expression is provided.
4011
     * @var string
4012
     * @config
4013
     */
4014
    private static $default_sort = null;
4015
4016
    /**
4017
     * Default list of fields that can be scaffolded by the ModelAdmin
4018
     * search interface.
4019
     *
4020
     * Overriding the default filter, with a custom defined filter:
4021
     * <code>
4022
     *  static $searchable_fields = array(
4023
     *     "Name" => "PartialMatchFilter"
4024
     *  );
4025
     * </code>
4026
     *
4027
     * Overriding the default form fields, with a custom defined field.
4028
     * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
4029
     * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
4030
     * <code>
4031
     *  static $searchable_fields = array(
4032
     *    "Name" => array(
4033
     *      "field" => "TextField"
4034
     *    )
4035
     *  );
4036
     * </code>
4037
     *
4038
     * Overriding the default form field, filter and title:
4039
     * <code>
4040
     *  static $searchable_fields = array(
4041
     *    "Organisation.ZipCode" => array(
4042
     *      "field" => "TextField",
4043
     *      "filter" => "PartialMatchFilter",
4044
     *      "title" => 'Organisation ZIP'
4045
     *    )
4046
     *  );
4047
     * </code>
4048
     * @config
4049
     * @var array
4050
     */
4051
    private static $searchable_fields = null;
4052
4053
    /**
4054
     * User defined labels for searchable_fields, used to override
4055
     * default display in the search form.
4056
     * @config
4057
     * @var array
4058
     */
4059
    private static $field_labels = [];
4060
4061
    /**
4062
     * Provides a default list of fields to be used by a 'summary'
4063
     * view of this object.
4064
     * @config
4065
     * @var array
4066
     */
4067
    private static $summary_fields = [];
4068
4069
    public function provideI18nEntities()
4070
    {
4071
        // Note: see http://guides.rubyonrails.org/i18n.html#pluralization for rules
4072
        // Best guess for a/an rule. Better guesses require overriding in subclasses
4073
        $pluralName = $this->plural_name();
4074
        $singularName = $this->singular_name();
4075
        $conjunction = preg_match('/^[aeiou]/i', $singularName) ? 'An ' : 'A ';
4076
        return [
4077
            static::class . '.SINGULARNAME' => $this->singular_name(),
4078
            static::class . '.PLURALNAME' => $pluralName,
4079
            static::class . '.PLURALS' => [
4080
                'one' => $conjunction . $singularName,
4081
                'other' => '{count} ' . $pluralName
4082
            ]
4083
        ];
4084
    }
4085
4086
    /**
4087
     * Returns true if the given method/parameter has a value
4088
     * (Uses the DBField::hasValue if the parameter is a database field)
4089
     *
4090
     * @param string $field The field name
4091
     * @param array $arguments
4092
     * @param bool $cache
4093
     * @return boolean
4094
     */
4095
    public function hasValue($field, $arguments = null, $cache = true)
4096
    {
4097
        // has_one fields should not use dbObject to check if a value is given
4098
        $hasOne = static::getSchema()->hasOneComponent(static::class, $field);
4099
        if (!$hasOne && ($obj = $this->dbObject($field))) {
4100
            return $obj->exists();
4101
        } else {
4102
            return parent::hasValue($field, $arguments, $cache);
4103
        }
4104
    }
4105
4106
    /**
4107
     * If selected through a many_many through relation, this is the instance of the joined record
4108
     *
4109
     * @return DataObject
4110
     */
4111
    public function getJoin()
4112
    {
4113
        return $this->joinRecord;
4114
    }
4115
4116
    /**
4117
     * Set joining object
4118
     *
4119
     * @param DataObject $object
4120
     * @param string $alias Alias
4121
     * @return $this
4122
     */
4123
    public function setJoin(DataObject $object, $alias = null)
4124
    {
4125
        $this->joinRecord = $object;
4126
        if ($alias) {
4127
            if (static::getSchema()->fieldSpec(static::class, $alias)) {
4128
                throw new InvalidArgumentException(
4129
                    "Joined record $alias cannot also be a db field"
4130
                );
4131
            }
4132
            $this->record[$alias] = $object;
4133
        }
4134
        return $this;
4135
    }
4136
4137
    /**
4138
     * Find objects in the given relationships, merging them into the given list
4139
     *
4140
     * @param string $source Config property to extract relationships from
4141
     * @param bool $recursive True if recursive
4142
     * @param ArrayList $list If specified, items will be added to this list. If not, a new
4143
     * instance of ArrayList will be constructed and returned
4144
     * @return ArrayList The list of related objects
4145
     */
4146
    public function findRelatedObjects($source, $recursive = true, $list = null)
4147
    {
4148
        if (!$list) {
4149
            $list = new ArrayList();
4150
        }
4151
4152
        // Skip search for unsaved records
4153
        if (!$this->isInDB()) {
4154
            return $list;
4155
        }
4156
4157
        $relationships = $this->config()->get($source) ?: [];
4158
        foreach ($relationships as $relationship) {
4159
            // Warn if invalid config
4160
            if (!$this->hasMethod($relationship)) {
4161
                trigger_error(sprintf(
4162
                    "Invalid %s config value \"%s\" on object on class \"%s\"",
4163
                    $source,
4164
                    $relationship,
4165
                    get_class($this)
4166
                ), E_USER_WARNING);
4167
                continue;
4168
            }
4169
4170
            // Inspect value of this relationship
4171
            $items = $this->{$relationship}();
4172
4173
            // Merge any new item
4174
            $newItems = $this->mergeRelatedObjects($list, $items);
4175
4176
            // Recurse if necessary
4177
            if ($recursive) {
4178
                foreach ($newItems as $item) {
4179
                    /** @var DataObject $item */
4180
                    $item->findRelatedObjects($source, true, $list);
4181
                }
4182
            }
4183
        }
4184
        return $list;
4185
    }
4186
4187
    /**
4188
     * Helper method to merge owned/owning items into a list.
4189
     * Items already present in the list will be skipped.
4190
     *
4191
     * @param ArrayList $list Items to merge into
4192
     * @param mixed $items List of new items to merge
4193
     * @return ArrayList List of all newly added items that did not already exist in $list
4194
     */
4195
    public function mergeRelatedObjects($list, $items)
4196
    {
4197
        $added = new ArrayList();
4198
        if (!$items) {
4199
            return $added;
4200
        }
4201
        if ($items instanceof DataObject) {
4202
            $items = [$items];
4203
        }
4204
4205
        /** @var DataObject $item */
4206
        foreach ($items as $item) {
4207
            $this->mergeRelatedObject($list, $added, $item);
4208
        }
4209
        return $added;
4210
    }
4211
4212
    /**
4213
     * Merge single object into a list, but ensures that existing objects are not
4214
     * re-added.
4215
     *
4216
     * @param ArrayList $list Global list
4217
     * @param ArrayList $added Additional list to insert into
4218
     * @param DataObject $item Item to add
4219
     */
4220
    protected function mergeRelatedObject($list, $added, $item)
4221
    {
4222
        // Identify item
4223
        $itemKey = get_class($item) . '/' . $item->ID;
4224
4225
        // Write if saved, versioned, and not already added
4226
        if ($item->isInDB() && !isset($list[$itemKey])) {
4227
            $list[$itemKey] = $item;
4228
            $added[$itemKey] = $item;
4229
        }
4230
4231
        // Add joined record (from many_many through) automatically
4232
        $joined = $item->getJoin();
4233
        if ($joined) {
0 ignored issues
show
introduced by
$joined is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
4234
            $this->mergeRelatedObject($list, $added, $joined);
4235
        }
4236
    }
4237
}
4238