Passed
Push — 4 ( 34eb17...84b405 )
by Robbie
08:27 queued 12s
created

DataObject::getCMSCompositeValidator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

1102
                        $leftComponents->/** @scrutinizer ignore-call */ 
1103
                                         addMany($rightComponents->column('ID'));
Loading history...
1103
                    }
1104
                    $leftComponents->write();
1105
                }
1106
            }
1107
1108
            if ($hasMany = $this->hasMany()) {
1109
                foreach ($hasMany as $relationship => $class) {
1110
                    $leftComponents = $leftObj->getComponents($relationship);
1111
                    $rightComponents = $rightObj->getComponents($relationship);
1112
                    if ($rightComponents && $rightComponents->exists()) {
1113
                        $leftComponents->addMany($rightComponents->column('ID'));
1114
                    }
1115
                    $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

1115
                    $leftComponents->/** @scrutinizer ignore-call */ 
1116
                                     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

1115
                    $leftComponents->/** @scrutinizer ignore-call */ 
1116
                                     write();
Loading history...
1116
                }
1117
            }
1118
        }
1119
1120
        return true;
1121
    }
1122
1123
    /**
1124
     * Forces the record to think that all its data has changed.
1125
     * Doesn't write to the database. Force-change preseved until
1126
     * next write. Existing CHANGE_VALUE or CHANGE_STRICT values
1127
     * are preserved.
1128
     *
1129
     * @return $this
1130
     */
1131
    public function forceChange()
1132
    {
1133
        // Ensure lazy fields loaded
1134
        $this->loadLazyFields();
1135
1136
        // Populate the null values in record so that they actually get written
1137
        foreach (array_keys(static::getSchema()->fieldSpecs(static::class)) as $fieldName) {
1138
            if (!isset($this->record[$fieldName])) {
1139
                $this->record[$fieldName] = null;
1140
            }
1141
        }
1142
1143
        $this->changeForced = true;
1144
1145
        return $this;
1146
    }
1147
1148
    /**
1149
     * Validate the current object.
1150
     *
1151
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1152
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1153
     *
1154
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1155
     * and onAfterWrite() won't get called either.
1156
     *
1157
     * It is expected that you call validate() in your own application to test that an object is valid before
1158
     * attempting a write, and respond appropriately if it isn't.
1159
     *
1160
     * @see {@link ValidationResult}
1161
     * @return ValidationResult
1162
     */
1163
    public function validate()
1164
    {
1165
        $result = ValidationResult::create();
1166
        $this->extend('validate', $result);
1167
        return $result;
1168
    }
1169
1170
    /**
1171
     * Public accessor for {@see DataObject::validate()}
1172
     *
1173
     * @return ValidationResult
1174
     */
1175
    public function doValidate()
1176
    {
1177
        Deprecation::notice('5.0', 'Use validate');
1178
        return $this->validate();
1179
    }
1180
1181
    /**
1182
     * Event handler called before writing to the database.
1183
     * You can overload this to clean up or otherwise process data before writing it to the
1184
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1185
     *
1186
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1187
     *
1188
     * @uses DataExtension->onBeforeWrite()
1189
     */
1190
    protected function onBeforeWrite()
1191
    {
1192
        $this->brokenOnWrite = false;
1193
1194
        $dummy = null;
1195
        $this->extend('onBeforeWrite', $dummy);
1196
    }
1197
1198
    /**
1199
     * Event handler called after writing to the database.
1200
     * You can overload this to act upon changes made to the data after it is written.
1201
     * $this->changed will have a record
1202
     * database.  Don't forget to call parent::onAfterWrite(), though!
1203
     *
1204
     * @uses DataExtension->onAfterWrite()
1205
     */
1206
    protected function onAfterWrite()
1207
    {
1208
        $dummy = null;
1209
        $this->extend('onAfterWrite', $dummy);
1210
    }
