Passed
Push — fix-6460 ( 79e717...379c3e )
by Sam
06:06
created

DataObject::__construct()   D

Complexity

Conditions 16
Paths 180

Size

Total Lines 95
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 52
nc 180
nop 3
dl 0
loc 95
rs 4.9
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
     * Value for 2nd argument to constructor, indicating that a new record is being created
186
     * Setters will be called on fields passed, and defaults will be populated
187
     */
188
    const CREATE_OBJECT = 0;
189
190
    /**
191
     * Value for 2nd argument to constructor, indicating that a record is a singleton representing the whole type,
192
     * e.g. to call requireTable() in dev/build
193
     * Defaults will not be populated and data passed will be ignored
194
     */
195
    const CREATE_SINGLETON = 1;
196
197
    /**
198
     * Value for 2nd argument to constructor, indicating that a record is being hydrated from the database
199
     * Neither setters and nor default population will be called
200
     */
201
    const CREATE_HYDRATED = 2;
202
203
    /**
204
     * An array indexed by fieldname, true if the field has been changed.
205
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
206
     * the changed state.
207
     *
208
     * @var array
209
     */
210
    private $changed = [];
211
212
    /**
213
     * A flag to indicate that a "strict" change of the entire record been forced
214
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
215
     * the changed state.
216
     *
217
     * @var boolean
218
     */
219
    private $changeForced = false;
220
221
    /**
222
     * The database record (in the same format as $record), before
223
     * any changes.
224
     * @var array
225
     */
226
    protected $original = [];
227
228
    /**
229
     * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
230
     * @var boolean
231
     */
232
    protected $brokenOnDelete = false;
233
234
    /**
235
     * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
236
     * @var boolean
237
     */
238
    protected $brokenOnWrite = false;
239
240
    /**
241
     * Should dataobjects be validated before they are written?
242
     *
243
     * Caution: Validation can contain safeguards against invalid/malicious data,
244
     * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
245
     * to only disable validation for very specific use cases.
246
     *
247
     * @config
248
     * @var boolean
249
     */
250
    private static $validation_enabled = true;
251
252
    /**
253
     * Static caches used by relevant functions.
254
     *
255
     * @var array
256
     */
257
    protected static $_cache_get_one;
258
259
    /**
260
     * Cache of field labels
261
     *
262
     * @var array
263
     */
264
    protected static $_cache_field_labels = array();
265
266
    /**
267
     * Base fields which are not defined in static $db
268
     *
269
     * @config
270
     * @var array
271
     */
272
    private static $fixed_fields = array(
273
        'ID' => 'PrimaryKey',
274
        'ClassName' => 'DBClassName',
275
        'LastEdited' => 'DBDatetime',
276
        'Created' => 'DBDatetime',
277
    );
278
279
    /**
280
     * Override table name for this class. If ignored will default to FQN of class.
281
     * This option is not inheritable, and must be set on each class.
282
     * If left blank naming will default to the legacy (3.x) behaviour.
283
     *
284
     * @var string
285
     */
286
    private static $table_name = null;
287
288
    /**
289
     * Non-static relationship cache, indexed by component name.
290
     *
291
     * @var DataObject[]
292
     */
293
    protected $components = [];
294
295
    /**
296
     * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
297
     *
298
     * @var UnsavedRelationList[]
299
     */
300
    protected $unsavedRelations;
301
302
    /**
303
     * List of relations that should be cascade deleted, similar to `owns`
304
     * Note: This will trigger delete on many_many objects, not only the mapping table.
305
     * For many_many through you can specify the components you want to delete separately
306
     * (many_many or has_many sub-component)
307
     *
308
     * @config
309
     * @var array
310
     */
311
    private static $cascade_deletes = [];
312
313
    /**
314
     * List of relations that should be cascade duplicate.
315
     * many_many duplications are shallow only.
316
     *
317
     * Note: If duplicating a many_many through you should refer to the
318
     * has_many intermediary relation instead, otherwise extra fields
319
     * will be omitted from the duplicated relation.
320
     *
321
     * @var array
322
     */
