Passed
Push — 4 ( 406b28...765d15 )
by Daniel
35:43 queued 27:07
created

DataObject::reset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 8
rs 10
c 0
b 0
f 0
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): {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

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

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

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

Loading history...
484
                    $this->duplicateManyManyRelation($sourceObject, $destinationObject, $relation);
485
                    break;
486
                }
487
                case array_key_exists($relation, $hasMany): {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

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

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

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

Loading history...
488
                    $this->duplicateHasManyRelation($sourceObject, $destinationObject, $relation);
489
                    break;
490
                }
491
                case array_key_exists($relation, $hasOne): {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

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

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

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

Loading history...
492
                    $this->duplicateHasOneRelation($sourceObject, $destinationObject, $relation);
493
                    break;
494
                }
495
                case array_key_exists($relation, $belongsTo): {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

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

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

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

Loading history...
496
                    $this->duplicateBelongsToRelation($sourceObject, $destinationObject, $relation);
497
                    break;
498
                }
499
                default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

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

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

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

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

Loading history...
500
                    $sourceType = get_class($sourceObject);
501
                    throw new InvalidArgumentException(
502
                        "Cannot duplicate unknown relation {$relation} on parent type {$sourceType}"
503
                    );
504
                }
505
            }
506
        }
507
    }
508
509
    /**
510
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
511
     *
512
     * @deprecated 4.1.0:5.0.0 Use duplicateRelations() instead
513
     * @param DataObject $sourceObject the source object to duplicate from
514
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
515
     * @param bool|string $filter
516
     */
517
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
518
    {
519
        Deprecation::notice('5.0', 'Use duplicateRelations() instead');
520
521
        // Get list of relations to duplicate
522
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
523
            $relations = $sourceObject->config()->get($filter);
524
        } elseif ($filter === true) {
525
            $relations = $sourceObject->manyMany();
526
        } else {
527
            throw new InvalidArgumentException("Invalid many_many duplication filter");
528
        }
529
        foreach ($relations as $manyManyName => $type) {
530
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
531
        }
532
    }
533
534
    /**
535
     * Duplicates a single many_many relation from one object to another.
536
     *
537
     * @param DataObject $sourceObject
538
     * @param DataObject $destinationObject
539
     * @param string $relation
540
     */
541
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $relation)
542
    {
543
        // Copy all components from source to destination
544
        $source = $sourceObject->getManyManyComponents($relation);
545
        $dest = $destinationObject->getManyManyComponents($relation);
546
547
        if ($source instanceof ManyManyList) {
548
            $extraFieldNames = $source->getExtraFields();
549
        } else {
550
            $extraFieldNames = [];
551
        }
552
553
        foreach ($source as $item) {
554
            // Merge extra fields
555
            $extraFields = [];
556
            foreach ($extraFieldNames as $fieldName => $fieldType) {
557
                $extraFields[$fieldName] = $item->getField($fieldName);
558
            }
559
            $dest->add($item, $extraFields);
560
        }
561
    }
562
563
    /**
564
     * Duplicates a single many_many relation from one object to another.
565
     *
566
     * @param DataObject $sourceObject
567
     * @param DataObject $destinationObject
568
     * @param string $relation
569
     */
570
    protected function duplicateHasManyRelation($sourceObject, $destinationObject, $relation)
571
    {
572
        // Copy all components from source to destination
573
        $source = $sourceObject->getComponents($relation);
574
        $dest = $destinationObject->getComponents($relation);
575
576
        /** @var DataObject $item */
577
        foreach ($source as $item) {
578
            // Don't write on duplicate; Wait until ParentID is available later.
579
            // writeRelations() will eventually write these records when converting
580
            // from UnsavedRelationList
581
            $clonedItem = $item->duplicate(false);
582
            $dest->add($clonedItem);
583
        }
584
    }
585
586
    /**
587
     * Duplicates a single has_one relation from one object to another.
588
     * Note: Child object will be force written.
589
     *
590
     * @param DataObject $sourceObject
591
     * @param DataObject $destinationObject
592
     * @param string $relation
593
     */
594
    protected function duplicateHasOneRelation($sourceObject, $destinationObject, $relation)
595
    {
596
        // Check if original object exists
597
        $item = $sourceObject->getComponent($relation);
598
        if (!$item->isInDB()) {
599
            return;
600
        }
601
602
        $clonedItem = $item->duplicate(false);
603
        $destinationObject->setComponent($relation, $clonedItem);
604
    }
605
606
    /**
607
     * Duplicates a single belongs_to relation from one object to another.
608
     * Note: This will force a write on both parent / child objects.
609
     *
610
     * @param DataObject $sourceObject
611
     * @param DataObject $destinationObject
612
     * @param string $relation
613
     */
614
    protected function duplicateBelongsToRelation($sourceObject, $destinationObject, $relation)
615
    {
616
        // Check if original object exists
617
        $item = $sourceObject->getComponent($relation);
618
        if (!$item->isInDB()) {
619
            return;
620
        }
621
622
        $clonedItem = $item->duplicate(false);
623
        $destinationObject->setComponent($relation, $clonedItem);
624
        // After $clonedItem is assigned the appropriate FieldID / FieldClass, force write
625
        // @todo Write this component in onAfterWrite instead, assigning the FieldID then
626
        // https://github.com/silverstripe/silverstripe-framework/issues/7818
627
        $clonedItem->write();
628
    }
629
630
    /**
631
     * Return obsolete class name, if this is no longer a valid class
632
     *
633
     * @return string
634
     */
635
    public function getObsoleteClassName()
636
    {
637
        $className = $this->getField("ClassName");
638
        if (!ClassInfo::exists($className)) {
639
            return $className;
640
        }
641
        return null;
642
    }
643
644
    /**
645
     * Gets name of this class
646
     *
647
     * @return string
648
     */
649
    public function getClassName()
650
    {
651
        $className = $this->getField("ClassName");
652
        if (!ClassInfo::exists($className)) {
653
            return static::class;
654
        }
655
        return $className;
656
    }
657
658
    /**
659
     * Set the ClassName attribute. {@link $class} is also updated.
660
     * Warning: This will produce an inconsistent record, as the object
661
     * instance will not automatically switch to the new subclass.
662
     * Please use {@link newClassInstance()} for this purpose,
663
     * or destroy and reinstanciate the record.
664
     *
665
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
666
     * @return $this
667
     */
668
    public function setClassName($className)
669
    {
670
        $className = trim($className);
671
        if (!$className || !is_subclass_of($className, self::class)) {
672
            return $this;
673
        }
674
675
        $this->setField("ClassName", $className);
676
        $this->setField('RecordClassName', $className);
677
        return $this;
678
    }
679
680
    /**
681
     * Create a new instance of a different class from this object's record.
682
     * This is useful when dynamically changing the type of an instance. Specifically,
683
     * it ensures that the instance of the class is a match for the className of the
684
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
685
     * property manually before calling this method, as it will confuse change detection.
686
     *
687
     * If the new class is different to the original class, defaults are populated again
688
     * because this will only occur automatically on instantiation of a DataObject if
689
     * there is no record, or the record has no ID. In this case, we do have an ID but
690
     * we still need to repopulate the defaults.
691
     *
692
     * @param string $newClassName The name of the new class
693
     *
694
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
695
     */
696
    public function newClassInstance($newClassName)
697
    {
698
        if (!is_subclass_of($newClassName, self::class)) {
699
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
700
        }
701
702
        $originalClass = $this->ClassName;
703
704
        /** @var DataObject $newInstance */
705
        $newInstance = Injector::inst()->create($newClassName, $this->record, false);
706
707
        // Modify ClassName
708
        if ($newClassName != $originalClass) {
709
            $newInstance->setClassName($newClassName);
710
            $newInstance->populateDefaults();
711
            $newInstance->forceChange();
712
        }
713
714
        return $newInstance;
715
    }
716
717
    /**
718
     * Adds methods from the extensions.
719
     * Called by Object::__construct() once per class.
720
     */
721
    public function defineMethods()
722
    {
723
        parent::defineMethods();
724
725
        if (static::class === self::class) {
0 ignored issues
show
introduced by
The condition static::class === self::class is always true.
Loading history...
726
            return;
727
        }
728
729
        // Set up accessors for joined items
730
        if ($manyMany = $this->manyMany()) {
731
            foreach ($manyMany as $relationship => $class) {
732
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
733
            }
734
        }
735
        if ($hasMany = $this->hasMany()) {
736
            foreach ($hasMany as $relationship => $class) {
737
                $this->addWrapperMethod($relationship, 'getComponents');
738
            }
739
        }
740
        if ($hasOne = $this->hasOne()) {
741
            foreach ($hasOne as $relationship => $class) {
742
                $this->addWrapperMethod($relationship, 'getComponent');
743
            }
744
        }
745
        if ($belongsTo = $this->belongsTo()) {
746
            foreach (array_keys($belongsTo) as $relationship) {
747
                $this->addWrapperMethod($relationship, 'getComponent');
748
            }
749
        }
750
    }
751
752
    /**
753
     * Returns true if this object "exists", i.e., has a sensible value.
754
     * The default behaviour for a DataObject is to return true if
755
     * the object exists in the database, you can override this in subclasses.
756
     *
757
     * @return boolean true if this object exists
758
     */
759
    public function exists()
760
    {
761
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
762
    }
763
764
    /**
765
     * Returns TRUE if all values (other than "ID") are
766
     * considered empty (by weak boolean comparison).
767
     *
768
     * @return boolean
769
     */
770
    public function isEmpty()
771
    {
772
        $fixed = DataObject::config()->uninherited('fixed_fields');
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
773
        foreach ($this->toMap() as $field => $value) {
774
            // only look at custom fields
775
            if (isset($fixed[$field])) {
776
                continue;
777
            }
778
779
            $dbObject = $this->dbObject($field);
780
            if (!$dbObject) {
781
                continue;
782
            }
783
            if ($dbObject->exists()) {
784
                return false;
785
            }
786
        }
787
        return true;
788
    }
789
790
    /**
791
     * Pluralise this item given a specific count.
792
     *
793
     * E.g. "0 Pages", "1 File", "3 Images"
794
     *
795
     * @param string $count
796
     * @return string
797
     */
798
    public function i18n_pluralise($count)
799
    {
800
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
801
        return i18n::_t(
802
            static::class . '.PLURALS',
803
            $default,
804
            ['count' => $count]
805
        );
806
    }
807
808
    /**
809
     * Get the user friendly singular name of this DataObject.
810
     * If the name is not defined (by redefining $singular_name in the subclass),
811
     * this returns the class name.
812
     *
813
     * @return string User friendly singular name of this DataObject
814
     */
815
    public function singular_name()
816
    {
817
        $name = $this->config()->get('singular_name');
818
        if ($name) {
819
            return $name;
820
        }
821
        return ucwords(trim(strtolower(preg_replace(
822
            '/_?([A-Z])/',
823
            ' $1',
824
            ClassInfo::shortName($this)
825
        ))));
826
    }
827
828
    /**
829
     * Get the translated user friendly singular name of this DataObject
830
     * same as singular_name() but runs it through the translating function
831
     *
832
     * Translating string is in the form:
833
     *     $this->class.SINGULARNAME
834
     * Example:
835
     *     Page.SINGULARNAME
836
     *
837
     * @return string User friendly translated singular name of this DataObject
838
     */
839
    public function i18n_singular_name()
840
    {
841
        return _t(static::class . '.SINGULARNAME', $this->singular_name());
842
    }
843
844
    /**
845
     * Get the user friendly plural name of this DataObject
846
     * If the name is not defined (by renaming $plural_name in the subclass),
847
     * this returns a pluralised version of the class name.
848
     *
849
     * @return string User friendly plural name of this DataObject
850
     */
851
    public function plural_name()
852
    {
853
        if ($name = $this->config()->get('plural_name')) {
854
            return $name;
855
        }
856
        $name = $this->singular_name();
857
        //if the penultimate character is not a vowel, replace "y" with "ies"
858
        if (preg_match('/[^aeiou]y$/i', $name)) {
859
            $name = substr($name, 0, -1) . 'ie';
860
        }
861
        return ucfirst($name . 's');
862
    }
