Passed
Pull Request — 4 (#9735)
by Steve
08:57
created

DataObject::delete()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 38
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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

1154
                        $leftComponents->/** @scrutinizer ignore-call */ 
1155
                                         addMany($rightComponents->column('ID'));
Loading history...
1155
                    }
1156
                    $leftComponents->write();
1157
                }
1158
            }
1159
1160
            if ($hasMany = $this->hasMany()) {
1161
                foreach ($hasMany as $relationship => $class) {
1162
                    $leftComponents = $leftObj->getComponents($relationship);
1163
                    $rightComponents = $rightObj->getComponents($relationship);
1164
                    if ($rightComponents && $rightComponents->exists()) {
1165
                        $leftComponents->addMany($rightComponents->column('ID'));
1166
                    }
1167
                    $leftComponents->write();
0 ignored issues
show
Bug introduced by
The method write() does not exist on SilverStripe\ORM\UnsavedRelationList. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

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

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

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

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

2529
            $compositeValidator->addValidator($this->/** @scrutinizer ignore-call */ getCMSValidator());
Loading history...
2530
        }
2531
2532
        // Extend validator - forward support, will be supported beyond 5.0.0
2533
        $this->invokeWithExtensions('updateCMSCompositeValidator', $compositeValidator);
2534
2535
        return $compositeValidator;
2536
    }
2537
2538
    /**
2539
     * Used for simple frontend forms without relation editing
2540
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2541
     * by default. To customize, either overload this method in your
2542
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2543
     *
2544
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2545
     *
2546
     * @param array $params See {@link scaffoldFormFields()}
2547
     * @return FieldList Always returns a simple field collection without TabSet.
2548
     */
2549
    public function getFrontEndFields($params = null)
2550
    {
2551
        $untabbedFields = $this->scaffoldFormFields($params);
2552
        $this->extend('updateFrontEndFields', $untabbedFields);
2553
2554
        return $untabbedFields;
2555
    }
2556
2557
    public function getViewerTemplates($suffix = '')
2558
    {
2559
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2560
    }
2561
2562
    /**
2563
     * Gets the value of a field.
2564
     * Called by {@link __get()} and any getFieldName() methods you might create.
2565
     *
2566
     * @param string $field The name of the field
2567
     * @return mixed The field value
2568
     */
2569
    public function getField($field)
2570
    {
2571
        // If we already have a value in $this->record, then we should just return that
2572
        if (isset($this->record[$field])) {
2573
            return $this->record[$field];
2574
        }
2575
2576
        // Do we have a field that needs to be lazy loaded?
2577
        if (isset($this->record[$field . '_Lazy'])) {
2578
            $tableClass = $this->record[$field . '_Lazy'];
2579
            $this->loadLazyFields($tableClass);
2580
        }
2581
        $schema = static::getSchema();
2582
2583
        // Support unary relations as fields
2584
        if ($schema->unaryComponent(static::class, $field)) {
2585
            return $this->getComponent($field);
2586
        }
2587
2588
        // In case of complex fields, return the DBField object
2589
        if ($schema->compositeField(static::class, $field)) {
2590
            $this->record[$field] = $this->dbObject($field);
2591
        }
2592
2593
        return isset($this->record[$field]) ? $this->record[$field] : null;
2594
    }
2595
2596
    /**
2597
     * Loads all the stub fields that an initial lazy load didn't load fully.
2598
     *
2599
     * @param string $class Class to load the values from. Others are joined as required.
2600
     * Not specifying a tableClass will load all lazy fields from all tables.
2601
     * @return bool Flag if lazy loading succeeded
2602
     */
2603
    protected function loadLazyFields($class = null)
2604
    {
2605
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2606
            return false;
2607
        }
2608
2609
        if (!$class) {
2610
            $loaded = [];
2611
2612
            foreach ($this->record as $key => $value) {
2613
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2614
                    $this->loadLazyFields($value);
2615
                    $loaded[$value] = $value;
2616
                }
2617
            }
2618
2619
            return false;
2620
        }
2621
2622
        $dataQuery = new DataQuery($class);
2623
2624
        // Reset query parameter context to that of this DataObject
2625
        if ($params = $this->getSourceQueryParams()) {
2626
            foreach ($params as $key => $value) {
2627
                $dataQuery->setQueryParam($key, $value);
2628
            }
2629
        }
2630
2631
        // Limit query to the current record, unless it has the Versioned extension,
2632
        // in which case it requires special handling through augmentLoadLazyFields()
2633
        $schema = static::getSchema();
2634
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2635
        $dataQuery->where([
2636
            $baseIDColumn => $this->record['ID']
2637
        ])->limit(1);
