Passed
Push — zero-is-false ( 5bf3f2...d7e1e1 )
by Sam
08:16
created

DataObject::forceChange()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1586
            foreach ($this->unsavedRelations as $name => $list) {
1587
                $list->changeToList($this->$name());
1588
            }
1589
            $this->unsavedRelations = array();
1590
        }
1591
    }
1592
1593
    /**
1594
     * Write the cached components to the database. Cached components could refer to two different instances of the
1595
     * same record.
1596
     *
1597
     * @param bool $recursive Recursively write components
1598
     * @return DataObject $this
1599
     */
1600
    public function writeComponents($recursive = false)
1601
    {
1602
        foreach ($this->components as $component) {
1603
            $component->write(false, false, false, $recursive);
1604
        }
1605
1606
        if ($join = $this->getJoin()) {
1607
            $join->write(false, false, false, $recursive);
1608
        }
1609
1610
        return $this;
1611
    }
1612
1613
    /**
1614
     * Delete this data object.
1615
     * $this->onBeforeDelete() gets called.
1616
     * Note that in Versioned objects, both Stage and Live will be deleted.
1617
     * @uses DataExtension->augmentSQL()
1618
     */
1619
    public function delete()
1620
    {
1621
        $this->brokenOnDelete = true;
1622
        $this->onBeforeDelete();
1623
        if ($this->brokenOnDelete) {
1624
            user_error(static::class . " has a broken onBeforeDelete() function."
1625
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1626
        }
1627
1628
        // Deleting a record without an ID shouldn't do anything
1629
        if (!$this->ID) {
1630
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1631
        }
1632
1633
        // TODO: This is quite ugly.  To improve:
1634
        //  - move the details of the delete code in the DataQuery system
1635
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1636
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1637
        $srcQuery = DataList::create(static::class)
1638
            ->filter('ID', $this->ID)
1639
            ->dataQuery()
1640
            ->query();
1641
        $queriedTables = $srcQuery->queriedTables();
1642
        $this->extend('updateDeleteTables', $queriedTables, $srcQuery);
1643
        foreach ($queriedTables as $table) {
1644
            $delete = SQLDelete::create("\"$table\"", array('"ID"' => $this->ID));
1645
            $this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
1646
            $delete->execute();
1647
        }
1648
        // Remove this item out of any caches
1649
        $this->flushCache();
1650
1651
        $this->onAfterDelete();
1652
1653
        $this->OldID = $this->ID;
1654
        $this->ID = 0;
1655
    }
1656
1657
    /**
1658
     * Delete the record with the given ID.
1659
     *
1660
     * @param string $className The class name of the record to be deleted
1661
     * @param int $id ID of record to be deleted
1662
     */
1663
    public static function delete_by_id($className, $id)
1664
    {
1665
        $obj = DataObject::get_by_id($className, $id);
1666
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1667
            $obj->delete();
1668
        } else {
1669
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1670
        }
1671
    }
1672
1673
    /**
1674
     * Get the class ancestry, including the current class name.
1675
     * The ancestry will be returned as an array of class names, where the 0th element
1676
     * will be the class that inherits directly from DataObject, and the last element
1677
     * will be the current class.
1678
     *
1679
     * @return array Class ancestry
1680
     */
1681
    public function getClassAncestry()
1682
    {
1683
        return ClassInfo::ancestry(static::class);
1684
    }
1685
1686
    /**
1687
     * Return a unary component object from a one to one relationship, as a DataObject.
1688
     * If no component is available, an 'empty component' will be returned for
1689
     * non-polymorphic relations, or for polymorphic relations with a class set.
1690
     *
1691
     * @param string $componentName Name of the component
1692
     * @return DataObject The component object. It's exact type will be that of the component.
1693
     * @throws Exception
1694
     */
1695
    public function getComponent($componentName)
1696
    {
1697
        if (isset($this->components[$componentName])) {
1698
            return $this->components[$componentName];
1699
        }
1700
1701
        $schema = static::getSchema();
1702
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1703
            $joinField = $componentName . 'ID';
1704
            $joinID = $this->getField($joinField);
1705
1706
            // Extract class name for polymorphic relations
1707
            if ($class === self::class) {
1708
                $class = $this->getField($componentName . 'Class');
1709
                if (empty($class)) {
1710
                    return null;
1711
                }
1712
            }
1713
1714
            if ($joinID) {
1715
                // Ensure that the selected object originates from the same stage, subsite, etc
1716
                $component = DataObject::get($class)
1717
                    ->filter('ID', $joinID)
1718
                    ->setDataQueryParam($this->getInheritableQueryParams())
1719
                    ->first();
1720
            }
1721
1722
            if (empty($component)) {
1723
                $component = Injector::inst()->create($class);
1724
            }
1725
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1726
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1727
            $joinID = $this->ID;
1728
1729
            if ($joinID) {
1730
                // Prepare filter for appropriate join type
1731
                if ($polymorphic) {
1732
                    $filter = array(
1733
                        "{$joinField}ID" => $joinID,
1734
                        "{$joinField}Class" => static::class,
1735
                    );
1736
                } else {
1737
                    $filter = array(
1738
                        $joinField => $joinID
1739
                    );
1740
                }
1741
1742
                // Ensure that the selected object originates from the same stage, subsite, etc
1743
                $component = DataObject::get($class)
1744
                    ->filter($filter)
1745
                    ->setDataQueryParam($this->getInheritableQueryParams())
1746
                    ->first();
1747
            }
1748
1749
            if (empty($component)) {
1750
                $component = Injector::inst()->create($class);
1751
                if ($polymorphic) {
1752
                    $component->{$joinField . 'ID'} = $this->ID;
1753
                    $component->{$joinField . 'Class'} = static::class;
1754
                } else {
1755
                    $component->$joinField = $this->ID;
1756
                }
1757
            }
1758
        } else {
1759
            throw new InvalidArgumentException(
1760
                "DataObject->getComponent(): Could not find component '$componentName'."
1761
            );
1762
        }
1763
1764
        $this->components[$componentName] = $component;
1765
        return $component;
1766
    }
1767
1768
    /**
1769
     * Assign an item to the given component
1770
     *
1771
     * @param string $componentName
1772
     * @param DataObject|null $item
1773
     * @return $this
1774
     */
1775
    public function setComponent($componentName, $item)
1776
    {
1777
        // Validate component
1778
        $schema = static::getSchema();
1779
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1780
            // Force item to be written if not by this point
1781
            // @todo This could be lazy-written in a beforeWrite hook, but force write here for simplicity
1782
            // https://github.com/silverstripe/silverstripe-framework/issues/7818
1783
            if ($item && !$item->isInDB()) {
1784
                $item->write();
1785
            }
1786
1787
            // Update local ID
1788
            $joinField = $componentName . 'ID';
1789
            $this->setField($joinField, $item ? $item->ID : null);
1790
            // Update Class (Polymorphic has_one)
1791
            // Extract class name for polymorphic relations
1792
            if ($class === self::class) {
1793
                $this->setField($componentName . 'Class', $item ? get_class($item) : null);
1794
            }
1795
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
1796
            if ($item) {
1797
                // For belongs_to, add to has_one on other component
1798
                $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1799
                if (!$polymorphic) {
1800
                    $joinField = substr($joinField, 0, -2);
1801
                }
1802
                $item->setComponent($joinField, $this);
1803
            }
1804
        } else {
1805
            throw new InvalidArgumentException(
1806
                "DataObject->setComponent(): Could not find component '$componentName'."
1807
            );
1808
        }
1809
1810
        $this->components[$componentName] = $item;