863
864
    /**
865
     * Get the translated user friendly plural name of this DataObject
866
     * Same as plural_name but runs it through the translation function
867
     * Translation string is in the form:
868
     *      $this->class.PLURALNAME
869
     * Example:
870
     *      Page.PLURALNAME
871
     *
872
     * @return string User friendly translated plural name of this DataObject
873
     */
874
    public function i18n_plural_name()
875
    {
876
        return _t(static::class . '.PLURALNAME', $this->plural_name());
877
    }
878
879
    /**
880
     * Standard implementation of a title/label for a specific
881
     * record. Tries to find properties 'Title' or 'Name',
882
     * and falls back to the 'ID'. Useful to provide
883
     * user-friendly identification of a record, e.g. in errormessages
884
     * or UI-selections.
885
     *
886
     * Overload this method to have a more specialized implementation,
887
     * e.g. for an Address record this could be:
888
     * <code>
889
     * function getTitle() {
890
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
891
     * }
892
     * </code>
893
     *
894
     * @return string
895
     */
896
    public function getTitle()
897
    {
898
        $schema = static::getSchema();
899
        if ($schema->fieldSpec($this, 'Title')) {
900
            return $this->getField('Title');
901
        }
902
        if ($schema->fieldSpec($this, 'Name')) {
903
            return $this->getField('Name');
904
        }
905
906
        return "#{$this->ID}";
907
    }
908
909
    /**
910
     * Returns the associated database record - in this case, the object itself.
911
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
912
     *
913
     * @return DataObject Associated database record
914
     */
915
    public function data()
916
    {
917
        return $this;
918
    }
919
920
    /**
921
     * Convert this object to a map.
922
     *
923
     * @return array The data as a map.
924
     */
925
    public function toMap()
926
    {
927
        $this->loadLazyFields();
928
        return $this->record;
929
    }
930
931
    /**
932
     * Return all currently fetched database fields.
933
     *
934
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
935
     * Obviously, this makes it a lot faster.
936
     *
937
     * @return array The data as a map.
938
     */
939
    public function getQueriedDatabaseFields()
940
    {
941
        return $this->record;
942
    }
943
944
    /**
945
     * Update a number of fields on this object, given a map of the desired changes.
946
     *
947
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
948
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
949
     *
950
     * Doesn't write the main object, but if you use the dot syntax, it will write()
951
     * the related objects that it alters.
952
     *
953
     * When using this method with user supplied data, it's very important to
954
     * whitelist the allowed keys.
955
     *
956
     * @param array $data A map of field name to data values to update.
957
     * @return DataObject $this
958
     */
959
    public function update($data)
960
    {
961
        foreach ($data as $key => $value) {
962
            // Implement dot syntax for updates
963
            if (strpos($key, '.') !== false) {
964
                $relations = explode('.', $key);
965
                $fieldName = array_pop($relations);
966
                /** @var static $relObj */
967
                $relObj = $this;
968
                $relation = null;
969
                foreach ($relations as $i => $relation) {
970
                    // no support for has_many or many_many relationships,
971
                    // as the updater wouldn't know which object to write to (or create)
972
                    if ($relObj->$relation() instanceof DataObject) {
973
                        $parentObj = $relObj;
974
                        $relObj = $relObj->$relation();
975
                        // If the intermediate relationship objects haven't been created, then write them
976
                        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...
977
                            $relObj->write();
978
                            $relatedFieldName = $relation . "ID";
979
                            $parentObj->$relatedFieldName = $relObj->ID;
980
                            $parentObj->write();
981
                        }
982
                    } else {
983
                        user_error(
984
                            "DataObject::update(): Can't traverse relationship '$relation'," .
985
                            "it has to be a has_one relationship or return a single DataObject",
986
                            E_USER_NOTICE
987
                        );
988
                        // unset relation object so we don't write properties to the wrong object
989
                        $relObj = null;
990
                        break;
991
                    }
992
                }
993
994
                if ($relObj) {
995
                    $relObj->$fieldName = $value;
996
                    $relObj->write();
997
                    $relatedFieldName = $relation . "ID";
998
                    $this->$relatedFieldName = $relObj->ID;
999
                    $relObj->flushCache();
1000
                } else {
1001
                    $class = static::class;
1002
                    user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
1003
                }
1004
            } else {
1005
                $this->$key = $value;
1006
            }
1007
        }
1008
        return $this;
1009
    }
1010
1011
    /**
1012
     * Pass changes as a map, and try to
1013
     * get automatic casting for these fields.
1014
     * Doesn't write to the database. To write the data,
1015
     * use the write() method.
1016
     *
1017
     * @param array $data A map of field name to data values to update.
1018
     * @return DataObject $this
1019
     */
1020
    public function castedUpdate($data)
1021
    {
1022
        foreach ($data as $k => $v) {
1023
            $this->setCastedField($k, $v);
1024
        }
1025
        return $this;
1026
    }
1027
1028
    /**
1029
     * Merges data and relations from another object of same class,
1030
     * without conflict resolution. Allows to specify which
1031
     * dataset takes priority in case its not empty.
1032
     * has_one-relations are just transferred with priority 'right'.
1033
     * has_many and many_many-relations are added regardless of priority.
1034
     *
1035
     * Caution: has_many/many_many relations are moved rather than duplicated,
1036
     * meaning they are not connected to the merged object any longer.
1037
     * Caution: Just saves updated has_many/many_many relations to the database,
1038
     * doesn't write the updated object itself (just writes the object-properties).
1039
     * Caution: Does not delete the merged object.
1040
     * Caution: Does now overwrite Created date on the original object.
1041
     *
1042
     * @param DataObject $rightObj
1043
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
1044
     * @param bool $includeRelations Merge any existing relations (optional)
1045
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
1046
     *                            Only applicable with $priority='right'. (optional)
1047
     * @return Boolean
1048
     */
1049
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
1050
    {
1051
        $leftObj = $this;
1052
1053
        if ($leftObj->ClassName != $rightObj->ClassName) {
1054
            // we can't merge similiar subclasses because they might have additional relations
1055
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
1056
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
1057
            return false;
1058
        }
1059
1060
        if (!$rightObj->ID) {
1061
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
1062
				to make sure all relations are transferred properly.').", E_USER_WARNING);
1063
            return false;
1064
        }
1065
1066
        // makes sure we don't merge data like ID or ClassName
1067
        $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1068
        foreach ($rightData as $key => $rightSpec) {
1069
            // Don't merge ID
1070
            if ($key === 'ID') {
1071
                continue;
1072
            }
1073
1074
            // Only merge relations if allowed
1075
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
1076
                continue;
1077
            }
1078
1079
            // don't merge conflicting values if priority is 'left'
1080
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
1081
                continue;
1082
            }
1083
1084
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
1085
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
1086
                continue;
1087
            }
1088
1089
            // TODO remove redundant merge of has_one fields
1090
            $leftObj->{$key} = $rightObj->{$key};
1091
        }
1092
1093
        // merge relations
1094
        if ($includeRelations) {
1095
            if ($manyMany = $this->manyMany()) {
1096
                foreach ($manyMany as $relationship => $class) {
1097
                    /** @var DataObject $leftComponents */
1098
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
1099
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
1100
                    if ($rightComponents && $rightComponents->exists()) {
1101
                        $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

1101
                        $leftComponents->/** @scrutinizer ignore-call */ 
1102
                                         addMany($rightComponents->column('ID'));
Loading history...
1102
                    }
1103
                    $leftComponents->write();
1104
                }
1105
            }
1106
1107
            if ($hasMany = $this->hasMany()) {
1108
                foreach ($hasMany as $relationship => $class) {
1109
                    $leftComponents = $leftObj->getComponents($relationship);
1110
                    $rightComponents = $rightObj->getComponents($relationship);
1111
                    if ($rightComponents && $rightComponents->exists()) {
1112
                        $leftComponents->addMany($rightComponents->column('ID'));
1113
                    }
1114
                    $leftComponents->write();
0 ignored issues
show
Bug introduced by
The method write() does not exist on SilverStripe\ORM\UnsavedRelationList. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

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

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

1114
                    $leftComponents->/** @scrutinizer ignore-call */ 
1115
                                     write();
Loading history...
1115
                }
1116
            }
1117
        }
1118
1119
        return true;
1120
    }
1121
1122
    /**
1123
     * Forces the record to think that all its data has changed.
1124
     * Doesn't write to the database. Force-change preseved until
1125
     * next write. Existing CHANGE_VALUE or CHANGE_STRICT values
1126
     * are preserved.
1127
     *
1128
     * @return $this
1129
     */
1130
    public function forceChange()
1131
    {
1132
        // Ensure lazy fields loaded
1133
        $this->loadLazyFields();
1134
1135
        // Populate the null values in record so that they actually get written
1136
        foreach (array_keys(static::getSchema()->fieldSpecs(static::class)) as $fieldName) {
1137
            if (!isset($this->record[$fieldName])) {
1138
                $this->record[$fieldName] = null;
1139
            }
1140
        }
1141
1142
        $this->changeForced = true;
1143
1144
        return $this;
1145
    }
1146
1147
    /**
1148
     * Validate the current object.
1149
     *
1150
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1151
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1152
     *
1153
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1154
     * and onAfterWrite() won't get called either.
1155
     *
1156
     * It is expected that you call validate() in your own application to test that an object is valid before
1157
     * attempting a write, and respond appropriately if it isn't.
1158
     *
1159
     * @see {@link ValidationResult}
1160
     * @return ValidationResult
1161
     */
1162
    public function validate()
1163
    {
1164
        $result = ValidationResult::create();
1165
        $this->extend('validate', $result);
1166
        return $result;
1167
    }
1168
1169
    /**
1170
     * Public accessor for {@see DataObject::validate()}
1171
     *
1172
     * @return ValidationResult
1173
     */
1174
    public function doValidate()
1175
    {
1176
        Deprecation::notice('5.0', 'Use validate');
1177
        return $this->validate();
1178
    }
1179
1180
    /**
1181
     * Event handler called before writing to the database.
1182
     * You can overload this to clean up or otherwise process data before writing it to the
1183
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1184
     *
1185
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1186
     *
1187
     * @uses DataExtension->onBeforeWrite()
1188
     */
1189
    protected function onBeforeWrite()
1190
    {
1191
        $this->brokenOnWrite = false;
1192
1193
        $dummy = null;
1194
        $this->extend('onBeforeWrite', $dummy);
1195
    }
1196
1197
    /**
1198
     * Event handler called after writing to the database.
1199
     * You can overload this to act upon changes made to the data after it is written.
1200
     * $this->changed will have a record
1201
     * database.  Don't forget to call parent::onAfterWrite(), though!
1202
     *
1203
     * @uses DataExtension->onAfterWrite()
1204
     */
1205
    protected function onAfterWrite()
1206
    {
1207
        $dummy = null;
1208
        $this->extend('onAfterWrite', $dummy);
1209
    }
1210
1211
    /**
1212
     * Find all objects that will be cascade deleted if this object is deleted
1213
     *
1214
     * Notes:
1215
     *   - If this object is versioned, objects will only be searched in the same stage as the given record.
1216
     *   - This will only be useful prior to deletion, as post-deletion this record will no longer exist.
1217
     *
1218
     * @param bool $recursive True if recursive
1219
     * @param ArrayList $list Optional list to add items to
1220
     * @return ArrayList list of objects
1221
     */
1222
    public function findCascadeDeletes($recursive = true, $list = null)
1223
    {
1224
        // Find objects in these relationships
1225
        return $this->findRelatedObjects('cascade_deletes', $recursive, $list);
1226
    }
1227
1228
    /**
1229
     * Event handler called before deleting from the database.
1230
     * You can overload this to clean up or otherwise process data before delete this
1231
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1232
     *
1233
     * @uses DataExtension->onBeforeDelete()
1234
     */
1235
    protected function onBeforeDelete()
1236
    {
1237
        $this->brokenOnDelete = false;
1238
1239
        $dummy = null;
1240
        $this->extend('onBeforeDelete', $dummy);
1241
1242
        // Cascade deletes
1243
        $deletes = $this->findCascadeDeletes(false);
1244
        foreach ($deletes as $delete) {
1245
            $delete->delete();
1246
        }
1247
    }
1248
1249
    protected function onAfterDelete()
1250
    {
1251
        $this->extend('onAfterDelete');
1252
    }
