Passed
Push — 4.3 ( 651d53...a8605b )
by Daniel
27:34 queued 17:45
created

DataObject::prepareManipulationTable()   C

Complexity

Conditions 12
Paths 80

Size

Total Lines 51
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 27
nc 80
nop 5
dl 0
loc 51
rs 6.9666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

1087
                        $leftComponents->/** @scrutinizer ignore-call */ 
1088
                                         addMany($rightComponents->column('ID'));
Loading history...
1088
                    }
1089
                    $leftComponents->write();
1090
                }
1091
            }
1092
1093
            if ($hasMany = $this->hasMany()) {
1094
                foreach ($hasMany as $relationship => $class) {
1095
                    $leftComponents = $leftObj->getComponents($relationship);
1096
                    $rightComponents = $rightObj->getComponents($relationship);
1097
                    if ($rightComponents && $rightComponents->exists()) {
1098
                        $leftComponents->addMany($rightComponents->column('ID'));
1099
                    }
1100
                    $leftComponents->write();
0 ignored issues
show
Bug introduced by
The method write() does not exist on SilverStripe\ORM\HasManyList. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

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

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

1100
                    $leftComponents->/** @scrutinizer ignore-call */ 
1101
                                     write();
Loading history...
1101
                }
1102
            }
1103
        }
1104
1105
        return true;
1106
    }
1107
1108
    /**
1109
     * Forces the record to think that all its data has changed.
1110
     * Doesn't write to the database. Only sets fields as changed
1111
     * if they are not already marked as changed.
1112
     *
1113
     * @return $this
1114
     */
1115
    public function forceChange()
1116
    {
1117
        // Ensure lazy fields loaded
1118
        $this->loadLazyFields();
1119
        $fields = static::getSchema()->fieldSpecs(static::class);
1120
1121
        // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1122
        $fieldNames = array_unique(array_merge(
1123
            array_keys($this->record),
1124
            array_keys($fields)
1125
        ));
1126
1127
        foreach ($fieldNames as $fieldName) {
1128
            if (!isset($this->changed[$fieldName])) {
1129
                $this->changed[$fieldName] = self::CHANGE_STRICT;
1130
            }
1131
            // Populate the null values in record so that they actually get written
1132
            if (!isset($this->record[$fieldName])) {
1133
                $this->record[$fieldName] = null;
1134
            }
1135
        }
1136
1137
        // @todo Find better way to allow versioned to write a new version after forceChange
1138
        if ($this->isChanged('Version')) {
1139
            unset($this->changed['Version']);
1140
        }
1141
        return $this;
1142
    }
1143
1144
    /**
1145
     * Validate the current object.
1146
     *
1147
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1148
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1149
     *
1150
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1151
     * and onAfterWrite() won't get called either.
1152
     *
1153
     * It is expected that you call validate() in your own application to test that an object is valid before
1154
     * attempting a write, and respond appropriately if it isn't.
1155
     *
1156
     * @see {@link ValidationResult}
1157
     * @return ValidationResult
1158
     */
1159
    public function validate()
1160
    {
1161
        $result = ValidationResult::create();
1162
        $this->extend('validate', $result);
1163
        return $result;
1164
    }
1165
1166
    /**
1167
     * Public accessor for {@see DataObject::validate()}
1168
     *
1169
     * @return ValidationResult
1170
     */
1171
    public function doValidate()
1172
    {
1173
        Deprecation::notice('5.0', 'Use validate');
1174
        return $this->validate();
1175
    }
1176
1177
    /**
1178
     * Event handler called before writing to the database.
1179
     * You can overload this to clean up or otherwise process data before writing it to the
1180
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1181
     *
1182
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1183
     *
1184
     * @uses DataExtension->onBeforeWrite()
1185
     */
1186
    protected function onBeforeWrite()
1187
    {
1188
        $this->brokenOnWrite = false;
1189
1190
        $dummy = null;
1191
        $this->extend('onBeforeWrite', $dummy);
1192
    }
1193
1194
    /**
1195
     * Event handler called after writing to the database.
1196
     * You can overload this to act upon changes made to the data after it is written.
1197
     * $this->changed will have a record
1198
     * database.  Don't forget to call parent::onAfterWrite(), though!
1199
     *
1200
     * @uses DataExtension->onAfterWrite()
1201
     */
1202
    protected function onAfterWrite()
1203
    {
1204
        $dummy = null;
1205
        $this->extend('onAfterWrite', $dummy);
1206
    }
1207
1208
    /**
1209
     * Find all objects that will be cascade deleted if this object is deleted
1210
     *
1211
     * Notes:
1212
     *   - If this object is versioned, objects will only be searched in the same stage as the given record.
1213
     *   - This will only be useful prior to deletion, as post-deletion this record will no longer exist.
1214
     *
1215
     * @param bool $recursive True if recursive
1216
     * @param ArrayList $list Optional list to add items to
1217
     * @return ArrayList list of objects
1218
     */
1219
    public function findCascadeDeletes($recursive = true, $list = null)
1220
    {
1221
        // Find objects in these relationships
1222
        return $this->findRelatedObjects('cascade_deletes', $recursive, $list);
1223
    }
1224
1225
    /**
1226
     * Event handler called before deleting from the database.
1227
     * You can overload this to clean up or otherwise process data before delete this
1228
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1229
     *
1230
     * @uses DataExtension->onBeforeDelete()
1231
     */
1232
    protected function onBeforeDelete()
1233
    {
1234
        $this->brokenOnDelete = false;
1235
1236
        $dummy = null;
1237
        $this->extend('onBeforeDelete', $dummy);
1238
1239
        // Cascade deletes
1240
        $deletes = $this->findCascadeDeletes(false);
1241
        foreach ($deletes as $delete) {
1242
            $delete->delete();
1243
        }
1244
    }
1245
1246
    protected function onAfterDelete()
1247
    {
1248
        $this->extend('onAfterDelete');
1249
    }
1250
1251
    /**
1252
     * Load the default values in from the self::$defaults array.
1253
     * Will traverse the defaults of the current class and all its parent classes.
1254
     * Called by the constructor when creating new records.
1255
     *
1256
     * @uses DataExtension->populateDefaults()
1257
     * @return DataObject $this
1258
     */
1259
    public function populateDefaults()
1260
    {
1261
        $classes = array_reverse(ClassInfo::ancestry($this));
1262
1263
        foreach ($classes as $class) {
1264
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1265
1266
            if ($defaults && !is_array($defaults)) {
1267
                user_error(
1268
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1269
                    E_USER_WARNING
1270
                );
1271
                $defaults = null;
1272
            }
1273
1274
            if ($defaults) {
1275
                foreach ($defaults as $fieldName => $fieldValue) {
1276
                    // SRM 2007-03-06: Stricter check
1277
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1278
                        $this->$fieldName = $fieldValue;
1279
                    }
1280
                    // Set many-many defaults with an array of ids
1281
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1282
                        /** @var ManyManyList $manyManyJoin */
1283
                        $manyManyJoin = $this->$fieldName();
1284
                        $manyManyJoin->setByIDList($fieldValue);
1285
                    }
1286
                }
1287
            }
1288
            if ($class == self::class) {
1289
                break;
1290
            }
1291
        }
1292
1293
        $this->extend('populateDefaults');
1294
        return $this;
1295
    }
1296
1297
    /**
1298
     * Determine validation of this object prior to write
1299
     *
1300
     * @return ValidationException Exception generated by this write, or null if valid
1301
     */
1302
    protected function validateWrite()
1303
    {
1304
        if ($this->ObsoleteClassName) {
1305
            return new ValidationException(
1306
                "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - " .
1307
                "you need to change the ClassName before you can write it"
1308
            );
1309
        }
1310
1311
        // Note: Validation can only be disabled at the global level, not per-model
1312
        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...
1313
            $result = $this->validate();
1314
            if (!$result->isValid()) {
1315
                return new ValidationException($result);
1316
            }
1317
        }
1318
        return null;
1319
    }
1320
1321
    /**
1322
     * Prepare an object prior to write
1323
     *
1324
     * @throws ValidationException
1325
     */
1326
    protected function preWrite()