1811
        return $this;
1812
    }
1813
1814
    /**
1815
     * Returns a one-to-many relation as a HasManyList
1816
     *
1817
     * @param string $componentName Name of the component
1818
     * @param int|array $id Optional ID(s) for parent of this relation, if not the current record
1819
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1820
     */
1821
    public function getComponents($componentName, $id = null)
1822
    {
1823
        if (!isset($id)) {
1824
            $id = $this->ID;
1825
        }
1826
        $result = null;
1827
1828
        $schema = $this->getSchema();
1829
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1830
        if (!$componentClass) {
1831
            throw new InvalidArgumentException(sprintf(
1832
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1833
                $componentName,
1834
                static::class
1835
            ));
1836
        }
1837
1838
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1839
        if (!$id) {
1840
            if (!isset($this->unsavedRelations[$componentName])) {
1841
                $this->unsavedRelations[$componentName] =
1842
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1843
            }
1844
            return $this->unsavedRelations[$componentName];
1845
        }
1846
1847
        // Determine type and nature of foreign relation
1848
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1849
        /** @var HasManyList $result */
1850
        if ($polymorphic) {
1851
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
1852
        } else {
1853
            $result = HasManyList::create($componentClass, $joinField);
1854
        }
1855
1856
        return $result
1857
            ->setDataQueryParam($this->getInheritableQueryParams())
1858
            ->forForeignID($id);
1859
    }
1860
1861
    /**
1862
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1863
     *
1864
     * @param string $relationName Relation name.
1865
     * @return string Class name, or null if not found.
1866
     */
1867
    public function getRelationClass($relationName)
1868
    {
1869
        // Parse many_many
1870
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1871
        if ($manyManyComponent) {
1872
            return $manyManyComponent['childClass'];
1873
        }
1874
1875
        // Go through all relationship configuration fields.
1876
        $config = $this->config();
1877
        $candidates = array_merge(
1878
            ($relations = $config->get('has_one')) ? $relations : array(),
1879
            ($relations = $config->get('has_many')) ? $relations : array(),
1880
            ($relations = $config->get('belongs_to')) ? $relations : array()
1881
        );
1882
1883
        if (isset($candidates[$relationName])) {
1884
            $remoteClass = $candidates[$relationName];
1885
1886
            // If dot notation is present, extract just the first part that contains the class.
1887
            if (($fieldPos = strpos($remoteClass, '.')) !== false) {
1888
                return substr($remoteClass, 0, $fieldPos);
1889
            }
1890
1891
            // Otherwise just return the class
1892
            return $remoteClass;
1893
        }
1894
1895
        return null;
1896
    }
1897
1898
    /**
1899
     * Given a relation name, determine the relation type
1900
     *
1901
     * @param string $component Name of component
1902
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1903
     */
1904
    public function getRelationType($component)
1905
    {
1906
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1907
        $config = $this->config();
1908
        foreach ($types as $type) {
1909
            $relations = $config->get($type);
1910
            if ($relations && isset($relations[$component])) {
1911
                return $type;
1912
            }
1913
        }
1914
        return null;
1915
    }
1916
1917
    /**
1918
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1919
     * side of the relation.
1920
     *
1921
     * Notes on behaviour:
1922
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1923
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1924
     *  - Polymorphic relationships do not have two natural endpoints (only on one side)
1925
     *   and thus attempting to infer them will return nothing.
1926
     *  - Cannot be used on unsaved objects.
1927
     *
1928
     * @param string $remoteClass
1929
     * @param string $remoteRelation
1930
     * @return DataList|DataObject The component, either as a list or single object
1931
     * @throws BadMethodCallException
1932
     * @throws InvalidArgumentException
1933
     */
1934
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1935
    {
1936
        $remote = DataObject::singleton($remoteClass);
1937
        $class = $remote->getRelationClass($remoteRelation);
1938
        $schema = static::getSchema();
1939
1940
        // Validate arguments
1941
        if (!$this->isInDB()) {
1942
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1943
        }
1944
        if (empty($class)) {
1945
            throw new InvalidArgumentException(sprintf(
1946
                "%s invoked with invalid relation %s.%s",
1947
                __METHOD__,
1948
                $remoteClass,
1949
                $remoteRelation
1950
            ));
1951
        }
1952
        // If relation is polymorphic, do not infer recriprocal relationship
1953
        if ($class === self::class) {
1954
            return null;
1955
        }
1956
        if (!is_a($this, $class, true)) {
1957
            throw new InvalidArgumentException(sprintf(
1958
                "Relation %s on %s does not refer to objects of type %s",
1959
                $remoteRelation,
1960
                $remoteClass,
1961
                static::class
1962
            ));
1963
        }
1964
1965
        // Check the relation type to mock
1966
        $relationType = $remote->getRelationType($remoteRelation);
1967
        switch ($relationType) {
1968
            case 'has_one': {
1969
                // Mock has_many
1970
                $joinField = "{$remoteRelation}ID";
1971
                $componentClass = $schema->classForField($remoteClass, $joinField);
1972
                $result = HasManyList::create($componentClass, $joinField);
1973
                return $result
1974
                    ->setDataQueryParam($this->getInheritableQueryParams())
1975
                    ->forForeignID($this->ID);
1976
            }
1977
            case 'belongs_to':
1978
            case 'has_many': {
1979
                // These relations must have a has_one on the other end, so find it
1980
                $joinField = $schema->getRemoteJoinField(
1981
                    $remoteClass,
1982
                    $remoteRelation,
1983
                    $relationType,
1984
                    $polymorphic
1985
                );
1986
                // If relation is polymorphic, do not infer recriprocal relationship automatically
1987
                if ($polymorphic) {
1988
                    return null;
1989
                }
1990
                $joinID = $this->getField($joinField);
1991
                if (empty($joinID)) {
1992
                    return null;
1993
                }
1994
                // Get object by joined ID
1995
                return DataObject::get($remoteClass)
1996
                    ->filter('ID', $joinID)
1997
                    ->setDataQueryParam($this->getInheritableQueryParams())
1998
                    ->first();
1999
            }
2000
            case 'many_many':
2001
            case 'belongs_many_many': {
2002
                // Get components and extra fields from parent
2003
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
2004
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
2005
2006
                // Reverse parent and component fields and create an inverse ManyManyList
2007
                /** @var RelationList $result */
2008
                $result = Injector::inst()->create(
2009
                    $manyMany['relationClass'],
2010
                    $manyMany['parentClass'], // Substitute parent class for dataClass
2011
                    $manyMany['join'],
2012
                    $manyMany['parentField'], // Reversed parent / child field
2013
                    $manyMany['childField'], // Reversed parent / child field
2014
                    $extraFields,
2015
                    $manyMany['childClass'], // substitute child class for parentClass
2016
                    $remoteClass // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2017
                );
2018
                $this->extend('updateManyManyComponents', $result);
2019
2020
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2021
                // foreignID set elsewhere.
2022
                return $result
2023
                    ->setDataQueryParam($this->getInheritableQueryParams())
2024
                    ->forForeignID($this->ID);
2025
            }
2026
            default: {
2027
                return null;
2028
            }
2029
        }
2030
    }
2031
2032
    /**
2033
     * Returns a many-to-many component, as a ManyManyList.
2034
     * @param string $componentName Name of the many-many component
2035
     * @param int|array $id Optional ID for parent of this relation, if not the current record
2036
     * @return ManyManyList|UnsavedRelationList The set of components
2037
     */
2038
    public function getManyManyComponents($componentName, $id = null)