323
    private static $cascade_duplicates = [];
324
325
    /**
326
     * Get schema object
327
     *
328
     * @return DataObjectSchema
329
     */
330
    public static function getSchema()
331
    {
332
        return Injector::inst()->get(DataObjectSchema::class);
333
    }
334
335
    /**
336
     * Construct a new DataObject.
337
     *
338
     * @param array $record Initial record content, or rehydrated record content, depending on $creationType
339
     * @param boolean $creationType Set to DataObject::CREATE_OBJECT, DataObject::CREATE_HYDRATED, or DataObject::CREATE_SINGLETON. Used by SilverStripe internals as best left as the default by regular users.
340
     * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
341
     */
342
    public function __construct($record = [], $creationType = self::CREATE_OBJECT, $queryParams = array())
343
    {
344
        parent::__construct();
345
346
        // Legacy $record default
347
        if ($record === null) {
0 ignored issues
show
introduced by
The condition $record === null is always false.
Loading history...
348
            $record = [];
349
        }
350
351
        // Legacy $isSingleton boolean
352
        if (is_bool($creationType)) {
353
            $creationType = $creationType ? self::CREATE_SINGLETON : self::CREATE_OBJECT;
354
        }
355
356
        // Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
357
        $this->setSourceQueryParams($queryParams);
358
359
        // Set $this->record to $record, but ignore NULLs
360
        $this->record = [];
361
362
        switch ($creationType) {
363
            // Hydrate a record from the database
364
            case self::CREATE_HYDRATED:
365
                if (!is_array($record) || empty($record['ID'])) {
0 ignored issues
show
introduced by
The condition is_array($record) is always true.
Loading history...
366
                    var_dump($record);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($record) looks like debug code. Are you sure you do not want to remove it?
Loading history...
367
                    throw new \InvalidArgumentException("Hydrated records must be passed a record array including an ID");
368
                }
369
370
                $this->record = $record;
371
372
                // Identify fields that should be lazy loaded, but only on existing records
373
                // Get all field specs scoped to class for later lazy loading
374
                $fields = static::getSchema()->fieldSpecs(
375
                    static::class,
376
                    DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
377
                );
378
                foreach ($fields as $field => $fieldSpec) {
379
                    $fieldClass = strtok($fieldSpec, ".");
380
                    if (!array_key_exists($field, $record)) {
381
                        $this->record[$field . '_Lazy'] = $fieldClass;
382
                    }
383
                }
384
385
                $this->original = $this->record;
386
                $this->changed = [];
387
                $this->changeForced = false;
388
389
                break;
390
391
            // Create a new object, using the constructor argument as the initial content
392
            case self::CREATE_OBJECT:
393
                if ($record instanceof stdClass) {
0 ignored issues
show
introduced by
$record is never a sub-type of stdClass.
Loading history...
394
                    $record = (array)$record;
395
                }
396
397
                if (!is_array($record)) {
0 ignored issues
show
introduced by
The condition is_array($record) is always true.
Loading history...
398
                    if (is_object($record)) {
399
                        $passed = "an object of type '" . get_class($record) . "'";
400
                    } else {
401
                        $passed = "The value '$record'";
402
                    }
403
404
                    user_error(
405
                        "DataObject::__construct passed $passed.  It's supposed to be passed an array,"
406
                        . " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
407
                        E_USER_WARNING
408
                    );
409
                    $record = [];
410
                }
411
412
                // Default columns
413
                $this->record['ID'] = empty($record['ID']) ? 0 : $record['ID'];
414
                $this->record['ClassName'] = $this->record['RecordClassName'] = static::class;
415
                unset($record['ID']);
416
                $this->original = $this->record;
417
418
                $this->populateDefaults();
419
420
                // prevent populateDefaults() and setField() from marking overwritten defaults as changed
421
                $this->changed = [];
422
                $this->changeForced = false;
423
424
                // Set the data passed in the constructor, allowing for defaults and calling setters
425
                // This will mark fields as changed
426
                if ($record) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
427
                    $this->update($record);
428
                }
429
                break;
430
431
            case self::CREATE_SINGLETON:
432
                // No setting happens for a singleton
433
                $this->original = $this->record;
434
                $this->changed = [];
435
                $this->changeForced = false;
436
                break;
437
        }