1327
    {
1328
        // Validate this object
1329
        if ($writeException = $this->validateWrite()) {
1330
            // Used by DODs to clean up after themselves, eg, Versioned
1331
            $this->invokeWithExtensions('onAfterSkippedWrite');
1332
            throw $writeException;
1333
        }
1334
1335
        // Check onBeforeWrite
1336
        $this->brokenOnWrite = true;
1337
        $this->onBeforeWrite();
1338
        if ($this->brokenOnWrite) {
1339
            user_error(static::class . " has a broken onBeforeWrite() function."
1340
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1341
        }
1342
    }
1343
1344
    /**
1345
     * Detects and updates all changes made to this object
1346
     *
1347
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1348
     * @return bool True if any changes are detected
1349
     */
1350
    protected function updateChanges($forceChanges = false)
1351
    {
1352
        if ($forceChanges) {
1353
            // Force changes, but only for loaded fields
1354
            foreach ($this->record as $field => $value) {
1355
                $this->changed[$field] = static::CHANGE_VALUE;
1356
            }
1357
            return true;
1358
        }
1359
        return $this->isChanged();
1360
    }
1361
1362
    /**
1363
     * Writes a subset of changes for a specific table to the given manipulation
1364
     *
1365
     * @param string $baseTable Base table
1366
     * @param string $now Timestamp to use for the current time
1367
     * @param bool $isNewRecord Whether this should be treated as a new record write
1368
     * @param array $manipulation Manipulation to write to
1369
     * @param string $class Class of table to manipulate
1370
     */
1371
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1372
    {
1373
        $schema = $this->getSchema();
1374
        $table = $schema->tableName($class);
1375
        $manipulation[$table] = array();
1376
1377
        // Extract records for this table
1378
        foreach ($this->record as $fieldName => $fieldValue) {
1379
            // we're not attempting to reset the BaseTable->ID
1380
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1381
            if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1382
                continue;
1383
            }
1384
1385
            // Ensure this field pertains to this table
1386
            $specification = $schema->fieldSpec(
1387
                $class,
1388
                $fieldName,
1389
                DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
1390
            );
1391
            if (!$specification) {
1392
                continue;
1393
            }
1394
1395
            // if database column doesn't correlate to a DBField instance...
1396
            $fieldObj = $this->dbObject($fieldName);
1397
            if (!$fieldObj) {
1398
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1399
            }
1400
1401
            // Write to manipulation
1402
            $fieldObj->writeToManipulation($manipulation[$table]);
1403
        }
1404
1405
        // Ensure update of Created and LastEdited columns
1406
        if ($baseTable === $table) {
1407
            $manipulation[$table]['fields']['LastEdited'] = $now;
1408
            if ($isNewRecord) {
1409
                $manipulation[$table]['fields']['Created'] = empty($this->record['Created'])
1410
                    ? $now
1411
                    : $this->record['Created'];
1412
                $manipulation[$table]['fields']['ClassName'] = static::class;
1413
            }
1414
        }
1415
1416
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1417
        // attempt an update, as though it were a normal update.
1418
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1419
        $manipulation[$table]['class'] = $class;
1420
        if ($this->isInDB()) {
1421
            $manipulation[$table]['id'] = $this->record['ID'];
1422
        }
1423
    }
1424
1425
    /**
1426
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1427
     *
1428
     * Does nothing if an ID is already assigned for this record
1429
     *
1430
     * @param string $baseTable Base table
1431
     * @param string $now Timestamp to use for the current time
1432
     */
1433
    protected function writeBaseRecord($baseTable, $now)
1434
    {
1435
        // Generate new ID if not specified
1436
        if ($this->isInDB()) {
1437
            return;
1438
        }
1439
1440
        // Perform an insert on the base table
1441
        $manipulation = [];
1442
        $this->prepareManipulationTable($baseTable, $now, true, $manipulation, $this->baseClass());
1443
        DB::manipulate($manipulation);
1444
1445
        $this->changed['ID'] = self::CHANGE_VALUE;
1446
        $this->record['ID'] = DB::get_generated_id($baseTable);
1447
    }
1448
1449
    /**
1450
     * Generate and write the database manipulation for all changed fields
1451
     *
1452
     * @param string $baseTable Base table
1453
     * @param string $now Timestamp to use for the current time
1454
     * @param bool $isNewRecord If this is a new record
1455
     * @throws InvalidArgumentException
1456
     */
1457
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1458
    {
1459
        // Generate database manipulations for each class
1460
        $manipulation = array();
1461
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1462
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1463
        }
1464
1465
        // Allow extensions to extend this manipulation
1466
        $this->extend('augmentWrite', $manipulation);
1467
1468
        // New records have their insert into the base data table done first, so that they can pass the
1469
        // generated ID on to the rest of the manipulation
1470
        if ($isNewRecord) {
1471
            $manipulation[$baseTable]['command'] = 'update';
1472
        }
1473
1474
        // Make sure none of our field assignment are arrays
1475
        foreach ($manipulation as $tableManipulation) {
1476
            if (!isset($tableManipulation['fields'])) {
1477
                continue;
1478
            }
1479
            foreach ($tableManipulation['fields'] as $fieldName => $fieldValue) {
1480
                if (is_array($fieldValue)) {
1481
                    $dbObject = $this->dbObject($fieldName);
1482
                    // If the field allows non-scalar values we'll let it do dynamic assignments
1483
                    if ($dbObject && $dbObject->scalarValueOnly()) {
1484
                        throw new InvalidArgumentException(
1485
                            'DataObject::writeManipulation: parameterised field assignments are disallowed'
1486
                        );
1487
                    }
1488
                }
1489
            }
1490
        }
1491
1492
        // Perform the manipulation
1493
        DB::manipulate($manipulation);
1494
    }
1495
1496
    /**
1497
     * Writes all changes to this object to the database.
1498
     *  - It will insert a record whenever ID isn't set, otherwise update.
1499
     *  - All relevant tables will be updated.
1500
     *  - $this->onBeforeWrite() gets called beforehand.
1501
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1502
     *
1503
     * @uses DataExtension->augmentWrite()
1504
     *
1505
     * @param boolean $showDebug Show debugging information
1506
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1507
     * @param boolean $forceWrite Write to database even if there are no changes
1508
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1509
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1510
     *                                 {@link getManyManyComponents()} (Default: false)
1511
     * @return int The ID of the record
1512
     * @throws ValidationException Exception that can be caught and handled by the calling function
1513
     */
1514
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1515
    {
1516
        $now = DBDatetime::now()->Rfc2822();
1517
1518
        // Execute pre-write tasks
1519
        $this->preWrite();
1520
1521
        // Check if we are doing an update or an insert
1522
        $isNewRecord = !$this->isInDB() || $forceInsert;
1523
1524
        // Check changes exist, abort if there are none
1525
        $hasChanges = $this->updateChanges($isNewRecord);
1526
        if ($hasChanges || $forceWrite || $isNewRecord) {
1527
            // Ensure Created and LastEdited are populated
1528
            if (!isset($this->record['Created'])) {
1529
                $this->record['Created'] = $now;
1530
            }
1531
            $this->record['LastEdited'] = $now;
1532
1533
            // New records have their insert into the base data table done first, so that they can pass the
1534
            // generated primary key on to the rest of the manipulation
1535
            $baseTable = $this->baseTable();
1536
            $this->writeBaseRecord($baseTable, $now);
1537
1538
            // Write the DB manipulation for all changed fields
1539
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1540
1541
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1542
            $this->writeRelations();
1543
            $this->onAfterWrite();
1544
            $this->changed = array();
1545
        } else {
1546
            if ($showDebug) {
1547
                Debug::message("no changes for DataObject");
1548
            }
1549
1550
            // Used by DODs to clean up after themselves, eg, Versioned
1551
            $this->invokeWithExtensions('onAfterSkippedWrite');
1552
        }
1553
1554
        // Write relations as necessary
1555
        if ($writeComponents) {
1556
            $this->writeComponents(true);
1557
        }
1558
1559
        // Clears the cache for this object so get_one returns the correct object.
1560
        $this->flushCache();
1561
1562
        return $this->record['ID'];
1563
    }
1564
1565
    /**
1566
     * Writes cached relation lists to the database, if possible
1567
     */
1568
    public function writeRelations()
1569
    {
1570
        if (!$this->isInDB()) {
1571
            return;
1572
        }
1573
1574
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1575
        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...
1576
            foreach ($this->unsavedRelations as $name => $list) {
1577
                $list->changeToList($this->$name());
1578
            }
1579
            $this->unsavedRelations = array();
1580
        }
1581
    }
1582
1583
    /**
1584
     * Write the cached components to the database. Cached components could refer to two different instances of the
1585
     * same record.
1586
     *
1587
     * @param bool $recursive Recursively write components
1588
     * @return DataObject $this
1589
     */
1590
    public function writeComponents($recursive = false)
1591
    {
1592
        foreach ($this->components as $component) {
1593
            $component->write(false, false, false, $recursive);
1594
        }
1595
1596
        if ($join = $this->getJoin()) {
1597
            $join->write(false, false, false, $recursive);
1598
        }
1599
1600
        return $this;
1601
    }