1211
1212
    /**
1213
     * Find all objects that will be cascade deleted if this object is deleted
1214
     *
1215
     * Notes:
1216
     *   - If this object is versioned, objects will only be searched in the same stage as the given record.
1217
     *   - This will only be useful prior to deletion, as post-deletion this record will no longer exist.
1218
     *
1219
     * @param bool $recursive True if recursive
1220
     * @param ArrayList $list Optional list to add items to
1221
     * @return ArrayList list of objects
1222
     */
1223
    public function findCascadeDeletes($recursive = true, $list = null)
1224
    {
1225
        // Find objects in these relationships
1226
        return $this->findRelatedObjects('cascade_deletes', $recursive, $list);
1227
    }
1228
1229
    /**
1230
     * Event handler called before deleting from the database.
1231
     * You can overload this to clean up or otherwise process data before delete this
1232
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1233
     *
1234
     * @uses DataExtension->onBeforeDelete()
1235
     */
1236
    protected function onBeforeDelete()
1237
    {
1238
        $this->brokenOnDelete = false;
1239
1240
        $dummy = null;
1241
        $this->extend('onBeforeDelete', $dummy);
1242
1243
        // Cascade deletes
1244
        $deletes = $this->findCascadeDeletes(false);
1245
        foreach ($deletes as $delete) {
1246
            $delete->delete();
1247
        }
1248
    }
1249
1250
    protected function onAfterDelete()
1251
    {
1252
        $this->extend('onAfterDelete');
1253
    }
1254
1255
    /**
1256
     * Load the default values in from the self::$defaults array.
1257
     * Will traverse the defaults of the current class and all its parent classes.
1258
     * Called by the constructor when creating new records.
1259
     *
1260
     * @uses DataExtension->populateDefaults()
1261
     * @return DataObject $this
1262
     */
1263
    public function populateDefaults()
1264
    {
1265
        $classes = array_reverse(ClassInfo::ancestry($this));
1266
1267
        foreach ($classes as $class) {
1268
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1269
1270
            if ($defaults && !is_array($defaults)) {
1271
                user_error(
1272
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1273
                    E_USER_WARNING
1274
                );
1275
                $defaults = null;
1276
            }
1277
1278
            if ($defaults) {
1279
                foreach ($defaults as $fieldName => $fieldValue) {
1280
                    // SRM 2007-03-06: Stricter check
1281
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1282
                        $this->$fieldName = $fieldValue;
1283
                    }
1284
                    // Set many-many defaults with an array of ids
1285
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1286
                        /** @var ManyManyList $manyManyJoin */
1287
                        $manyManyJoin = $this->$fieldName();
1288
                        $manyManyJoin->setByIDList($fieldValue);
1289
                    }
1290
                }
1291
            }
1292
            if ($class == self::class) {
1293
                break;
1294
            }
1295
        }
1296
1297
        $this->extend('populateDefaults');
1298
        return $this;
1299
    }
1300
1301
    /**
1302
     * Determine validation of this object prior to write
1303
     *
1304
     * @return ValidationException Exception generated by this write, or null if valid
1305
     */
1306
    protected function validateWrite()
1307
    {
1308
        if ($this->ObsoleteClassName) {
1309
            return new ValidationException(
1310
                "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - " .
1311
                "you need to change the ClassName before you can write it"
1312
            );
1313
        }
1314
1315
        // Note: Validation can only be disabled at the global level, not per-model
1316
        if (DataObject::config()->uninherited('validation_enabled')) {
1317
            $result = $this->validate();
1318
            if (!$result->isValid()) {
1319
                return new ValidationException($result);
1320
            }
1321
        }
1322
        return null;
1323
    }
1324
1325
    /**
1326
     * Prepare an object prior to write
1327
     *
1328
     * @throws ValidationException
1329
     */
1330
    protected function preWrite()
