Passed
Pull Request — 4.3 (#9221)
by Maxime
06:54
created

DataObject::skipWriteComponents()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 26
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

1614
                $component->write(/** @scrutinizer ignore-type */ ...$args);
Loading history...
1615
            }
1616
        }
1617
1618
        if ($join = $this->getJoin()) {
1619
            if (!$this->skipWriteComponents($recursive, $join, $skip)) {
1620
                $join->write(...$args);
1621
            }
1622
        }
1623
1624
        return $this;
1625
    }
1626
1627
    /**
1628
     * Check if target is in the skip list and add it if it isn't.
1629
     * @param bool $recursive
1630
     * @param DataObject $target
1631
     * @param array $skip
1632
     * @return bool Whether the target is already in the list
1633
     */
1634
    private function skipWriteComponents($recursive, DataObject $target, array &$skip)
1635
    {
1636
        // We only care about the skip list if our call is meant to be recursive
1637
        if (!$recursive) {
1638
            return false;
1639
        }
1640
1641
        // Get our Skip array keys
1642
        $classname = get_class($target);
1643
        $id = $target->ID;
1644
1645
        // Check if the target is in the skip list
1646
        if (isset($skip[$classname])) {
1647
            if (in_array($id, $skip[$classname])) {
1648
                // Skip the object
1649
                return true;
1650
            }
1651
        } else {
1652
            // This is the first object of this class
1653
            $skip[$classname] = [];
1654
        }
1655
1656
        // Add the target to our skip list
1657
        $skip[$classname][] = $id;
1658
1659
        return false;
1660
    }
1661
1662
    /**
1663
     * Delete this data object.
1664
     * $this->onBeforeDelete() gets called.
1665
     * Note that in Versioned objects, both Stage and Live will be deleted.
1666
     * @uses DataExtension->augmentSQL()
1667
     */
1668
    public function delete()
1669
    {
1670
        $this->brokenOnDelete = true;
1671
        $this->onBeforeDelete();
1672
        if ($this->brokenOnDelete) {
1673
            user_error(static::class . " has a broken onBeforeDelete() function."
1674
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1675
        }
1676
1677
        // Deleting a record without an ID shouldn't do anything
1678
        if (!$this->ID) {
1679
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1680
        }
1681
1682
        // TODO: This is quite ugly.  To improve:
1683
        //  - move the details of the delete code in the DataQuery system
1684
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1685
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1686
        $srcQuery = DataList::create(static::class)
1687
            ->filter('ID', $this->ID)
1688
            ->dataQuery()
1689
            ->query();
1690
        $queriedTables = $srcQuery->queriedTables();
1691
        $this->extend('updateDeleteTables', $queriedTables, $srcQuery);
1692
        foreach ($queriedTables as $table) {
1693
            $delete = SQLDelete::create("\"$table\"", array('"ID"' => $this->ID));
1694
            $this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
1695
            $delete->execute();
1696
        }
1697
        // Remove this item out of any caches
1698
        $this->flushCache();
1699
1700
        $this->onAfterDelete();
1701
1702
        $this->OldID = $this->ID;
1703
        $this->ID = 0;
1704
    }
1705
1706
    /**
1707
     * Delete the record with the given ID.
1708
     *
1709
     * @param string $className The class name of the record to be deleted
1710
     * @param int $id ID of record to be deleted
1711
     */
1712
    public static function delete_by_id($className, $id)
1713
    {
1714
        $obj = DataObject::get_by_id($className, $id);
1715
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1716
            $obj->delete();
1717
        } else {
1718
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1719
        }
1720
    }
1721
1722
    /**
1723
     * Get the class ancestry, including the current class name.
1724
     * The ancestry will be returned as an array of class names, where the 0th element
1725
     * will be the class that inherits directly from DataObject, and the last element
1726
     * will be the current class.
1727
     *
1728
     * @return array Class ancestry
1729
     */
1730
    public function getClassAncestry()
1731
    {
1732
        return ClassInfo::ancestry(static::class);
1733
    }
1734
1735
    /**
1736
     * Return a unary component object from a one to one relationship, as a DataObject.
1737
     * If no component is available, an 'empty component' will be returned for
1738
     * non-polymorphic relations, or for polymorphic relations with a class set.
1739
     *
1740
     * @param string $componentName Name of the component
1741
     * @return DataObject The component object. It's exact type will be that of the component.
1742
     * @throws Exception
1743
     */
1744
    public function getComponent($componentName)
1745
    {
1746
        if (isset($this->components[$componentName])) {
1747
            return $this->components[$componentName];
1748
        }
1749
1750
        $schema = static::getSchema();
1751
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1752
            $joinField = $componentName . 'ID';
1753
            $joinID = $this->getField($joinField);
1754
1755
            // Extract class name for polymorphic relations
1756
            if ($class === self::class) {
1757
                $class = $this->getField($componentName . 'Class');
1758
                if (empty($class)) {
1759
                    return null;
1760
                }
1761
            }
1762
1763
            if ($joinID) {
1764
                // Ensure that the selected object originates from the same stage, subsite, etc
1765
                $component = DataObject::get($class)
1766
                    ->filter('ID', $joinID)
1767
                    ->setDataQueryParam($this->getInheritableQueryParams())
1768
                    ->first();
1769
            }
1770
1771
            if (empty($component)) {
1772
                $component = Injector::inst()->create($class);
1773
            }
1774
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1775
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1776
            $joinID = $this->ID;
1777
1778
            if ($joinID) {
1779
                // Prepare filter for appropriate join type
1780
                if ($polymorphic) {
1781
                    $filter = array(
1782
                        "{$joinField}ID" => $joinID,
1783
                        "{$joinField}Class" => static::class,
1784
                    );
1785
                } else {
1786
                    $filter = array(
1787
                        $joinField => $joinID
1788
                    );
1789
                }
1790
1791
                // Ensure that the selected object originates from the same stage, subsite, etc
1792
                $component = DataObject::get($class)
1793
                    ->filter($filter)
1794
                    ->setDataQueryParam($this->getInheritableQueryParams())
1795
                    ->first();
1796
            }
1797
1798
            if (empty($component)) {
1799
                $component = Injector::inst()->create($class);
1800
                if ($polymorphic) {
1801
                    $component->{$joinField . 'ID'} = $this->ID;
1802
                    $component->{$joinField . 'Class'} = static::class;
1803
                } else {
1804
                    $component->$joinField = $this->ID;
1805
                }
1806
            }
1807
        } else {
1808
            throw new InvalidArgumentException(
1809
                "DataObject->getComponent(): Could not find component '$componentName'."
1810
            );
1811
        }
1812
1813
        $this->components[$componentName] = $component;
1814
        return $component;
1815
    }