1602
1603
    /**
1604
     * Delete this data object.
1605
     * $this->onBeforeDelete() gets called.
1606
     * Note that in Versioned objects, both Stage and Live will be deleted.
1607
     * @uses DataExtension->augmentSQL()
1608
     */
1609
    public function delete()
1610
    {
1611
        $this->brokenOnDelete = true;
1612
        $this->onBeforeDelete();
1613
        if ($this->brokenOnDelete) {
1614
            user_error(static::class . " has a broken onBeforeDelete() function."
1615
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1616
        }
1617
1618
        // Deleting a record without an ID shouldn't do anything
1619
        if (!$this->ID) {
1620
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1621
        }
1622
1623
        // TODO: This is quite ugly.  To improve:
1624
        //  - move the details of the delete code in the DataQuery system
1625
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1626
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1627
        $srcQuery = DataList::create(static::class)
1628
            ->filter('ID', $this->ID)
1629
            ->dataQuery()
1630
            ->query();
1631
        $queriedTables = $srcQuery->queriedTables();
1632
        $this->extend('updateDeleteTables', $queriedTables, $srcQuery);
1633
        foreach ($queriedTables as $table) {
1634
            $delete = SQLDelete::create("\"$table\"", array('"ID"' => $this->ID));
1635
            $this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
1636
            $delete->execute();
1637
        }
1638
        // Remove this item out of any caches
1639
        $this->flushCache();
1640
1641
        $this->onAfterDelete();
1642
1643
        $this->OldID = $this->ID;
1644
        $this->ID = 0;
1645
    }
1646
1647
    /**
1648
     * Delete the record with the given ID.
1649
     *
1650
     * @param string $className The class name of the record to be deleted
1651
     * @param int $id ID of record to be deleted
1652
     */
1653
    public static function delete_by_id($className, $id)
1654
    {
1655
        $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...
1656
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1657
            $obj->delete();
1658
        } else {
1659
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1660
        }
1661
    }
1662
1663
    /**
1664
     * Get the class ancestry, including the current class name.
1665
     * The ancestry will be returned as an array of class names, where the 0th element
1666
     * will be the class that inherits directly from DataObject, and the last element
1667
     * will be the current class.
1668
     *
1669
     * @return array Class ancestry
1670
     */
1671
    public function getClassAncestry()
1672
    {
1673
        return ClassInfo::ancestry(static::class);
1674
    }
1675
1676
    /**
1677
     * Return a unary component object from a one to one relationship, as a DataObject.
1678
     * If no component is available, an 'empty component' will be returned for
1679
     * non-polymorphic relations, or for polymorphic relations with a class set.
1680
     *
1681
     * @param string $componentName Name of the component
1682
     * @return DataObject The component object. It's exact type will be that of the component.
1683
     * @throws Exception
1684
     */
1685
    public function getComponent($componentName)
1686
    {
1687
        if (isset($this->components[$componentName])) {
1688
            return $this->components[$componentName];
1689
        }
1690
1691
        $schema = static::getSchema();
1692
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1693
            $joinField = $componentName . 'ID';
1694
            $joinID = $this->getField($joinField);
1695
1696
            // Extract class name for polymorphic relations
1697
            if ($class === self::class) {
1698
                $class = $this->getField($componentName . 'Class');
1699
                if (empty($class)) {
1700
                    return null;
1701
                }
1702
            }
1703
1704
            if ($joinID) {
1705
                // Ensure that the selected object originates from the same stage, subsite, etc
1706
                $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...
1707
                    ->filter('ID', $joinID)
1708
                    ->setDataQueryParam($this->getInheritableQueryParams())
1709
                    ->first();
1710
            }
1711
1712
            if (empty($component)) {
1713
                $component = Injector::inst()->create($class);
1714
            }
1715
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1716
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1717
            $joinID = $this->ID;
1718
1719
            if ($joinID) {
1720
                // Prepare filter for appropriate join type
1721
                if ($polymorphic) {
1722
                    $filter = array(
1723
                        "{$joinField}ID" => $joinID,
1724
                        "{$joinField}Class" => static::class,
1725
                    );
1726
                } else {
1727
                    $filter = array(
1728
                        $joinField => $joinID
1729
                    );
1730
                }
1731
1732
                // Ensure that the selected object originates from the same stage, subsite, etc
1733
                $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...
1734
                    ->filter($filter)
1735
                    ->setDataQueryParam($this->getInheritableQueryParams())
1736
                    ->first();
1737
            }
1738
1739
            if (empty($component)) {
1740
                $component = Injector::inst()->create($class);
1741
                if ($polymorphic) {
1742
                    $component->{$joinField . 'ID'} = $this->ID;
1743
                    $component->{$joinField . 'Class'} = static::class;
1744
                } else {
1745
                    $component->$joinField = $this->ID;
1746
                }
1747
            }
1748
        } else {
1749
            throw new InvalidArgumentException(
1750
                "DataObject->getComponent(): Could not find component '$componentName'."
1751
            );
1752
        }
1753
1754
        $this->components[$componentName] = $component;
1755
        return $component;
1756
    }
1757
1758
    /**
1759
     * Assign an item to the given component
1760
     *
1761
     * @param string $componentName
1762
     * @param DataObject|null $item
1763
     * @return $this
1764
     */
1765
    public function setComponent($componentName, $item)
1766
    {
1767
        // Validate component
1768
        $schema = static::getSchema();
1769
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1770
            // Force item to be written if not by this point
1771
            // @todo This could be lazy-written in a beforeWrite hook, but force write here for simplicity
1772
            // https://github.com/silverstripe/silverstripe-framework/issues/7818
1773
            if ($item && !$item->isInDB()) {
1774
                $item->write();
1775
            }
1776
1777
            // Update local ID
1778
            $joinField = $componentName . 'ID';
1779
            $this->setField($joinField, $item ? $item->ID : null);
1780
            // Update Class (Polymorphic has_one)
1781
            // Extract class name for polymorphic relations
1782
            if ($class === self::class) {
1783
                $this->setField($componentName . 'Class', $item ? get_class($item) : null);
1784
            }
1785
        } 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...
1786
            if ($item) {
1787
                // For belongs_to, add to has_one on other component
1788
                $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1789
                if (!$polymorphic) {
1790
                    $joinField = substr($joinField, 0, -2);
1791
                }
1792
                $item->setComponent($joinField, $this);
1793
            }
1794
        } else {
1795
            throw new InvalidArgumentException(
1796
                "DataObject->setComponent(): Could not find component '$componentName'."
1797
            );
1798
        }
1799
1800
        $this->components[$componentName] = $item;
1801
        return $this;
1802
    }
1803
1804
    /**
1805
     * Returns a one-to-many relation as a HasManyList
1806
     *
1807
     * @param string $componentName Name of the component
1808
     * @param int|array $id Optional ID(s) for parent of this relation, if not the current record
1809
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1810
     */
1811
    public function getComponents($componentName, $id = null)
1812
    {
1813
        if (!isset($id)) {
1814
            $id = $this->ID;
1815
        }
1816
        $result = null;
1817
1818
        $schema = $this->getSchema();
1819
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1820
        if (!$componentClass) {
1821
            throw new InvalidArgumentException(sprintf(
1822
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1823
                $componentName,
1824
                static::class
1825
            ));
1826
        }
1827
1828
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1829
        if (!$id) {
1830
            if (!isset($this->unsavedRelations[$componentName])) {
1831
                $this->unsavedRelations[$componentName] =
1832
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1833
            }
1834
            return $this->unsavedRelations[$componentName];
1835
        }
1836
1837
        // Determine type and nature of foreign relation
1838
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1839
        /** @var HasManyList $result */
1840
        if ($polymorphic) {
1841
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
1842
        } else {
1843
            $result = HasManyList::create($componentClass, $joinField);
1844
        }
1845
1846
        return $result
1847
            ->setDataQueryParam($this->getInheritableQueryParams())
1848
            ->forForeignID($id);
1849
    }
1850
1851
    /**
1852
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1853
     *
1854
     * @param string $relationName Relation name.
1855
     * @return string Class name, or null if not found.
1856
     */
1857
    public function getRelationClass($relationName)
1858
    {
1859
        // Parse many_many
1860
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1861
        if ($manyManyComponent) {
1862
            return $manyManyComponent['childClass'];
1863
        }
1864
1865
        // Go through all relationship configuration fields.
1866
        $config = $this->config();
1867
        $candidates = array_merge(
1868
            ($relations = $config->get('has_one')) ? $relations : array(),
1869
            ($relations = $config->get('has_many')) ? $relations : array(),
1870
            ($relations = $config->get('belongs_to')) ? $relations : array()
1871
        );
1872
1873
        if (isset($candidates[$relationName])) {
1874
            $remoteClass = $candidates[$relationName];
1875
1876
            // If dot notation is present, extract just the first part that contains the class.
1877
            if (($fieldPos = strpos($remoteClass, '.')) !== false) {
1878
                return substr($remoteClass, 0, $fieldPos);
1879
            }
1880
1881
            // Otherwise just return the class
1882
            return $remoteClass;
1883
        }
1884
1885
        return null;
1886
    }