1331
    {
1332
        // Validate this object
1333
        if ($writeException = $this->validateWrite()) {
1334
            // Used by DODs to clean up after themselves, eg, Versioned
1335
            $this->invokeWithExtensions('onAfterSkippedWrite');
1336
            throw $writeException;
1337
        }
1338
1339
        // Check onBeforeWrite
1340
        $this->brokenOnWrite = true;
1341
        $this->onBeforeWrite();
1342
        if ($this->brokenOnWrite) {
1343
            user_error(static::class . " has a broken onBeforeWrite() function."
1344
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1345
        }
1346
    }
1347
1348
    /**
1349
     * Detects and updates all changes made to this object
1350
     *
1351
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1352
     * @return bool True if any changes are detected
1353
     */
1354
    protected function updateChanges($forceChanges = false)
1355
    {
1356
        if ($forceChanges) {
1357
            // Force changes, but only for loaded fields
1358
            foreach ($this->record as $field => $value) {
1359
                $this->changed[$field] = static::CHANGE_VALUE;
1360
            }
1361
            return true;
1362
        }
1363
        return $this->isChanged();
1364
    }
1365
1366
    /**
1367
     * Writes a subset of changes for a specific table to the given manipulation
1368
     *
1369
     * @param string $baseTable Base table
1370
     * @param string $now Timestamp to use for the current time
1371
     * @param bool $isNewRecord Whether this should be treated as a new record write
1372
     * @param array $manipulation Manipulation to write to
1373
     * @param string $class Class of table to manipulate
1374
     */
1375
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1376
    {
1377
        $schema = $this->getSchema();
1378
        $table = $schema->tableName($class);
1379
        $manipulation[$table] = [];
1380
1381
        $changed = $this->getChangedFields();
1382
1383
        // Extract records for this table
1384
        foreach ($this->record as $fieldName => $fieldValue) {
1385
            // we're not attempting to reset the BaseTable->ID
1386
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1387
            if (empty($changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1388
                continue;
1389
            }
1390
1391
            // Ensure this field pertains to this table
1392
            $specification = $schema->fieldSpec(
1393
                $class,
1394
                $fieldName,
1395
                DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
1396
            );
1397
            if (!$specification) {
1398
                continue;
1399
            }
1400
1401
            // if database column doesn't correlate to a DBField instance...
1402
            $fieldObj = $this->dbObject($fieldName);
1403
            if (!$fieldObj) {
1404
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1405
            }
1406
1407
            // Write to manipulation
1408
            $fieldObj->writeToManipulation($manipulation[$table]);
1409
        }
1410
1411
        // Ensure update of Created and LastEdited columns
1412
        if ($baseTable === $table) {
1413
            $manipulation[$table]['fields']['LastEdited'] = $now;
1414
            if ($isNewRecord) {
1415
                $manipulation[$table]['fields']['Created'] = empty($this->record['Created'])
1416
                    ? $now
1417
                    : $this->record['Created'];
1418
                $manipulation[$table]['fields']['ClassName'] = static::class;
1419
            }
1420
        }
1421
1422
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1423
        // attempt an update, as though it were a normal update.
1424
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1425
        $manipulation[$table]['class'] = $class;
1426
        if ($this->isInDB()) {
1427
            $manipulation[$table]['id'] = $this->record['ID'];
1428
        }
1429
    }
1430
1431
    /**
1432
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1433
     *
1434
     * Does nothing if an ID is already assigned for this record
1435
     *
1436
     * @param string $baseTable Base table
1437
     * @param string $now Timestamp to use for the current time
1438
     */
1439
    protected function writeBaseRecord($baseTable, $now)
1440
    {
1441
        // Generate new ID if not specified
1442
        if ($this->isInDB()) {
1443
            return;
1444
        }
1445
1446
        // Perform an insert on the base table
1447
        $manipulation = [];
1448
        $this->prepareManipulationTable($baseTable, $now, true, $manipulation, $this->baseClass());
1449
        DB::manipulate($manipulation);
1450
1451
        $this->changed['ID'] = self::CHANGE_VALUE;
1452
        $this->record['ID'] = DB::get_generated_id($baseTable);
1453
    }
1454
1455
    /**
1456
     * Generate and write the database manipulation for all changed fields
1457
     *
1458
     * @param string $baseTable Base table
1459
     * @param string $now Timestamp to use for the current time
1460
     * @param bool $isNewRecord If this is a new record
1461
     * @throws InvalidArgumentException
1462
     */
1463
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1464
    {
1465
        // Generate database manipulations for each class
1466
        $manipulation = [];
1467
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1468
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1469
        }
1470
1471
        // Allow extensions to extend this manipulation
1472
        $this->extend('augmentWrite', $manipulation);
1473
1474
        // New records have their insert into the base data table done first, so that they can pass the
1475
        // generated ID on to the rest of the manipulation
1476
        if ($isNewRecord) {
1477
            $manipulation[$baseTable]['command'] = 'update';
1478
        }
1479
1480
        // Make sure none of our field assignment are arrays
1481
        foreach ($manipulation as $tableManipulation) {
1482
            if (!isset($tableManipulation['fields'])) {
1483
                continue;
1484
            }
1485
            foreach ($tableManipulation['fields'] as $fieldName => $fieldValue) {
1486
                if (is_array($fieldValue)) {
1487
                    $dbObject = $this->dbObject($fieldName);
1488
                    // If the field allows non-scalar values we'll let it do dynamic assignments
1489
                    if ($dbObject && $dbObject->scalarValueOnly()) {
1490
                        throw new InvalidArgumentException(
1491
                            'DataObject::writeManipulation: parameterised field assignments are disallowed'
1492
                        );
1493
                    }
1494
                }
1495
            }
1496
        }
1497
1498
        // Perform the manipulation
1499
        DB::manipulate($manipulation);
1500
    }
1501
1502
    /**
1503
     * Writes all changes to this object to the database.
1504
     *  - It will insert a record whenever ID isn't set, otherwise update.
1505
     *  - All relevant tables will be updated.
1506
     *  - $this->onBeforeWrite() gets called beforehand.
1507
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1508
     *
1509
     * @uses DataExtension->augmentWrite()
1510
     *
1511
     * @param boolean       $showDebug Show debugging information
1512
     * @param boolean       $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1513
     * @param boolean       $forceWrite Write to database even if there are no changes
1514
     * @param boolean|array $writeComponents Call write() on all associated component instances which were previously
1515
     *                      retrieved through {@link getComponent()}, {@link getComponents()} or
1516
     *                      {@link getManyManyComponents()}. Default to `false`. The parameter can also be provided in
1517
     *                      the form of an array: `['recursive' => true, skip => ['Page'=>[1,2,3]]`. This avoid infinite
1518
     *                      loops when one DataObject are components of each other.
1519
     * @return int The ID of the record
1520
     * @throws ValidationException Exception that can be caught and handled by the calling function
1521
     */
1522
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1523
    {
1524
        $now = DBDatetime::now()->Rfc2822();
1525
1526
        // Execute pre-write tasks
1527
        $this->preWrite();
1528
1529
        // Check if we are doing an update or an insert
1530
        $isNewRecord = !$this->isInDB() || $forceInsert;
1531
1532
        // Check changes exist, abort if there are none
1533
        $hasChanges = $this->updateChanges($isNewRecord);
1534
        if ($hasChanges || $forceWrite || $isNewRecord) {
1535
            // Ensure Created and LastEdited are populated
1536
            if (!isset($this->record['Created'])) {
1537
                $this->record['Created'] = $now;
1538
            }
1539
            $this->record['LastEdited'] = $now;
1540
1541
            // New records have their insert into the base data table done first, so that they can pass the
1542
            // generated primary key on to the rest of the manipulation
1543
            $baseTable = $this->baseTable();
1544
            $this->writeBaseRecord($baseTable, $now);
1545
1546
            // Write the DB manipulation for all changed fields
1547
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1548
1549
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1550
            $this->writeRelations();
1551
            $this->onAfterWrite();
1552
1553
            // Reset isChanged data
1554
            // DBComposites properly bound to the parent record will also have their isChanged value reset
1555
            $this->changed = [];
1556
            $this->changeForced = false;
1557
            $this->original = $this->record;
1558
        } else {
1559
            if ($showDebug) {
1560
                Debug::message("no changes for DataObject");
1561
            }
1562
1563
            // Used by DODs to clean up after themselves, eg, Versioned
1564
            $this->invokeWithExtensions('onAfterSkippedWrite');
1565
        }
1566
1567
        // Write relations as necessary
1568
        if ($writeComponents) {
1569
            $recursive = true;
1570
            $skip = [];
1571
            if (is_array($writeComponents)) {
1572
                $recursive = isset($writeComponents['recursive']) && $writeComponents['recursive'];
1573
                $skip = isset($writeComponents['skip']) && is_array($writeComponents['skip'])
1574
                    ? $writeComponents['skip']
1575
                    : [];
1576
            }
1577
            $this->writeComponents($recursive, $skip);
1578
        }
1579
1580
        // Clears the cache for this object so get_one returns the correct object.
1581
        $this->flushCache();
1582
1583
        return $this->record['ID'];
1584
    }
1585
1586
    /**
1587
     * Writes cached relation lists to the database, if possible
1588
     */
1589
    public function writeRelations()
1590
    {
1591
        if (!$this->isInDB()) {
1592
            return;
1593
        }
1594
1595
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1596
        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...
1597
            foreach ($this->unsavedRelations as $name => $list) {
1598
                $list->changeToList($this->$name());
1599
            }
1600
            $this->unsavedRelations = [];
1601
        }
1602
    }