2039
    {
2040
        if (!isset($id)) {
2041
            $id = $this->ID;
2042
        }
2043
        $schema = static::getSchema();
2044
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
2045
        if (!$manyManyComponent) {
2046
            throw new InvalidArgumentException(sprintf(
2047
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
2048
                $componentName,
2049
                static::class
2050
            ));
2051
        }
2052
2053
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
2054
        if (!$id) {
2055
            if (!isset($this->unsavedRelations[$componentName])) {
2056
                $this->unsavedRelations[$componentName] =
2057
                    new UnsavedRelationList(
2058
                        $manyManyComponent['parentClass'],
2059
                        $componentName,
2060
                        $manyManyComponent['childClass']
2061
                    );
2062
            }
2063
            return $this->unsavedRelations[$componentName];
2064
        }
2065
2066
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
2067
        /** @var RelationList $result */
2068
        $result = Injector::inst()->create(
2069
            $manyManyComponent['relationClass'],
2070
            $manyManyComponent['childClass'],
2071
            $manyManyComponent['join'],
2072
            $manyManyComponent['childField'],
2073
            $manyManyComponent['parentField'],
2074
            $extraFields,
2075
            $manyManyComponent['parentClass'],
2076
            static::class // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2077
        );
2078
2079
        // Store component data in query meta-data
2080
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
2081
            /** @var DataQuery $query */
2082
            $query->setQueryParam('Component.ExtraFields', $extraFields);
2083
        });
2084
2085
        // If we have a default sort set for our "join" then we should overwrite any default already set.
2086
        $joinSort = Config::inst()->get($manyManyComponent['join'], 'default_sort');
2087
        if (!empty($joinSort)) {
2088
            $result = $result->sort($joinSort);
2089
        }
2090
2091
        $this->extend('updateManyManyComponents', $result);
2092
2093
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2094
        // foreignID set elsewhere.
2095
        return $result
2096
            ->setDataQueryParam($this->getInheritableQueryParams())
2097
            ->forForeignID($id);
2098
    }
2099
2100
    /**
2101
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
2102
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
2103
     *
2104
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
2105
     *                          their classes.
2106
     */
2107
    public function hasOne()
2108
    {
2109
        return (array)$this->config()->get('has_one');
2110
    }
2111
2112
    /**
2113
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
2114
     * their class name will be returned.
2115
     *
2116
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2117
     *        the field data stripped off. It defaults to TRUE.
2118
     * @return string|array
2119
     */
2120
    public function belongsTo($classOnly = true)
2121
    {
2122
        $belongsTo = (array)$this->config()->get('belongs_to');
2123
        if ($belongsTo && $classOnly) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $belongsTo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2124
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
2125
        } else {
2126
            return $belongsTo ? $belongsTo : array();
2127
        }
2128
    }
2129
2130
    /**
2131
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2132
     * relationships and their classes will be returned.
2133
     *
2134
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2135
     *        the field data stripped off. It defaults to TRUE.
2136
     * @return string|array|false
2137
     */
2138
    public function hasMany($classOnly = true)
2139
    {
2140
        $hasMany = (array)$this->config()->get('has_many');
2141
        if ($hasMany && $classOnly) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasMany of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2142
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2143
        } else {
2144
            return $hasMany ? $hasMany : array();
2145
        }
2146
    }
2147
2148
    /**
2149
     * Return the many-to-many extra fields specification.
2150
     *
2151
     * If you don't specify a component name, it returns all
2152
     * extra fields for all components available.
2153
     *
2154
     * @return array|null
2155
     */
2156
    public function manyManyExtraFields()
2157
    {
2158
        return $this->config()->get('many_many_extraFields');
2159
    }
2160
2161
    /**
2162
     * Return information about a many-to-many component.
2163
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2164
     * components are returned.
2165
     *
2166
     * @see DataObjectSchema::manyManyComponent()
2167
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2168
     */
2169
    public function manyMany()
2170
    {
2171
        $config = $this->config();
2172
        $manyManys = (array)$config->get('many_many');
2173
        $belongsManyManys = (array)$config->get('belongs_many_many');
2174
        $items = array_merge($manyManys, $belongsManyManys);
2175
        return $items;
2176
    }
2177
2178
    /**
2179
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
2180
     *
2181
     * This is experimental, and is currently only a Postgres-specific enhancement.
2182
     *
2183
     * @param string $class
2184
     * @return array|false
2185
     */
2186
    public function database_extensions($class)
2187
    {
2188
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2189
        if ($extensions) {
2190
            return $extensions;
2191
        } else {
2192
            return false;
2193
        }
2194
    }
2195
2196
    /**
2197
     * Generates a SearchContext to be used for building and processing
2198
     * a generic search form for properties on this object.
2199
     *
2200
     * @return SearchContext
2201
     */
2202
    public function getDefaultSearchContext()
2203
    {
2204
        return new SearchContext(
2205
            static::class,
2206
            $this->scaffoldSearchFields(),
2207
            $this->defaultSearchFilters()
2208
        );
2209
    }
2210
2211
    /**
2212
     * Determine which properties on the DataObject are
2213
     * searchable, and map them to their default {@link FormField}
2214
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2215
     *
2216
     * Some additional logic is included for switching field labels, based on
2217
     * how generic or specific the field type is.
2218
     *
2219
     * Used by {@link SearchContext}.
2220
     *
2221
     * @param array $_params
2222
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2223
     *   'restrictFields': Numeric array of a field name whitelist
2224
     * @return FieldList
2225
     */
2226
    public function scaffoldSearchFields($_params = null)
2227
    {
2228
        $params = array_merge(
2229
            array(
2230
                'fieldClasses' => false,
2231
                'restrictFields' => false
2232
            ),
2233
            (array)$_params
2234
        );
2235
        $fields = new FieldList();
2236
        foreach ($this->searchableFields() as $fieldName => $spec) {
2237
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
2238
                continue;
2239
            }
2240
2241
            // If a custom fieldclass is provided as a string, use it
2242
            $field = null;
2243
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2244
                $fieldClass = $params['fieldClasses'][$fieldName];
2245
                $field = new $fieldClass($fieldName);
2246
            // If we explicitly set a field, then construct that
2247
            } elseif (isset($spec['field'])) {
2248
                // If it's a string, use it as a class name and construct
2249
                if (is_string($spec['field'])) {
2250
                    $fieldClass = $spec['field'];
2251
                    $field = new $fieldClass($fieldName);
2252
2253
                // If it's a FormField object, then just use that object directly.
2254
                } elseif ($spec['field'] instanceof FormField) {
2255
                    $field = $spec['field'];
2256
2257
                // Otherwise we have a bug
2258
                } else {
2259
                    user_error("Bad value for searchable_fields, 'field' value: "
2260
                        . var_export($spec['field'], true), E_USER_WARNING);
2261
                }
2262
2263
            // Otherwise, use the database field's scaffolder
2264
            } elseif ($object = $this->relObject($fieldName)) {
2265
                if (is_object($object) && $object->hasMethod('scaffoldSearchField')) {
2266
                    $field = $object->scaffoldSearchField();
2267
                } else {
2268
                    throw new Exception(sprintf(
2269
                        "SearchField '%s' on '%s' does not return a valid DBField instance.",
2270
                        $fieldName,
2271
                        get_class($this)
2272
                    ));
2273
                }
2274
            }
2275
2276
            // Allow fields to opt out of search
2277
            if (!$field) {
2278
                continue;
2279
            }
2280
2281
            if (strstr($fieldName, '.')) {
2282
                $field->setName(str_replace('.', '__', $fieldName));
2283
            }
2284
            $field->setTitle($spec['title']);
2285
2286
            $fields->push($field);