438
439
    }
440
441
    /**
442
     * Destroy all of this objects dependant objects and local caches.
443
     * You'll need to call this to get the memory of an object that has components or extensions freed.
444
     */
445
    public function destroy()
446
    {
447
        $this->flushCache(false);
448
    }
449
450
    /**
451
     * Create a duplicate of this node. Can duplicate many_many relations
452
     *
453
     * @param bool $doWrite Perform a write() operation before returning the object.
454
     * If this is true, it will create the duplicate in the database.
455
     * @param array|null|false $relations List of relations to duplicate.
456
     * Will default to `cascade_duplicates` if null.
457
     * Set to 'false' to force none.
458
     * Set to specific array of names to duplicate to override these.
459
     * Note: If using versioned, this will additionally failover to `owns` config.
460
     * @return static A duplicate of this node. The exact type will be the type of this node.
461
     */
462
    public function duplicate($doWrite = true, $relations = null)
463
    {
464
        // Handle legacy behaviour
465
        if (is_string($relations) || $relations === true) {
0 ignored issues
show
introduced by
The condition $relations === true is always false.
Loading history...
466
            if ($relations === true) {
467
                $relations = 'many_many';
468
            }
469
            Deprecation::notice('5.0', 'Use cascade_duplicates config instead of providing a string to duplicate()');
470
            $relations = array_keys($this->config()->get($relations)) ?: [];
471
        }
472
473
        // Get duplicates
474
        if ($relations === null) {
475
            $relations = $this->config()->get('cascade_duplicates');
476
            // Remove any duplicate entries before duplicating them
477
            if (is_array($relations)) {
478
                $relations = array_unique($relations);
479
            }
480
        }
481
482
        // Create unsaved raw duplicate
483
        $map = $this->toMap();
484
        unset($map['Created']);
485
        /** @var static $clone */
486
        $clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams());
487
        $clone->ID = 0;
488
489
        // Note: Extensions such as versioned may update $relations here
490
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $relations);
491
        if ($relations) {
492
            $this->duplicateRelations($this, $clone, $relations);
493
        }
494
        if ($doWrite) {
495
            $clone->write();
496
        }
497
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $relations);
498
499
        return $clone;
500
    }
501
502
    /**
503
     * Copies the given relations from this object to the destination
504
     *
505
     * @param DataObject $sourceObject the source object to duplicate from
506
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
507
     * @param array $relations List of relations
508
     */
509
    protected function duplicateRelations($sourceObject, $destinationObject, $relations)
510
    {
511
        // Get list of duplicable relation types
512
        $manyMany = $sourceObject->manyMany();
513
        $hasMany = $sourceObject->hasMany();
514
        $hasOne = $sourceObject->hasOne();
515
        $belongsTo = $sourceObject->belongsTo();
516
517
        // Duplicate each relation based on type
518
        foreach ($relations as $relation) {
519
            switch (true) {
520
                case array_key_exists($relation, $manyMany): {
521
                    $this->duplicateManyManyRelation($sourceObject, $destinationObject, $relation);
522
                    break;
523
                }
524
                case array_key_exists($relation, $hasMany): {
525
                    $this->duplicateHasManyRelation($sourceObject, $destinationObject, $relation);
526
                    break;
527
                }
528
                case array_key_exists($relation, $hasOne): {
529
                    $this->duplicateHasOneRelation($sourceObject, $destinationObject, $relation);
530
                    break;
531
                }
532
                case array_key_exists($relation, $belongsTo): {
533
                    $this->duplicateBelongsToRelation($sourceObject, $destinationObject, $relation);
534
                    break;
535
                }
536
                default: {
537
                    $sourceType = get_class($sourceObject);
538
                    throw new InvalidArgumentException(
539
                        "Cannot duplicate unknown relation {$relation} on parent type {$sourceType}"
540
                    );
541
                }
542
            }
543
        }
544
    }