1603
1604
    /**
1605
     * Write the cached components to the database. Cached components could refer to two different instances of the
1606
     * same record.
1607
     *
1608
     * @param bool $recursive Recursively write components
1609
     * @param array $skip List of DataObject references to skip
1610
     * @return DataObject $this
1611
     */
1612
    public function writeComponents($recursive = false, $skip = [])
1613
    {
1614
        // Make sure we add our current object to the skip list
1615
        $this->skipWriteComponents($recursive, $this, $skip);
1616
1617
        // All our write calls have the same arguments ... just need make sure the skip list is pass by reference
1618
        $args = [
1619
            false, false, false,
1620
            $recursive ? ["recursive" => $recursive, "skip" => &$skip] : false
1621
        ];
1622
1623
        foreach ($this->components as $component) {
1624
            if (!$this->skipWriteComponents($recursive, $component, $skip)) {
1625
                $component->write(...$args);
0 ignored issues
show
Bug introduced by
It seems like $args can also be of type array<string,mixed|true>; however, parameter $showDebug of SilverStripe\ORM\DataObject::write() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

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

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

2473
            $compositeValidator->addValidator($this->/** @scrutinizer ignore-call */ getCMSValidator());
Loading history...
2474
        }
2475
2476
        // Extend validator - forward support, will be supported beyond 5.0.0
2477
        $this->invokeWithExtensions('updateCMSCompositeValidator', $compositeValidator);
2478
2479
        return $compositeValidator;
2480
    }
2481
2482
    /**
2483
     * Used for simple frontend forms without relation editing
2484
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2485
     * by default. To customize, either overload this method in your
2486
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2487
     *
2488
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2489
     *
2490
     * @param array $params See {@link scaffoldFormFields()}
2491
     * @return FieldList Always returns a simple field collection without TabSet.
2492
     */
2493
    public function getFrontEndFields($params = null)
2494
    {
2495
        $untabbedFields = $this->scaffoldFormFields($params);
2496
        $this->extend('updateFrontEndFields', $untabbedFields);
2497
2498
        return $untabbedFields;
2499
    }
2500
2501
    public function getViewerTemplates($suffix = '')
2502
    {
2503
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2504
    }
2505
2506
    /**
2507
     * Gets the value of a field.
2508
     * Called by {@link __get()} and any getFieldName() methods you might create.
2509
     *
2510
     * @param string $field The name of the field
2511
     * @return mixed The field value
2512
     */
2513
    public function getField($field)
2514
    {
2515
        // If we already have a value in $this->record, then we should just return that
2516
        if (isset($this->record[$field])) {
2517
            return $this->record[$field];
2518
        }
2519
2520
        // Do we have a field that needs to be lazy loaded?
2521
        if (isset($this->record[$field . '_Lazy'])) {
2522
            $tableClass = $this->record[$field . '_Lazy'];
2523
            $this->loadLazyFields($tableClass);
2524
        }
2525
        $schema = static::getSchema();
2526
2527
        // Support unary relations as fields
2528
        if ($schema->unaryComponent(static::class, $field)) {
2529
            return $this->getComponent($field);
2530
        }
2531
2532
        // In case of complex fields, return the DBField object
2533
        if ($schema->compositeField(static::class, $field)) {
2534
            $this->record[$field] = $this->dbObject($field);
2535
        }
2536
2537
        return isset($this->record[$field]) ? $this->record[$field] : null;
2538
    }
2539
2540
    /**
2541
     * Loads all the stub fields that an initial lazy load didn't load fully.
2542
     *
2543
     * @param string $class Class to load the values from. Others are joined as required.
2544
     * Not specifying a tableClass will load all lazy fields from all tables.
2545
     * @return bool Flag if lazy loading succeeded
2546
     */
2547
    protected function loadLazyFields($class = null)
2548
    {
2549
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2550
            return false;
2551
        }
2552
2553
        if (!$class) {
2554
            $loaded = [];
2555
2556
            foreach ($this->record as $key => $value) {
2557
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2558
                    $this->loadLazyFields($value);
2559
                    $loaded[$value] = $value;
2560
                }
2561
            }
2562
2563
            return false;
2564
        }