2638
2639
        $columns = [];
2640
2641
        // Add SQL for fields, both simple & multi-value
2642
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2643
        $databaseFields = $schema->databaseFields($class, false);
2644
        foreach ($databaseFields as $k => $v) {
2645
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2646
                $columns[] = $k;
2647
            }
2648
        }
2649
2650
        if ($columns) {
2651
            $query = $dataQuery->query();
2652
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2653
            $this->extend('augmentSQL', $query, $dataQuery);
2654
2655
            $dataQuery->setQueriedColumns($columns);
2656
            $newData = $dataQuery->execute()->record();
2657
2658
            // Load the data into record
2659
            if ($newData) {
2660
                foreach ($newData as $k => $v) {
2661
                    if (in_array($k, $columns)) {
2662
                        $this->record[$k] = $v;
2663
                        $this->original[$k] = $v;
2664
                        unset($this->record[$k . '_Lazy']);
2665
                    }
2666
                }
2667
2668
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2669
            } else {
2670
                foreach ($columns as $k) {
2671
                    $this->record[$k] = null;
2672
                    $this->original[$k] = null;
2673
                    unset($this->record[$k . '_Lazy']);
2674
                }
2675
            }
2676
        }
2677
        return true;
2678
    }
2679
2680
    /**
2681
     * Return the fields that have changed since the last write.
2682
     *
2683
     * The change level affects what the functions defines as "changed":
2684
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2685
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2686
     *   for example a change from 0 to null would not be included.
2687
     *
2688
     * Example return:
2689
     * <code>
2690
     * array(
2691
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2692
     * )
2693
     * </code>
2694
     *
2695
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2696
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2697
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2698
     * @return array
2699
     */
2700
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2701
    {
2702
        $changedFields = [];
2703
2704
        // Update the changed array with references to changed obj-fields
2705
        foreach ($this->record as $k => $v) {
2706
            // Prevents DBComposite infinite looping on isChanged
2707
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2708
                continue;
2709
            }
2710
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2711
                $this->changed[$k] = self::CHANGE_VALUE;
2712
            }
2713
        }
2714
2715
        // If change was forced, then derive change data from $this->record
2716
        if ($this->changeForced && $changeLevel <= self::CHANGE_STRICT) {
2717
            $changed = array_combine(
2718
                array_keys($this->record),
2719
                array_fill(0, count($this->record), self::CHANGE_STRICT)
2720
            );
2721
            // @todo Find better way to allow versioned to write a new version after forceChange
2722
            unset($changed['Version']);
2723
        } else {
2724
            $changed = $this->changed;
2725
        }
2726
2727
        if (is_array($databaseFieldsOnly)) {
2728
            $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

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

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

3170
                $component = $singleton->dbObject($relation) ?: $component->/** @scrutinizer ignore-call */ relation($relation);
Loading history...
3171
            } elseif ($component instanceof DataObject && ($dbObject = $component->dbObject($relation))) {
3172
                $component = $dbObject;
3173
            } elseif ($component instanceof ViewableData && $component->hasField($relation)) {
3174
                $component = $component->obj($relation);
3175
            } else {
3176
                throw new LogicException(
3177
                    "$relation is not a relation/field on " . get_class($component)
3178
                );
3179
            }
3180
        }
3181
        return $component;
3182
    }
3183
3184
    /**
3185
     * Traverses to a field referenced by relationships between data objects, returning the value
3186
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
3187
     *
3188
     * @param string $fieldName string
3189
     * @return mixed Will return null on a missing value
3190
     */
3191
    public function relField($fieldName)
3192
    {
3193
        // Navigate to relative parent using relObject() if needed
3194
        $component = $this;
3195
        if (($pos = strrpos($fieldName, '.')) !== false) {
3196
            $relation = substr($fieldName, 0, $pos);
3197
            $fieldName = substr($fieldName, $pos + 1);
3198
            $component = $this->relObject($relation);
3199
        }
3200
3201
        // Bail if the component is null
3202
        if (!$component) {
3203
            return null;
3204
        }
3205
        if (ClassInfo::hasMethod($component, $fieldName)) {
3206
            return $component->$fieldName();
3207
        }
3208
        return $component->$fieldName;
3209
    }
3210
3211
    /**
3212
     * Temporary hack to return an association name, based on class, to get around the mangle
3213
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3214
     *
3215
     * @param string $className
3216
     * @return string
3217
     */
3218
    public function getReverseAssociation($className)