2287
        }
2288
        return $fields;
2289
    }
2290
2291
    /**
2292
     * Scaffold a simple edit form for all properties on this dataobject,
2293
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2294
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2295
     *
2296
     * @uses FormScaffolder
2297
     *
2298
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2299
     * @return FieldList
2300
     */
2301
    public function scaffoldFormFields($_params = null)
2302
    {
2303
        $params = array_merge(
2304
            array(
2305
                'tabbed' => false,
2306
                'includeRelations' => false,
2307
                'restrictFields' => false,
2308
                'fieldClasses' => false,
2309
                'ajaxSafe' => false
2310
            ),
2311
            (array)$_params
2312
        );
2313
2314
        $fs = FormScaffolder::create($this);
2315
        $fs->tabbed = $params['tabbed'];
2316
        $fs->includeRelations = $params['includeRelations'];
2317
        $fs->restrictFields = $params['restrictFields'];
2318
        $fs->fieldClasses = $params['fieldClasses'];
2319
        $fs->ajaxSafe = $params['ajaxSafe'];
2320
2321
        return $fs->getFieldList();
2322
    }
2323
2324
    /**
2325
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2326
     * being called on extensions
2327
     *
2328
     * @param callable $callback The callback to execute
2329
     */
2330
    protected function beforeUpdateCMSFields($callback)
2331
    {
2332
        $this->beforeExtending('updateCMSFields', $callback);
2333
    }
2334
2335
    /**
2336
     * Centerpiece of every data administration interface in Silverstripe,
2337
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2338
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2339
     * generate this set. To customize, overload this method in a subclass
2340
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2341
     *
2342
     * <code>
2343
     * class MyCustomClass extends DataObject {
2344
     *  static $db = array('CustomProperty'=>'Boolean');
2345
     *
2346
     *  function getCMSFields() {
2347
     *    $fields = parent::getCMSFields();
2348
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2349
     *    return $fields;
2350
     *  }
2351
     * }
2352
     * </code>
2353
     *
2354
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2355
     *
2356
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2357
     */
2358
    public function getCMSFields()
2359
    {
2360
        $tabbedFields = $this->scaffoldFormFields(array(
2361
            // Don't allow has_many/many_many relationship editing before the record is first saved
2362
            'includeRelations' => ($this->ID > 0),
2363
            'tabbed' => true,
2364
            'ajaxSafe' => true
2365
        ));
2366
2367
        $this->extend('updateCMSFields', $tabbedFields);
2368
2369
        return $tabbedFields;
2370
    }
2371
2372
    /**
2373
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2374
     * including that dataobject's extensions customised actions could be added to the EditForm.
2375
     *
2376
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2377
     */
2378
    public function getCMSActions()
2379
    {
2380
        $actions = new FieldList();
2381
        $this->extend('updateCMSActions', $actions);
2382
        return $actions;
2383
    }
2384
2385
2386
    /**
2387
     * Used for simple frontend forms without relation editing
2388
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2389
     * by default. To customize, either overload this method in your
2390
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2391
     *
2392
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2393
     *
2394
     * @param array $params See {@link scaffoldFormFields()}
2395
     * @return FieldList Always returns a simple field collection without TabSet.
2396
     */
2397
    public function getFrontEndFields($params = null)
2398
    {
2399
        $untabbedFields = $this->scaffoldFormFields($params);
2400
        $this->extend('updateFrontEndFields', $untabbedFields);
2401
2402
        return $untabbedFields;
2403
    }
2404
2405
    public function getViewerTemplates($suffix = '')
2406
    {
2407
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2408
    }
2409
2410
    /**
2411
     * Gets the value of a field.
2412
     * Called by {@link __get()} and any getFieldName() methods you might create.
2413
     *
2414
     * @param string $field The name of the field
2415
     * @return mixed The field value
2416
     */
2417
    public function getField($field)
2418
    {
2419
        // If we already have a value in $this->record, then we should just return that
2420
        if (isset($this->record[$field])) {
2421
            return $this->record[$field];
2422
        }
2423
2424
        // Do we have a field that needs to be lazy loaded?
2425
        if (isset($this->record[$field . '_Lazy'])) {
2426
            $tableClass = $this->record[$field . '_Lazy'];
2427
            $this->loadLazyFields($tableClass);
2428
        }
2429
        $schema = static::getSchema();
2430
2431
        // Support unary relations as fields
2432
        if ($schema->unaryComponent(static::class, $field)) {
2433
            return $this->getComponent($field);
2434
        }
2435
2436
        // In case of complex fields, return the DBField object
2437
        if ($schema->compositeField(static::class, $field)) {
2438
            $this->record[$field] = $this->dbObject($field);
2439
        }
2440
2441
        return isset($this->record[$field]) ? $this->record[$field] : null;
2442
    }
2443
2444
    /**
2445
     * Loads all the stub fields that an initial lazy load didn't load fully.
2446
     *
2447
     * @param string $class Class to load the values from. Others are joined as required.
2448
     * Not specifying a tableClass will load all lazy fields from all tables.
2449
     * @return bool Flag if lazy loading succeeded
2450
     */
2451
    protected function loadLazyFields($class = null)
2452
    {
2453
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2454
            return false;
2455
        }
2456
2457
        if (!$class) {
2458
            $loaded = array();
2459
2460
            foreach ($this->record as $key => $value) {
2461
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2462
                    $this->loadLazyFields($value);
2463
                    $loaded[$value] = $value;
2464
                }
2465
            }
2466
2467
            return false;
2468
        }
2469
2470
        $dataQuery = new DataQuery($class);
2471
2472
        // Reset query parameter context to that of this DataObject
2473
        if ($params = $this->getSourceQueryParams()) {
2474
            foreach ($params as $key => $value) {
2475
                $dataQuery->setQueryParam($key, $value);
2476
            }
2477
        }
2478
2479
        // Limit query to the current record, unless it has the Versioned extension,
2480
        // in which case it requires special handling through augmentLoadLazyFields()
2481
        $schema = static::getSchema();
2482
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2483
        $dataQuery->where([
2484
            $baseIDColumn => $this->record['ID']
2485
        ])->limit(1);
2486
2487
        $columns = array();
2488
2489
        // Add SQL for fields, both simple & multi-value
2490
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2491
        $databaseFields = $schema->databaseFields($class, false);
2492
        foreach ($databaseFields as $k => $v) {
2493
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2494
                $columns[] = $k;
2495
            }
2496
        }
2497
2498
        if ($columns) {
2499
            $query = $dataQuery->query();
2500
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2501
            $this->extend('augmentSQL', $query, $dataQuery);
2502
2503
            $dataQuery->setQueriedColumns($columns);
2504
            $newData = $dataQuery->execute()->record();
2505
2506
            // Load the data into record
2507
            if ($newData) {
2508
                foreach ($newData as $k => $v) {
2509
                    if (in_array($k, $columns)) {
2510
                        $this->record[$k] = $v;
2511
                        $this->original[$k] = $v;
2512
                        unset($this->record[$k . '_Lazy']);
2513
                    }
2514
                }
2515
2516
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2517
            } else {
2518
                foreach ($columns as $k) {
2519
                    $this->record[$k] = null;
2520
                    $this->original[$k] = null;
2521
                    unset($this->record[$k . '_Lazy']);
2522
                }
2523
            }
2524
        }
2525
        return true;
2526
    }
2527
2528
    /**
2529
     * Return the fields that have changed since the last write.
2530
     *
2531
     * The change level affects what the functions defines as "changed":
2532
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2533
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2534
     *   for example a change from 0 to null would not be included.
2535
     *
2536
     * Example return:
2537
     * <code>
2538
     * array(
2539
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2540
     * )
2541
     * </code>
2542
     *
2543
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2544
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2545
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2546
     * @return array
2547
     */