1253
1254
    /**
1255
     * Load the default values in from the self::$defaults array.
1256
     * Will traverse the defaults of the current class and all its parent classes.
1257
     * Called by the constructor when creating new records.
1258
     *
1259
     * @uses DataExtension->populateDefaults()
1260
     * @return DataObject $this
1261
     */
1262
    public function populateDefaults()
1263
    {
1264
        $classes = array_reverse(ClassInfo::ancestry($this));
1265
1266
        foreach ($classes as $class) {
1267
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1268
1269
            if ($defaults && !is_array($defaults)) {
1270
                user_error(
1271
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1272
                    E_USER_WARNING
1273
                );
1274
                $defaults = null;
1275
            }
1276
1277
            if ($defaults) {
1278
                foreach ($defaults as $fieldName => $fieldValue) {
1279
                    // SRM 2007-03-06: Stricter check
1280
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1281
                        $this->$fieldName = $fieldValue;
1282
                    }
1283
                    // Set many-many defaults with an array of ids
1284
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1285
                        /** @var ManyManyList $manyManyJoin */
1286
                        $manyManyJoin = $this->$fieldName();
1287
                        $manyManyJoin->setByIDList($fieldValue);
1288
                    }
1289
                }
1290
            }
1291
            if ($class == self::class) {
1292
                break;
1293
            }
1294
        }
1295
1296
        $this->extend('populateDefaults');
1297
        return $this;
1298
    }
1299
1300
    /**
1301
     * Determine validation of this object prior to write
1302
     *
1303
     * @return ValidationException Exception generated by this write, or null if valid
1304
     */
1305
    protected function validateWrite()
1306
    {
1307
        if ($this->ObsoleteClassName) {
1308
            return new ValidationException(
1309
                "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - " .
1310
                "you need to change the ClassName before you can write it"
1311
            );
1312
        }
1313
1314
        // Note: Validation can only be disabled at the global level, not per-model
1315
        if (DataObject::config()->uninherited('validation_enabled')) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1316
            $result = $this->validate();
1317
            if (!$result->isValid()) {
1318
                return new ValidationException($result);
1319
            }
1320
        }
1321
        return null;
1322
    }
1323
1324
    /**
1325
     * Prepare an object prior to write
1326
     *
1327
     * @throws ValidationException
1328
     */
1329
    protected function preWrite()
1330
    {
1331
        // Validate this object
1332
        if ($writeException = $this->validateWrite()) {
1333
            // Used by DODs to clean up after themselves, eg, Versioned
1334
            $this->invokeWithExtensions('onAfterSkippedWrite');
1335
            throw $writeException;
1336
        }
1337
1338
        // Check onBeforeWrite
1339
        $this->brokenOnWrite = true;
1340
        $this->onBeforeWrite();
1341
        if ($this->brokenOnWrite) {
1342
            user_error(static::class . " has a broken onBeforeWrite() function."
1343
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1344
        }
1345
    }
1346
1347
    /**
1348
     * Detects and updates all changes made to this object
1349
     *
1350
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1351
     * @return bool True if any changes are detected
1352
     */
1353
    protected function updateChanges($forceChanges = false)
1354
    {
1355
        if ($forceChanges) {
1356
            // Force changes, but only for loaded fields
1357
            foreach ($this->record as $field => $value) {
1358
                $this->changed[$field] = static::CHANGE_VALUE;
1359
            }
1360
            return true;
1361
        }
1362
        return $this->isChanged();
1363
    }
1364
1365
    /**
1366
     * Writes a subset of changes for a specific table to the given manipulation
1367
     *
1368
     * @param string $baseTable Base table
1369
     * @param string $now Timestamp to use for the current time
1370
     * @param bool $isNewRecord Whether this should be treated as a new record write
1371
     * @param array $manipulation Manipulation to write to
1372
     * @param string $class Class of table to manipulate
1373
     */
1374
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1375
    {
1376
        $schema = $this->getSchema();
1377
        $table = $schema->tableName($class);
1378
        $manipulation[$table] = array();
1379
1380
        $changed = $this->getChangedFields();
1381
1382
        // Extract records for this table
1383
        foreach ($this->record as $fieldName => $fieldValue) {
1384
            // we're not attempting to reset the BaseTable->ID
1385
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1386
            if (empty($changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1387
                continue;
1388
            }
1389
1390
            // Ensure this field pertains to this table
1391
            $specification = $schema->fieldSpec(
1392
                $class,
1393
                $fieldName,
1394
                DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
1395
            );
1396
            if (!$specification) {
1397
                continue;
1398
            }
1399
1400
            // if database column doesn't correlate to a DBField instance...
1401
            $fieldObj = $this->dbObject($fieldName);
1402
            if (!$fieldObj) {
1403
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1404
            }
1405
1406
            // Write to manipulation
1407
            $fieldObj->writeToManipulation($manipulation[$table]);
1408
        }
1409
1410
        // Ensure update of Created and LastEdited columns
1411
        if ($baseTable === $table) {
1412
            $manipulation[$table]['fields']['LastEdited'] = $now;
1413
            if ($isNewRecord) {
1414
                $manipulation[$table]['fields']['Created'] = empty($this->record['Created'])
1415
                    ? $now
1416
                    : $this->record['Created'];
1417
                $manipulation[$table]['fields']['ClassName'] = static::class;
1418
            }
1419
        }
1420
1421
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1422
        // attempt an update, as though it were a normal update.
1423
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1424
        $manipulation[$table]['class'] = $class;
1425
        if ($this->isInDB()) {
1426
            $manipulation[$table]['id'] = $this->record['ID'];
1427
        }
1428
    }
1429
1430
    /**
1431
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1432
     *
1433
     * Does nothing if an ID is already assigned for this record
1434
     *
1435
     * @param string $baseTable Base table
1436
     * @param string $now Timestamp to use for the current time
1437
     */
1438
    protected function writeBaseRecord($baseTable, $now)
1439
    {
1440
        // Generate new ID if not specified
1441
        if ($this->isInDB()) {
1442
            return;
1443
        }
1444
1445
        // Perform an insert on the base table
1446
        $manipulation = [];
1447
        $this->prepareManipulationTable($baseTable, $now, true, $manipulation, $this->baseClass());
1448
        DB::manipulate($manipulation);
1449
1450
        $this->changed['ID'] = self::CHANGE_VALUE;
1451
        $this->record['ID'] = DB::get_generated_id($baseTable);
1452
    }
1453
1454
    /**
1455
     * Generate and write the database manipulation for all changed fields
1456
     *
1457
     * @param string $baseTable Base table
1458
     * @param string $now Timestamp to use for the current time
1459
     * @param bool $isNewRecord If this is a new record
1460
     * @throws InvalidArgumentException
1461
     */
1462
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1463
    {
1464
        // Generate database manipulations for each class
1465
        $manipulation = array();
1466
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1467
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1468
        }
1469
1470
        // Allow extensions to extend this manipulation
1471
        $this->extend('augmentWrite', $manipulation);
1472
1473
        // New records have their insert into the base data table done first, so that they can pass the
1474
        // generated ID on to the rest of the manipulation
1475
        if ($isNewRecord) {
1476
            $manipulation[$baseTable]['command'] = 'update';
1477
        }
1478
1479
        // Make sure none of our field assignment are arrays
1480
        foreach ($manipulation as $tableManipulation) {
1481
            if (!isset($tableManipulation['fields'])) {
1482
                continue;
1483
            }
1484
            foreach ($tableManipulation['fields'] as $fieldName => $fieldValue) {
1485
                if (is_array($fieldValue)) {
1486
                    $dbObject = $this->dbObject($fieldName);
1487
                    // If the field allows non-scalar values we'll let it do dynamic assignments
1488
                    if ($dbObject && $dbObject->scalarValueOnly()) {
1489
                        throw new InvalidArgumentException(
1490
                            'DataObject::writeManipulation: parameterised field assignments are disallowed'
1491
                        );
1492
                    }
1493
                }
1494
            }
1495
        }
1496
1497
        // Perform the manipulation
1498
        DB::manipulate($manipulation);
1499
    }
1500
1501
    /**
1502
     * Writes all changes to this object to the database.
1503
     *  - It will insert a record whenever ID isn't set, otherwise update.
1504
     *  - All relevant tables will be updated.
1505
     *  - $this->onBeforeWrite() gets called beforehand.
1506
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1507
     *
1508
     * @uses DataExtension->augmentWrite()
1509
     *
1510
     * @param boolean $showDebug Show debugging information
1511
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1512
     * @param boolean $forceWrite Write to database even if there are no changes
1513
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1514
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1515
     *                                 {@link getManyManyComponents()} (Default: false)
1516
     * @return int The ID of the record
1517
     * @throws ValidationException Exception that can be caught and handled by the calling function
1518
     */
1519
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1520
    {
1521
        $now = DBDatetime::now()->Rfc2822();
1522
1523
        // Execute pre-write tasks
1524
        $this->preWrite();
1525
1526
        // Check if we are doing an update or an insert
1527
        $isNewRecord = !$this->isInDB() || $forceInsert;
1528
1529
        // Check changes exist, abort if there are none
1530
        $hasChanges = $this->updateChanges($isNewRecord);
1531
        if ($hasChanges || $forceWrite || $isNewRecord) {
1532
            // Ensure Created and LastEdited are populated
1533
            if (!isset($this->record['Created'])) {
1534
                $this->record['Created'] = $now;
1535
            }
1536
            $this->record['LastEdited'] = $now;
1537
1538
            // New records have their insert into the base data table done first, so that they can pass the
1539
            // generated primary key on to the rest of the manipulation
1540
            $baseTable = $this->baseTable();
1541
            $this->writeBaseRecord($baseTable, $now);
1542
1543
            // Write the DB manipulation for all changed fields
1544
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1545
1546
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1547
            $this->writeRelations();
1548
            $this->onAfterWrite();
1549
1550
            // Reset isChanged data
1551
            // DBComposites properly bound to the parent record will also have their isChanged value reset
1552
            $this->changed = [];
1553
            $this->changeForced = false;
1554
            $this->original = $this->record;
1555
        } else {
1556
            if ($showDebug) {
1557
                Debug::message("no changes for DataObject");
1558
            }
1559
1560
            // Used by DODs to clean up after themselves, eg, Versioned
1561
            $this->invokeWithExtensions('onAfterSkippedWrite');
1562
        }
1563
1564
        // Write relations as necessary
1565
        if ($writeComponents) {
1566
            $this->writeComponents(true);
1567
        }
1568
1569
        // Clears the cache for this object so get_one returns the correct object.
1570
        $this->flushCache();
1571
1572
        return $this->record['ID'];
1573
    }
1574
1575
    /**
1576
     * Writes cached relation lists to the database, if possible
1577
     */
1578
    public function writeRelations()
1579
    {
1580
        if (!$this->isInDB()) {
1581
            return;
1582
        }
1583
1584
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1585
        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...
1586
            foreach ($this->unsavedRelations as $name => $list) {
1587
                $list->changeToList($this->$name());
1588
            }
1589
            $this->unsavedRelations = array();
1590
        }
1591
    }
1592
1593
    /**
1594
     * Write the cached components to the database. Cached components could refer to two different instances of the
1595
     * same record.
1596
     *
1597
     * @param bool $recursive Recursively write components
1598
     * @return DataObject $this
1599
     */
1600
    public function writeComponents($recursive = false)
1601
    {
1602
        foreach ($this->components as $component) {
1603
            $component->write(false, false, false, $recursive);
1604
        }
1605
1606
        if ($join = $this->getJoin()) {
1607
            $join->write(false, false, false, $recursive);
1608
        }
1609
1610
        return $this;
1611
    }
1612
1613
    /**
1614
     * Delete this data object.
1615
     * $this->onBeforeDelete() gets called.
1616
     * Note that in Versioned objects, both Stage and Live will be deleted.
1617
     * @uses DataExtension->augmentSQL()
1618
     */
1619
    public function delete()
