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

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

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

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

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

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

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

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

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

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

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

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

3328
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3329
                $hasAutoIncPK,
3330
                $this->config()->get('create_table_options'),
3331
                $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

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

3374
                DB::require_table($tableOrClass, $manymanyFields, /** @scrutinizer ignore-type */ $manymanyIndexes, true, null, $extensions);
Loading history...
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

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