2548
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2549
    {
2550
        $changedFields = array();
2551
2552
        // Update the changed array with references to changed obj-fields
2553
        foreach ($this->record as $k => $v) {
2554
            // Prevents DBComposite infinite looping on isChanged
2555
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2556
                continue;
2557
            }
2558
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2559
                $this->changed[$k] = self::CHANGE_VALUE;
2560
            }
2561
        }
2562
2563
        // If change was forced, then derive change data from $this->record
2564
        if ($this->changeForced && $changeLevel <= self::CHANGE_STRICT) {
2565
            $changed = array_combine(
2566
                array_keys($this->record),
2567
                array_fill(0, count($this->record), self::CHANGE_STRICT)
2568
            );
2569
            // @todo Find better way to allow versioned to write a new version after forceChange
2570
            unset($changed['Version']);
2571
        } else {
2572
            $changed = $this->changed;
2573
        }
2574
2575
        if (is_array($databaseFieldsOnly)) {
2576
            $fields = array_intersect_key($changed, array_flip($databaseFieldsOnly));
2577
        } elseif ($databaseFieldsOnly) {
2578
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2579
            $fields = array_intersect_key($changed, $fieldsSpecs);
2580
        } else {
2581
            $fields = $changed;
2582
        }
2583
2584
        // Filter the list to those of a certain change level
2585
        if ($changeLevel > self::CHANGE_STRICT) {
2586
            if ($fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2587
                foreach ($fields as $name => $level) {
2588
                    if ($level < $changeLevel) {
2589
                        unset($fields[$name]);
2590
                    }
2591
                }
2592
            }
2593
        }
2594
2595
        if ($fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2596
            foreach ($fields as $name => $level) {
2597
                $changedFields[$name] = array(
2598
                    'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2599
                    'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2600
                    'level' => $level
2601
                );
2602
            }
2603
        }
2604
2605
        return $changedFields;
2606
    }
2607
2608
    /**
2609
     * Uses {@link getChangedFields()} to determine if fields have been changed
2610
     * since loading them from the database.
2611
     *
2612
     * @param string $fieldName Name of the database field to check, will check for any if not given
2613
     * @param int $changeLevel See {@link getChangedFields()}
2614
     * @return boolean
2615
     */
2616
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2617
    {
2618
        $fields = $fieldName ? array($fieldName) : true;
2619
        $changed = $this->getChangedFields($fields, $changeLevel);
2620
        if (!isset($fieldName)) {
2621
            return !empty($changed);
2622
        } else {
2623
            return array_key_exists($fieldName, $changed);
2624
        }
2625
    }
2626
2627
    /**
2628
     * Set the value of the field
2629
     * Called by {@link __set()} and any setFieldName() methods you might create.
2630
     *
2631
     * @param string $fieldName Name of the field
2632
     * @param mixed $val New field value
2633
     * @return $this
2634
     */
2635
    public function setField($fieldName, $val)
2636
    {
2637
        $this->objCacheClear();
2638
        //if it's a has_one component, destroy the cache
2639
        if (substr($fieldName, -2) == 'ID') {
2640
            unset($this->components[substr($fieldName, 0, -2)]);
2641
        }
2642
2643
        // If we've just lazy-loaded the column, then we need to populate the $original array
2644
        if (isset($this->record[$fieldName . '_Lazy'])) {
2645
            $tableClass = $this->record[$fieldName . '_Lazy'];
2646
            $this->loadLazyFields($tableClass);
2647
        }
2648
2649
        // Support component assignent via field setter
2650
        $schema = static::getSchema();
2651
        if ($schema->unaryComponent(static::class, $fieldName)) {
2652
            unset($this->components[$fieldName]);
2653
            // Assign component directly
2654
            if (is_null($val) || $val instanceof DataObject) {
2655
                return $this->setComponent($fieldName, $val);
2656
            }
2657
            // Assign by ID instead of object
2658
            if (is_numeric($val)) {
2659
                $fieldName .= 'ID';
2660
            }
2661
        }
2662
2663
        // Situation 1: Passing an DBField
2664
        if ($val instanceof DBField) {
2665
            $val->setName($fieldName);
2666
            $val->saveInto($this);
2667
2668
            // Situation 1a: Composite fields should remain bound in case they are
2669
            // later referenced to update the parent dataobject
2670
            if ($val instanceof DBComposite) {
2671
                $val->bindTo($this);
2672
                $this->record[$fieldName] = $val;
2673
            }
2674
        // Situation 2: Passing a literal or non-DBField object
2675
        } else {
2676
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2677
            if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) {
2678
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2679
            }
2680
2681
            if (!empty($val) && !is_scalar($val)) {
2682
                $dbField = $this->dbObject($fieldName);
2683
                if ($dbField && $dbField->scalarValueOnly()) {
2684
                    throw new InvalidArgumentException(
2685
                        sprintf(
2686
                            'DataObject::setField: %s only accepts scalars',
2687
                            $fieldName
2688
                        )
2689
                    );
2690
                }
2691
            }
2692
2693
            // if a field is not existing or has strictly changed
2694
            if (!array_key_exists($fieldName, $this->original) || $this->original[$fieldName] !== $val) {
2695
                // TODO Add check for php-level defaults which are not set in the db
2696
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2697
                // At the very least, the type has changed
2698
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2699
2700
                if ((!array_key_exists($fieldName, $this->original) && $val)
2701
                    || (array_key_exists($fieldName, $this->original) && $this->original[$fieldName] != $val)
2702
                ) {
2703
                    // Value has changed as well, not just the type
2704
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2705
                }
2706
            // Value has been restored to its original, remove any record of the change
2707
            } elseif (isset($this->changed[$fieldName])) {
2708
                unset($this->changed[$fieldName]);
2709
            }
2710
2711
            // Value is saved regardless, since the change detection relates to the last write
2712
            $this->record[$fieldName] = $val;
2713
        }
2714
        return $this;
2715
    }
2716
2717
    /**
2718
     * Set the value of the field, using a casting object.
2719
     * This is useful when you aren't sure that a date is in SQL format, for example.
2720
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2721
     * can be saved into the Image table.
2722
     *
2723
     * @param string $fieldName Name of the field
2724
     * @param mixed $value New field value
2725
     * @return $this
2726
     */
2727
    public function setCastedField($fieldName, $value)
2728
    {
2729
        if (!$fieldName) {
2730
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2731
        }
2732
        $fieldObj = $this->dbObject($fieldName);
2733
        if ($fieldObj) {
0 ignored issues
show
introduced by
$fieldObj is of type SilverStripe\ORM\FieldType\DBField, thus it always evaluated to true.
Loading history...
2734
            $fieldObj->setValue($value);
2735
            $fieldObj->saveInto($this);
2736
        } else {
2737
            $this->$fieldName = $value;
2738
        }
2739
        return $this;
2740
    }
2741
2742
    /**
2743
     * {@inheritdoc}
2744
     */
2745
    public function castingHelper($field)
2746
    {
2747
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2748
        if ($fieldSpec) {
2749
            return $fieldSpec;
2750
        }
2751
2752
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2753
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2754
        $queryParams = $this->getSourceQueryParams();
2755
        if (!empty($queryParams['Component.ExtraFields'])) {
2756
            $extraFields = $queryParams['Component.ExtraFields'];
2757
2758
            if (isset($extraFields[$field])) {
2759
                return $extraFields[$field];
2760
            }
2761
        }
2762
2763
        return parent::castingHelper($field);
2764
    }