1620
    {
1621
        $this->brokenOnDelete = true;
1622
        $this->onBeforeDelete();
1623
        if ($this->brokenOnDelete) {
1624
            user_error(static::class . " has a broken onBeforeDelete() function."
1625
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1626
        }
1627
1628
        // Deleting a record without an ID shouldn't do anything
1629
        if (!$this->ID) {
1630
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1631
        }
1632
1633
        // TODO: This is quite ugly.  To improve:
1634
        //  - move the details of the delete code in the DataQuery system
1635
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1636
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1637
        $srcQuery = DataList::create(static::class)
1638
            ->filter('ID', $this->ID)
1639
            ->dataQuery()
1640
            ->query();
1641
        $queriedTables = $srcQuery->queriedTables();
1642
        $this->extend('updateDeleteTables', $queriedTables, $srcQuery);
1643
        foreach ($queriedTables as $table) {
1644
            $delete = SQLDelete::create("\"$table\"", array('"ID"' => $this->ID));
1645
            $this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
1646
            $delete->execute();
1647
        }
1648
        // Remove this item out of any caches
1649
        $this->flushCache();
1650
1651
        $this->onAfterDelete();
1652
1653
        $this->OldID = $this->ID;
1654
        $this->ID = 0;
1655
    }
1656
1657
    /**
1658
     * Delete the record with the given ID.
1659
     *
1660
     * @param string $className The class name of the record to be deleted
1661
     * @param int $id ID of record to be deleted
1662
     */
1663
    public static function delete_by_id($className, $id)
1664
    {
1665
        $obj = DataObject::get_by_id($className, $id);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1666
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1667
            $obj->delete();
1668
        } else {
1669
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1670
        }
1671
    }
1672
1673
    /**
1674
     * Get the class ancestry, including the current class name.
1675
     * The ancestry will be returned as an array of class names, where the 0th element
1676
     * will be the class that inherits directly from DataObject, and the last element
1677
     * will be the current class.
1678
     *
1679
     * @return array Class ancestry
1680
     */
1681
    public function getClassAncestry()
1682
    {
1683
        return ClassInfo::ancestry(static::class);
1684
    }
1685
1686
    /**
1687
     * Return a unary component object from a one to one relationship, as a DataObject.
1688
     * If no component is available, an 'empty component' will be returned for
1689
     * non-polymorphic relations, or for polymorphic relations with a class set.
1690
     *
1691
     * @param string $componentName Name of the component
1692
     * @return DataObject The component object. It's exact type will be that of the component.
1693
     * @throws Exception
1694
     */
1695
    public function getComponent($componentName)
1696
    {
1697
        if (isset($this->components[$componentName])) {
1698
            return $this->components[$componentName];
1699
        }
1700
1701
        $schema = static::getSchema();
1702
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1703
            $joinField = $componentName . 'ID';
1704
            $joinID = $this->getField($joinField);
1705
1706
            // Extract class name for polymorphic relations
1707
            if ($class === self::class) {
1708
                $class = $this->getField($componentName . 'Class');
1709
                if (empty($class)) {
1710
                    return null;
1711
                }
1712
            }
1713
1714
            if ($joinID) {
1715
                // Ensure that the selected object originates from the same stage, subsite, etc
1716
                $component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1717
                    ->filter('ID', $joinID)
1718
                    ->setDataQueryParam($this->getInheritableQueryParams())
1719
                    ->first();
1720
            }
1721
1722
            if (empty($component)) {
1723
                $component = Injector::inst()->create($class);
1724
            }
1725
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1726
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1727
            $joinID = $this->ID;
1728
1729
            if ($joinID) {
1730
                // Prepare filter for appropriate join type
1731
                if ($polymorphic) {
1732
                    $filter = array(
1733
                        "{$joinField}ID" => $joinID,
1734
                        "{$joinField}Class" => static::class,
1735
                    );
1736
                } else {
1737
                    $filter = array(
1738
                        $joinField => $joinID
1739
                    );
1740
                }
1741
1742
                // Ensure that the selected object originates from the same stage, subsite, etc
1743
                $component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1744
                    ->filter($filter)
1745
                    ->setDataQueryParam($this->getInheritableQueryParams())
1746
                    ->first();
1747
            }
1748
1749
            if (empty($component)) {
1750
                $component = Injector::inst()->create($class);
1751
                if ($polymorphic) {
1752
                    $component->{$joinField . 'ID'} = $this->ID;
1753
                    $component->{$joinField . 'Class'} = static::class;
1754
                } else {
1755
                    $component->$joinField = $this->ID;
1756
                }
1757
            }
1758
        } else {
1759
            throw new InvalidArgumentException(
1760
                "DataObject->getComponent(): Could not find component '$componentName'."
1761
            );
1762
        }
1763
1764
        $this->components[$componentName] = $component;
1765
        return $component;
1766
    }
1767
1768
    /**
1769
     * Assign an item to the given component
1770
     *
1771
     * @param string $componentName
1772
     * @param DataObject|null $item
1773
     * @return $this
1774
     */
1775
    public function setComponent($componentName, $item)
1776
    {
1777
        // Validate component
1778
        $schema = static::getSchema();
1779
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1780
            // Force item to be written if not by this point
1781
            // @todo This could be lazy-written in a beforeWrite hook, but force write here for simplicity
1782
            // https://github.com/silverstripe/silverstripe-framework/issues/7818
1783
            if ($item && !$item->isInDB()) {
1784
                $item->write();
1785
            }
1786
1787
            // Update local ID
1788
            $joinField = $componentName . 'ID';
1789
            $this->setField($joinField, $item ? $item->ID : null);
1790
            // Update Class (Polymorphic has_one)
1791
            // Extract class name for polymorphic relations
1792
            if ($class === self::class) {
1793
                $this->setField($componentName . 'Class', $item ? get_class($item) : null);
1794
            }
1795
        } 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...
1796
            if ($item) {
1797
                // For belongs_to, add to has_one on other component
1798
                $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1799
                if (!$polymorphic) {
1800
                    $joinField = substr($joinField, 0, -2);
1801
                }
1802
                $item->setComponent($joinField, $this);
1803
            }
1804
        } else {
1805
            throw new InvalidArgumentException(
1806
                "DataObject->setComponent(): Could not find component '$componentName'."
1807
            );
1808
        }
1809
1810
        $this->components[$componentName] = $item;
1811
        return $this;
1812
    }
1813
1814
    /**
1815
     * Returns a one-to-many relation as a HasManyList
1816
     *
1817
     * @param string $componentName Name of the component
1818
     * @param int|array $id Optional ID(s) for parent of this relation, if not the current record
1819
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1820
     */
1821
    public function getComponents($componentName, $id = null)
1822
    {
1823
        if (!isset($id)) {
1824
            $id = $this->ID;
1825
        }
1826
        $result = null;
1827
1828
        $schema = $this->getSchema();
1829
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1830
        if (!$componentClass) {
1831
            throw new InvalidArgumentException(sprintf(
1832
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1833
                $componentName,
1834
                static::class
1835
            ));
1836
        }
1837
1838
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1839
        if (!$id) {
1840
            if (!isset($this->unsavedRelations[$componentName])) {
1841
                $this->unsavedRelations[$componentName] =
1842
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1843
            }
1844
            return $this->unsavedRelations[$componentName];
1845
        }
1846
1847
        // Determine type and nature of foreign relation
1848
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1849
        /** @var HasManyList $result */
1850
        if ($polymorphic) {
1851
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
1852
        } else {
1853
            $result = HasManyList::create($componentClass, $joinField);
1854
        }
1855
1856
        return $result
1857
            ->setDataQueryParam($this->getInheritableQueryParams())
1858
            ->forForeignID($id);
1859
    }
1860
1861
    /**
1862
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1863
     *
1864
     * @param string $relationName Relation name.
1865
     * @return string Class name, or null if not found.
1866
     */
1867
    public function getRelationClass($relationName)
1868
    {
1869
        // Parse many_many
1870
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1871
        if ($manyManyComponent) {
1872
            return $manyManyComponent['childClass'];
1873
        }
1874
1875
        // Go through all relationship configuration fields.
1876
        $config = $this->config();
1877
        $candidates = array_merge(
1878
            ($relations = $config->get('has_one')) ? $relations : array(),
1879
            ($relations = $config->get('has_many')) ? $relations : array(),
1880
            ($relations = $config->get('belongs_to')) ? $relations : array()
1881
        );
1882
1883
        if (isset($candidates[$relationName])) {
1884
            $remoteClass = $candidates[$relationName];
1885
1886
            // If dot notation is present, extract just the first part that contains the class.
1887
            if (($fieldPos = strpos($remoteClass, '.')) !== false) {
1888
                return substr($remoteClass, 0, $fieldPos);
1889
            }
1890
1891
            // Otherwise just return the class
1892
            return $remoteClass;
1893
        }
1894
1895
        return null;
1896
    }
1897
1898
    /**
1899
     * Given a relation name, determine the relation type
1900
     *
1901
     * @param string $component Name of component
1902
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1903
     */
1904
    public function getRelationType($component)
1905
    {
1906
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1907
        $config = $this->config();
1908
        foreach ($types as $type) {
1909
            $relations = $config->get($type);
1910
            if ($relations && isset($relations[$component])) {
1911
                return $type;
1912
            }
1913
        }
1914
        return null;
1915
    }
1916
1917
    /**
1918
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1919
     * side of the relation.
1920
     *
1921
     * Notes on behaviour:
1922
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1923
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1924
     *  - Polymorphic relationships do not have two natural endpoints (only on one side)
1925
     *   and thus attempting to infer them will return nothing.
1926
     *  - Cannot be used on unsaved objects.
1927
     *
1928
     * @param string $remoteClass
1929
     * @param string $remoteRelation
1930
     * @return DataList|DataObject The component, either as a list or single object
1931
     * @throws BadMethodCallException
1932
     * @throws InvalidArgumentException
1933
     */
1934
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1935
    {
1936
        $remote = DataObject::singleton($remoteClass);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1937
        $class = $remote->getRelationClass($remoteRelation);
1938
        $schema = static::getSchema();
1939
1940
        // Validate arguments
1941
        if (!$this->isInDB()) {
1942
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1943
        }
1944
        if (empty($class)) {
1945
            throw new InvalidArgumentException(sprintf(
1946
                "%s invoked with invalid relation %s.%s",
1947
                __METHOD__,
1948
                $remoteClass,
1949
                $remoteRelation
1950
            ));
1951
        }
1952
        // If relation is polymorphic, do not infer recriprocal relationship
1953
        if ($class === self::class) {
1954
            return null;
1955
        }
1956
        if (!is_a($this, $class, true)) {
1957
            throw new InvalidArgumentException(sprintf(
1958
                "Relation %s on %s does not refer to objects of type %s",
1959
                $remoteRelation,
1960
                $remoteClass,
1961
                static::class
1962
            ));
1963
        }
1964
1965
        // Check the relation type to mock
1966
        $relationType = $remote->getRelationType($remoteRelation);
1967
        switch ($relationType) {
1968
            case 'has_one': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

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

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

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

Loading history...
1969
                // Mock has_many
1970
                $joinField = "{$remoteRelation}ID";
1971
                $componentClass = $schema->classForField($remoteClass, $joinField);
1972
                $result = HasManyList::create($componentClass, $joinField);
1973
                return $result
1974
                    ->setDataQueryParam($this->getInheritableQueryParams())
1975
                    ->forForeignID($this->ID);
1976
            }
1977
            case 'belongs_to':
1978
            case 'has_many': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

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

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

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

Loading history...
1979
                // These relations must have a has_one on the other end, so find it
1980
                $joinField = $schema->getRemoteJoinField(
1981
                    $remoteClass,
1982
                    $remoteRelation,
1983
                    $relationType,
1984
                    $polymorphic
1985
                );
1986
                // If relation is polymorphic, do not infer recriprocal relationship automatically
1987
                if ($polymorphic) {
1988
                    return null;
1989
                }
1990
                $joinID = $this->getField($joinField);
1991
                if (empty($joinID)) {
1992
                    return null;
1993
                }
1994
                // Get object by joined ID
1995
                return DataObject::get($remoteClass)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1996
                    ->filter('ID', $joinID)
1997
                    ->setDataQueryParam($this->getInheritableQueryParams())
1998
                    ->first();
1999
            }
2000
            case 'many_many':
2001
            case 'belongs_many_many': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

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

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

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

Loading history...
2002
                // Get components and extra fields from parent
2003
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
2004
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
2005
2006
                // Reverse parent and component fields and create an inverse ManyManyList