1887
1888
    /**
1889
     * Given a relation name, determine the relation type
1890
     *
1891
     * @param string $component Name of component
1892
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1893
     */
1894
    public function getRelationType($component)
1895
    {
1896
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1897
        $config = $this->config();
1898
        foreach ($types as $type) {
1899
            $relations = $config->get($type);
1900
            if ($relations && isset($relations[$component])) {
1901
                return $type;
1902
            }
1903
        }
1904
        return null;
1905
    }
1906
1907
    /**
1908
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1909
     * side of the relation.
1910
     *
1911
     * Notes on behaviour:
1912
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1913
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1914
     *  - Polymorphic relationships do not have two natural endpoints (only on one side)
1915
     *   and thus attempting to infer them will return nothing.
1916
     *  - Cannot be used on unsaved objects.
1917
     *
1918
     * @param string $remoteClass
1919
     * @param string $remoteRelation
1920
     * @return DataList|DataObject The component, either as a list or single object
1921
     * @throws BadMethodCallException
1922
     * @throws InvalidArgumentException
1923
     */
1924
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1925
    {
1926
        $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...
1927
        $class = $remote->getRelationClass($remoteRelation);
1928
        $schema = static::getSchema();
1929
1930
        // Validate arguments
1931
        if (!$this->isInDB()) {
1932
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1933
        }
1934
        if (empty($class)) {
1935
            throw new InvalidArgumentException(sprintf(
1936
                "%s invoked with invalid relation %s.%s",
1937
                __METHOD__,
1938
                $remoteClass,
1939
                $remoteRelation
1940
            ));
1941
        }
1942
        // If relation is polymorphic, do not infer recriprocal relationship
1943
        if ($class === self::class) {
1944
            return null;
1945
        }
1946
        if (!is_a($this, $class, true)) {
1947
            throw new InvalidArgumentException(sprintf(
1948
                "Relation %s on %s does not refer to objects of type %s",
1949
                $remoteRelation,
1950
                $remoteClass,
1951
                static::class
1952
            ));
1953
        }
1954
1955
        // Check the relation type to mock
1956
        $relationType = $remote->getRelationType($remoteRelation);
1957
        switch ($relationType) {
1958
            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...
1959
                // Mock has_many
1960
                $joinField = "{$remoteRelation}ID";
1961
                $componentClass = $schema->classForField($remoteClass, $joinField);
1962
                $result = HasManyList::create($componentClass, $joinField);
1963
                return $result
1964
                    ->setDataQueryParam($this->getInheritableQueryParams())
1965
                    ->forForeignID($this->ID);
1966
            }
1967
            case 'belongs_to':
1968
            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...
1969
                // These relations must have a has_one on the other end, so find it
1970
                $joinField = $schema->getRemoteJoinField(
1971
                    $remoteClass,
1972
                    $remoteRelation,
1973
                    $relationType,
1974
                    $polymorphic
1975
                );
1976
                // If relation is polymorphic, do not infer recriprocal relationship automatically
1977
                if ($polymorphic) {
1978
                    return null;
1979
                }
1980
                $joinID = $this->getField($joinField);
1981
                if (empty($joinID)) {
1982
                    return null;
1983
                }
1984
                // Get object by joined ID
1985
                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...
1986
                    ->filter('ID', $joinID)
1987
                    ->setDataQueryParam($this->getInheritableQueryParams())
1988
                    ->first();
1989
            }
1990
            case 'many_many':
1991
            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...
1992
                // Get components and extra fields from parent
1993
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1994
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1995
1996
                // Reverse parent and component fields and create an inverse ManyManyList
1997
                /** @var RelationList $result */
1998
                $result = Injector::inst()->create(
1999
                    $manyMany['relationClass'],
2000
                    $manyMany['parentClass'], // Substitute parent class for dataClass
2001
                    $manyMany['join'],
2002
                    $manyMany['parentField'], // Reversed parent / child field
2003
                    $manyMany['childField'], // Reversed parent / child field
2004
                    $extraFields,
2005
                    $manyMany['childClass'], // substitute child class for parentClass
2006
                    $remoteClass // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2007
                );
2008
                $this->extend('updateManyManyComponents', $result);
2009
2010
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2011
                // foreignID set elsewhere.
2012
                return $result
2013
                    ->setDataQueryParam($this->getInheritableQueryParams())
2014
                    ->forForeignID($this->ID);
2015
            }
2016
            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...
2017
                return null;
2018
            }
2019
        }
2020
    }
2021
2022
    /**
2023
     * Returns a many-to-many component, as a ManyManyList.
2024
     * @param string $componentName Name of the many-many component
2025
     * @param int|array $id Optional ID for parent of this relation, if not the current record
2026
     * @return ManyManyList|UnsavedRelationList The set of components
2027
     */
2028
    public function getManyManyComponents($componentName, $id = null)
2029
    {
2030
        if (!isset($id)) {
2031
            $id = $this->ID;
2032
        }
2033
        $schema = static::getSchema();
2034
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
2035
        if (!$manyManyComponent) {
2036
            throw new InvalidArgumentException(sprintf(
2037
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
2038
                $componentName,
2039
                static::class
2040
            ));
2041
        }
2042
2043
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
2044
        if (!$id) {
2045
            if (!isset($this->unsavedRelations[$componentName])) {
2046
                $this->unsavedRelations[$componentName] =
2047
                    new UnsavedRelationList(
2048
                        $manyManyComponent['parentClass'],
2049
                        $componentName,
2050
                        $manyManyComponent['childClass']
2051
                    );
2052
            }
2053
            return $this->unsavedRelations[$componentName];
2054
        }
2055
2056
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
2057
        /** @var RelationList $result */
2058
        $result = Injector::inst()->create(
2059
            $manyManyComponent['relationClass'],
2060
            $manyManyComponent['childClass'],
2061
            $manyManyComponent['join'],
2062
            $manyManyComponent['childField'],
2063
            $manyManyComponent['parentField'],
2064
            $extraFields,
2065
            $manyManyComponent['parentClass'],
2066
            static::class // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2067
        );
2068
2069
        // Store component data in query meta-data
2070
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
2071
            /** @var DataQuery $query */
2072
            $query->setQueryParam('Component.ExtraFields', $extraFields);
2073
        });
2074
2075
        // If we have a default sort set for our "join" then we should overwrite any default already set.
2076
        $joinSort = Config::inst()->get($manyManyComponent['join'], 'default_sort');
2077
        if (!empty($joinSort)) {
2078
            $result = $result->sort($joinSort);
2079
        }
2080
2081
        $this->extend('updateManyManyComponents', $result);
2082
2083
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2084
        // foreignID set elsewhere.
2085
        return $result
2086
            ->setDataQueryParam($this->getInheritableQueryParams())
2087
            ->forForeignID($id);
2088
    }
2089
2090
    /**
2091
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
2092
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
2093
     *
2094
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
2095
     *                          their classes.
2096
     */
2097
    public function hasOne()
2098
    {
2099
        return (array)$this->config()->get('has_one');
2100
    }
2101
2102
    /**
2103
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
2104
     * their class name will be returned.
2105
     *
2106
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2107
     *        the field data stripped off. It defaults to TRUE.
2108
     * @return string|array
2109
     */
2110
    public function belongsTo($classOnly = true)
2111
    {
2112
        $belongsTo = (array)$this->config()->get('belongs_to');
2113
        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...
2114
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
2115
        } else {
2116
            return $belongsTo ? $belongsTo : array();
2117
        }
2118
    }
2119
2120
    /**
2121
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2122
     * relationships and their classes will be returned.
2123
     *
2124
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2125
     *        the field data stripped off. It defaults to TRUE.
2126
     * @return string|array|false
2127
     */
2128
    public function hasMany($classOnly = true)
2129
    {
2130
        $hasMany = (array)$this->config()->get('has_many');
2131
        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...
2132
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2133
        } else {
2134
            return $hasMany ? $hasMany : array();
2135
        }
2136
    }
2137
2138
    /**
2139
     * Return the many-to-many extra fields specification.
2140
     *
2141
     * If you don't specify a component name, it returns all
2142
     * extra fields for all components available.
2143
     *
2144
     * @return array|null
2145
     */
2146
    public function manyManyExtraFields()