545
546
    /**
547
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
548
     *
549
     * @deprecated 4.1.0:5.0.0 Use duplicateRelations() instead
550
     * @param DataObject $sourceObject the source object to duplicate from
551
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
552
     * @param bool|string $filter
553
     */
554
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
555
    {
556
        Deprecation::notice('5.0', 'Use duplicateRelations() instead');
557
558
        // Get list of relations to duplicate
559
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
560
            $relations = $sourceObject->config()->get($filter);
561
        } elseif ($filter === true) {
562
            $relations = $sourceObject->manyMany();
563
        } else {
564
            throw new InvalidArgumentException("Invalid many_many duplication filter");
565
        }
566
        foreach ($relations as $manyManyName => $type) {
567
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
568
        }
569
    }
570
571
    /**
572
     * Duplicates a single many_many relation from one object to another.
573
     *
574
     * @param DataObject $sourceObject
575
     * @param DataObject $destinationObject
576
     * @param string $relation
577
     */
578
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $relation)
579
    {
580
        // Copy all components from source to destination
581
        $source = $sourceObject->getManyManyComponents($relation);
582
        $dest = $destinationObject->getManyManyComponents($relation);
583
584
        if ($source instanceof ManyManyList) {
585
            $extraFieldNames = $source->getExtraFields();
586
        } else {
587
            $extraFieldNames = [];
588
        }
589
590
        foreach ($source as $item) {
591
            // Merge extra fields
592
            $extraFields = [];
593
            foreach ($extraFieldNames as $fieldName => $fieldType) {
594
                $extraFields[$fieldName] = $item->getField($fieldName);
595
            }
596
            $dest->add($item, $extraFields);
597
        }
598
    }
599
600
    /**
601
     * Duplicates a single many_many relation from one object to another.
602
     *
603
     * @param DataObject $sourceObject
604
     * @param DataObject $destinationObject
605
     * @param string $relation
606
     */
607
    protected function duplicateHasManyRelation($sourceObject, $destinationObject, $relation)
608
    {
609
        // Copy all components from source to destination
610
        $source = $sourceObject->getComponents($relation);
611
        $dest = $destinationObject->getComponents($relation);
612
613
        /** @var DataObject $item */
614
        foreach ($source as $item) {
615
            // Don't write on duplicate; Wait until ParentID is available later.
616
            // writeRelations() will eventually write these records when converting
617
            // from UnsavedRelationList
618
            $clonedItem = $item->duplicate(false);
619
            $dest->add($clonedItem);
620
        }
621
    }
622
623
    /**
624
     * Duplicates a single has_one relation from one object to another.
625
     * Note: Child object will be force written.
626
     *
627
     * @param DataObject $sourceObject
628
     * @param DataObject $destinationObject
629
     * @param string $relation
630
     */
631
    protected function duplicateHasOneRelation($sourceObject, $destinationObject, $relation)
632
    {
633
        // Check if original object exists
634
        $item = $sourceObject->getComponent($relation);
635
        if (!$item->isInDB()) {
636
            return;
637
        }
638
639
        $clonedItem = $item->duplicate(false);
640
        $destinationObject->setComponent($relation, $clonedItem);
641
    }
642
643
    /**
644
     * Duplicates a single belongs_to relation from one object to another.
645
     * Note: This will force a write on both parent / child objects.
646
     *
647
     * @param DataObject $sourceObject
648
     * @param DataObject $destinationObject
649
     * @param string $relation
650
     */
651
    protected function duplicateBelongsToRelation($sourceObject, $destinationObject, $relation)