2007
                /** @var RelationList $result */
2008
                $result = Injector::inst()->create(
2009
                    $manyMany['relationClass'],
2010
                    $manyMany['parentClass'], // Substitute parent class for dataClass
2011
                    $manyMany['join'],
2012
                    $manyMany['parentField'], // Reversed parent / child field
2013
                    $manyMany['childField'], // Reversed parent / child field
2014
                    $extraFields,
2015
                    $manyMany['childClass'], // substitute child class for parentClass
2016
                    $remoteClass // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2017
                );
2018
                $this->extend('updateManyManyComponents', $result);
2019
2020
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2021
                // foreignID set elsewhere.
2022
                return $result
2023
                    ->setDataQueryParam($this->getInheritableQueryParams())
2024
                    ->forForeignID($this->ID);
2025
            }
2026
            default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

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

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

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

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

Loading history...
2027
                return null;
2028
            }
2029
        }
2030
    }
2031
2032
    /**
2033
     * Returns a many-to-many component, as a ManyManyList.
2034
     * @param string $componentName Name of the many-many component
2035
     * @param int|array $id Optional ID for parent of this relation, if not the current record
2036
     * @return ManyManyList|UnsavedRelationList The set of components
2037
     */
2038
    public function getManyManyComponents($componentName, $id = null)
2039
    {
2040
        if (!isset($id)) {
2041
            $id = $this->ID;
2042
        }
2043
        $schema = static::getSchema();
2044
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
2045
        if (!$manyManyComponent) {
2046
            throw new InvalidArgumentException(sprintf(
2047
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
2048
                $componentName,
2049
                static::class
2050
            ));
2051
        }
2052
2053
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
2054
        if (!$id) {
2055
            if (!isset($this->unsavedRelations[$componentName])) {
2056
                $this->unsavedRelations[$componentName] =
2057
                    new UnsavedRelationList(
2058
                        $manyManyComponent['parentClass'],
2059
                        $componentName,
2060
                        $manyManyComponent['childClass']
2061
                    );
2062
            }
2063
            return $this->unsavedRelations[$componentName];
2064
        }
2065
2066
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
2067
        /** @var RelationList $result */
2068
        $result = Injector::inst()->create(
2069
            $manyManyComponent['relationClass'],
2070
            $manyManyComponent['childClass'],
2071
            $manyManyComponent['join'],
2072
            $manyManyComponent['childField'],
2073
            $manyManyComponent['parentField'],
2074
            $extraFields,
2075
            $manyManyComponent['parentClass'],
2076
            static::class // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2077
        );
2078
2079
        // Store component data in query meta-data
2080
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
2081
            /** @var DataQuery $query */
2082
            $query->setQueryParam('Component.ExtraFields', $extraFields);
2083
        });
2084
2085
        // If we have a default sort set for our "join" then we should overwrite any default already set.
2086
        $joinSort = Config::inst()->get($manyManyComponent['join'], 'default_sort');
2087
        if (!empty($joinSort)) {
2088
            $result = $result->sort($joinSort);
2089
        }
2090
2091
        $this->extend('updateManyManyComponents', $result);
2092
2093
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2094
        // foreignID set elsewhere.
2095
        return $result
2096
            ->setDataQueryParam($this->getInheritableQueryParams())
2097
            ->forForeignID($id);
2098
    }
2099
2100
    /**
2101
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
2102
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
2103
     *
2104
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
2105
     *                          their classes.
2106
     */
2107
    public function hasOne()
2108
    {
2109
        return (array)$this->config()->get('has_one');
2110
    }
2111
2112
    /**
2113
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
2114
     * their class name will be returned.
2115
     *
2116
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2117
     *        the field data stripped off. It defaults to TRUE.
2118
     * @return string|array
2119
     */
2120
    public function belongsTo($classOnly = true)
2121
    {
2122
        $belongsTo = (array)$this->config()->get('belongs_to');
2123
        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...
2124
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
2125
        } else {
2126
            return $belongsTo ? $belongsTo : array();
2127
        }
2128
    }
2129
2130
    /**
2131
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2132
     * relationships and their classes will be returned.
2133
     *
2134
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2135
     *        the field data stripped off. It defaults to TRUE.
2136
     * @return string|array|false
2137
     */
2138
    public function hasMany($classOnly = true)
2139
    {
2140
        $hasMany = (array)$this->config()->get('has_many');
2141
        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...
2142
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2143
        } else {
2144
            return $hasMany ? $hasMany : array();
2145
        }
2146
    }
2147
2148
    /**
2149
     * Return the many-to-many extra fields specification.
2150
     *
2151
     * If you don't specify a component name, it returns all
2152
     * extra fields for all components available.
2153
     *
2154
     * @return array|null
2155
     */
2156
    public function manyManyExtraFields()
2157
    {
2158
        return $this->config()->get('many_many_extraFields');
2159
    }
2160
2161
    /**
2162
     * Return information about a many-to-many component.
2163
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2164
     * components are returned.
2165
     *
2166
     * @see DataObjectSchema::manyManyComponent()
2167
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2168
     */
2169
    public function manyMany()
2170
    {
2171
        $config = $this->config();
2172
        $manyManys = (array)$config->get('many_many');
2173
        $belongsManyManys = (array)$config->get('belongs_many_many');
2174
        $items = array_merge($manyManys, $belongsManyManys);
2175
        return $items;
2176
    }
2177
2178
    /**
2179
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
2180
     *
2181
     * This is experimental, and is currently only a Postgres-specific enhancement.
2182
     *
2183
     * @param string $class
2184
     * @return array|false
2185
     */
2186
    public function database_extensions($class)
2187
    {
2188
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2189
        if ($extensions) {
2190
            return $extensions;
2191
        } else {
2192
            return false;
2193
        }
2194
    }
2195
2196
    /**
2197
     * Generates a SearchContext to be used for building and processing
2198
     * a generic search form for properties on this object.
2199
     *
2200
     * @return SearchContext
2201
     */
2202
    public function getDefaultSearchContext()
2203
    {
2204
        return new SearchContext(
2205
            static::class,
2206
            $this->scaffoldSearchFields(),
2207
            $this->defaultSearchFilters()
2208
        );
2209
    }
2210
2211
    /**
2212
     * Determine which properties on the DataObject are
2213
     * searchable, and map them to their default {@link FormField}
2214
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2215
     *
2216
     * Some additional logic is included for switching field labels, based on
2217
     * how generic or specific the field type is.
2218
     *
2219
     * Used by {@link SearchContext}.
2220
     *
2221
     * @param array $_params
2222
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2223
     *   'restrictFields': Numeric array of a field name whitelist
2224
     * @return FieldList
2225
     */
2226
    public function scaffoldSearchFields($_params = null)
2227
    {
2228
        $params = array_merge(
2229
            array(
2230
                'fieldClasses' => false,
2231
                'restrictFields' => false
2232
            ),
2233
            (array)$_params
2234
        );
2235
        $fields = new FieldList();
2236
        foreach ($this->searchableFields() as $fieldName => $spec) {
2237
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
2238
                continue;
2239
            }
2240
2241
            // If a custom fieldclass is provided as a string, use it
2242
            $field = null;
2243
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2244
                $fieldClass = $params['fieldClasses'][$fieldName];
2245
                $field = new $fieldClass($fieldName);
2246
            // If we explicitly set a field, then construct that
2247
            } elseif (isset($spec['field'])) {
2248
                // If it's a string, use it as a class name and construct
2249
                if (is_string($spec['field'])) {
2250
                    $fieldClass = $spec['field'];
2251
                    $field = new $fieldClass($fieldName);
2252
2253
                // If it's a FormField object, then just use that object directly.
2254
                } elseif ($spec['field'] instanceof FormField) {
2255
                    $field = $spec['field'];
2256
2257
                // Otherwise we have a bug
2258
                } else {
2259
                    user_error("Bad value for searchable_fields, 'field' value: "
2260
                        . var_export($spec['field'], true), E_USER_WARNING);
2261
                }
2262
2263
            // Otherwise, use the database field's scaffolder
2264
            } elseif ($object = $this->relObject($fieldName)) {
2265
                if (is_object($object) && $object->hasMethod('scaffoldSearchField')) {
2266
                    $field = $object->scaffoldSearchField();
2267
                } else {
2268
                    throw new Exception(sprintf(
2269
                        "SearchField '%s' on '%s' does not return a valid DBField instance.",
2270
                        $fieldName,
2271
                        get_class($this)
2272
                    ));
2273
                }
2274
            }
2275
2276
            // Allow fields to opt out of search
2277
            if (!$field) {
2278
                continue;
2279
            }
2280
2281
            if (strstr($fieldName, '.')) {
2282
                $field->setName(str_replace('.', '__', $fieldName));
2283
            }
2284
            $field->setTitle($spec['title']);
2285
2286
            $fields->push($field);
2287
        }
2288
        return $fields;
2289
    }
2290
2291
    /**
2292
     * Scaffold a simple edit form for all properties on this dataobject,
2293
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2294
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2295
     *
2296
     * @uses FormScaffolder
2297
     *
2298
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2299
     * @return FieldList
2300
     */
2301
    public function scaffoldFormFields($_params = null)
2302
    {
2303
        $params = array_merge(
2304
            array(
2305
                'tabbed' => false,
2306
                'includeRelations' => false,
2307
                'restrictFields' => false,
2308
                'fieldClasses' => false,
2309
                'ajaxSafe' => false
2310
            ),
2311
            (array)$_params
2312
        );
2313
2314
        $fs = FormScaffolder::create($this);
2315
        $fs->tabbed = $params['tabbed'];
2316
        $fs->includeRelations = $params['includeRelations'];
2317
        $fs->restrictFields = $params['restrictFields'];
2318
        $fs->fieldClasses = $params['fieldClasses'];
2319
        $fs->ajaxSafe = $params['ajaxSafe'];
2320
2321
        return $fs->getFieldList();
2322
    }
2323
2324
    /**
2325
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2326
     * being called on extensions
2327
     *
2328
     * @param callable $callback The callback to execute
2329
     */
2330
    protected function beforeUpdateCMSFields($callback)
2331
    {
2332
        $this->beforeExtending('updateCMSFields', $callback);
2333
    }
2334
2335
    /**
2336
     * Centerpiece of every data administration interface in Silverstripe,
2337
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2338
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2339
     * generate this set. To customize, overload this method in a subclass
2340
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2341
     *
2342
     * <code>
2343
     * class MyCustomClass extends DataObject {
2344
     *  static $db = array('CustomProperty'=>'Boolean');
2345
     *
2346
     *  function getCMSFields() {
2347
     *    $fields = parent::getCMSFields();
2348
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2349
     *    return $fields;
2350
     *  }
2351
     * }
2352
     * </code>
2353
     *
2354
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2355
     *
2356
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2357
     */
2358
    public function getCMSFields()
2359
    {
2360
        $tabbedFields = $this->scaffoldFormFields(array(
2361
            // Don't allow has_many/many_many relationship editing before the record is first saved
2362
            'includeRelations' => ($this->ID > 0),
2363
            'tabbed' => true,
2364
            'ajaxSafe' => true
2365
        ));
2366
2367
        $this->extend('updateCMSFields', $tabbedFields);
2368
2369
        return $tabbedFields;
2370
    }
2371
2372
    /**
2373
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2374
     * including that dataobject's extensions customised actions could be added to the EditForm.
2375
     *
2376
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2377
     */
2378
    public function getCMSActions()
2379
    {
2380
        $actions = new FieldList();
2381
        $this->extend('updateCMSActions', $actions);
2382
        return $actions;
2383
    }
2384
2385
2386
    /**
2387
     * Used for simple frontend forms without relation editing
2388
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2389
     * by default. To customize, either overload this method in your
2390
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2391
     *
2392
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2393
     *
2394
     * @param array $params See {@link scaffoldFormFields()}
2395
     * @return FieldList Always returns a simple field collection without TabSet.
2396
     */
2397
    public function getFrontEndFields($params = null)
2398
    {
2399
        $untabbedFields = $this->scaffoldFormFields($params);
2400
        $this->extend('updateFrontEndFields', $untabbedFields);
2401
2402
        return $untabbedFields;
2403
    }
2404
2405
    public function getViewerTemplates($suffix = '')