2565
2566
        $dataQuery = new DataQuery($class);
2567
2568
        // Reset query parameter context to that of this DataObject
2569
        if ($params = $this->getSourceQueryParams()) {
2570
            foreach ($params as $key => $value) {
2571
                $dataQuery->setQueryParam($key, $value);
2572
            }
2573
        }
2574
2575
        // Limit query to the current record, unless it has the Versioned extension,
2576
        // in which case it requires special handling through augmentLoadLazyFields()
2577
        $schema = static::getSchema();
2578
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2579
        $dataQuery->where([
2580
            $baseIDColumn => $this->record['ID']
2581
        ])->limit(1);
2582
2583
        $columns = [];
2584
2585
        // Add SQL for fields, both simple & multi-value
2586
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2587
        $databaseFields = $schema->databaseFields($class, false);
2588
        foreach ($databaseFields as $k => $v) {
2589
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2590
                $columns[] = $k;
2591
            }
2592
        }
2593
2594
        if ($columns) {
2595
            $query = $dataQuery->query();
2596
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2597
            $this->extend('augmentSQL', $query, $dataQuery);
2598
2599
            $dataQuery->setQueriedColumns($columns);
2600
            $newData = $dataQuery->execute()->record();
2601
2602
            // Load the data into record
2603
            if ($newData) {
2604
                foreach ($newData as $k => $v) {
2605
                    if (in_array($k, $columns)) {
2606
                        $this->record[$k] = $v;
2607
                        $this->original[$k] = $v;
2608
                        unset($this->record[$k . '_Lazy']);
2609
                    }
2610
                }
2611
2612
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2613
            } else {
2614
                foreach ($columns as $k) {
2615
                    $this->record[$k] = null;
2616
                    $this->original[$k] = null;
2617
                    unset($this->record[$k . '_Lazy']);
2618
                }
2619
            }
2620
        }