2147
    {
2148
        return $this->config()->get('many_many_extraFields');
2149
    }
2150
2151
    /**
2152
     * Return information about a many-to-many component.
2153
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2154
     * components are returned.
2155
     *
2156
     * @see DataObjectSchema::manyManyComponent()
2157
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2158
     */
2159
    public function manyMany()
2160
    {
2161
        $config = $this->config();
2162
        $manyManys = (array)$config->get('many_many');
2163
        $belongsManyManys = (array)$config->get('belongs_many_many');
2164
        $items = array_merge($manyManys, $belongsManyManys);
2165
        return $items;
2166
    }
2167
2168
    /**
2169
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
2170
     *
2171
     * This is experimental, and is currently only a Postgres-specific enhancement.
2172
     *
2173
     * @param string $class
2174
     * @return array|false
2175
     */
2176
    public function database_extensions($class)
2177
    {
2178
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2179
        if ($extensions) {
2180
            return $extensions;
2181
        } else {
2182
            return false;
2183
        }
2184
    }
2185
2186
    /**
2187
     * Generates a SearchContext to be used for building and processing
2188
     * a generic search form for properties on this object.
2189
     *
2190
     * @return SearchContext
2191
     */
2192
    public function getDefaultSearchContext()
2193
    {
2194
        return new SearchContext(
2195
            static::class,
2196
            $this->scaffoldSearchFields(),
2197
            $this->defaultSearchFilters()
2198
        );
2199
    }
2200
2201
    /**
2202
     * Determine which properties on the DataObject are
2203
     * searchable, and map them to their default {@link FormField}
2204
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2205
     *
2206
     * Some additional logic is included for switching field labels, based on
2207
     * how generic or specific the field type is.
2208
     *
2209
     * Used by {@link SearchContext}.
2210
     *
2211
     * @param array $_params
2212
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2213
     *   'restrictFields': Numeric array of a field name whitelist
2214
     * @return FieldList
2215
     */
2216
    public function scaffoldSearchFields($_params = null)
2217
    {
2218
        $params = array_merge(
2219
            array(
2220
                'fieldClasses' => false,
2221
                'restrictFields' => false
2222
            ),
2223
            (array)$_params
2224
        );
2225
        $fields = new FieldList();
2226
        foreach ($this->searchableFields() as $fieldName => $spec) {
2227
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
2228
                continue;
2229
            }
2230
2231
            // If a custom fieldclass is provided as a string, use it
2232
            $field = null;
2233
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2234
                $fieldClass = $params['fieldClasses'][$fieldName];
2235
                $field = new $fieldClass($fieldName);
2236
            // If we explicitly set a field, then construct that
2237
            } elseif (isset($spec['field'])) {
2238
                // If it's a string, use it as a class name and construct
2239
                if (is_string($spec['field'])) {
2240
                    $fieldClass = $spec['field'];
2241
                    $field = new $fieldClass($fieldName);
2242
2243
                // If it's a FormField object, then just use that object directly.
2244
                } elseif ($spec['field'] instanceof FormField) {
2245
                    $field = $spec['field'];
2246
2247
                // Otherwise we have a bug
2248
                } else {
2249
                    user_error("Bad value for searchable_fields, 'field' value: "
2250
                        . var_export($spec['field'], true), E_USER_WARNING);
2251
                }
2252
2253
            // Otherwise, use the database field's scaffolder
2254
            } elseif ($object = $this->relObject($fieldName)) {
2255
                $field = $object->scaffoldSearchField();
2256
            }
2257
2258
            // Allow fields to opt out of search
2259
            if (!$field) {
2260
                continue;
2261
            }
2262
2263
            if (strstr($fieldName, '.')) {
2264
                $field->setName(str_replace('.', '__', $fieldName));
2265
            }
2266
            $field->setTitle($spec['title']);
2267
2268
            $fields->push($field);
2269
        }
2270
        return $fields;
2271
    }
2272
2273
    /**
2274
     * Scaffold a simple edit form for all properties on this dataobject,
2275
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2276
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2277
     *
2278
     * @uses FormScaffolder
2279
     *
2280
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2281
     * @return FieldList
2282
     */
2283
    public function scaffoldFormFields($_params = null)
2284
    {
2285
        $params = array_merge(
2286
            array(
2287
                'tabbed' => false,
2288
                'includeRelations' => false,
2289
                'restrictFields' => false,
2290
                'fieldClasses' => false,
2291
                'ajaxSafe' => false
2292
            ),
2293
            (array)$_params
2294
        );
2295
2296
        $fs = FormScaffolder::create($this);
2297
        $fs->tabbed = $params['tabbed'];
2298
        $fs->includeRelations = $params['includeRelations'];
2299
        $fs->restrictFields = $params['restrictFields'];
2300
        $fs->fieldClasses = $params['fieldClasses'];
2301
        $fs->ajaxSafe = $params['ajaxSafe'];
2302
2303
        return $fs->getFieldList();
2304
    }
2305
2306
    /**
2307
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2308
     * being called on extensions
2309
     *
2310
     * @param callable $callback The callback to execute
2311
     */
2312
    protected function beforeUpdateCMSFields($callback)
2313
    {
2314
        $this->beforeExtending('updateCMSFields', $callback);
2315
    }
2316
2317
    /**
2318
     * Centerpiece of every data administration interface in Silverstripe,
2319
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2320
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2321
     * generate this set. To customize, overload this method in a subclass
2322
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2323
     *
2324
     * <code>
2325
     * class MyCustomClass extends DataObject {
2326
     *  static $db = array('CustomProperty'=>'Boolean');
2327
     *
2328
     *  function getCMSFields() {
2329
     *    $fields = parent::getCMSFields();
2330
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2331
     *    return $fields;
2332
     *  }
2333
     * }
2334
     * </code>
2335
     *
2336
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2337
     *
2338
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2339
     */
2340
    public function getCMSFields()
2341
    {
2342
        $tabbedFields = $this->scaffoldFormFields(array(
2343
            // Don't allow has_many/many_many relationship editing before the record is first saved
2344
            'includeRelations' => ($this->ID > 0),
2345
            'tabbed' => true,
2346
            'ajaxSafe' => true
2347
        ));
2348
2349
        $this->extend('updateCMSFields', $tabbedFields);
2350
2351
        return $tabbedFields;
2352
    }
2353
2354
    /**
2355
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2356
     * including that dataobject's extensions customised actions could be added to the EditForm.
2357
     *
2358
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2359
     */
2360
    public function getCMSActions()
2361
    {
2362
        $actions = new FieldList();
2363
        $this->extend('updateCMSActions', $actions);
2364
        return $actions;
2365
    }
2366
2367
2368
    /**
2369
     * Used for simple frontend forms without relation editing
2370
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2371
     * by default. To customize, either overload this method in your
2372
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2373
     *
2374
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2375
     *
2376
     * @param array $params See {@link scaffoldFormFields()}
2377
     * @return FieldList Always returns a simple field collection without TabSet.
2378
     */
2379
    public function getFrontEndFields($params = null)
2380
    {
2381
        $untabbedFields = $this->scaffoldFormFields($params);
2382
        $this->extend('updateFrontEndFields', $untabbedFields);
2383
2384
        return $untabbedFields;
2385
    }
2386
2387
    public function getViewerTemplates($suffix = '')
2388
    {
2389
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2390
    }
2391
2392
    /**
2393
     * Gets the value of a field.
2394
     * Called by {@link __get()} and any getFieldName() methods you might create.
2395
     *
2396
     * @param string $field The name of the field
2397
     * @return mixed The field value
2398
     */
2399
    public function getField($field)
2400
    {
2401
        // If we already have a value in $this->record, then we should just return that
2402
        if (isset($this->record[$field])) {
2403
            return $this->record[$field];
2404
        }
2405
2406
        // Do we have a field that needs to be lazy loaded?
2407
        if (isset($this->record[$field . '_Lazy'])) {
2408
            $tableClass = $this->record[$field . '_Lazy'];
2409
            $this->loadLazyFields($tableClass);
2410
        }
2411
        $schema = static::getSchema();
2412
2413
        // Support unary relations as fields
2414
        if ($schema->unaryComponent(static::class, $field)) {
2415
            return $this->getComponent($field);
2416
        }
2417
2418
        // In case of complex fields, return the DBField object
2419
        if ($schema->compositeField(static::class, $field)) {
2420
            $this->record[$field] = $this->dbObject($field);
2421
        }
2422
2423
        return isset($this->record[$field]) ? $this->record[$field] : null;
2424
    }
