Passed
Push — less-original ( 51a40c...30cf10 )
by Sam
07:37
created

DataObject::inferReciprocalComponent()   C

Complexity

Conditions 13
Paths 14

Size

Total Lines 94
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 63
nc 14
nop 2
dl 0
loc 94
rs 6.1006
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

3321
        /** @scrutinizer ignore-call */ 
3322
        $extensions = self::database_extensions(static::class);
Loading history...
3322
3323
        if (empty($table)) {
3324
            throw new LogicException(
3325
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3326
            );
3327
        }
3328
3329
        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...
3330
            $hasAutoIncPK = get_parent_class($this) === self::class;
3331
            DB::require_table(
3332
                $table,
3333
                $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

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

3334
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3335
                $hasAutoIncPK,
3336
                $this->config()->get('create_table_options'),
3337
                $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

3337
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3338
            );
3339
        } else {
3340
            DB::dont_require_table($table);
3341
        }
3342
3343
        // Build any child tables for many_many items
3344
        if ($manyMany = $this->uninherited('many_many')) {
3345
            $extras = $this->uninherited('many_many_extraFields');
3346
            foreach ($manyMany as $component => $spec) {
3347
                // Get many_many spec
3348
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3349
                $parentField = $manyManyComponent['parentField'];
3350
                $childField = $manyManyComponent['childField'];
3351
                $tableOrClass = $manyManyComponent['join'];
3352
3353
                // Skip if backed by actual class
3354
                if (class_exists($tableOrClass)) {
3355
                    continue;
3356
                }
3357
3358
                // Build fields
3359
                $manymanyFields = array(
3360
                    $parentField => "Int",
3361
                    $childField => "Int",
3362
                );
3363
                if (isset($extras[$component])) {
3364
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3365
                }
3366
3367
                // Build index list
3368
                $manymanyIndexes = [
3369
                    $parentField => [
3370
                        'type' => 'index',
3371
                        'name' => $parentField,
3372
                        'columns' => [$parentField],
3373
                    ],
3374
                    $childField => [
3375
                        'type' => 'index',
3376
                        'name' => $childField,
3377
                        'columns' => [$childField],
3378
                    ],
3379
                ];
3380
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Bug introduced by
$manymanyFields of type string[]|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

3380
                DB::require_table($tableOrClass, /** @scrutinizer ignore-type */ $manymanyFields, $manymanyIndexes, true, null, $extensions);
Loading history...
Bug introduced by
$manymanyIndexes of type array<mixed,array<string,mixed|string|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

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