2765
2766
    /**
2767
     * Returns true if the given field exists in a database column on any of
2768
     * the objects tables and optionally look up a dynamic getter with
2769
     * get<fieldName>().
2770
     *
2771
     * @param string $field Name of the field
2772
     * @return boolean True if the given field exists
2773
     */
2774
    public function hasField($field)
2775
    {
2776
        $schema = static::getSchema();
2777
        return (
2778
            array_key_exists($field, $this->record)
2779
            || array_key_exists($field, $this->components)
2780
            || $schema->fieldSpec(static::class, $field)
2781
            || $schema->unaryComponent(static::class, $field)
2782
            || $this->hasMethod("get{$field}")
2783
        );
2784
    }
2785
2786
    /**
2787
     * Returns true if the given field exists as a database column
2788
     *
2789
     * @param string $field Name of the field
2790
     *
2791
     * @return boolean
2792
     */
2793
    public function hasDatabaseField($field)
2794
    {
2795
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2796
        return !empty($spec);
2797
    }
2798
2799
    /**
2800
     * Returns true if the member is allowed to do the given action.
2801
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2802
     *
2803
     * @param string $perm The permission to be checked, such as 'View'.
2804
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2805
     * in user.
2806
     * @param array $context Additional $context to pass to extendedCan()
2807
     *
2808
     * @return boolean True if the the member is allowed to do the given action
2809
     */
2810
    public function can($perm, $member = null, $context = array())
2811
    {
2812
        if (!$member) {
2813
            $member = Security::getCurrentUser();
2814
        }
2815
2816
        if ($member && Permission::checkMember($member, "ADMIN")) {
2817
            return true;
2818
        }
2819
2820
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2821
            $method = 'can' . ucfirst($perm);
2822
            return $this->$method($member);
2823
        }
2824
2825
        $results = $this->extendedCan('can', $member);
2826
        if (isset($results)) {
2827
            return $results;
2828
        }
2829
2830
        return ($member && Permission::checkMember($member, $perm));
2831
    }
2832
2833
    /**
2834
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2835
     * expected to return one of three values:
2836
     *
2837
     *  - false: Disallow this permission, regardless of what other extensions say
2838
     *  - true: Allow this permission, as long as no other extensions return false
2839
     *  - NULL: Don't affect the outcome
2840
     *
2841
     * This method itself returns a tri-state value, and is designed to be used like this:
2842
     *
2843
     * <code>
2844
     * $extended = $this->extendedCan('canDoSomething', $member);
2845
     * if($extended !== null) return $extended;
2846
     * else return $normalValue;
2847
     * </code>
2848
     *
2849
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2850
     * @param Member|int $member
2851
     * @param array $context Optional context
2852
     * @return boolean|null
2853
     */
2854
    public function extendedCan($methodName, $member, $context = array())
2855
    {
2856
        $results = $this->extend($methodName, $member, $context);
2857
        if ($results && is_array($results)) {
2858
            // Remove NULLs
2859
            $results = array_filter($results, function ($v) {
2860
                return !is_null($v);
2861
            });
2862
            // If there are any non-NULL responses, then return the lowest one of them.
2863
            // If any explicitly deny the permission, then we don't get access
2864
            if ($results) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2865
                return min($results);
2866
            }
2867
        }
2868
        return null;
2869
    }
2870
2871
    /**
2872
     * @param Member $member
2873
     * @return boolean
2874
     */
2875
    public function canView($member = null)
2876
    {
2877
        $extended = $this->extendedCan(__FUNCTION__, $member);
2878
        if ($extended !== null) {
2879
            return $extended;
2880
        }
2881
        return Permission::check('ADMIN', 'any', $member);
2882
    }
2883
2884
    /**
2885
     * @param Member $member
2886
     * @return boolean
2887
     */
2888
    public function canEdit($member = null)
2889
    {
2890
        $extended = $this->extendedCan(__FUNCTION__, $member);
2891
        if ($extended !== null) {
2892
            return $extended;
2893
        }
2894
        return Permission::check('ADMIN', 'any', $member);
2895
    }
2896
2897
    /**
2898
     * @param Member $member
2899
     * @return boolean
2900
     */
2901
    public function canDelete($member = null)
2902
    {
2903
        $extended = $this->extendedCan(__FUNCTION__, $member);
2904
        if ($extended !== null) {
2905
            return $extended;
2906
        }
2907
        return Permission::check('ADMIN', 'any', $member);
2908
    }
2909
2910
    /**
2911
     * @param Member $member
2912
     * @param array $context Additional context-specific data which might
2913
     * affect whether (or where) this object could be created.
2914
     * @return boolean
2915
     */
2916
    public function canCreate($member = null, $context = array())
2917
    {
2918
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2919
        if ($extended !== null) {
2920
            return $extended;
2921
        }
2922
        return Permission::check('ADMIN', 'any', $member);
2923
    }
2924
2925
    /**
2926
     * Debugging used by Debug::show()
2927
     *
2928
     * @return string HTML data representing this object
2929
     */
2930
    public function debug()
2931
    {
2932
        $class = static::class;
2933
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2934
        if ($this->record) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2935
            foreach ($this->record as $fieldName => $fieldVal) {
2936
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2937
            }
2938
        }
2939
        $val .= "</ul>\n";
2940
        return $val;
2941
    }
2942
2943
    /**
2944
     * Return the DBField object that represents the given field.
2945
     * This works similarly to obj() with 2 key differences:
2946
     *   - it still returns an object even when the field has no value.
2947
     *   - it only matches fields and not methods
2948
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2949
     *
2950
     * @param string $fieldName Name of the field
2951
     * @return DBField The field as a DBField object
2952
     */
2953
    public function dbObject($fieldName)
2954
    {
2955
        // Check for field in DB
2956
        $schema = static::getSchema();
2957
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2958
        if (!$helper) {
2959
            return null;
2960
        }
2961
2962
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2963
            $tableClass = $this->record[$fieldName . '_Lazy'];
2964
            $this->loadLazyFields($tableClass);
2965
        }
2966
2967
        $value = isset($this->record[$fieldName])
2968
            ? $this->record[$fieldName]
2969
            : null;
2970
2971
        // If we have a DBField object in $this->record, then return that
2972
        if ($value instanceof DBField) {
2973
            return $value;
2974
        }
2975
2976
        list($class, $spec) = explode('.', $helper);
2977
        /** @var DBField $obj */
2978
        $table = $schema->tableName($class);
2979
        $obj = Injector::inst()->create($spec, $fieldName);
2980
        $obj->setTable($table);
2981
        $obj->setValue($value, $this, false);
2982
        return $obj;
2983
    }
2984
2985
    /**
2986
     * Traverses to a DBField referenced by relationships between data objects.
2987
     *
2988
     * The path to the related field is specified with dot separated syntax
2989
     * (eg: Parent.Child.Child.FieldName).
2990
     *
2991
     * If a relation is blank, this will return null instead.
2992
     * If a relation name is invalid (e.g. non-relation on a parent) this
2993
     * can throw a LogicException.
2994
     *
2995
     * @param string $fieldPath List of paths on this object. All items in this path
2996
     * must be ViewableData implementors
2997
     *
2998
     * @return mixed DBField of the field on the object or a DataList instance.
2999
     * @throws LogicException If accessing invalid relations
3000
     */
3001
    public function relObject($fieldPath)