2621
        return true;
2622
    }
2623
2624
    /**
2625
     * Return the fields that have changed since the last write.
2626
     *
2627
     * The change level affects what the functions defines as "changed":
2628
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2629
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2630
     *   for example a change from 0 to null would not be included.
2631
     *
2632
     * Example return:
2633
     * <code>
2634
     * array(
2635
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2636
     * )
2637
     * </code>
2638
     *
2639
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2640
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2641
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2642
     * @return array
2643
     */
2644
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2645
    {
2646
        $changedFields = [];
2647
2648
        // Update the changed array with references to changed obj-fields
2649
        foreach ($this->record as $k => $v) {
2650
            // Prevents DBComposite infinite looping on isChanged
2651
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2652
                continue;
2653
            }
2654
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2655
                $this->changed[$k] = self::CHANGE_VALUE;
2656
            }
2657
        }
2658
2659
        // If change was forced, then derive change data from $this->record
2660
        if ($this->changeForced && $changeLevel <= self::CHANGE_STRICT) {
2661
            $changed = array_combine(
2662
                array_keys($this->record),
2663
                array_fill(0, count($this->record), self::CHANGE_STRICT)
2664
            );
2665
            // @todo Find better way to allow versioned to write a new version after forceChange
2666
            unset($changed['Version']);
2667
        } else {
2668
            $changed = $this->changed;
2669
        }