2425
2426
    /**
2427
     * Loads all the stub fields that an initial lazy load didn't load fully.
2428
     *
2429
     * @param string $class Class to load the values from. Others are joined as required.
2430
     * Not specifying a tableClass will load all lazy fields from all tables.
2431
     * @return bool Flag if lazy loading succeeded
2432
     */
2433
    protected function loadLazyFields($class = null)
2434
    {
2435
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2436
            return false;
2437
        }
2438
2439
        if (!$class) {
2440
            $loaded = array();
2441
2442
            foreach ($this->record as $key => $value) {
2443
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2444
                    $this->loadLazyFields($value);
2445
                    $loaded[$value] = $value;
2446
                }
2447
            }
2448
2449
            return false;
2450
        }
2451
2452
        $dataQuery = new DataQuery($class);
2453
2454
        // Reset query parameter context to that of this DataObject
2455
        if ($params = $this->getSourceQueryParams()) {
2456
            foreach ($params as $key => $value) {
2457
                $dataQuery->setQueryParam($key, $value);
2458
            }
2459
        }
2460
2461
        // Limit query to the current record, unless it has the Versioned extension,
2462
        // in which case it requires special handling through augmentLoadLazyFields()
2463
        $schema = static::getSchema();
2464
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2465
        $dataQuery->where([
2466
            $baseIDColumn => $this->record['ID']
2467
        ])->limit(1);
2468
2469
        $columns = array();
2470
2471
        // Add SQL for fields, both simple & multi-value
2472
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2473
        $databaseFields = $schema->databaseFields($class, false);
2474
        foreach ($databaseFields as $k => $v) {
2475
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2476
                $columns[] = $k;
2477
            }
2478
        }
2479
2480
        if ($columns) {
2481
            $query = $dataQuery->query();
2482
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2483
            $this->extend('augmentSQL', $query, $dataQuery);
2484
2485
            $dataQuery->setQueriedColumns($columns);
2486
            $newData = $dataQuery->execute()->record();
2487
2488
            // Load the data into record
2489
            if ($newData) {
2490
                foreach ($newData as $k => $v) {
2491
                    if (in_array($k, $columns)) {
2492
                        $this->record[$k] = $v;
2493
                        $this->original[$k] = $v;
2494
                        unset($this->record[$k . '_Lazy']);
2495
                    }
2496
                }
2497
2498
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2499
            } else {
2500
                foreach ($columns as $k) {
2501
                    $this->record[$k] = null;
2502
                    $this->original[$k] = null;
2503
                    unset($this->record[$k . '_Lazy']);
2504
                }
2505
            }
2506
        }
2507
        return true;
2508
    }
2509
2510
    /**
2511
     * Return the fields that have changed.
2512
     *
2513
     * The change level affects what the functions defines as "changed":
2514
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2515
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2516
     *   for example a change from 0 to null would not be included.
2517
     *
2518
     * Example return:
2519
     * <code>
2520
     * array(
2521
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2522
     * )
2523
     * </code>
2524
     *
2525
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2526
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2527
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2528
     * @return array
2529
     */
2530
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2531
    {
2532
        $changedFields = array();
2533
2534
        // Update the changed array with references to changed obj-fields
2535
        foreach ($this->record as $k => $v) {
2536
            // Prevents DBComposite infinite looping on isChanged
2537
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2538
                continue;
2539
            }
2540
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2541
                $this->changed[$k] = self::CHANGE_VALUE;
2542
            }
2543
        }
2544
2545
        if (is_array($databaseFieldsOnly)) {
2546
            $fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2547
        } elseif ($databaseFieldsOnly) {
2548
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2549
            $fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
2550
        } else {
2551
            $fields = $this->changed;
2552
        }
2553
2554
        // Filter the list to those of a certain change level
2555
        if ($changeLevel > self::CHANGE_STRICT) {
2556
            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...
2557
                foreach ($fields as $name => $level) {
2558
                    if ($level < $changeLevel) {
2559
                        unset($fields[$name]);
2560
                    }
2561
                }
2562
            }
2563
        }
2564
2565
        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...
2566
            foreach ($fields as $name => $level) {
2567
                $changedFields[$name] = array(
2568
                    'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2569
                    'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2570
                    'level' => $level
2571
                );
2572
            }
2573
        }
2574
2575
        return $changedFields;
2576
    }
2577
2578
    /**
2579
     * Uses {@link getChangedFields()} to determine if fields have been changed
2580
     * since loading them from the database.
2581
     *
2582
     * @param string $fieldName Name of the database field to check, will check for any if not given
2583
     * @param int $changeLevel See {@link getChangedFields()}
2584
     * @return boolean
2585
     */
2586
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2587
    {
2588
        $fields = $fieldName ? array($fieldName) : true;
2589
        $changed = $this->getChangedFields($fields, $changeLevel);
2590
        if (!isset($fieldName)) {
2591
            return !empty($changed);
2592
        } else {
2593
            return array_key_exists($fieldName, $changed);
2594
        }
2595
    }
2596
2597
    /**
2598
     * Set the value of the field
2599
     * Called by {@link __set()} and any setFieldName() methods you might create.
2600
     *
2601
     * @param string $fieldName Name of the field
2602
     * @param mixed $val New field value
2603
     * @return $this
2604
     */
2605
    public function setField($fieldName, $val)
2606
    {
2607
        $this->objCacheClear();
2608
        //if it's a has_one component, destroy the cache
2609
        if (substr($fieldName, -2) == 'ID') {
2610
            unset($this->components[substr($fieldName, 0, -2)]);
2611
        }
2612
2613
        // If we've just lazy-loaded the column, then we need to populate the $original array
2614
        if (isset($this->record[$fieldName . '_Lazy'])) {
2615
            $tableClass = $this->record[$fieldName . '_Lazy'];
2616
            $this->loadLazyFields($tableClass);
2617
        }
2618
2619
        // Support component assignent via field setter
2620
        $schema = static::getSchema();
2621
        if ($schema->unaryComponent(static::class, $fieldName)) {
2622
            unset($this->components[$fieldName]);
2623
            // Assign component directly
2624
            if (is_null($val) || $val instanceof DataObject) {
2625
                return $this->setComponent($fieldName, $val);
2626
            }
2627
            // Assign by ID instead of object
2628
            if (is_numeric($val)) {
2629
                $fieldName .= 'ID';
2630
            }
2631
        }
2632
2633
        // Situation 1: Passing an DBField
2634
        if ($val instanceof DBField) {
2635
            $val->setName($fieldName);
2636
            $val->saveInto($this);
2637
2638
            // Situation 1a: Composite fields should remain bound in case they are
2639
            // later referenced to update the parent dataobject
2640
            if ($val instanceof DBComposite) {
2641
                $val->bindTo($this);
2642
                $this->record[$fieldName] = $val;
2643
            }
2644
        // Situation 2: Passing a literal or non-DBField object
2645
        } else {
2646
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2647
            if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) {
2648
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2649
            }
2650
2651
            if (!empty($val) && !is_scalar($val)) {
2652
                $dbField = $this->dbObject($fieldName);
2653
                if ($dbField && $dbField->scalarValueOnly()) {
2654
                    throw new InvalidArgumentException(
2655
                        sprintf(
2656
                            'DataObject::setField: %s only accepts scalars',
2657
                            $fieldName
2658
                        )
2659
                    );
2660
                }
2661
            }
2662
2663
            // if a field is not existing or has strictly changed
2664
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2665
                // TODO Add check for php-level defaults which are not set in the db
2666
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2667
                // At the very least, the type has changed
2668
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2669
2670
                if ((!isset($this->record[$fieldName]) && $val)
2671
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2672
                ) {
2673
                    // Value has changed as well, not just the type
2674
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2675
                }
2676
2677
                // Value is always saved back when strict check succeeds.
2678
                $this->record[$fieldName] = $val;
2679
            }
2680
        }
2681
        return $this;
2682
    }
2683
2684
    /**
2685
     * Set the value of the field, using a casting object.
2686
     * This is useful when you aren't sure that a date is in SQL format, for example.
2687
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2688
     * can be saved into the Image table.
2689
     *
2690
     * @param string $fieldName Name of the field
2691
     * @param mixed $value New field value
2692
     * @return $this
2693
     */
2694
    public function setCastedField($fieldName, $value)
2695
    {
2696
        if (!$fieldName) {
2697
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2698
        }
2699
        $fieldObj = $this->dbObject($fieldName);
2700
        if ($fieldObj) {
0 ignored issues
show
introduced by
$fieldObj is of type SilverStripe\ORM\FieldType\DBField, thus it always evaluated to true.
Loading history...
2701
            $fieldObj->setValue($value);
2702
            $fieldObj->saveInto($this);
2703
        } else {
2704
            $this->$fieldName = $value;
2705
        }
2706
        return $this;
2707
    }