1816
1817
    /**
1818
     * Assign an item to the given component
1819
     *
1820
     * @param string $componentName
1821
     * @param DataObject|null $item
1822
     * @return $this
1823
     */
1824
    public function setComponent($componentName, $item)
1825
    {
1826
        // Validate component
1827
        $schema = static::getSchema();
1828
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1829
            // Force item to be written if not by this point
1830
            // @todo This could be lazy-written in a beforeWrite hook, but force write here for simplicity
1831
            // https://github.com/silverstripe/silverstripe-framework/issues/7818
1832
            if ($item && !$item->isInDB()) {
1833
                $item->write();
1834
            }
1835
1836
            // Update local ID
1837
            $joinField = $componentName . 'ID';
1838
            $this->setField($joinField, $item ? $item->ID : null);
1839
            // Update Class (Polymorphic has_one)
1840
            // Extract class name for polymorphic relations
1841
            if ($class === self::class) {
1842
                $this->setField($componentName . 'Class', $item ? get_class($item) : null);
1843
            }
1844
        } 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...
1845
            if ($item) {
1846
                // For belongs_to, add to has_one on other component
1847
                $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1848
                if (!$polymorphic) {
1849
                    $joinField = substr($joinField, 0, -2);
1850
                }
1851
                $item->setComponent($joinField, $this);
1852
            }
1853
        } else {
1854
            throw new InvalidArgumentException(
1855
                "DataObject->setComponent(): Could not find component '$componentName'."
1856
            );
1857
        }
1858
1859
        $this->components[$componentName] = $item;
1860
        return $this;
1861
    }
1862
1863
    /**
1864
     * Returns a one-to-many relation as a HasManyList
1865
     *
1866
     * @param string $componentName Name of the component
1867
     * @param int|array $id Optional ID(s) for parent of this relation, if not the current record
1868
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1869
     */
1870
    public function getComponents($componentName, $id = null)
1871
    {
1872
        if (!isset($id)) {
1873
            $id = $this->ID;
1874
        }
1875
        $result = null;
1876
1877
        $schema = $this->getSchema();
1878
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1879
        if (!$componentClass) {
1880
            throw new InvalidArgumentException(sprintf(
1881
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1882
                $componentName,
1883
                static::class
1884
            ));
1885
        }
1886
1887
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1888
        if (!$id) {
1889
            if (!isset($this->unsavedRelations[$componentName])) {
1890
                $this->unsavedRelations[$componentName] =
1891
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1892
            }
1893
            return $this->unsavedRelations[$componentName];
1894
        }
1895
1896
        // Determine type and nature of foreign relation
1897
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1898
        /** @var HasManyList $result */
1899
        if ($polymorphic) {
1900
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
1901
        } else {
1902
            $result = HasManyList::create($componentClass, $joinField);
1903
        }
1904
1905
        return $result
1906
            ->setDataQueryParam($this->getInheritableQueryParams())
1907
            ->forForeignID($id);
1908
    }
1909
1910
    /**
1911
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1912
     *
1913
     * @param string $relationName Relation name.
1914
     * @return string Class name, or null if not found.
1915
     */
1916
    public function getRelationClass($relationName)
1917
    {
1918
        // Parse many_many
1919
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1920
        if ($manyManyComponent) {
1921
            return $manyManyComponent['childClass'];
1922
        }
1923
1924
        // Go through all relationship configuration fields.
1925
        $config = $this->config();
1926
        $candidates = array_merge(
1927
            ($relations = $config->get('has_one')) ? $relations : array(),
1928
            ($relations = $config->get('has_many')) ? $relations : array(),
1929
            ($relations = $config->get('belongs_to')) ? $relations : array()
1930
        );
1931
1932
        if (isset($candidates[$relationName])) {
1933
            $remoteClass = $candidates[$relationName];
1934
1935
            // If dot notation is present, extract just the first part that contains the class.
1936
            if (($fieldPos = strpos($remoteClass, '.')) !== false) {
1937
                return substr($remoteClass, 0, $fieldPos);
1938
            }
1939
1940
            // Otherwise just return the class
1941
            return $remoteClass;
1942
        }
1943
1944
        return null;
1945
    }
1946
1947
    /**
1948
     * Given a relation name, determine the relation type
1949
     *
1950
     * @param string $component Name of component
1951
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1952
     */
1953
    public function getRelationType($component)
1954
    {
1955
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1956
        $config = $this->config();
1957
        foreach ($types as $type) {
1958
            $relations = $config->get($type);
1959
            if ($relations && isset($relations[$component])) {
1960
                return $type;
1961
            }
1962
        }
1963
        return null;
1964
    }
1965
1966
    /**
1967
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1968
     * side of the relation.
1969
     *
1970
     * Notes on behaviour:
1971
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1972
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1973
     *  - Polymorphic relationships do not have two natural endpoints (only on one side)
1974
     *   and thus attempting to infer them will return nothing.
1975
     *  - Cannot be used on unsaved objects.
1976
     *
1977
     * @param string $remoteClass
1978
     * @param string $remoteRelation
1979
     * @return DataList|DataObject The component, either as a list or single object
1980
     * @throws BadMethodCallException
1981
     * @throws InvalidArgumentException
1982
     */