652
    {
653
        // Check if original object exists
654
        $item = $sourceObject->getComponent($relation);
655
        if (!$item->isInDB()) {
656
            return;
657
        }
658
659
        $clonedItem = $item->duplicate(false);
660
        $destinationObject->setComponent($relation, $clonedItem);
661
        // After $clonedItem is assigned the appropriate FieldID / FieldClass, force write
662
        // @todo Write this component in onAfterWrite instead, assigning the FieldID then
663
        // https://github.com/silverstripe/silverstripe-framework/issues/7818
664
        $clonedItem->write();
665
    }
666
667
    /**
668
     * Return obsolete class name, if this is no longer a valid class
669
     *
670
     * @return string
671
     */
672
    public function getObsoleteClassName()
673
    {
674
        $className = $this->getField("ClassName");
675
        if (!ClassInfo::exists($className)) {
676
            return $className;
677
        }
678
        return null;
679
    }
680
681
    /**
682
     * Gets name of this class
683
     *
684
     * @return string
685
     */
686
    public function getClassName()
687
    {
688
        $className = $this->getField("ClassName");
689
        if (!ClassInfo::exists($className)) {
690
            return static::class;
691
        }
692
        return $className;
693
    }
694
695
    /**
696
     * Set the ClassName attribute. {@link $class} is also updated.
697
     * Warning: This will produce an inconsistent record, as the object
698
     * instance will not automatically switch to the new subclass.
699
     * Please use {@link newClassInstance()} for this purpose,
700
     * or destroy and reinstanciate the record.
701
     *
702
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
703
     * @return $this
704
     */
705
    public function setClassName($className)
706
    {
707
        $className = trim($className);
708
        if (!$className || !is_subclass_of($className, self::class)) {
709
            return $this;
710
        }
711
712
        $this->setField("ClassName", $className);
713
        $this->setField('RecordClassName', $className);
714
        return $this;
715
    }
716
717
    /**
718
     * Create a new instance of a different class from this object's record.
719
     * This is useful when dynamically changing the type of an instance. Specifically,
720
     * it ensures that the instance of the class is a match for the className of the
721
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
722
     * property manually before calling this method, as it will confuse change detection.
723
     *
724
     * If the new class is different to the original class, defaults are populated again
725
     * because this will only occur automatically on instantiation of a DataObject if
726
     * there is no record, or the record has no ID. In this case, we do have an ID but
727
     * we still need to repopulate the defaults.
728
     *
729
     * @param string $newClassName The name of the new class
730
     *
731
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
732
     */
733
    public function newClassInstance($newClassName)
734
    {
735
        if (!is_subclass_of($newClassName, self::class)) {
736
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
737
        }
738
739
        $originalClass = $this->ClassName;
740
741
        /** @var DataObject $newInstance */
742
        $newInstance = Injector::inst()->create($newClassName, $this->record, self::CREATE_HYDRATED);
743
744
        // Modify ClassName
745
        if ($newClassName != $originalClass) {
746
            $newInstance->setClassName($newClassName);
747
            $newInstance->populateDefaults();
748
            $newInstance->forceChange();
749
        }
750
751
        return $newInstance;
752
    }
753
754
    /**
755
     * Adds methods from the extensions.
756
     * Called by Object::__construct() once per class.
757
     */
758
    public function defineMethods()
759
    {
760
        parent::defineMethods();
761
762
        if (static::class === self::class) {
0 ignored issues
show
introduced by
The condition static::class === self::class is always true.
Loading history...
763
            return;
764
        }
765
766
        // Set up accessors for joined items
767
        if ($manyMany = $this->manyMany()) {
768
            foreach ($manyMany as $relationship => $class) {
769
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
770
            }
771
        }
772
        if ($hasMany = $this->hasMany()) {
773
            foreach ($hasMany as $relationship => $class) {
774
                $this->addWrapperMethod($relationship, 'getComponents');
775
            }
776
        }
777
        if ($hasOne = $this->hasOne()) {
778
            foreach ($hasOne as $relationship => $class) {
779
                $this->addWrapperMethod($relationship, 'getComponent');
780
            }
781
        }
782
        if ($belongsTo = $this->belongsTo()) {
783
            foreach (array_keys($belongsTo) as $relationship) {
784
                $this->addWrapperMethod($relationship, 'getComponent');
785
            }
786
        }
787
    }