2406
    {
2407
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2408
    }
2409
2410
    /**
2411
     * Gets the value of a field.
2412
     * Called by {@link __get()} and any getFieldName() methods you might create.
2413
     *
2414
     * @param string $field The name of the field
2415
     * @return mixed The field value
2416
     */
2417
    public function getField($field)
2418
    {
2419
        // If we already have a value in $this->record, then we should just return that
2420
        if (isset($this->record[$field])) {
2421
            return $this->record[$field];
2422
        }
2423
2424
        // Do we have a field that needs to be lazy loaded?
2425
        if (isset($this->record[$field . '_Lazy'])) {
2426
            $tableClass = $this->record[$field . '_Lazy'];
2427
            $this->loadLazyFields($tableClass);
2428
        }
2429
        $schema = static::getSchema();
2430
2431
        // Support unary relations as fields
2432
        if ($schema->unaryComponent(static::class, $field)) {
2433
            return $this->getComponent($field);
2434
        }
2435
2436
        // In case of complex fields, return the DBField object
2437
        if ($schema->compositeField(static::class, $field)) {
2438
            $this->record[$field] = $this->dbObject($field);
2439
        }
2440
2441
        return isset($this->record[$field]) ? $this->record[$field] : null;
2442
    }
2443
2444
    /**
2445
     * Loads all the stub fields that an initial lazy load didn't load fully.
2446
     *
2447
     * @param string $class Class to load the values from. Others are joined as required.
2448
     * Not specifying a tableClass will load all lazy fields from all tables.
2449
     * @return bool Flag if lazy loading succeeded
2450
     */
2451
    protected function loadLazyFields($class = null)
2452
    {
2453
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2454
            return false;
2455
        }
2456
2457
        if (!$class) {
2458
            $loaded = array();
2459
2460
            foreach ($this->record as $key => $value) {
2461
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2462
                    $this->loadLazyFields($value);
2463
                    $loaded[$value] = $value;
2464
                }
2465
            }
2466
2467
            return false;
2468
        }
2469
2470
        $dataQuery = new DataQuery($class);
2471
2472
        // Reset query parameter context to that of this DataObject
2473
        if ($params = $this->getSourceQueryParams()) {
2474
            foreach ($params as $key => $value) {
2475
                $dataQuery->setQueryParam($key, $value);
2476
            }
2477
        }
2478
2479
        // Limit query to the current record, unless it has the Versioned extension,
2480
        // in which case it requires special handling through augmentLoadLazyFields()
2481
        $schema = static::getSchema();
2482
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2483
        $dataQuery->where([
2484
            $baseIDColumn => $this->record['ID']
2485
        ])->limit(1);
2486
2487
        $columns = array();
2488
2489
        // Add SQL for fields, both simple & multi-value
2490
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2491
        $databaseFields = $schema->databaseFields($class, false);
2492
        foreach ($databaseFields as $k => $v) {
2493
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2494
                $columns[] = $k;
2495
            }
2496
        }
2497
2498
        if ($columns) {
2499
            $query = $dataQuery->query();
2500
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2501
            $this->extend('augmentSQL', $query, $dataQuery);
2502
2503
            $dataQuery->setQueriedColumns($columns);
2504
            $newData = $dataQuery->execute()->record();
2505
2506
            // Load the data into record
2507
            if ($newData) {
2508
                foreach ($newData as $k => $v) {
2509
                    if (in_array($k, $columns)) {
2510
                        $this->record[$k] = $v;
2511
                        $this->original[$k] = $v;
2512
                        unset($this->record[$k . '_Lazy']);
2513
                    }
2514
                }
2515
2516
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2517
            } else {
2518
                foreach ($columns as $k) {
2519
                    $this->record[$k] = null;
2520
                    $this->original[$k] = null;
2521
                    unset($this->record[$k . '_Lazy']);
2522
                }
2523
            }
2524
        }
2525
        return true;
2526
    }
2527
2528
    /**
2529
     * Return the fields that have changed since the last write.
2530
     *
2531
     * The change level affects what the functions defines as "changed":
2532
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2533
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2534
     *   for example a change from 0 to null would not be included.
2535
     *
2536
     * Example return:
2537
     * <code>
2538
     * array(
2539
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2540
     * )
2541
     * </code>
2542
     *
2543
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2544
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2545
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2546
     * @return array
2547
     */
2548
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2549
    {
2550
        $changedFields = array();
2551
2552
        // Update the changed array with references to changed obj-fields
2553
        foreach ($this->record as $k => $v) {
2554
            // Prevents DBComposite infinite looping on isChanged
2555
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2556
                continue;
2557
            }
2558
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2559
                $this->changed[$k] = self::CHANGE_VALUE;
2560
            }
2561
        }
2562
2563
        // If change was forced, then derive change data from $this->record
2564
        if ($this->changeForced && $changeLevel <= self::CHANGE_STRICT) {
2565
            $changed = array_combine(
2566
                array_keys($this->record),
2567
                array_fill(0, count($this->record), self::CHANGE_STRICT)
2568
            );
2569
            // @todo Find better way to allow versioned to write a new version after forceChange
2570
            unset($changed['Version']);
2571
        } else {
2572
            $changed = $this->changed;
2573
        }
2574
2575
        if (is_array($databaseFieldsOnly)) {
2576
            $fields = array_intersect_key($changed, array_flip($databaseFieldsOnly));
2577
        } elseif ($databaseFieldsOnly) {
2578
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2579
            $fields = array_intersect_key($changed, $fieldsSpecs);
2580
        } else {
2581
            $fields = $changed;
2582
        }
2583
2584
        // Filter the list to those of a certain change level
2585
        if ($changeLevel > self::CHANGE_STRICT) {
2586
            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...
2587
                foreach ($fields as $name => $level) {
2588
                    if ($level < $changeLevel) {
2589
                        unset($fields[$name]);
2590
                    }
2591
                }
2592
            }
2593
        }
2594
2595
        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...
2596
            foreach ($fields as $name => $level) {
2597
                $changedFields[$name] = array(
2598
                    'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2599
                    'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2600
                    'level' => $level
2601
                );
2602
            }
2603
        }
2604
2605
        return $changedFields;
2606
    }
2607
2608
    /**
2609
     * Uses {@link getChangedFields()} to determine if fields have been changed
2610
     * since loading them from the database.
2611
     *
2612
     * @param string $fieldName Name of the database field to check, will check for any if not given
2613
     * @param int $changeLevel See {@link getChangedFields()}
2614
     * @return boolean
2615
     */
2616
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2617
    {
2618
        $fields = $fieldName ? array($fieldName) : true;
2619
        $changed = $this->getChangedFields($fields, $changeLevel);
2620
        if (!isset($fieldName)) {
2621
            return !empty($changed);
2622
        } else {
2623
            return array_key_exists($fieldName, $changed);
2624
        }
2625
    }
2626
2627
    /**
2628
     * Set the value of the field
2629
     * Called by {@link __set()} and any setFieldName() methods you might create.
2630
     *
2631
     * @param string $fieldName Name of the field
2632
     * @param mixed $val New field value
2633
     * @return $this
2634
     */
2635
    public function setField($fieldName, $val)
2636
    {
2637
        $this->objCacheClear();
2638
        //if it's a has_one component, destroy the cache
2639
        if (substr($fieldName, -2) == 'ID') {
2640
            unset($this->components[substr($fieldName, 0, -2)]);
2641
        }
2642
2643
        // If we've just lazy-loaded the column, then we need to populate the $original array
2644
        if (isset($this->record[$fieldName . '_Lazy'])) {
2645
            $tableClass = $this->record[$fieldName . '_Lazy'];
2646
            $this->loadLazyFields($tableClass);
2647
        }
2648
2649
        // Support component assignent via field setter
2650
        $schema = static::getSchema();
2651
        if ($schema->unaryComponent(static::class, $fieldName)) {
2652
            unset($this->components[$fieldName]);
2653
            // Assign component directly
2654
            if (is_null($val) || $val instanceof DataObject) {
2655
                return $this->setComponent($fieldName, $val);
2656
            }
2657
            // Assign by ID instead of object
2658
            if (is_numeric($val)) {
2659
                $fieldName .= 'ID';
2660
            }
2661
        }
2662
2663
        // Situation 1: Passing an DBField
2664
        if ($val instanceof DBField) {
2665
            $val->setName($fieldName);
2666
            $val->saveInto($this);
2667
2668
            // Situation 1a: Composite fields should remain bound in case they are
2669
            // later referenced to update the parent dataobject
2670
            if ($val instanceof DBComposite) {
2671
                $val->bindTo($this);
2672
                $this->record[$fieldName] = $val;
2673
            }
2674
        // Situation 2: Passing a literal or non-DBField object
2675
        } else {
2676
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2677
            if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) {
2678
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2679
            }
2680
2681
            if (!empty($val) && !is_scalar($val)) {
2682
                $dbField = $this->dbObject($fieldName);
2683
                if ($dbField && $dbField->scalarValueOnly()) {
2684
                    throw new InvalidArgumentException(
2685
                        sprintf(
2686
                            'DataObject::setField: %s only accepts scalars',
2687
                            $fieldName
2688
                        )
2689
                    );
2690
                }
2691
            }
2692
2693
            // if a field is not existing or has strictly changed
2694
            if (!array_key_exists($fieldName, $this->original) || $this->original[$fieldName] !== $val) {
2695
                // TODO Add check for php-level defaults which are not set in the db
2696
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2697
                // At the very least, the type has changed
2698
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2699
2700
                if ((!array_key_exists($fieldName, $this->original) && $val)
2701
                    || (array_key_exists($fieldName, $this->original) && $this->original[$fieldName] != $val)
2702
                ) {
2703
                    // Value has changed as well, not just the type
2704
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2705
                }
2706
            // Value has been restored to its original, remove any record of the change
2707
            } elseif (isset($this->changed[$fieldName])) {
2708
                unset($this->changed[$fieldName]);
2709
            }
2710
2711
            // Value is saved regardless, since the change detection relates to the last write
2712
            $this->record[$fieldName] = $val;
2713
        }
2714
        return $this;
2715
    }
2716
2717
    /**
2718
     * Set the value of the field, using a casting object.
2719
     * This is useful when you aren't sure that a date is in SQL format, for example.
2720
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2721
     * can be saved into the Image table.
2722
     *
2723
     * @param string $fieldName Name of the field
2724
     * @param mixed $value New field value
2725
     * @return $this
2726
     */
2727
    public function setCastedField($fieldName, $value)
2728
    {
2729
        if (!$fieldName) {
2730
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2731
        }
2732
        $fieldObj = $this->dbObject($fieldName);
2733
        if ($fieldObj) {
0 ignored issues
show
introduced by
$fieldObj is of type SilverStripe\ORM\FieldType\DBField, thus it always evaluated to true.
Loading history...
2734
            $fieldObj->setValue($value);
2735
            $fieldObj->saveInto($this);
2736
        } else {
2737
            $this->$fieldName = $value;
2738
        }
2739
        return $this;
2740
    }
2741
2742
    /**
2743
     * {@inheritdoc}
2744
     */
2745
    public function castingHelper($field)
2746
    {
2747
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2748
        if ($fieldSpec) {
2749
            return $fieldSpec;
2750
        }
2751
2752
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2753
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2754
        $queryParams = $this->getSourceQueryParams();
2755
        if (!empty($queryParams['Component.ExtraFields'])) {
2756
            $extraFields = $queryParams['Component.ExtraFields'];
2757
2758
            if (isset($extraFields[$field])) {
2759
                return $extraFields[$field];
2760
            }
2761
        }
2762
2763
        return parent::castingHelper($field);
2764
    }
2765
2766
    /**
2767
     * Returns true if the given field exists in a database column on any of
2768
     * the objects tables and optionally look up a dynamic getter with
2769
     * get<fieldName>().
2770
     *
2771
     * @param string $field Name of the field
2772
     * @return boolean True if the given field exists
2773
     */
2774
    public function hasField($field)
2775
    {
2776
        $schema = static::getSchema();
2777
        return (
2778
            array_key_exists($field, $this->record)
2779
            || array_key_exists($field, $this->components)
2780
            || $schema->fieldSpec(static::class, $field)
2781
            || $schema->unaryComponent(static::class, $field)
2782
            || $this->hasMethod("get{$field}")
2783
        );
2784
    }