3219
    {
3220
        if (is_array($this->manyMany())) {
0 ignored issues
show
introduced by
The condition is_array($this->manyMany()) is always true.
Loading history...
3221
            $many_many = array_flip($this->manyMany());
3222
            if (array_key_exists($className, $many_many)) {
3223
                return $many_many[$className];
3224
            }
3225
        }
3226
        if (is_array($this->hasMany())) {
0 ignored issues
show
introduced by
The condition is_array($this->hasMany()) is always true.
Loading history...
3227
            $has_many = array_flip($this->hasMany());
3228
            if (array_key_exists($className, $has_many)) {
3229
                return $has_many[$className];
3230
            }
3231
        }
3232
        if (is_array($this->hasOne())) {
0 ignored issues
show
introduced by
The condition is_array($this->hasOne()) is always true.
Loading history...
3233
            $has_one = array_flip($this->hasOne());
3234
            if (array_key_exists($className, $has_one)) {
3235
                return $has_one[$className];
3236
            }
3237
        }
3238
3239
        return false;
3240
    }
3241
3242
    /**
3243
     * Return all objects matching the filter
3244
     * sub-classes are automatically selected and included
3245
     *
3246
     * @param string $callerClass The class of objects to be returned
3247
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3248
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3249
     * @param string|array $sort A sort expression to be inserted into the ORDER
3250
     * BY clause.  If omitted, self::$default_sort will be used.
3251
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3252
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3253
     * @param string $containerClass The container class to return the results in.
3254
     *
3255
     * @todo $containerClass is Ignored, why?
3256
     *
3257
     * @return DataList The objects matching the filter, in the class specified by $containerClass
3258
     */
3259
    public static function get(
3260
        $callerClass = null,
3261
        $filter = "",
3262
        $sort = "",
3263
        $join = "",
3264
        $limit = null,
3265
        $containerClass = DataList::class
3266
    ) {
3267
        // Validate arguments
3268
        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...
3269
            $callerClass = get_called_class();
3270
            if ($callerClass === self::class) {
3271
                throw new InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3272
            }
3273
            if ($filter || $sort || $join || $limit || ($containerClass !== DataList::class)) {
3274
                throw new InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3275
                    . ' arguments');
3276
            }
3277
        } elseif ($callerClass === self::class) {
3278
            throw new InvalidArgumentException('DataObject::get() cannot query non-subclass DataObject directly');
3279
        }
3280
        if ($join) {
3281
            throw new InvalidArgumentException(
3282
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3283
            );
3284
        }
3285
3286
        // Build and decorate with args
3287
        $result = DataList::create($callerClass);
3288
        if ($filter) {
3289
            $result = $result->where($filter);
3290
        }
3291
        if ($sort) {
3292
            $result = $result->sort($sort);
3293
        }
3294
        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

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

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

3537
        /** @scrutinizer ignore-call */ 
3538
        $extensions = self::database_extensions(static::class);
Loading history...
3538
3539
        if (empty($table)) {
3540
            throw new LogicException(
3541
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3542
            );
3543
        }
3544
3545
        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...
3546
            $hasAutoIncPK = get_parent_class($this) === self::class;
3547
            DB::require_table(
3548
                $table,
3549
                $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

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

3550
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3551
                $hasAutoIncPK,
3552
                $this->config()->get('create_table_options'),
3553
                $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

3553
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3554
            );
3555
        } else {
3556
            DB::dont_require_table($table);
3557
        }
3558
3559
        // Build any child tables for many_many items
3560
        if ($manyMany = $this->uninherited('many_many')) {
3561
            $extras = $this->uninherited('many_many_extraFields');
3562
            foreach ($manyMany as $component => $spec) {
3563
                // Get many_many spec
3564
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3565
                $parentField = $manyManyComponent['parentField'];
3566
                $childField = $manyManyComponent['childField'];
3567
                $tableOrClass = $manyManyComponent['join'];
3568
3569
                // Skip if backed by actual class
3570
                if (class_exists($tableOrClass)) {
3571
                    continue;
3572
                }
3573
3574
                // Build fields
3575
                $manymanyFields = [
3576
                    $parentField => "Int",
3577
                    $childField => "Int",
3578
                ];
3579
                if (isset($extras[$component])) {
3580
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3581
                }
3582
3583
                // Build index list
3584
                $manymanyIndexes = [
3585
                    $parentField => [
3586
                        'type' => 'index',
3587
                        'name' => $parentField,
3588
                        'columns' => [$parentField],
3589
                    ],
3590
                    $childField => [
3591
                        'type' => 'index',
3592
                        'name' => $childField,
3593
                        'columns' => [$childField],
3594
                    ],
3595
                ];
3596
                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

3596
                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

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