3002
    {
3003
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
3004
        $component = $this;
3005
3006
        // Parse all relations
3007
        foreach (explode('.', $fieldPath) as $relation) {
3008
            if (!$component) {
3009
                return null;
3010
            }
3011
3012
            // Inspect relation type
3013
            if (ClassInfo::hasMethod($component, $relation)) {
3014
                $component = $component->$relation();
3015
            } elseif ($component instanceof Relation || $component instanceof DataList) {
3016
                // $relation could either be a field (aggregate), or another relation
3017
                $singleton = DataObject::singleton($component->dataClass());
0 ignored issues
show
Bug introduced by
The method dataClass() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

3017
                $singleton = DataObject::singleton($component->/** @scrutinizer ignore-call */ dataClass());
Loading history...
3018
                $component = $singleton->dbObject($relation) ?: $component->relation($relation);
0 ignored issues
show
Bug introduced by
The method relation() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

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

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

3142
        if ($limit && strpos(/** @scrutinizer ignore-type */ $limit, ',') !== false) {
Loading history...
3143
            $limitArguments = explode(',', $limit);
0 ignored issues
show
Bug introduced by
It seems like $limit can also be of type array; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

3143
            $limitArguments = explode(',', /** @scrutinizer ignore-type */ $limit);
Loading history...
3144
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
3145
        } elseif ($limit) {
3146
            $result = $result->limit($limit);
3147
        }
3148
3149
        return $result;
3150
    }
3151
3152
3153
    /**
3154
     * Return the first item matching the given query.
3155
     * All calls to get_one() are cached.
3156
     *
3157
     * @param string $callerClass The class of objects to be returned
3158
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3159
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3160
     * @param boolean $cache Use caching
3161
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3162
     *
3163
     * @return DataObject|null The first item matching the query
3164
     */
3165
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
3166
    {
3167
        $SNG = singleton($callerClass);
3168
3169
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3170
        $cacheKey = md5(serialize($cacheComponents));
3171
3172
        $item = null;
3173
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3174
            $dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
3175
            $item = $dl->first();
3176
3177
            if ($cache) {
3178
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3179
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
3180
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
3181
                }
3182
            }
3183
        }
3184
3185
        if ($cache) {
3186
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
3187
        }
3188
3189
        return $item;
3190
    }
3191
3192
    /**
3193
     * Flush the cached results for all relations (has_one, has_many, many_many)
3194
     * Also clears any cached aggregate data.
3195
     *
3196
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3197
     *                            When false will just clear session-local cached data
3198
     * @return DataObject $this
3199
     */
3200
    public function flushCache($persistent = true)
3201
    {
3202
        if (static::class == self::class) {
0 ignored issues
show
introduced by
The condition static::class == self::class is always true.
Loading history...
3203
            self::$_cache_get_one = array();
3204
            return $this;
3205
        }
3206
3207
        $classes = ClassInfo::ancestry(static::class);
3208
        foreach ($classes as $class) {
3209
            if (isset(self::$_cache_get_one[$class])) {
3210
                unset(self::$_cache_get_one[$class]);
3211
            }
3212
        }
3213
3214
        $this->extend('flushCache');
3215
3216
        $this->components = array();
3217
        return $this;
3218
    }
3219
3220
    /**
3221
     * Flush the get_one global cache and destroy associated objects.
3222
     */
3223
    public static function flush_and_destroy_cache()
3224
    {
3225
        if (self::$_cache_get_one) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::_cache_get_one of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3226
            foreach (self::$_cache_get_one as $class => $items) {
3227
                if (is_array($items)) {
3228
                    foreach ($items as $item) {
3229
                        if ($item) {
3230
                            $item->destroy();
3231
                        }
3232
                    }
3233
                }
3234
            }
3235
        }
3236
        self::$_cache_get_one = array();
3237
    }
3238
3239
    /**
3240
     * Reset all global caches associated with DataObject.
3241
     */
3242
    public static function reset()
3243
    {
3244
        // @todo Decouple these
3245
        DBEnum::flushCache();
3246
        ClassInfo::reset_db_cache();
3247
        static::getSchema()->reset();
3248
        self::$_cache_get_one = array();
3249
        self::$_cache_field_labels = array();
3250
    }
3251
3252
    /**
3253
     * Return the given element, searching by ID.
3254
     *
3255
     * This can be called either via `DataObject::get_by_id(MyClass::class, $id)`
3256
     * or `MyClass::get_by_id($id)`
3257
     *
3258
     * @param string|int $classOrID The class of the object to be returned, or id if called on target class
3259
     * @param int|bool $idOrCache The id of the element, or cache if called on target class
3260
     * @param boolean $cache See {@link get_one()}
3261
     *
3262
     * @return static The element
3263
     */
3264
    public static function get_by_id($classOrID, $idOrCache = null, $cache = true)
3265
    {
3266
        // Shift arguments if passing id in first or second argument
3267
        list ($class, $id, $cached) = is_numeric($classOrID)
3268
            ? [get_called_class(), $classOrID, isset($idOrCache) ? $idOrCache : $cache]
3269
            : [$classOrID, $idOrCache, $cache];
3270
3271
        // Validate class
3272
        if ($class === self::class) {
3273
            throw new InvalidArgumentException('DataObject::get_by_id() cannot query non-subclass DataObject directly');
3274
        }
3275
3276
        // Pass to get_one
3277
        $column = static::getSchema()->sqlColumnForField($class, 'ID');
3278
        return DataObject::get_one($class, [$column => $id], $cached);
3279
    }
3280
3281
    /**
3282
     * Get the name of the base table for this object
3283
     *
3284
     * @return string
3285
     */
3286
    public function baseTable()
3287
    {
3288
        return static::getSchema()->baseDataTable($this);
3289
    }
3290
3291
    /**
3292
     * Get the base class for this object
3293
     *
3294
     * @return string
3295
     */
3296
    public function baseClass()
3297
    {
3298
        return static::getSchema()->baseDataClass($this);
3299
    }
3300
3301
    /**
3302
     * @var array Parameters used in the query that built this object.
3303
     * This can be used by decorators (e.g. lazy loading) to
3304
     * run additional queries using the same context.
3305
     */
3306
    protected $sourceQueryParams;
3307
3308
    /**
3309
     * @see $sourceQueryParams
3310
     * @return array
3311
     */
3312
    public function getSourceQueryParams()
3313
    {
3314
        return $this->sourceQueryParams;
3315
    }
3316
3317
    /**
3318
     * Get list of parameters that should be inherited to relations on this object
3319
     *
3320
     * @return array
3321
     */
3322
    public function getInheritableQueryParams()
3323
    {
3324
        $params = $this->getSourceQueryParams();
3325
        $this->extend('updateInheritableQueryParams', $params);
3326
        return $params;
3327
    }
3328
3329
    /**
3330
     * @see $sourceQueryParams
3331
     * @param array
3332
     */
3333
    public function setSourceQueryParams($array)
3334
    {
3335
        $this->sourceQueryParams = $array;
3336
    }
3337
3338
    /**
3339
     * @see $sourceQueryParams
3340
     * @param string $key
3341
     * @param string $value
3342
     */
3343
    public function setSourceQueryParam($key, $value)
3344
    {
3345
        $this->sourceQueryParams[$key] = $value;
3346
    }
3347
3348
    /**
3349
     * @see $sourceQueryParams
3350
     * @param string $key
3351
     * @return string
3352
     */
3353
    public function getSourceQueryParam($key)
3354
    {
3355
        if (isset($this->sourceQueryParams[$key])) {
3356
            return $this->sourceQueryParams[$key];
3357
        }
3358
        return null;
3359
    }
3360
3361
    //-------------------------------------------------------------------------------------------//