1983
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1984
    {
1985
        $remote = DataObject::singleton($remoteClass);
1986
        $class = $remote->getRelationClass($remoteRelation);
1987
        $schema = static::getSchema();
1988
1989
        // Validate arguments
1990
        if (!$this->isInDB()) {
1991
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1992
        }
1993
        if (empty($class)) {
1994
            throw new InvalidArgumentException(sprintf(
1995
                "%s invoked with invalid relation %s.%s",
1996
                __METHOD__,
1997
                $remoteClass,
1998
                $remoteRelation
1999
            ));
2000
        }
2001
        // If relation is polymorphic, do not infer recriprocal relationship
2002
        if ($class === self::class) {
2003
            return null;
2004
        }
2005
        if (!is_a($this, $class, true)) {
2006
            throw new InvalidArgumentException(sprintf(
2007
                "Relation %s on %s does not refer to objects of type %s",
2008
                $remoteRelation,
2009
                $remoteClass,
2010
                static::class
2011
            ));
2012
        }
2013
2014
        // Check the relation type to mock
2015
        $relationType = $remote->getRelationType($remoteRelation);
2016
        switch ($relationType) {
2017
            case 'has_one': {
2018
                // Mock has_many
2019
                $joinField = "{$remoteRelation}ID";
2020
                $componentClass = $schema->classForField($remoteClass, $joinField);
2021
                $result = HasManyList::create($componentClass, $joinField);
2022
                return $result
2023
                    ->setDataQueryParam($this->getInheritableQueryParams())
2024
                    ->forForeignID($this->ID);
2025
            }
2026
            case 'belongs_to':
2027
            case 'has_many': {
2028
                // These relations must have a has_one on the other end, so find it
2029
                $joinField = $schema->getRemoteJoinField(
2030
                    $remoteClass,
2031
                    $remoteRelation,
2032
                    $relationType,
2033
                    $polymorphic
2034
                );
2035
                // If relation is polymorphic, do not infer recriprocal relationship automatically
2036
                if ($polymorphic) {
2037
                    return null;
2038
                }
2039
                $joinID = $this->getField($joinField);
2040
                if (empty($joinID)) {
2041
                    return null;
2042
                }
2043
                // Get object by joined ID
2044
                return DataObject::get($remoteClass)
2045
                    ->filter('ID', $joinID)
2046
                    ->setDataQueryParam($this->getInheritableQueryParams())
2047
                    ->first();
2048
            }
2049
            case 'many_many':
2050
            case 'belongs_many_many': {
2051
                // Get components and extra fields from parent
2052
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
2053
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
2054
2055
                // Reverse parent and component fields and create an inverse ManyManyList
2056
                /** @var RelationList $result */
2057
                $result = Injector::inst()->create(
2058
                    $manyMany['relationClass'],
2059
                    $manyMany['parentClass'], // Substitute parent class for dataClass
2060
                    $manyMany['join'],
2061
                    $manyMany['parentField'], // Reversed parent / child field
2062
                    $manyMany['childField'], // Reversed parent / child field
2063
                    $extraFields,
2064
                    $manyMany['childClass'], // substitute child class for parentClass
2065
                    $remoteClass // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2066
                );
2067
                $this->extend('updateManyManyComponents', $result);
2068
2069
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2070
                // foreignID set elsewhere.
2071
                return $result
2072
                    ->setDataQueryParam($this->getInheritableQueryParams())
2073
                    ->forForeignID($this->ID);
2074
            }
2075
            default: {
2076
                return null;
2077
            }
2078
        }
2079
    }
2080
2081
    /**
2082
     * Returns a many-to-many component, as a ManyManyList.
2083
     * @param string $componentName Name of the many-many component
2084
     * @param int|array $id Optional ID for parent of this relation, if not the current record
2085
     * @return ManyManyList|UnsavedRelationList The set of components
2086
     */
2087
    public function getManyManyComponents($componentName, $id = null)
2088
    {
2089
        if (!isset($id)) {
2090
            $id = $this->ID;
2091
        }
2092
        $schema = static::getSchema();
2093
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
2094
        if (!$manyManyComponent) {
2095
            throw new InvalidArgumentException(sprintf(
2096
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
2097
                $componentName,
2098
                static::class
2099
            ));
2100
        }
2101
2102
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
2103
        if (!$id) {
2104
            if (!isset($this->unsavedRelations[$componentName])) {
2105
                $this->unsavedRelations[$componentName] =
2106
                    new UnsavedRelationList(
2107
                        $manyManyComponent['parentClass'],
2108
                        $componentName,
2109
                        $manyManyComponent['childClass']
2110
                    );
2111
            }
2112
            return $this->unsavedRelations[$componentName];
2113
        }
2114
2115
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
2116
        /** @var RelationList $result */
2117
        $result = Injector::inst()->create(
2118
            $manyManyComponent['relationClass'],
2119
            $manyManyComponent['childClass'],
2120
            $manyManyComponent['join'],
2121
            $manyManyComponent['childField'],
2122
            $manyManyComponent['parentField'],
2123
            $extraFields,
2124
            $manyManyComponent['parentClass'],
2125
            static::class // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2126
        );
2127
2128
        // Store component data in query meta-data
2129
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
2130
            /** @var DataQuery $query */
2131
            $query->setQueryParam('Component.ExtraFields', $extraFields);
2132
        });
2133
2134
        // If we have a default sort set for our "join" then we should overwrite any default already set.
2135
        $joinSort = Config::inst()->get($manyManyComponent['join'], 'default_sort');
2136
        if (!empty($joinSort)) {
2137
            $result = $result->sort($joinSort);
2138
        }
2139
2140
        $this->extend('updateManyManyComponents', $result);
2141
2142
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2143
        // foreignID set elsewhere.
2144
        return $result
2145
            ->setDataQueryParam($this->getInheritableQueryParams())
2146
            ->forForeignID($id);
2147
    }
2148
2149
    /**
2150
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
2151
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
2152
     *
2153
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
2154
     *                          their classes.
2155
     */
2156
    public function hasOne()
2157
    {
2158
        return (array)$this->config()->get('has_one');
2159
    }
2160
2161
    /**
2162
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
2163
     * their class name will be returned.
2164
     *
2165
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2166
     *        the field data stripped off. It defaults to TRUE.
2167
     * @return string|array
2168
     */
2169
    public function belongsTo($classOnly = true)
2170
    {
2171
        $belongsTo = (array)$this->config()->get('belongs_to');
2172
        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...
2173
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
2174
        } else {
2175
            return $belongsTo ? $belongsTo : array();
2176
        }
2177
    }
2178
2179
    /**
2180
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2181
     * relationships and their classes will be returned.
2182
     *
2183
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2184
     *        the field data stripped off. It defaults to TRUE.
2185
     * @return string|array|false
2186
     */
2187
    public function hasMany($classOnly = true)
2188
    {
2189
        $hasMany = (array)$this->config()->get('has_many');
2190
        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...
2191
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2192
        } else {
2193
            return $hasMany ? $hasMany : array();
2194
        }
2195
    }
2196
2197
    /**
2198
     * Return the many-to-many extra fields specification.
2199
     *
2200
     * If you don't specify a component name, it returns all
2201
     * extra fields for all components available.
2202
     *
2203
     * @return array|null
2204
     */
2205
    public function manyManyExtraFields()
2206
    {
2207
        return $this->config()->get('many_many_extraFields');
2208
    }
2209
2210
    /**
2211
     * Return information about a many-to-many component.
2212
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2213
     * components are returned.
2214
     *
2215
     * @see DataObjectSchema::manyManyComponent()
2216
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2217
     */
2218
    public function manyMany()
2219
    {
2220
        $config = $this->config();
2221
        $manyManys = (array)$config->get('many_many');
2222
        $belongsManyManys = (array)$config->get('belongs_many_many');
2223
        $items = array_merge($manyManys, $belongsManyManys);
2224
        return $items;
2225
    }
2226
2227
    /**
2228
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
2229
     *
2230
     * This is experimental, and is currently only a Postgres-specific enhancement.
2231
     *
2232
     * @param string $class
2233
     * @return array|false
2234
     */
2235
    public function database_extensions($class)
2236
    {
2237
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2238
        if ($extensions) {
2239
            return $extensions;
2240
        } else {
2241
            return false;
2242
        }
2243
    }