2785
2786
    /**
2787
     * Returns true if the given field exists as a database column
2788
     *
2789
     * @param string $field Name of the field
2790
     *
2791
     * @return boolean
2792
     */
2793
    public function hasDatabaseField($field)
2794
    {
2795
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2796
        return !empty($spec);
2797
    }
2798
2799
    /**
2800
     * Returns true if the member is allowed to do the given action.
2801
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2802
     *
2803
     * @param string $perm The permission to be checked, such as 'View'.
2804
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2805
     * in user.
2806
     * @param array $context Additional $context to pass to extendedCan()
2807
     *
2808
     * @return boolean True if the the member is allowed to do the given action
2809
     */
2810
    public function can($perm, $member = null, $context = array())
2811
    {
2812
        if (!$member) {
2813
            $member = Security::getCurrentUser();
2814
        }
2815
2816
        if ($member && Permission::checkMember($member, "ADMIN")) {
2817
            return true;
2818
        }
2819
2820
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2821
            $method = 'can' . ucfirst($perm);
2822
            return $this->$method($member);
2823
        }
2824
2825
        $results = $this->extendedCan('can', $member);
2826
        if (isset($results)) {
2827
            return $results;
2828
        }
2829
2830
        return ($member && Permission::checkMember($member, $perm));
2831
    }
2832
2833
    /**
2834
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2835
     * expected to return one of three values:
2836
     *
2837
     *  - false: Disallow this permission, regardless of what other extensions say
2838
     *  - true: Allow this permission, as long as no other extensions return false
2839
     *  - NULL: Don't affect the outcome
2840
     *
2841
     * This method itself returns a tri-state value, and is designed to be used like this:
2842
     *
2843
     * <code>
2844
     * $extended = $this->extendedCan('canDoSomething', $member);
2845
     * if($extended !== null) return $extended;
2846
     * else return $normalValue;
2847
     * </code>
2848
     *
2849
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2850
     * @param Member|int $member
2851
     * @param array $context Optional context
2852
     * @return boolean|null
2853
     */
2854
    public function extendedCan($methodName, $member, $context = array())
2855
    {
2856
        $results = $this->extend($methodName, $member, $context);
2857
        if ($results && is_array($results)) {
2858
            // Remove NULLs
2859
            $results = array_filter($results, function ($v) {
2860
                return !is_null($v);
2861
            });
2862
            // If there are any non-NULL responses, then return the lowest one of them.
2863
            // If any explicitly deny the permission, then we don't get access
2864
            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...
2865
                return min($results);
2866
            }
2867
        }
2868
        return null;
2869
    }
2870
2871
    /**
2872
     * @param Member $member
2873
     * @return boolean
2874
     */
2875
    public function canView($member = null)
2876
    {
2877
        $extended = $this->extendedCan(__FUNCTION__, $member);
2878
        if ($extended !== null) {
2879
            return $extended;
2880
        }
2881
        return Permission::check('ADMIN', 'any', $member);
2882
    }
2883
2884
    /**
2885
     * @param Member $member
2886
     * @return boolean
2887
     */
2888
    public function canEdit($member = null)
2889
    {
2890
        $extended = $this->extendedCan(__FUNCTION__, $member);
2891
        if ($extended !== null) {
2892
            return $extended;
2893
        }
2894
        return Permission::check('ADMIN', 'any', $member);
2895
    }
2896
2897
    /**
2898
     * @param Member $member
2899
     * @return boolean
2900
     */
2901
    public function canDelete($member = null)
2902
    {
2903
        $extended = $this->extendedCan(__FUNCTION__, $member);
2904
        if ($extended !== null) {
2905
            return $extended;
2906
        }
2907
        return Permission::check('ADMIN', 'any', $member);
2908
    }
2909
2910
    /**
2911
     * @param Member $member
2912
     * @param array $context Additional context-specific data which might
2913
     * affect whether (or where) this object could be created.
2914
     * @return boolean
2915
     */
2916
    public function canCreate($member = null, $context = array())
2917
    {
2918
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2919
        if ($extended !== null) {
2920
            return $extended;
2921
        }
2922
        return Permission::check('ADMIN', 'any', $member);
2923
    }
2924
2925
    /**
2926
     * Debugging used by Debug::show()
2927
     *
2928
     * @return string HTML data representing this object
2929
     */
2930
    public function debug()
2931
    {
2932
        $class = static::class;
2933
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2934
        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...
2935
            foreach ($this->record as $fieldName => $fieldVal) {
2936
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2937
            }
2938
        }
2939
        $val .= "</ul>\n";
2940
        return $val;
2941
    }
2942
2943
    /**
2944
     * Return the DBField object that represents the given field.
2945
     * This works similarly to obj() with 2 key differences:
2946
     *   - it still returns an object even when the field has no value.
2947
     *   - it only matches fields and not methods
2948
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2949
     *
2950
     * @param string $fieldName Name of the field
2951
     * @return DBField The field as a DBField object
2952
     */
2953
    public function dbObject($fieldName)
2954
    {
2955
        // Check for field in DB
2956
        $schema = static::getSchema();
2957
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2958
        if (!$helper) {
2959
            return null;
2960
        }
2961
2962
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2963
            $tableClass = $this->record[$fieldName . '_Lazy'];
2964
            $this->loadLazyFields($tableClass);
2965
        }
2966
2967
        $value = isset($this->record[$fieldName])
2968
            ? $this->record[$fieldName]
2969
            : null;
2970
2971
        // If we have a DBField object in $this->record, then return that
2972
        if ($value instanceof DBField) {
2973
            return $value;
2974
        }
2975
2976
        list($class, $spec) = explode('.', $helper);
2977
        /** @var DBField $obj */
2978
        $table = $schema->tableName($class);
2979
        $obj = Injector::inst()->create($spec, $fieldName);
2980
        $obj->setTable($table);
2981
        $obj->setValue($value, $this, false);
2982
        return $obj;
2983
    }
2984
2985
    /**
2986
     * Traverses to a DBField referenced by relationships between data objects.
2987
     *
2988
     * The path to the related field is specified with dot separated syntax
2989
     * (eg: Parent.Child.Child.FieldName).
2990
     *
2991
     * If a relation is blank, this will return null instead.
2992
     * If a relation name is invalid (e.g. non-relation on a parent) this
2993
     * can throw a LogicException.
2994
     *
2995
     * @param string $fieldPath List of paths on this object. All items in this path
2996
     * must be ViewableData implementors
2997
     *
2998
     * @return mixed DBField of the field on the object or a DataList instance.
2999
     * @throws LogicException If accessing invalid relations
3000
     */
3001
    public function relObject($fieldPath)
3002
    {
3003
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
3004
        $component = $this;
3005
3006
        // Parse all relations
3007
        foreach (explode('.', $fieldPath) as $relation) {
3008
            if (!$component) {
3009
                return null;
3010
            }
3011
3012
            // Inspect relation type
3013
            if (ClassInfo::hasMethod($component, $relation)) {
3014
                $component = $component->$relation();
3015
            } elseif ($component instanceof Relation || $component instanceof DataList) {
3016
                // $relation could either be a field (aggregate), or another relation
3017
                $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

3017
                $singleton = DataObject::singleton($component->/** @scrutinizer ignore-call */ dataClass());
Loading history...
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3018
                $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

3018
                $component = $singleton->dbObject($relation) ?: $component->/** @scrutinizer ignore-call */ relation($relation);
Loading history...
3019
            } elseif ($component instanceof DataObject && ($dbObject = $component->dbObject($relation))) {
3020
                $component = $dbObject;
3021
            } elseif ($component instanceof ViewableData && $component->hasField($relation)) {
3022
                $component = $component->obj($relation);
3023
            } else {
3024
                throw new LogicException(
3025
                    "$relation is not a relation/field on " . get_class($component)
3026
                );
3027
            }
3028
        }
3029
        return $component;
3030
    }
3031
3032
    /**
3033
     * Traverses to a field referenced by relationships between data objects, returning the value
3034
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3035
     *
3036
     * @param string $fieldName string
3037
     * @return mixed Will return null on a missing value
3038
     */
3039
    public function relField($fieldName)
3040
    {
3041
        // Navigate to relative parent using relObject() if needed
3042
        $component = $this;
3043
        if (($pos = strrpos($fieldName, '.')) !== false) {
3044
            $relation = substr($fieldName, 0, $pos);
3045
            $fieldName = substr($fieldName, $pos + 1);
3046
            $component = $this->relObject($relation);
3047
        }
3048
3049
        // Bail if the component is null
3050
        if (!$component) {
3051
            return null;
3052
        }
3053
        if (ClassInfo::hasMethod($component, $fieldName)) {
3054
            return $component->$fieldName();
3055
        }
3056
        return $component->$fieldName;
3057
    }
3058
3059
    /**
3060
     * Temporary hack to return an association name, based on class, to get around the mangle
3061
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3062
     *
3063
     * @param string $className
3064
     * @return string
3065
     */
3066
    public function getReverseAssociation($className)
3067
    {
3068
        if (is_array($this->manyMany())) {
0 ignored issues
show
introduced by
The condition is_array($this->manyMany()) is always true.
Loading history...
3069
            $many_many = array_flip($this->manyMany());
3070
            if (array_key_exists($className, $many_many)) {
3071
                return $many_many[$className];
3072
            }
3073
        }
3074
        if (is_array($this->hasMany())) {
0 ignored issues
show
introduced by
The condition is_array($this->hasMany()) is always true.
Loading history...
3075
            $has_many = array_flip($this->hasMany());
3076
            if (array_key_exists($className, $has_many)) {
3077
                return $has_many[$className];
3078
            }
3079
        }
3080
        if (is_array($this->hasOne())) {
0 ignored issues
show
introduced by
The condition is_array($this->hasOne()) is always true.
Loading history...
3081
            $has_one = array_flip($this->hasOne());
3082
            if (array_key_exists($className, $has_one)) {
3083
                return $has_one[$className];
3084
            }
3085
        }
3086
3087
        return false;
3088
    }
3089
3090
    /**
3091
     * Return all objects matching the filter
3092
     * sub-classes are automatically selected and included
3093
     *
3094
     * @param string $callerClass The class of objects to be returned
3095
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3096
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3097
     * @param string|array $sort A sort expression to be inserted into the ORDER
3098
     * BY clause.  If omitted, self::$default_sort will be used.
3099
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3100
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3101
     * @param string $containerClass The container class to return the results in.
3102
     *
3103
     * @todo $containerClass is Ignored, why?
3104
     *
3105
     * @return DataList The objects matching the filter, in the class specified by $containerClass
3106
     */
3107
    public static function get(
3108
        $callerClass = null,
3109
        $filter = "",
3110
        $sort = "",
3111
        $join = "",
3112
        $limit = null,
3113
        $containerClass = DataList::class
3114
    ) {
3115
        // Validate arguments
3116
        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...
3117
            $callerClass = get_called_class();
3118
            if ($callerClass === self::class) {
3119
                throw new InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3120
            }
3121
            if ($filter || $sort || $join || $limit || ($containerClass !== DataList::class)) {
3122
                throw new InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3123
                    . ' arguments');
3124
            }
3125
        } elseif ($callerClass === self::class) {
3126
            throw new InvalidArgumentException('DataObject::get() cannot query non-subclass DataObject directly');
3127
        }
3128
        if ($join) {
3129
            throw new InvalidArgumentException(
3130
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3131
            );
3132
        }
3133
3134
        // Build and decorate with args
3135
        $result = DataList::create($callerClass);
3136
        if ($filter) {
3137
            $result = $result->where($filter);
3138
        }
3139
        if ($sort) {
3140
            $result = $result->sort($sort);
3141
        }
3142
        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

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

3143
            $limitArguments = explode(',', /** @scrutinizer ignore-type */ $limit);
Loading history...
3144
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
3145
        } elseif ($limit) {
3146
            $result = $result->limit($limit);
3147
        }
3148
3149
        return $result;
3150
    }
3151
3152
3153
    /**
3154
     * Return the first item matching the given query.
3155
     * All calls to get_one() are cached.
3156
     *
3157
     * @param string $callerClass The class of objects to be returned
3158
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3159
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3160
     * @param boolean $cache Use caching
3161
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3162
     *
3163
     * @return DataObject|null The first item matching the query
3164
     */