3362
3363
    /**
3364
     * Check the database schema and update it as necessary.
3365
     *
3366
     * @uses DataExtension->augmentDatabase()
3367
     */
3368
    public function requireTable()
3369
    {
3370
        // Only build the table if we've actually got fields
3371
        $schema = static::getSchema();
3372
        $table = $schema->tableName(static::class);
3373
        $fields = $schema->databaseFields(static::class, false);
3374
        $indexes = $schema->databaseIndexes(static::class, false);
3375
        $extensions = self::database_extensions(static::class);
0 ignored issues
show
Bug Best Practice introduced by
The method SilverStripe\ORM\DataObject::database_extensions() is not static, but was called statically. ( Ignorable by Annotation )

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

3375
        /** @scrutinizer ignore-call */ 
3376
        $extensions = self::database_extensions(static::class);
Loading history...
3376
3377
        if (empty($table)) {
3378
            throw new LogicException(
3379
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3380
            );
3381
        }
3382
3383
        if ($fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3384
            $hasAutoIncPK = get_parent_class($this) === self::class;
3385
            DB::require_table(
3386
                $table,
3387
                $fields,
0 ignored issues
show
Bug introduced by
$fields of type array is incompatible with the type string expected by parameter $fieldSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

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

3387
                /** @scrutinizer ignore-type */ $fields,
Loading history...
3388
                $indexes,
0 ignored issues
show
Bug introduced by
$indexes of type array is incompatible with the type string expected by parameter $indexSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

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

3388
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3389
                $hasAutoIncPK,
3390
                $this->config()->get('create_table_options'),
3391
                $extensions
0 ignored issues
show
Bug introduced by
It seems like $extensions can also be of type false; however, parameter $extensions of SilverStripe\ORM\DB::require_table() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

3434
                DB::require_table($tableOrClass, /** @scrutinizer ignore-type */ $manymanyFields, $manymanyIndexes, true, null, $extensions);
Loading history...
3435
            }
3436
        }
3437
3438
        // Let any extentions make their own database fields
3439
        $this->extend('augmentDatabase', $dummy);
3440
    }
3441
3442
    /**
3443
     * Add default records to database. This function is called whenever the
3444
     * database is built, after the database tables have all been created. Overload
3445
     * this to add default records when the database is built, but make sure you
3446
     * call parent::requireDefaultRecords().
3447
     *
3448
     * @uses DataExtension->requireDefaultRecords()
3449
     */
3450
    public function requireDefaultRecords()
3451
    {
3452
        $defaultRecords = $this->config()->uninherited('default_records');
3453
3454
        if (!empty($defaultRecords)) {
3455
            $hasData = DataObject::get_one(static::class);
3456
            if (!$hasData) {
3457
                $className = static::class;
3458
                foreach ($defaultRecords as $record) {
3459
                    $obj = Injector::inst()->create($className, $record);
3460
                    $obj->write();
3461
                }
3462
                DB::alteration_message("Added default records to $className table", "created");
3463
            }
3464
        }
3465
3466
        // Let any extentions make their own database default data
3467
        $this->extend('requireDefaultRecords', $dummy);
3468
    }
3469
3470
    /**
3471
     * Get the default searchable fields for this object, as defined in the
3472
     * $searchable_fields list. If searchable fields are not defined on the
3473
     * data object, uses a default selection of summary fields.
3474
     *
3475
     * @return array
3476
     */
3477
    public function searchableFields()
3478
    {
3479
        // can have mixed format, need to make consistent in most verbose form
3480
        $fields = $this->config()->get('searchable_fields');
3481
        $labels = $this->fieldLabels();
3482
3483
        // fallback to summary fields (unless empty array is explicitly specified)
3484
        if (!$fields && !is_array($fields)) {
3485
            $summaryFields = array_keys($this->summaryFields());
3486
            $fields = array();
3487
3488
            // remove the custom getters as the search should not include them
3489
            $schema = static::getSchema();
3490
            if ($summaryFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $summaryFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3491
                foreach ($summaryFields as $key => $name) {
3492
                    $spec = $name;
3493
3494
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3495
                    if (($fieldPos = strpos($name, '.')) !== false) {
3496
                        $name = substr($name, 0, $fieldPos);
3497
                    }
3498
3499
                    if ($schema->fieldSpec($this, $name)) {
3500
                        $fields[] = $name;
3501
                    } elseif ($this->relObject($spec)) {
3502
                        $fields[] = $spec;
3503
                    }
3504
                }
3505
            }
3506
        }
3507
3508
        // we need to make sure the format is unified before
3509
        // augmenting fields, so extensions can apply consistent checks
3510
        // but also after augmenting fields, because the extension
3511
        // might use the shorthand notation as well
3512
3513
        // rewrite array, if it is using shorthand syntax
3514
        $rewrite = array();
3515
        foreach ($fields as $name => $specOrName) {
3516
            $identifer = (is_int($name)) ? $specOrName : $name;
3517
3518
            if (is_int($name)) {
3519
                // Format: array('MyFieldName')
3520
                $rewrite[$identifer] = array();
3521
            } elseif (is_array($specOrName) && ($relObject = $this->relObject($identifer))) {
3522
                // Format: array('MyFieldName' => array(
3523
                //   'filter => 'ExactMatchFilter',
3524
                //   'field' => 'NumericField', // optional
3525
                //   'title' => 'My Title', // optional
3526
                // ))
3527
                $rewrite[$identifer] = array_merge(
3528
                    array('filter' => $relObject->config()->get('default_search_filter_class')),
3529
                    (array)$specOrName
3530
                );
3531
            } else {
3532
                // Format: array('MyFieldName' => 'ExactMatchFilter')
3533
                $rewrite[$identifer] = array(
3534
                    'filter' => $specOrName,
3535
                );
3536
            }
3537
            if (!isset($rewrite[$identifer]['title'])) {
3538
                $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3539
                    ? $labels[$identifer] : FormField::name_to_label($identifer);
3540
            }
3541
            if (!isset($rewrite[$identifer]['filter'])) {
3542
                /** @skipUpgrade */
3543
                $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3544
            }
3545
        }
3546
3547
        $fields = $rewrite;
3548
3549
        // apply DataExtensions if present
3550
        $this->extend('updateSearchableFields', $fields);
3551
3552
        return $fields;
3553
    }
3554
3555
    /**
3556
     * Get any user defined searchable fields labels that
3557
     * exist. Allows overriding of default field names in the form
3558
     * interface actually presented to the user.
3559
     *
3560
     * The reason for keeping this separate from searchable_fields,
3561
     * which would be a logical place for this functionality, is to
3562
     * avoid bloating and complicating the configuration array. Currently
3563
     * much of this system is based on sensible defaults, and this property
3564
     * would generally only be set in the case of more complex relationships
3565
     * between data object being required in the search interface.
3566
     *
3567
     * Generates labels based on name of the field itself, if no static property
3568
     * {@link self::field_labels} exists.
3569
     *
3570
     * @uses $field_labels
3571
     * @uses FormField::name_to_label()
3572
     *
3573
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3574
     *
3575
     * @return array Array of all element labels
3576
     */
3577
    public function fieldLabels($includerelations = true)
3578
    {
3579
        $cacheKey = static::class . '_' . $includerelations;
3580
3581
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
3582
            $customLabels = $this->config()->get('field_labels');
3583
            $autoLabels = array();
3584
3585
            // get all translated static properties as defined in i18nCollectStatics()
3586
            $ancestry = ClassInfo::ancestry(static::class);
3587
            $ancestry = array_reverse($ancestry);
3588
            if ($ancestry) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ancestry of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

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