2244
2245
    /**
2246
     * Generates a SearchContext to be used for building and processing
2247
     * a generic search form for properties on this object.
2248
     *
2249
     * @return SearchContext
2250
     */
2251
    public function getDefaultSearchContext()
2252
    {
2253
        return new SearchContext(
2254
            static::class,
2255
            $this->scaffoldSearchFields(),
2256
            $this->defaultSearchFilters()
2257
        );
2258
    }
2259
2260
    /**
2261
     * Determine which properties on the DataObject are
2262
     * searchable, and map them to their default {@link FormField}
2263
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2264
     *
2265
     * Some additional logic is included for switching field labels, based on
2266
     * how generic or specific the field type is.
2267
     *
2268
     * Used by {@link SearchContext}.
2269
     *
2270
     * @param array $_params
2271
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2272
     *   'restrictFields': Numeric array of a field name whitelist
2273
     * @return FieldList
2274
     */
2275
    public function scaffoldSearchFields($_params = null)
2276
    {
2277
        $params = array_merge(
2278
            array(
2279
                'fieldClasses' => false,
2280
                'restrictFields' => false
2281
            ),
2282
            (array)$_params
2283
        );
2284
        $fields = new FieldList();
2285
        foreach ($this->searchableFields() as $fieldName => $spec) {
2286
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
2287
                continue;
2288
            }
2289
2290
            // If a custom fieldclass is provided as a string, use it
2291
            $field = null;
2292
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2293
                $fieldClass = $params['fieldClasses'][$fieldName];
2294
                $field = new $fieldClass($fieldName);
2295
            // If we explicitly set a field, then construct that
2296
            } elseif (isset($spec['field'])) {
2297
                // If it's a string, use it as a class name and construct
2298
                if (is_string($spec['field'])) {
2299
                    $fieldClass = $spec['field'];
2300
                    $field = new $fieldClass($fieldName);
2301
2302
                // If it's a FormField object, then just use that object directly.
2303
                } elseif ($spec['field'] instanceof FormField) {
2304
                    $field = $spec['field'];
2305
2306
                // Otherwise we have a bug
2307
                } else {
2308
                    user_error("Bad value for searchable_fields, 'field' value: "
2309
                        . var_export($spec['field'], true), E_USER_WARNING);
2310
                }
2311
2312
            // Otherwise, use the database field's scaffolder
2313
            } elseif ($object = $this->relObject($fieldName)) {
2314
                $field = $object->scaffoldSearchField();
2315
            }
2316
2317
            // Allow fields to opt out of search
2318
            if (!$field) {
2319
                continue;
2320
            }
2321
2322
            if (strstr($fieldName, '.')) {
2323
                $field->setName(str_replace('.', '__', $fieldName));
2324
            }
2325
            $field->setTitle($spec['title']);
2326
2327
            $fields->push($field);
2328
        }
2329
        return $fields;
2330
    }
2331
2332
    /**
2333
     * Scaffold a simple edit form for all properties on this dataobject,
2334
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2335
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2336
     *
2337
     * @uses FormScaffolder
2338
     *
2339
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2340
     * @return FieldList
2341
     */
2342
    public function scaffoldFormFields($_params = null)
2343
    {
2344
        $params = array_merge(
2345
            array(
2346
                'tabbed' => false,
2347
                'includeRelations' => false,
2348
                'restrictFields' => false,
2349
                'fieldClasses' => false,
2350
                'ajaxSafe' => false
2351
            ),
2352
            (array)$_params
2353
        );
2354
2355
        $fs = FormScaffolder::create($this);
2356
        $fs->tabbed = $params['tabbed'];
2357
        $fs->includeRelations = $params['includeRelations'];
2358
        $fs->restrictFields = $params['restrictFields'];
2359
        $fs->fieldClasses = $params['fieldClasses'];
2360
        $fs->ajaxSafe = $params['ajaxSafe'];
2361
2362
        return $fs->getFieldList();
2363
    }
2364
2365
    /**
2366
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2367
     * being called on extensions
2368
     *
2369
     * @param callable $callback The callback to execute
2370
     */
2371
    protected function beforeUpdateCMSFields($callback)
2372
    {
2373
        $this->beforeExtending('updateCMSFields', $callback);
2374
    }
2375
2376
    /**
2377
     * Centerpiece of every data administration interface in Silverstripe,
2378
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2379
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2380
     * generate this set. To customize, overload this method in a subclass
2381
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2382
     *
2383
     * <code>
2384
     * class MyCustomClass extends DataObject {
2385
     *  static $db = array('CustomProperty'=>'Boolean');
2386
     *
2387
     *  function getCMSFields() {
2388
     *    $fields = parent::getCMSFields();
2389
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2390
     *    return $fields;
2391
     *  }
2392
     * }
2393
     * </code>
2394
     *
2395
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2396
     *
2397
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2398
     */
2399
    public function getCMSFields()
2400
    {
2401
        $tabbedFields = $this->scaffoldFormFields(array(
2402
            // Don't allow has_many/many_many relationship editing before the record is first saved
2403
            'includeRelations' => ($this->ID > 0),
2404
            'tabbed' => true,
2405
            'ajaxSafe' => true
2406
        ));
2407
2408
        $this->extend('updateCMSFields', $tabbedFields);
2409
2410
        return $tabbedFields;
2411
    }
2412
2413
    /**
2414
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2415
     * including that dataobject's extensions customised actions could be added to the EditForm.
2416
     *
2417
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2418
     */
2419
    public function getCMSActions()
2420
    {
2421
        $actions = new FieldList();
2422
        $this->extend('updateCMSActions', $actions);
2423
        return $actions;
2424
    }
2425
2426
2427
    /**
2428
     * Used for simple frontend forms without relation editing
2429
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2430
     * by default. To customize, either overload this method in your
2431
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2432
     *
2433
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2434
     *
2435
     * @param array $params See {@link scaffoldFormFields()}
2436
     * @return FieldList Always returns a simple field collection without TabSet.
2437
     */
2438
    public function getFrontEndFields($params = null)
2439
    {
2440
        $untabbedFields = $this->scaffoldFormFields($params);
2441
        $this->extend('updateFrontEndFields', $untabbedFields);
2442
2443
        return $untabbedFields;
2444
    }
2445
2446
    public function getViewerTemplates($suffix = '')
2447
    {
2448
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2449
    }
2450
2451
    /**
2452
     * Gets the value of a field.
2453
     * Called by {@link __get()} and any getFieldName() methods you might create.
2454
     *
2455
     * @param string $field The name of the field
2456
     * @return mixed The field value
2457
     */
2458
    public function getField($field)