3165
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
3166
    {
3167
        $SNG = singleton($callerClass);
3168
3169
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3170
        $cacheKey = md5(serialize($cacheComponents));
3171
3172
        $item = null;
3173
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3174
            $dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3175
            $item = $dl->first();
3176
3177
            if ($cache) {
3178
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3179
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
3180
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
3181
                }
3182
            }
3183
        }
3184
3185
        if ($cache) {
3186
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
3187
        }
3188
3189
        return $item;
3190
    }
3191
3192
    /**
3193
     * Flush the cached results for all relations (has_one, has_many, many_many)
3194
     * Also clears any cached aggregate data.
3195
     *
3196
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3197
     *                            When false will just clear session-local cached data
3198
     * @return DataObject $this
3199
     */
3200
    public function flushCache($persistent = true)
3201
    {
3202
        if (static::class == self::class) {
0 ignored issues
show
introduced by
The condition static::class == self::class is always true.
Loading history...
3203
            self::$_cache_get_one = array();
3204
            return $this;
3205
        }
3206
3207
        $classes = ClassInfo::ancestry(static::class);
3208
        foreach ($classes as $class) {
3209
            if (isset(self::$_cache_get_one[$class])) {
3210
                unset(self::$_cache_get_one[$class]);
3211
            }
3212
        }
3213
3214
        $this->extend('flushCache');
3215
3216
        $this->components = array();
3217
        return $this;
3218
    }
3219
3220
    /**
3221
     * Flush the get_one global cache and destroy associated objects.
3222
     */
3223
    public static function flush_and_destroy_cache()
3224
    {
3225
        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...
3226
            foreach (self::$_cache_get_one as $class => $items) {
3227
                if (is_array($items)) {
3228
                    foreach ($items as $item) {
3229
                        if ($item) {
3230
                            $item->destroy();
3231
                        }
3232
                    }
3233
                }
3234
            }
3235
        }
3236
        self::$_cache_get_one = array();
3237
    }
3238
3239
    /**
3240
     * Reset all global caches associated with DataObject.
3241
     */
3242
    public static function reset()
3243
    {
3244
        // @todo Decouple these
3245
        DBEnum::flushCache();
3246
        ClassInfo::reset_db_cache();
3247
        static::getSchema()->reset();
3248
        self::$_cache_get_one = array();
3249
        self::$_cache_field_labels = array();
3250
    }
3251
3252
    /**
3253
     * Return the given element, searching by ID.
3254
     *
3255
     * This can be called either via `DataObject::get_by_id(MyClass::class, $id)`
3256
     * or `MyClass::get_by_id($id)`
3257
     *
3258
     * @param string|int $classOrID The class of the object to be returned, or id if called on target class
3259
     * @param int|bool $idOrCache The id of the element, or cache if called on target class
3260
     * @param boolean $cache See {@link get_one()}
3261
     *
3262
     * @return static The element
3263
     */
3264
    public static function get_by_id($classOrID, $idOrCache = null, $cache = true)
3265
    {
3266
        // Shift arguments if passing id in first or second argument
3267
        list ($class, $id, $cached) = is_numeric($classOrID)
3268
            ? [get_called_class(), $classOrID, isset($idOrCache) ? $idOrCache : $cache]
3269
            : [$classOrID, $idOrCache, $cache];
3270
3271
        // Validate class
3272
        if ($class === self::class) {
3273
            throw new InvalidArgumentException('DataObject::get_by_id() cannot query non-subclass DataObject directly');
3274
        }
3275
3276
        // Pass to get_one
3277
        $column = static::getSchema()->sqlColumnForField($class, 'ID');
3278
        return DataObject::get_one($class, [$column => $id], $cached);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3279
    }
3280
3281
    /**
3282
     * Get the name of the base table for this object
3283
     *
3284
     * @return string
3285
     */
3286
    public function baseTable()
3287
    {
3288
        return static::getSchema()->baseDataTable($this);
3289
    }
3290
3291
    /**
3292
     * Get the base class for this object
3293
     *
3294
     * @return string
3295
     */
3296
    public function baseClass()
3297
    {
3298
        return static::getSchema()->baseDataClass($this);
3299
    }
3300
3301
    /**
3302
     * @var array Parameters used in the query that built this object.
3303
     * This can be used by decorators (e.g. lazy loading) to
3304
     * run additional queries using the same context.
3305
     */
3306
    protected $sourceQueryParams;
3307
3308
    /**
3309
     * @see $sourceQueryParams
3310
     * @return array
3311
     */
3312
    public function getSourceQueryParams()
3313
    {
3314
        return $this->sourceQueryParams;
3315
    }
3316
3317
    /**
3318
     * Get list of parameters that should be inherited to relations on this object
3319
     *
3320
     * @return array
3321
     */
3322
    public function getInheritableQueryParams()
3323
    {
3324
        $params = $this->getSourceQueryParams();
3325
        $this->extend('updateInheritableQueryParams', $params);
3326
        return $params;
3327
    }
3328
3329
    /**
3330
     * @see $sourceQueryParams
3331
     * @param array
3332
     */
3333
    public function setSourceQueryParams($array)
3334
    {
3335
        $this->sourceQueryParams = $array;
3336
    }
3337
3338
    /**
3339
     * @see $sourceQueryParams
3340
     * @param string $key
3341
     * @param string $value
3342
     */
3343
    public function setSourceQueryParam($key, $value)
3344
    {
3345
        $this->sourceQueryParams[$key] = $value;
3346
    }
3347
3348
    /**
3349
     * @see $sourceQueryParams
3350
     * @param string $key
3351
     * @return string
3352
     */
3353
    public function getSourceQueryParam($key)
3354
    {
3355
        if (isset($this->sourceQueryParams[$key])) {
3356
            return $this->sourceQueryParams[$key];
3357
        }
3358
        return null;
3359
    }
3360
3361
    //-------------------------------------------------------------------------------------------//
3362
3363
    /**
3364
     * Check the database schema and update it as necessary.
3365
     *
3366
     * @uses DataExtension->augmentDatabase()
3367
     */
3368
    public function requireTable()
3369
    {
3370
        // Only build the table if we've actually got fields
3371
        $schema = static::getSchema();
3372
        $table = $schema->tableName(static::class);
3373
        $fields = $schema->databaseFields(static::class, false);
3374
        $indexes = $schema->databaseIndexes(static::class, false);
3375
        $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

3375
        /** @scrutinizer ignore-call */ 
3376
        $extensions = self::database_extensions(static::class);
Loading history...
3376
3377
        if (empty($table)) {
3378
            throw new LogicException(
3379
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3380
            );
3381
        }
3382
3383
        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...
3384
            $hasAutoIncPK = get_parent_class($this) === self::class;
3385
            DB::require_table(
3386
                $table,
3387
                $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

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

3388
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3389
                $hasAutoIncPK,
3390
                $this->config()->get('create_table_options'),
3391
                $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

3391
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3392
            );
3393
        } else {
3394
            DB::dont_require_table($table);
3395
        }
3396
3397
        // Build any child tables for many_many items
3398
        if ($manyMany = $this->uninherited('many_many')) {
3399
            $extras = $this->uninherited('many_many_extraFields');
3400
            foreach ($manyMany as $component => $spec) {
3401
                // Get many_many spec
3402
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3403
                $parentField = $manyManyComponent['parentField'];
3404
                $childField = $manyManyComponent['childField'];
3405
                $tableOrClass = $manyManyComponent['join'];
3406
3407
                // Skip if backed by actual class
3408
                if (class_exists($tableOrClass)) {
3409
                    continue;
3410
                }
3411
3412
                // Build fields
3413
                $manymanyFields = array(
3414
                    $parentField => "Int",
3415
                    $childField => "Int",
3416
                );
3417
                if (isset($extras[$component])) {
3418
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3419
                }
3420
3421
                // Build index list
3422
                $manymanyIndexes = [
3423
                    $parentField => [
3424
                        'type' => 'index',
3425
                        'name' => $parentField,
3426
                        'columns' => [$parentField],
3427
                    ],
3428
                    $childField => [
3429
                        'type' => 'index',
3430
                        'name' => $childField,
3431
                        'columns' => [$childField],
3432
                    ],
3433
                ];
3434
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Bug introduced by
$manymanyIndexes of type array<mixed,array<string,array|mixed|string>> is incompatible with the type string expected by parameter $indexSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

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

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

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

3434
                DB::require_table($tableOrClass, /** @scrutinizer ignore-type */ $manymanyFields, $manymanyIndexes, true, null, $extensions);
Loading history...
3435
            }
3436
        }
3437
3438
        // Let any extentions make their own database fields
3439
        $this->extend('augmentDatabase', $dummy);
3440
    }
3441
3442
    /**
3443
     * Add default records to database. This function is called whenever the
3444
     * database is built, after the database tables have all been created. Overload
3445
     * this to add default records when the database is built, but make sure you
3446
     * call parent::requireDefaultRecords().
3447
     *
3448
     * @uses DataExtension->requireDefaultRecords()
3449
     */
3450
    public function requireDefaultRecords()
3451
    {
3452
        $defaultRecords = $this->config()->uninherited('default_records');
3453
3454
        if (!empty($defaultRecords)) {
3455
            $hasData = DataObject::get_one(static::class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3456
            if (!$hasData) {
3457
                $className = static::class;
3458
                foreach ($defaultRecords as $record) {
3459
                    $obj = Injector::inst()->create($className, $record);
3460
                    $obj->write();
3461
                }
3462
                DB::alteration_message("Added default records to $className table", "created");
3463
            }
3464
        }
3465
3466
        // Let any extentions make their own database default data
3467
        $this->extend('requireDefaultRecords', $dummy);
3468
    }
3469
3470
    /**
3471
     * Get the default searchable fields for this object, as defined in the
3472
     * $searchable_fields list. If searchable fields are not defined on the
3473
     * data object, uses a default selection of summary fields.
3474
     *
3475
     * @return array
3476
     */
3477
    public function searchableFields()
3478
    {
3479
        // can have mixed format, need to make consistent in most verbose form
3480
        $fields = $this->config()->get('searchable_fields');
3481
        $labels = $this->fieldLabels();
3482
3483
        // fallback to summary fields (unless empty array is explicitly specified)
3484
        if (!$fields && !is_array($fields)) {
3485
            $summaryFields = array_keys($this->summaryFields());
3486
            $fields = array();
3487
3488
            // remove the custom getters as the search should not include them
3489
            $schema = static::getSchema();
3490
            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...
3491
                foreach ($summaryFields as $key => $name) {
3492
                    $spec = $name;
3493
3494
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3495
                    if (($fieldPos = strpos($name, '.')) !== false) {
3496
                        $name = substr($name, 0, $fieldPos);
3497
                    }
3498
3499
                    if ($schema->fieldSpec($this, $name)) {
3500
                        $fields[] = $name;
3501
                    } elseif ($this->relObject($spec)) {
3502
                        $fields[] = $spec;
3503
                    }
3504
                }
3505
            }
3506
        }
3507
3508
        // we need to make sure the format is unified before
3509
        // augmenting fields, so extensions can apply consistent checks
3510
        // but also after augmenting fields, because the extension
3511
        // might use the shorthand notation as well
3512
3513
        // rewrite array, if it is using shorthand syntax
3514
        $rewrite = array();
3515
        foreach ($fields as $name => $specOrName) {
3516
            $identifer = (is_int($name)) ? $specOrName : $name;
3517
3518
            if (is_int($name)) {
3519
                // Format: array('MyFieldName')
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3520
                $rewrite[$identifer] = array();
3521
            } elseif (is_array($specOrName) && ($relObject = $this->relObject($identifer))) {
3522
                // Format: array('MyFieldName' => array(
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3523
                //   'filter => 'ExactMatchFilter',
3524
                //   'field' => 'NumericField', // optional
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3525
                //   'title' => 'My Title', // optional
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3526
                // ))
3527
                $rewrite[$identifer] = array_merge(
3528
                    array('filter' => $relObject->config()->get('default_search_filter_class')),
3529
                    (array)$specOrName
3530
                );
3531
            } else {
3532
                // Format: array('MyFieldName' => 'ExactMatchFilter')
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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