788
789
    /**
790
     * Returns true if this object "exists", i.e., has a sensible value.
791
     * The default behaviour for a DataObject is to return true if
792
     * the object exists in the database, you can override this in subclasses.
793
     *
794
     * @return boolean true if this object exists
795
     */
796
    public function exists()
797
    {
798
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
799
    }
800
801
    /**
802
     * Returns TRUE if all values (other than "ID") are
803
     * considered empty (by weak boolean comparison).
804
     *
805
     * @return boolean
806
     */
807
    public function isEmpty()
808
    {
809
        $fixed = DataObject::config()->uninherited('fixed_fields');
810
        foreach ($this->toMap() as $field => $value) {
811
            // only look at custom fields
812
            if (isset($fixed[$field])) {
813
                continue;
814
            }
815
816
            $dbObject = $this->dbObject($field);
817
            if (!$dbObject) {
818
                continue;
819
            }
820
            if ($dbObject->exists()) {
821
                return false;
822
            }
823
        }
824
        return true;
825
    }
826
827
    /**
828
     * Pluralise this item given a specific count.
829
     *
830
     * E.g. "0 Pages", "1 File", "3 Images"
831
     *
832
     * @param string $count
833
     * @return string
834
     */
835
    public function i18n_pluralise($count)
836
    {
837
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
838
        return i18n::_t(
839
            static::class . '.PLURALS',
840
            $default,
841
            ['count' => $count]
842
        );
843
    }
844
845
    /**
846
     * Get the user friendly singular name of this DataObject.
847
     * If the name is not defined (by redefining $singular_name in the subclass),
848
     * this returns the class name.
849
     *
850
     * @return string User friendly singular name of this DataObject
851
     */
852
    public function singular_name()
853
    {
854
        $name = $this->config()->get('singular_name');
855
        if ($name) {
856
            return $name;
857
        }
858
        return ucwords(trim(strtolower(preg_replace(
859
            '/_?([A-Z])/',
860
            ' $1',
861
            ClassInfo::shortName($this)
862
        ))));
863
    }
864
865
    /**
866
     * Get the translated user friendly singular name of this DataObject
867
     * same as singular_name() but runs it through the translating function
868
     *
869
     * Translating string is in the form:
870
     *     $this->class.SINGULARNAME
871
     * Example:
872
     *     Page.SINGULARNAME
873
     *
874
     * @return string User friendly translated singular name of this DataObject
875
     */
876
    public function i18n_singular_name()
877
    {
878
        return _t(static::class . '.SINGULARNAME', $this->singular_name());
879
    }
880
881
    /**
882
     * Get the user friendly plural name of this DataObject
883
     * If the name is not defined (by renaming $plural_name in the subclass),
884
     * this returns a pluralised version of the class name.
885
     *
886
     * @return string User friendly plural name of this DataObject
887
     */
888
    public function plural_name()
889
    {
890
        if ($name = $this->config()->get('plural_name')) {
891
            return $name;
892
        }
893
        $name = $this->singular_name();
894
        //if the penultimate character is not a vowel, replace "y" with "ies"
895
        if (preg_match('/[^aeiou]y$/i', $name)) {
896
            $name = substr($name, 0, -1) . 'ie';
897
        }
898
        return ucfirst($name . 's');
899
    }
900
901
    /**
902
     * Get the translated user friendly plural name of this DataObject
903
     * Same as plural_name but runs it through the translation function
904
     * Translation string is in the form:
905
     *      $this->class.PLURALNAME
906
     * Example:
907
     *      Page.PLURALNAME
908
     *
909
     * @return string User friendly translated plural name of this DataObject
910
     */
911
    public function i18n_plural_name()
912
    {
913
        return _t(static::class . '.PLURALNAME', $this->plural_name());
914
    }