2459
    {
2460
        // If we already have a value in $this->record, then we should just return that
2461
        if (isset($this->record[$field])) {
2462
            return $this->record[$field];
2463
        }
2464
2465
        // Do we have a field that needs to be lazy loaded?
2466
        if (isset($this->record[$field . '_Lazy'])) {
2467
            $tableClass = $this->record[$field . '_Lazy'];
2468
            $this->loadLazyFields($tableClass);
2469
        }
2470
        $schema = static::getSchema();
2471
2472
        // Support unary relations as fields
2473
        if ($schema->unaryComponent(static::class, $field)) {
2474
            return $this->getComponent($field);
2475
        }
2476
2477
        // In case of complex fields, return the DBField object
2478
        if ($schema->compositeField(static::class, $field)) {
2479
            $this->record[$field] = $this->dbObject($field);
2480
        }
2481
2482
        return isset($this->record[$field]) ? $this->record[$field] : null;
2483
    }
2484
2485
    /**
2486
     * Loads all the stub fields that an initial lazy load didn't load fully.
2487
     *
2488
     * @param string $class Class to load the values from. Others are joined as required.
2489
     * Not specifying a tableClass will load all lazy fields from all tables.
2490
     * @return bool Flag if lazy loading succeeded
2491
     */
2492
    protected function loadLazyFields($class = null)
2493
    {
2494
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2495
            return false;
2496
        }
2497
2498
        if (!$class) {
2499
            $loaded = array();
2500
2501
            foreach ($this->record as $key => $value) {
2502
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2503
                    $this->loadLazyFields($value);
2504
                    $loaded[$value] = $value;
2505
                }
2506
            }
2507
2508
            return false;
2509
        }
2510
2511
        $dataQuery = new DataQuery($class);
2512
2513
        // Reset query parameter context to that of this DataObject
2514
        if ($params = $this->getSourceQueryParams()) {
2515
            foreach ($params as $key => $value) {
2516
                $dataQuery->setQueryParam($key, $value);
2517
            }
2518
        }
2519
2520
        // Limit query to the current record, unless it has the Versioned extension,
2521
        // in which case it requires special handling through augmentLoadLazyFields()
2522
        $schema = static::getSchema();
2523
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2524
        $dataQuery->where([
2525
            $baseIDColumn => $this->record['ID']
2526
        ])->limit(1);
2527
2528
        $columns = array();
2529
2530
        // Add SQL for fields, both simple & multi-value
2531
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2532
        $databaseFields = $schema->databaseFields($class, false);
2533
        foreach ($databaseFields as $k => $v) {
2534
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2535
                $columns[] = $k;
2536
            }
2537
        }
2538
2539
        if ($columns) {
2540
            $query = $dataQuery->query();
2541
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2542
            $this->extend('augmentSQL', $query, $dataQuery);
2543
2544
            $dataQuery->setQueriedColumns($columns);
2545
            $newData = $dataQuery->execute()->record();
2546
2547
            // Load the data into record
2548
            if ($newData) {
2549
                foreach ($newData as $k => $v) {
2550
                    if (in_array($k, $columns)) {
2551
                        $this->record[$k] = $v;
2552
                        $this->original[$k] = $v;
2553
                        unset($this->record[$k . '_Lazy']);
2554
                    }
2555
                }
2556
2557
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2558
            } else {
2559
                foreach ($columns as $k) {
2560
                    $this->record[$k] = null;
2561
                    $this->original[$k] = null;
2562
                    unset($this->record[$k . '_Lazy']);
2563
                }
2564
            }
2565
        }
2566
        return true;
2567
    }
2568
2569
    /**
2570
     * Return the fields that have changed.
2571
     *
2572
     * The change level affects what the functions defines as "changed":
2573
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2574
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2575
     *   for example a change from 0 to null would not be included.
2576
     *
2577
     * Example return:
2578
     * <code>
2579
     * array(
2580
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2581
     * )
2582
     * </code>
2583
     *
2584
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2585
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2586
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2587
     * @return array
2588
     */
2589
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2590
    {
2591
        $changedFields = array();
2592
2593
        // Update the changed array with references to changed obj-fields
2594
        foreach ($this->record as $k => $v) {
2595
            // Prevents DBComposite infinite looping on isChanged
2596
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2597
                continue;
2598
            }
2599
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2600
                $this->changed[$k] = self::CHANGE_VALUE;
2601
            }
2602
        }
2603
2604
        if (is_array($databaseFieldsOnly)) {
2605
            $fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2606
        } elseif ($databaseFieldsOnly) {
2607
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2608
            $fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
2609
        } else {
2610
            $fields = $this->changed;
2611
        }
2612
2613
        // Filter the list to those of a certain change level
2614
        if ($changeLevel > self::CHANGE_STRICT) {
2615
            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...
2616
                foreach ($fields as $name => $level) {
2617
                    if ($level < $changeLevel) {
2618
                        unset($fields[$name]);
2619
                    }
2620
                }
2621
            }
2622
        }
2623
2624
        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...
2625
            foreach ($fields as $name => $level) {
2626
                $changedFields[$name] = array(
2627
                    'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2628
                    'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2629
                    'level' => $level
2630
                );
2631
            }
2632
        }
2633
2634
        return $changedFields;
2635
    }
2636
2637
    /**
2638
     * Uses {@link getChangedFields()} to determine if fields have been changed
2639
     * since loading them from the database.
2640
     *
2641
     * @param string $fieldName Name of the database field to check, will check for any if not given
2642
     * @param int $changeLevel See {@link getChangedFields()}
2643
     * @return boolean
2644
     */
2645
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2646
    {
2647
        $fields = $fieldName ? array($fieldName) : true;
2648
        $changed = $this->getChangedFields($fields, $changeLevel);
2649
        if (!isset($fieldName)) {
2650
            return !empty($changed);
2651
        } else {
2652
            return array_key_exists($fieldName, $changed);
2653
        }
2654
    }
2655
2656
    /**
2657
     * Set the value of the field
2658
     * Called by {@link __set()} and any setFieldName() methods you might create.
2659
     *
2660
     * @param string $fieldName Name of the field
2661
     * @param mixed $val New field value
2662
     * @return $this
2663
     */
2664
    public function setField($fieldName, $val)