2670
2671
        if (is_array($databaseFieldsOnly)) {
2672
            $fields = array_intersect_key($changed, array_flip($databaseFieldsOnly));
0 ignored issues
show
Bug introduced by
It seems like $changed can also be of type false; however, parameter $array1 of array_intersect_key() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

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

3113
                $singleton = DataObject::singleton($component->/** @scrutinizer ignore-call */ dataClass());
Loading history...
3114
                $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

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

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

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

3481
        /** @scrutinizer ignore-call */ 
3482
        $extensions = self::database_extensions(static::class);
Loading history...
3482
3483
        if (empty($table)) {
3484
            throw new LogicException(
3485
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3486
            );
3487
        }
3488
3489
        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...
3490
            $hasAutoIncPK = get_parent_class($this) === self::class;
3491
            DB::require_table(
3492
                $table,
3493
                $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

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

3494
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3495
                $hasAutoIncPK,
3496
                $this->config()->get('create_table_options'),
3497
                $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

3497
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3498
            );
3499
        } else {
3500
            DB::dont_require_table($table);
3501
        }
3502
3503
        // Build any child tables for many_many items
3504
        if ($manyMany = $this->uninherited('many_many')) {
3505
            $extras = $this->uninherited('many_many_extraFields');
3506
            foreach ($manyMany as $component => $spec) {
3507
                // Get many_many spec
3508
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3509
                $parentField = $manyManyComponent['parentField'];
3510
                $childField = $manyManyComponent['childField'];
3511
                $tableOrClass = $manyManyComponent['join'];
3512
3513
                // Skip if backed by actual class
3514
                if (class_exists($tableOrClass)) {
3515
                    continue;
3516
                }
3517
3518
                // Build fields
3519
                $manymanyFields = [
3520
                    $parentField => "Int",
3521
                    $childField => "Int",
3522
                ];
3523
                if (isset($extras[$component])) {
3524
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3525
                }
3526
3527
                // Build index list
3528
                $manymanyIndexes = [
3529
                    $parentField => [
3530
                        'type' => 'index',
3531
                        'name' => $parentField,
3532
                        'columns' => [$parentField],
3533
                    ],
3534
                    $childField => [
3535
                        'type' => 'index',
3536
                        'name' => $childField,
3537
                        'columns' => [$childField],
3538
                    ],
3539
                ];
3540
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Bug introduced by
$manymanyFields of type array|string[] is incompatible with the type string expected by parameter $fieldSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

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

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

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

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