915
916
    /**
917
     * Standard implementation of a title/label for a specific
918
     * record. Tries to find properties 'Title' or 'Name',
919
     * and falls back to the 'ID'. Useful to provide
920
     * user-friendly identification of a record, e.g. in errormessages
921
     * or UI-selections.
922
     *
923
     * Overload this method to have a more specialized implementation,
924
     * e.g. for an Address record this could be:
925
     * <code>
926
     * function getTitle() {
927
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
928
     * }
929
     * </code>
930
     *
931
     * @return string
932
     */
933
    public function getTitle()
934
    {
935
        $schema = static::getSchema();
936
        if ($schema->fieldSpec($this, 'Title')) {
937
            return $this->getField('Title');
938
        }
939
        if ($schema->fieldSpec($this, 'Name')) {
940
            return $this->getField('Name');
941
        }
942
943
        return "#{$this->ID}";
944
    }
945
946
    /**
947
     * Returns the associated database record - in this case, the object itself.
948
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
949
     *
950
     * @return DataObject Associated database record
951
     */
952
    public function data()
953
    {
954
        return $this;
955
    }
956
957
    /**
958
     * Convert this object to a map.
959
     *
960
     * @return array The data as a map.
961
     */
962
    public function toMap()
963
    {
964
        $this->loadLazyFields();
965
        return $this->record;
966
    }
967
968
    /**
969
     * Return all currently fetched database fields.
970
     *
971
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
972
     * Obviously, this makes it a lot faster.
973
     *
974
     * @return array The data as a map.
975
     */
976
    public function getQueriedDatabaseFields()
977
    {
978
        return $this->record;
979
    }
980
981
    /**
982
     * Update a number of fields on this object, given a map of the desired changes.
983
     *
984
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
985
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
986
     *
987
     * update() doesn't write the main object, but if you use the dot syntax, it will write()
988
     * the related objects that it alters.
989
     *
990
     * @param array $data A map of field name to data values to update.
991
     * @return DataObject $this
992
     */
993
    public function update($data)
994
    {
995
        foreach ($data as $key => $value) {
996
            // Implement dot syntax for updates
997
            if (strpos($key, '.') !== false) {
998
                $relations = explode('.', $key);
999
                $fieldName = array_pop($relations);
1000
                /** @var static $relObj */
1001
                $relObj = $this;
1002
                $relation = null;
1003
                foreach ($relations as $i => $relation) {
1004
                    // no support for has_many or many_many relationships,
1005
                    // as the updater wouldn't know which object to write to (or create)
1006
                    if ($relObj->$relation() instanceof DataObject) {
1007
                        $parentObj = $relObj;
1008
                        $relObj = $relObj->$relation();
1009
                        // If the intermediate relationship objects haven't been created, then write them
1010
                        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...
1011
                            $relObj->write();
1012
                            $relatedFieldName = $relation . "ID";
1013
                            $parentObj->$relatedFieldName = $relObj->ID;
1014
                            $parentObj->write();
1015
                        }
1016
                    } else {
1017
                        user_error(
1018
                            "DataObject::update(): Can't traverse relationship '$relation'," .
1019
                            "it has to be a has_one relationship or return a single DataObject",
1020
                            E_USER_NOTICE
1021
                        );
1022
                        // unset relation object so we don't write properties to the wrong object
1023
                        $relObj = null;
1024
                        break;
1025
                    }
1026
                }
1027
1028
                if ($relObj) {
1029
                    $relObj->$fieldName = $value;
1030
                    $relObj->write();
1031
                    $relatedFieldName = $relation . "ID";
1032
                    $this->$relatedFieldName = $relObj->ID;
1033
                    $relObj->flushCache();
1034
                } else {
1035
                    $class = static::class;
1036
                    user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
1037
                }
1038
            } else {
1039
                $this->$key = $value;
1040
            }
1041
        }
1042
        return $this;
1043
    }
1044
1045
    /**
1046
     * Pass changes as a map, and try to
1047
     * get automatic casting for these fields.
1048
     * Doesn't write to the database. To write the data,
1049
     * use the write() method.
1050
     *
1051
     * @param array $data A map of field name to data values to update.
1052
     * @return DataObject $this
1053
     */