2708
2709
    /**
2710
     * {@inheritdoc}
2711
     */
2712
    public function castingHelper($field)
2713
    {
2714
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2715
        if ($fieldSpec) {
2716
            return $fieldSpec;
2717
        }
2718
2719
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2720
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2721
        $queryParams = $this->getSourceQueryParams();
2722
        if (!empty($queryParams['Component.ExtraFields'])) {
2723
            $extraFields = $queryParams['Component.ExtraFields'];
2724
2725
            if (isset($extraFields[$field])) {
2726
                return $extraFields[$field];
2727
            }
2728
        }
2729
2730
        return parent::castingHelper($field);
2731
    }
2732
2733
    /**
2734
     * Returns true if the given field exists in a database column on any of
2735
     * the objects tables and optionally look up a dynamic getter with
2736
     * get<fieldName>().
2737
     *
2738
     * @param string $field Name of the field
2739
     * @return boolean True if the given field exists
2740
     */
2741
    public function hasField($field)
2742
    {
2743
        $schema = static::getSchema();
2744
        return (
2745
            array_key_exists($field, $this->record)
2746
            || array_key_exists($field, $this->components)
2747
            || $schema->fieldSpec(static::class, $field)
2748
            || $schema->unaryComponent(static::class, $field)
2749
            || $this->hasMethod("get{$field}")
2750
        );
2751
    }
2752
2753
    /**
2754
     * Returns true if the given field exists as a database column
2755
     *
2756
     * @param string $field Name of the field
2757
     *
2758
     * @return boolean
2759
     */
2760
    public function hasDatabaseField($field)
2761
    {
2762
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2763
        return !empty($spec);
2764
    }
2765
2766
    /**
2767
     * Returns true if the member is allowed to do the given action.
2768
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2769
     *
2770
     * @param string $perm The permission to be checked, such as 'View'.
2771
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2772
     * in user.
2773
     * @param array $context Additional $context to pass to extendedCan()
2774
     *
2775
     * @return boolean True if the the member is allowed to do the given action
2776
     */
2777
    public function can($perm, $member = null, $context = array())
2778
    {
2779
        if (!$member) {
2780
            $member = Security::getCurrentUser();
2781
        }
2782
2783
        if ($member && Permission::checkMember($member, "ADMIN")) {
2784
            return true;
2785
        }
2786
2787
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2788
            $method = 'can' . ucfirst($perm);
2789
            return $this->$method($member);
2790
        }
2791
2792
        $results = $this->extendedCan('can', $member);
2793
        if (isset($results)) {
2794
            return $results;
2795
        }
2796
2797
        return ($member && Permission::checkMember($member, $perm));
2798
    }
2799
2800
    /**
2801
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2802
     * expected to return one of three values:
2803
     *
2804
     *  - false: Disallow this permission, regardless of what other extensions say
2805
     *  - true: Allow this permission, as long as no other extensions return false
2806
     *  - NULL: Don't affect the outcome
2807
     *
2808
     * This method itself returns a tri-state value, and is designed to be used like this:
2809
     *
2810
     * <code>
2811
     * $extended = $this->extendedCan('canDoSomething', $member);
2812
     * if($extended !== null) return $extended;
2813
     * else return $normalValue;
2814
     * </code>
2815
     *
2816
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2817
     * @param Member|int $member
2818
     * @param array $context Optional context
2819
     * @return boolean|null
2820
     */
2821
    public function extendedCan($methodName, $member, $context = array())
2822
    {
2823
        $results = $this->extend($methodName, $member, $context);
2824
        if ($results && is_array($results)) {
2825
            // Remove NULLs
2826
            $results = array_filter($results, function ($v) {
2827
                return !is_null($v);
2828
            });
2829
            // If there are any non-NULL responses, then return the lowest one of them.
2830
            // If any explicitly deny the permission, then we don't get access
2831
            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...
2832
                return min($results);
2833
            }
2834
        }
2835
        return null;
2836
    }
2837
2838
    /**
2839
     * @param Member $member
2840
     * @return boolean
2841
     */
2842
    public function canView($member = null)
2843
    {
2844
        $extended = $this->extendedCan(__FUNCTION__, $member);
2845
        if ($extended !== null) {
2846
            return $extended;
2847
        }
2848
        return Permission::check('ADMIN', 'any', $member);
2849
    }
2850
2851
    /**
2852
     * @param Member $member
2853
     * @return boolean
2854
     */
2855
    public function canEdit($member = null)
2856
    {
2857
        $extended = $this->extendedCan(__FUNCTION__, $member);
2858
        if ($extended !== null) {
2859
            return $extended;
2860
        }
2861
        return Permission::check('ADMIN', 'any', $member);
2862
    }
2863
2864
    /**
2865
     * @param Member $member
2866
     * @return boolean
2867
     */
2868
    public function canDelete($member = null)
2869
    {
2870
        $extended = $this->extendedCan(__FUNCTION__, $member);
2871
        if ($extended !== null) {
2872
            return $extended;
2873
        }
2874
        return Permission::check('ADMIN', 'any', $member);
2875
    }
2876
2877
    /**
2878
     * @param Member $member
2879
     * @param array $context Additional context-specific data which might
2880
     * affect whether (or where) this object could be created.
2881
     * @return boolean
2882
     */
2883
    public function canCreate($member = null, $context = array())
2884
    {
2885
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2886
        if ($extended !== null) {
2887
            return $extended;
2888
        }
2889
        return Permission::check('ADMIN', 'any', $member);
2890
    }
2891
2892
    /**
2893
     * Debugging used by Debug::show()
2894
     *
2895
     * @return string HTML data representing this object
2896
     */
2897
    public function debug()
2898
    {
2899
        $class = static::class;
2900
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2901
        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...
2902
            foreach ($this->record as $fieldName => $fieldVal) {
2903
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2904
            }
2905
        }
2906
        $val .= "</ul>\n";
2907
        return $val;
2908
    }
2909
2910
    /**
2911
     * Return the DBField object that represents the given field.
2912
     * This works similarly to obj() with 2 key differences:
2913
     *   - it still returns an object even when the field has no value.
2914
     *   - it only matches fields and not methods
2915
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2916
     *
2917
     * @param string $fieldName Name of the field
2918
     * @return DBField The field as a DBField object
2919
     */
2920
    public function dbObject($fieldName)
2921
    {
2922
        // Check for field in DB
2923
        $schema = static::getSchema();
2924
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2925
        if (!$helper) {
2926
            return null;
2927
        }
2928
2929
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2930
            $tableClass = $this->record[$fieldName . '_Lazy'];
2931
            $this->loadLazyFields($tableClass);
2932
        }
2933
2934
        $value = isset($this->record[$fieldName])
2935
            ? $this->record[$fieldName]
2936
            : null;
2937
2938
        // If we have a DBField object in $this->record, then return that
2939
        if ($value instanceof DBField) {
2940
            return $value;
2941
        }
2942
2943
        list($class, $spec) = explode('.', $helper);
2944
        /** @var DBField $obj */
2945
        $table = $schema->tableName($class);
2946
        $obj = Injector::inst()->create($spec, $fieldName);
2947
        $obj->setTable($table);
2948
        $obj->setValue($value, $this, false);
2949
        return $obj;
2950
    }
2951
2952
    /**
2953
     * Traverses to a DBField referenced by relationships between data objects.
2954
     *
2955
     * The path to the related field is specified with dot separated syntax
2956
     * (eg: Parent.Child.Child.FieldName).
2957
     *
2958
     * If a relation is blank, this will return null instead.
2959
     * If a relation name is invalid (e.g. non-relation on a parent) this
2960
     * can throw a LogicException.
2961
     *
2962
     * @param string $fieldPath List of paths on this object. All items in this path
2963
     * must be ViewableData implementors
2964
     *
2965
     * @return mixed DBField of the field on the object or a DataList instance.
2966
     * @throws LogicException If accessing invalid relations
2967
     */
2968
    public function relObject($fieldPath)