2665
    {
2666
        $this->objCacheClear();
2667
        //if it's a has_one component, destroy the cache
2668
        if (substr($fieldName, -2) == 'ID') {
2669
            unset($this->components[substr($fieldName, 0, -2)]);
2670
        }
2671
2672
        // If we've just lazy-loaded the column, then we need to populate the $original array
2673
        if (isset($this->record[$fieldName . '_Lazy'])) {
2674
            $tableClass = $this->record[$fieldName . '_Lazy'];
2675
            $this->loadLazyFields($tableClass);
2676
        }
2677
2678
        // Support component assignent via field setter
2679
        $schema = static::getSchema();
2680
        if ($schema->unaryComponent(static::class, $fieldName)) {
2681
            unset($this->components[$fieldName]);
2682
            // Assign component directly
2683
            if (is_null($val) || $val instanceof DataObject) {
2684
                return $this->setComponent($fieldName, $val);
2685
            }
2686
            // Assign by ID instead of object
2687
            if (is_numeric($val)) {
2688
                $fieldName .= 'ID';
2689
            }
2690
        }
2691
2692
        // Situation 1: Passing an DBField
2693
        if ($val instanceof DBField) {
2694
            $val->setName($fieldName);
2695
            $val->saveInto($this);
2696
2697
            // Situation 1a: Composite fields should remain bound in case they are
2698
            // later referenced to update the parent dataobject
2699
            if ($val instanceof DBComposite) {
2700
                $val->bindTo($this);
2701
                $this->record[$fieldName] = $val;
2702
            }
2703
        // Situation 2: Passing a literal or non-DBField object
2704
        } else {
2705
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2706
            if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) {
2707
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2708
            }
2709
2710
            if (!empty($val) && !is_scalar($val)) {
2711
                $dbField = $this->dbObject($fieldName);
2712
                if ($dbField && $dbField->scalarValueOnly()) {
2713
                    throw new InvalidArgumentException(
2714
                        sprintf(
2715
                            'DataObject::setField: %s only accepts scalars',
2716
                            $fieldName
2717
                        )
2718
                    );
2719
                }
2720
            }
2721
2722
            // if a field is not existing or has strictly changed
2723
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2724
                // TODO Add check for php-level defaults which are not set in the db
2725
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2726
                // At the very least, the type has changed
2727
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2728
2729
                if ((!isset($this->record[$fieldName]) && $val)
2730
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2731
                ) {
2732
                    // Value has changed as well, not just the type
2733
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2734
                }
2735
2736
                // Value is always saved back when strict check succeeds.
2737
                $this->record[$fieldName] = $val;
2738
            }
2739
        }
2740
        return $this;
2741
    }
2742
2743
    /**
2744
     * Set the value of the field, using a casting object.
2745
     * This is useful when you aren't sure that a date is in SQL format, for example.
2746
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2747
     * can be saved into the Image table.
2748
     *
2749
     * @param string $fieldName Name of the field
2750
     * @param mixed $value New field value
2751
     * @return $this
2752
     */
2753
    public function setCastedField($fieldName, $value)
2754
    {
2755
        if (!$fieldName) {
2756
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2757
        }
2758
        $fieldObj = $this->dbObject($fieldName);
2759
        if ($fieldObj) {
0 ignored issues
show
introduced by
$fieldObj is of type SilverStripe\ORM\FieldType\DBField, thus it always evaluated to true.
Loading history...
2760
            $fieldObj->setValue($value);
2761
            $fieldObj->saveInto($this);
2762
        } else {
2763
            $this->$fieldName = $value;
2764
        }
2765
        return $this;
2766
    }
2767
2768
    /**
2769
     * {@inheritdoc}
2770
     */
2771
    public function castingHelper($field)
2772
    {
2773
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2774
        if ($fieldSpec) {
2775
            return $fieldSpec;
2776
        }
2777
2778
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2779
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2780
        $queryParams = $this->getSourceQueryParams();
2781
        if (!empty($queryParams['Component.ExtraFields'])) {
2782
            $extraFields = $queryParams['Component.ExtraFields'];
2783
2784
            if (isset($extraFields[$field])) {
2785
                return $extraFields[$field];
2786
            }
2787
        }
2788
2789
        return parent::castingHelper($field);
2790
    }
2791
2792
    /**
2793
     * Returns true if the given field exists in a database column on any of
2794
     * the objects tables and optionally look up a dynamic getter with
2795
     * get<fieldName>().
2796
     *
2797
     * @param string $field Name of the field
2798
     * @return boolean True if the given field exists
2799
     */
2800
    public function hasField($field)
2801
    {
2802
        $schema = static::getSchema();
2803
        return (
2804
            array_key_exists($field, $this->record)
2805
            || array_key_exists($field, $this->components)
2806
            || $schema->fieldSpec(static::class, $field)
2807
            || $schema->unaryComponent(static::class, $field)
2808
            || $this->hasMethod("get{$field}")
2809
        );
2810
    }
2811
2812
    /**
2813
     * Returns true if the given field exists as a database column
2814
     *
2815
     * @param string $field Name of the field
2816
     *
2817
     * @return boolean
2818
     */
2819
    public function hasDatabaseField($field)
2820
    {
2821
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2822
        return !empty($spec);
2823
    }
2824
2825
    /**
2826
     * Returns true if the member is allowed to do the given action.
2827
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2828
     *
2829
     * @param string $perm The permission to be checked, such as 'View'.
2830
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2831
     * in user.
2832
     * @param array $context Additional $context to pass to extendedCan()
2833
     *
2834
     * @return boolean True if the the member is allowed to do the given action
2835
     */
2836
    public function can($perm, $member = null, $context = array())
2837
    {
2838
        if (!$member) {
2839
            $member = Security::getCurrentUser();
2840
        }
2841
2842
        if ($member && Permission::checkMember($member, "ADMIN")) {
2843
            return true;
2844
        }
2845
2846
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2847
            $method = 'can' . ucfirst($perm);
2848
            return $this->$method($member);
2849
        }
2850
2851
        $results = $this->extendedCan('can', $member);
2852
        if (isset($results)) {
2853
            return $results;
2854
        }
2855
2856
        return ($member && Permission::checkMember($member, $perm));
2857
    }
2858
2859
    /**
2860
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2861
     * expected to return one of three values:
2862
     *
2863
     *  - false: Disallow this permission, regardless of what other extensions say
2864
     *  - true: Allow this permission, as long as no other extensions return false
2865
     *  - NULL: Don't affect the outcome
2866
     *
2867
     * This method itself returns a tri-state value, and is designed to be used like this:
2868
     *
2869
     * <code>
2870
     * $extended = $this->extendedCan('canDoSomething', $member);
2871
     * if($extended !== null) return $extended;
2872
     * else return $normalValue;
2873
     * </code>
2874
     *
2875
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2876
     * @param Member|int $member
2877
     * @param array $context Optional context
2878
     * @return boolean|null
2879
     */
2880
    public function extendedCan($methodName, $member, $context = array())
2881
    {
2882
        $results = $this->extend($methodName, $member, $context);
2883
        if ($results && is_array($results)) {
2884
            // Remove NULLs
2885
            $results = array_filter($results, function ($v) {
2886
                return !is_null($v);
2887
            });
2888
            // If there are any non-NULL responses, then return the lowest one of them.
2889
            // If any explicitly deny the permission, then we don't get access
2890
            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...
2891
                return min($results);
2892
            }
2893
        }