1054
    public function castedUpdate($data)
1055
    {
1056
        foreach ($data as $k => $v) {
1057
            $this->setCastedField($k, $v);
1058
        }
1059
        return $this;
1060
    }
1061
1062
    /**
1063
     * Merges data and relations from another object of same class,
1064
     * without conflict resolution. Allows to specify which
1065
     * dataset takes priority in case its not empty.
1066
     * has_one-relations are just transferred with priority 'right'.
1067
     * has_many and many_many-relations are added regardless of priority.
1068
     *
1069
     * Caution: has_many/many_many relations are moved rather than duplicated,
1070
     * meaning they are not connected to the merged object any longer.
1071
     * Caution: Just saves updated has_many/many_many relations to the database,
1072
     * doesn't write the updated object itself (just writes the object-properties).
1073
     * Caution: Does not delete the merged object.
1074
     * Caution: Does now overwrite Created date on the original object.
1075
     *
1076
     * @param DataObject $rightObj
1077
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
1078
     * @param bool $includeRelations Merge any existing relations (optional)
1079
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
1080
     *                            Only applicable with $priority='right'. (optional)
1081
     * @return Boolean
1082
     */
1083
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
1084
    {
1085
        $leftObj = $this;
1086
1087
        if ($leftObj->ClassName != $rightObj->ClassName) {
1088
            // we can't merge similiar subclasses because they might have additional relations
1089
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
1090
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
1091
            return false;
1092
        }
1093
1094
        if (!$rightObj->ID) {
1095
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
1096
				to make sure all relations are transferred properly.').", E_USER_WARNING);
1097
            return false;
1098
        }
1099
1100
        // makes sure we don't merge data like ID or ClassName
1101
        $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
1102
        foreach ($rightData as $key => $rightSpec) {
1103
            // Don't merge ID
1104
            if ($key === 'ID') {
1105
                continue;
1106
            }
1107
1108
            // Only merge relations if allowed
1109
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
1110
                continue;
1111
            }
1112
1113
            // don't merge conflicting values if priority is 'left'
1114
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
1115
                continue;
1116
            }
1117
1118
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
1119
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
1120
                continue;
1121
            }
1122
1123
            // TODO remove redundant merge of has_one fields
1124
            $leftObj->{$key} = $rightObj->{$key};
1125
        }
1126
1127
        // merge relations
1128
        if ($includeRelations) {
1129
            if ($manyMany = $this->manyMany()) {
1130
                foreach ($manyMany as $relationship => $class) {
1131
                    /** @var DataObject $leftComponents */
1132
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
1133
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
1134
                    if ($rightComponents && $rightComponents->exists()) {
1135
                        $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

1135
                        $leftComponents->/** @scrutinizer ignore-call */ 
1136
                                         addMany($rightComponents->column('ID'));
Loading history...
1136
                    }
1137
                    $leftComponents->write();
1138
                }
1139
            }
1140
1141
            if ($hasMany = $this->hasMany()) {
1142
                foreach ($hasMany as $relationship => $class) {
1143
                    $leftComponents = $leftObj->getComponents($relationship);
1144
                    $rightComponents = $rightObj->getComponents($relationship);
1145
                    if ($rightComponents && $rightComponents->exists()) {
1146
                        $leftComponents->addMany($rightComponents->column('ID'));
1147
                    }
1148
                    $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

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

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

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

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

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

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

3368
        /** @scrutinizer ignore-call */ 
3369
        $extensions = self::database_extensions(static::class);
Loading history...
3369
3370
        if (empty($table)) {
3371
            throw new LogicException(
3372
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3373
            );
3374
        }
3375
3376
        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...
3377
            $hasAutoIncPK = get_parent_class($this) === self::class;
3378
            DB::require_table(
3379
                $table,
3380
                $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

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

3381
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3382
                $hasAutoIncPK,
3383
                $this->config()->get('create_table_options'),
3384
                $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

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

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

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

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

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