2969
    {
2970
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
2971
        $component = $this;
2972
2973
        // Parse all relations
2974
        foreach (explode('.', $fieldPath) as $relation) {
2975
            if (!$component) {
2976
                return null;
2977
            }
2978
2979
            // Inspect relation type
2980
            if (ClassInfo::hasMethod($component, $relation)) {
2981
                $component = $component->$relation();
2982
            } elseif ($component instanceof Relation || $component instanceof DataList) {
2983
                // $relation could either be a field (aggregate), or another relation
2984
                $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

2984
                $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...
2985
                $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

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

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

3110
            $limitArguments = explode(',', /** @scrutinizer ignore-type */ $limit);
Loading history...
3111
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
3112
        } elseif ($limit) {
3113
            $result = $result->limit($limit);
3114
        }
3115
3116
        return $result;
3117
    }
3118
3119
3120
    /**
3121
     * Return the first item matching the given query.
3122
     * All calls to get_one() are cached.
3123
     *
3124
     * @param string $callerClass The class of objects to be returned
3125
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3126
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3127
     * @param boolean $cache Use caching
3128
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3129
     *
3130
     * @return DataObject|null The first item matching the query
3131
     */
3132
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
3133
    {
3134
        $SNG = singleton($callerClass);
3135
3136
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3137
        $cacheKey = md5(serialize($cacheComponents));
3138
3139
        $item = null;
3140
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3141
            $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...
3142
            $item = $dl->first();
3143
3144
            if ($cache) {
3145
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3146
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
3147
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
3148
                }
3149
            }
3150
        }
3151
3152
        if ($cache) {
3153
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
3154
        }
3155
3156
        return $item;
3157
    }
3158
3159
    /**
3160
     * Flush the cached results for all relations (has_one, has_many, many_many)
3161
     * Also clears any cached aggregate data.
3162
     *
3163
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3164
     *                            When false will just clear session-local cached data
3165
     * @return DataObject $this
3166
     */
3167
    public function flushCache($persistent = true)
3168
    {
3169
        if (static::class == self::class) {
0 ignored issues
show
introduced by
The condition static::class == self::class is always true.
Loading history...
3170
            self::$_cache_get_one = array();
3171
            return $this;
3172
        }
3173
3174
        $classes = ClassInfo::ancestry(static::class);
3175
        foreach ($classes as $class) {
3176
            if (isset(self::$_cache_get_one[$class])) {
3177
                unset(self::$_cache_get_one[$class]);
3178
            }
3179
        }
3180
3181
        $this->extend('flushCache');
3182
3183
        $this->components = array();
3184
        return $this;
3185
    }
3186
3187
    /**
3188
     * Flush the get_one global cache and destroy associated objects.
3189
     */
3190
    public static function flush_and_destroy_cache()
3191
    {
3192
        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...
3193
            foreach (self::$_cache_get_one as $class => $items) {
3194
                if (is_array($items)) {
3195
                    foreach ($items as $item) {
3196
                        if ($item) {
3197
                            $item->destroy();
3198
                        }
3199
                    }
3200
                }
3201
            }
3202
        }
3203
        self::$_cache_get_one = array();
3204
    }
3205
3206
    /**
3207
     * Reset all global caches associated with DataObject.
3208
     */
3209
    public static function reset()
3210
    {
3211
        // @todo Decouple these
3212
        DBClassName::clear_classname_cache();
3213
        ClassInfo::reset_db_cache();
3214
        static::getSchema()->reset();
3215
        self::$_cache_get_one = array();
3216
        self::$_cache_field_labels = array();
3217
    }
3218
3219
    /**
3220
     * Return the given element, searching by ID.
3221
     *
3222
     * This can be called either via `DataObject::get_by_id(MyClass::class, $id)`
3223
     * or `MyClass::get_by_id($id)`
3224
     *
3225
     * @param string|int $classOrID The class of the object to be returned, or id if called on target class
3226
     * @param int|bool $idOrCache The id of the element, or cache if called on target class
3227
     * @param boolean $cache See {@link get_one()}
3228
     *
3229
     * @return static The element
3230
     */
3231
    public static function get_by_id($classOrID, $idOrCache = null, $cache = true)
3232
    {
3233
        // Shift arguments if passing id in first or second argument
3234
        list ($class, $id, $cached) = is_numeric($classOrID)
3235
            ? [get_called_class(), $classOrID, isset($idOrCache) ? $idOrCache : $cache]
3236
            : [$classOrID, $idOrCache, $cache];
3237
3238
        // Validate class
3239
        if ($class === self::class) {
3240
            throw new InvalidArgumentException('DataObject::get_by_id() cannot query non-subclass DataObject directly');
3241
        }
3242
3243
        // Pass to get_one
3244
        $column = static::getSchema()->sqlColumnForField($class, 'ID');
3245
        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...
3246
    }
3247
3248
    /**
3249
     * Get the name of the base table for this object
3250
     *
3251
     * @return string
3252
     */
3253
    public function baseTable()
3254
    {
3255
        return static::getSchema()->baseDataTable($this);
3256
    }
3257
3258
    /**
3259
     * Get the base class for this object
3260
     *
3261
     * @return string
3262
     */
3263
    public function baseClass()
3264
    {
3265
        return static::getSchema()->baseDataClass($this);
3266
    }
3267
3268
    /**
3269
     * @var array Parameters used in the query that built this object.
3270
     * This can be used by decorators (e.g. lazy loading) to
3271
     * run additional queries using the same context.
3272
     */
3273
    protected $sourceQueryParams;
3274
3275
    /**
3276
     * @see $sourceQueryParams
3277
     * @return array
3278
     */
3279
    public function getSourceQueryParams()
3280
    {
3281
        return $this->sourceQueryParams;
3282
    }
3283
3284
    /**
3285
     * Get list of parameters that should be inherited to relations on this object
3286
     *
3287
     * @return array
3288
     */
3289
    public function getInheritableQueryParams()
3290
    {
3291
        $params = $this->getSourceQueryParams();
3292
        $this->extend('updateInheritableQueryParams', $params);
3293
        return $params;
3294
    }
3295
3296
    /**
3297
     * @see $sourceQueryParams
3298
     * @param array
3299
     */
3300
    public function setSourceQueryParams($array)
3301
    {
3302
        $this->sourceQueryParams = $array;
3303
    }
3304
3305
    /**
3306
     * @see $sourceQueryParams
3307
     * @param string $key
3308
     * @param string $value
3309
     */
3310
    public function setSourceQueryParam($key, $value)
3311
    {
3312
        $this->sourceQueryParams[$key] = $value;
3313
    }
3314
3315
    /**
3316
     * @see $sourceQueryParams
3317
     * @param string $key
3318
     * @return string
3319
     */
3320
    public function getSourceQueryParam($key)
3321
    {
3322
        if (isset($this->sourceQueryParams[$key])) {
3323
            return $this->sourceQueryParams[$key];
3324
        }
3325
        return null;
3326
    }
3327
3328
    //-------------------------------------------------------------------------------------------//
3329
3330
    /**
3331
     * Check the database schema and update it as necessary.
3332
     *
3333
     * @uses DataExtension->augmentDatabase()
3334
     */
3335
    public function requireTable()
3336
    {
3337
        // Only build the table if we've actually got fields
3338
        $schema = static::getSchema();
3339
        $table = $schema->tableName(static::class);
3340
        $fields = $schema->databaseFields(static::class, false);
3341
        $indexes = $schema->databaseIndexes(static::class, false);
3342
        $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

3342
        /** @scrutinizer ignore-call */ 
3343
        $extensions = self::database_extensions(static::class);
Loading history...
3343
3344
        if (empty($table)) {
3345
            throw new LogicException(
3346
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3347
            );
3348
        }
3349
3350
        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...
3351
            $hasAutoIncPK = get_parent_class($this) === self::class;
3352
            DB::require_table(
3353
                $table,
3354
                $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

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

3355
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3356
                $hasAutoIncPK,
3357
                $this->config()->get('create_table_options'),
3358
                $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

3358
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3359
            );
3360
        } else {
3361
            DB::dont_require_table($table);
3362
        }
3363
3364
        // Build any child tables for many_many items
3365
        if ($manyMany = $this->uninherited('many_many')) {
3366
            $extras = $this->uninherited('many_many_extraFields');
3367
            foreach ($manyMany as $component => $spec) {
3368
                // Get many_many spec
3369
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3370
                $parentField = $manyManyComponent['parentField'];
3371
                $childField = $manyManyComponent['childField'];
3372
                $tableOrClass = $manyManyComponent['join'];
3373
3374
                // Skip if backed by actual class
3375
                if (class_exists($tableOrClass)) {
3376
                    continue;
3377
                }
3378
3379
                // Build fields
3380
                $manymanyFields = array(
3381
                    $parentField => "Int",
3382
                    $childField => "Int",
3383
                );
3384
                if (isset($extras[$component])) {
3385
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3386
                }
3387
3388
                // Build index list
3389
                $manymanyIndexes = [
3390
                    $parentField => [
3391
                        'type' => 'index',
3392
                        'name' => $parentField,
3393
                        'columns' => [$parentField],
3394
                    ],
3395
                    $childField => [
3396
                        'type' => 'index',
3397
                        'name' => $childField,
3398
                        'columns' => [$childField],
3399
                    ],
3400
                ];
3401
                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

3401
                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

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