2894
        return null;
2895
    }
2896
2897
    /**
2898
     * @param Member $member
2899
     * @return boolean
2900
     */
2901
    public function canView($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
     * @return boolean
2913
     */
2914
    public function canEdit($member = null)
2915
    {
2916
        $extended = $this->extendedCan(__FUNCTION__, $member);
2917
        if ($extended !== null) {
2918
            return $extended;
2919
        }
2920
        return Permission::check('ADMIN', 'any', $member);
2921
    }
2922
2923
    /**
2924
     * @param Member $member
2925
     * @return boolean
2926
     */
2927
    public function canDelete($member = null)
2928
    {
2929
        $extended = $this->extendedCan(__FUNCTION__, $member);
2930
        if ($extended !== null) {
2931
            return $extended;
2932
        }
2933
        return Permission::check('ADMIN', 'any', $member);
2934
    }
2935
2936
    /**
2937
     * @param Member $member
2938
     * @param array $context Additional context-specific data which might
2939
     * affect whether (or where) this object could be created.
2940
     * @return boolean
2941
     */
2942
    public function canCreate($member = null, $context = array())
2943
    {
2944
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2945
        if ($extended !== null) {
2946
            return $extended;
2947
        }
2948
        return Permission::check('ADMIN', 'any', $member);
2949
    }
2950
2951
    /**
2952
     * Debugging used by Debug::show()
2953
     *
2954
     * @return string HTML data representing this object
2955
     */
2956
    public function debug()
2957
    {
2958
        $class = static::class;
2959
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2960
        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...
2961
            foreach ($this->record as $fieldName => $fieldVal) {
2962
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2963
            }
2964
        }
2965
        $val .= "</ul>\n";
2966
        return $val;
2967
    }
2968
2969
    /**
2970
     * Return the DBField object that represents the given field.
2971
     * This works similarly to obj() with 2 key differences:
2972
     *   - it still returns an object even when the field has no value.
2973
     *   - it only matches fields and not methods
2974
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2975
     *
2976
     * @param string $fieldName Name of the field
2977
     * @return DBField The field as a DBField object
2978
     */
2979
    public function dbObject($fieldName)
2980
    {
2981
        // Check for field in DB
2982
        $schema = static::getSchema();
2983
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2984
        if (!$helper) {
2985
            return null;
2986
        }
2987
2988
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2989
            $tableClass = $this->record[$fieldName . '_Lazy'];
2990
            $this->loadLazyFields($tableClass);
2991
        }
2992
2993
        $value = isset($this->record[$fieldName])
2994
            ? $this->record[$fieldName]
2995
            : null;
2996
2997
        // If we have a DBField object in $this->record, then return that
2998
        if ($value instanceof DBField) {
2999
            return $value;
3000
        }
3001
3002
        list($class, $spec) = explode('.', $helper);
3003
        /** @var DBField $obj */
3004
        $table = $schema->tableName($class);
3005
        $obj = Injector::inst()->create($spec, $fieldName);
3006
        $obj->setTable($table);
3007
        $obj->setValue($value, $this, false);
3008
        return $obj;
3009
    }
3010
3011
    /**
3012
     * Traverses to a DBField referenced by relationships between data objects.
3013
     *
3014
     * The path to the related field is specified with dot separated syntax
3015
     * (eg: Parent.Child.Child.FieldName).
3016
     *
3017
     * If a relation is blank, this will return null instead.
3018
     * If a relation name is invalid (e.g. non-relation on a parent) this
3019
     * can throw a LogicException.
3020
     *
3021
     * @param string $fieldPath List of paths on this object. All items in this path
3022
     * must be ViewableData implementors
3023
     *
3024
     * @return mixed DBField of the field on the object or a DataList instance.
3025
     * @throws LogicException If accessing invalid relations
3026
     */
3027
    public function relObject($fieldPath)
3028
    {
3029
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
3030
        $component = $this;
3031
3032
        // Parse all relations
3033
        foreach (explode('.', $fieldPath) as $relation) {
3034
            if (!$component) {
3035
                return null;
3036
            }
3037
3038
            // Inspect relation type
3039
            if (ClassInfo::hasMethod($component, $relation)) {
3040
                $component = $component->$relation();
3041
            } elseif ($component instanceof Relation || $component instanceof DataList) {
3042
                // $relation could either be a field (aggregate), or another relation
3043
                $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

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

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

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

3169
            $limitArguments = explode(',', /** @scrutinizer ignore-type */ $limit);
Loading history...
3170
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
3171
        } elseif ($limit) {
3172
            $result = $result->limit($limit);
3173
        }
3174
3175
        return $result;
3176
    }
3177
3178
3179
    /**
3180
     * Return the first item matching the given query.
3181
     * All calls to get_one() are cached.
3182
     *
3183
     * @param string $callerClass The class of objects to be returned
3184
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3185
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3186
     * @param boolean $cache Use caching
3187
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3188
     *
3189
     * @return DataObject|null The first item matching the query
3190
     */
3191
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
3192
    {
3193
        $SNG = singleton($callerClass);
3194
3195
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3196
        $cacheKey = md5(serialize($cacheComponents));
3197
3198
        $item = null;
3199
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3200
            $dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
3201
            $item = $dl->first();
3202
3203
            if ($cache) {
3204
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3205
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
3206
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
3207
                }
3208
            }
3209
        }
3210
3211
        if ($cache) {
3212
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
3213
        }
3214
3215
        return $item;
3216
    }
3217
3218
    /**
3219
     * Flush the cached results for all relations (has_one, has_many, many_many)
3220
     * Also clears any cached aggregate data.
3221
     *
3222
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3223
     *                            When false will just clear session-local cached data
3224
     * @return DataObject $this
3225
     */
3226
    public function flushCache($persistent = true)
3227
    {
3228
        if (static::class == self::class) {
0 ignored issues
show
introduced by
The condition static::class == self::class is always true.
Loading history...
3229
            self::$_cache_get_one = array();
3230
            return $this;
3231
        }
3232
3233
        $classes = ClassInfo::ancestry(static::class);
3234
        foreach ($classes as $class) {
3235
            if (isset(self::$_cache_get_one[$class])) {
3236
                unset(self::$_cache_get_one[$class]);
3237
            }
3238
        }
3239
3240
        $this->extend('flushCache');
3241
3242
        $this->components = array();
3243
        return $this;
3244
    }
3245
3246
    /**
3247
     * Flush the get_one global cache and destroy associated objects.
3248
     */
3249
    public static function flush_and_destroy_cache()
3250
    {
3251
        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...
3252
            foreach (self::$_cache_get_one as $class => $items) {
3253
                if (is_array($items)) {
3254
                    foreach ($items as $item) {
3255
                        if ($item) {
3256
                            $item->destroy();
3257
                        }
3258
                    }
3259
                }
3260
            }
3261
        }
3262
        self::$_cache_get_one = array();
3263
    }
3264
3265
    /**
3266
     * Reset all global caches associated with DataObject.
3267
     */
3268
    public static function reset()
3269
    {
3270
        // @todo Decouple these
3271
        DBClassName::clear_classname_cache();
3272
        ClassInfo::reset_db_cache();
3273
        static::getSchema()->reset();
3274
        self::$_cache_get_one = array();
3275
        self::$_cache_field_labels = array();
3276
    }
3277
3278
    /**
3279
     * Return the given element, searching by ID.
3280
     *
3281
     * This can be called either via `DataObject::get_by_id(MyClass::class, $id)`
3282
     * or `MyClass::get_by_id($id)`
3283
     *
3284
     * @param string|int $classOrID The class of the object to be returned, or id if called on target class
3285
     * @param int|bool $idOrCache The id of the element, or cache if called on target class
3286
     * @param boolean $cache See {@link get_one()}
3287
     *
3288
     * @return static The element
3289
     */
3290
    public static function get_by_id($classOrID, $idOrCache = null, $cache = true)
3291
    {
3292
        // Shift arguments if passing id in first or second argument
3293
        list ($class, $id, $cached) = is_numeric($classOrID)
3294
            ? [get_called_class(), $classOrID, isset($idOrCache) ? $idOrCache : $cache]
3295
            : [$classOrID, $idOrCache, $cache];
3296
3297
        // Validate class
3298
        if ($class === self::class) {
3299
            throw new InvalidArgumentException('DataObject::get_by_id() cannot query non-subclass DataObject directly');
3300
        }
3301
3302
        // Pass to get_one
3303
        $column = static::getSchema()->sqlColumnForField($class, 'ID');
3304
        return DataObject::get_one($class, [$column => $id], $cached);
3305
    }
3306
3307
    /**
3308
     * Get the name of the base table for this object
3309
     *
3310
     * @return string
3311
     */
3312
    public function baseTable()
3313
    {
3314
        return static::getSchema()->baseDataTable($this);
3315
    }
3316
3317
    /**
3318
     * Get the base class for this object
3319
     *
3320
     * @return string
3321
     */
3322
    public function baseClass()
3323
    {
3324
        return static::getSchema()->baseDataClass($this);
3325
    }
3326
3327
    /**
3328
     * @var array Parameters used in the query that built this object.
3329
     * This can be used by decorators (e.g. lazy loading) to
3330
     * run additional queries using the same context.
3331
     */
3332
    protected $sourceQueryParams;
3333
3334
    /**
3335
     * @see $sourceQueryParams
3336
     * @return array
3337
     */
3338
    public function getSourceQueryParams()
3339
    {
3340
        return $this->sourceQueryParams;
3341
    }
3342
3343
    /**
3344
     * Get list of parameters that should be inherited to relations on this object
3345
     *
3346
     * @return array
3347
     */
3348
    public function getInheritableQueryParams()
3349
    {
3350
        $params = $this->getSourceQueryParams();
3351
        $this->extend('updateInheritableQueryParams', $params);
3352
        return $params;
3353
    }
3354
3355
    /**
3356
     * @see $sourceQueryParams
3357
     * @param array
3358
     */
3359
    public function setSourceQueryParams($array)
3360
    {
3361
        $this->sourceQueryParams = $array;
3362
    }
3363
3364
    /**
3365
     * @see $sourceQueryParams
3366
     * @param string $key
3367
     * @param string $value
3368
     */
3369
    public function setSourceQueryParam($key, $value)
3370
    {
3371
        $this->sourceQueryParams[$key] = $value;
3372
    }
3373
3374
    /**
3375
     * @see $sourceQueryParams
3376
     * @param string $key
3377
     * @return string
3378
     */
3379
    public function getSourceQueryParam($key)
3380
    {
3381
        if (isset($this->sourceQueryParams[$key])) {
3382
            return $this->sourceQueryParams[$key];
3383
        }
3384
        return null;
3385
    }
3386
3387
    //-------------------------------------------------------------------------------------------//
3388
3389
    /**
3390
     * Check the database schema and update it as necessary.
3391
     *
3392
     * @uses DataExtension->augmentDatabase()
3393
     */
3394
    public function requireTable()
3395
    {
3396
        // Only build the table if we've actually got fields
3397
        $schema = static::getSchema();
3398
        $table = $schema->tableName(static::class);
3399
        $fields = $schema->databaseFields(static::class, false);
3400
        $indexes = $schema->databaseIndexes(static::class, false);
3401
        $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

3401
        /** @scrutinizer ignore-call */ 
3402
        $extensions = self::database_extensions(static::class);
Loading history...
3402
3403
        if (empty($table)) {
3404
            throw new LogicException(
3405
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3406
            );
3407
        }
3408
3409
        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...
3410
            $hasAutoIncPK = get_parent_class($this) === self::class;
3411
            DB::require_table(
3412
                $table,
3413
                $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

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

3414
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3415
                $hasAutoIncPK,
3416
                $this->config()->get('create_table_options'),
3417
                $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

3417
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3418
            );
3419
        } else {
3420
            DB::dont_require_table($table);
3421
        }
3422
3423
        // Build any child tables for many_many items
3424
        if ($manyMany = $this->uninherited('many_many')) {
3425
            $extras = $this->uninherited('many_many_extraFields');
3426
            foreach ($manyMany as $component => $spec) {
3427
                // Get many_many spec
3428
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3429
                $parentField = $manyManyComponent['parentField'];
3430
                $childField = $manyManyComponent['childField'];
3431
                $tableOrClass = $manyManyComponent['join'];
3432
3433
                // Skip if backed by actual class
3434
                if (class_exists($tableOrClass)) {
3435
                    continue;
3436
                }
3437
3438
                // Build fields
3439
                $manymanyFields = array(
3440
                    $parentField => "Int",
3441
                    $childField => "Int",
3442
                );
3443
                if (isset($extras[$component])) {
3444
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3445
                }
3446
3447
                // Build index list
3448
                $manymanyIndexes = [
3449
                    $parentField => [
3450
                        'type' => 'index',
3451
                        'name' => $parentField,
3452
                        'columns' => [$parentField],
3453
                    ],
3454
                    $childField => [
3455
                        'type' => 'index',
3456
                        'name' => $childField,
3457
                        'columns' => [$childField],
3458
                    ],
3459
                ];
3460
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Bug introduced by
$manymanyFields of type array|string[] is incompatible with the type string expected by parameter $fieldSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

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

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

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

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