Passed
Push — master ( 17402d...0c4770 )
by Loz
11:26
created

src/ORM/DataObject.php (1 issue)

1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use BadMethodCallException;
6
use Exception;
7
use InvalidArgumentException;
8
use LogicException;
9
use SilverStripe\Control\HTTP;
10
use SilverStripe\Core\ClassInfo;
11
use SilverStripe\Core\Config\Config;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\Resettable;
14
use SilverStripe\Dev\Debug;
15
use SilverStripe\Dev\Deprecation;
16
use SilverStripe\Forms\FieldList;
17
use SilverStripe\Forms\FormField;
18
use SilverStripe\Forms\FormScaffolder;
19
use SilverStripe\i18n\i18n;
20
use SilverStripe\i18n\i18nEntityProvider;
21
use SilverStripe\ORM\Connect\MySQLSchemaManager;
22
use SilverStripe\ORM\FieldType\DBClassName;
23
use SilverStripe\ORM\FieldType\DBComposite;
24
use SilverStripe\ORM\FieldType\DBDatetime;
25
use SilverStripe\ORM\FieldType\DBField;
26
use SilverStripe\ORM\Filters\SearchFilter;
27
use SilverStripe\ORM\Queries\SQLDelete;
28
use SilverStripe\ORM\Queries\SQLInsert;
29
use SilverStripe\ORM\Search\SearchContext;
30
use SilverStripe\Security\Member;
31
use SilverStripe\Security\Permission;
32
use SilverStripe\Security\Security;
33
use SilverStripe\View\SSViewer;
34
use SilverStripe\View\ViewableData;
35
use stdClass;
36
37
/**
38
 * A single database record & abstract class for the data-access-model.
39
 *
40
 * <h2>Extensions</h2>
41
 *
42
 * See {@link Extension} and {@link DataExtension}.
43
 *
44
 * <h2>Permission Control</h2>
45
 *
46
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
47
 * strings which can be selected on a group-by-group basis.
48
 *
49
 * <code>
50
 * class Article extends DataObject implements PermissionProvider {
51
 *  static $api_access = true;
52
 *
53
 *  function canView($member = false) {
54
 *    return Permission::check('ARTICLE_VIEW');
55
 *  }
56
 *  function canEdit($member = false) {
57
 *    return Permission::check('ARTICLE_EDIT');
58
 *  }
59
 *  function canDelete() {
60
 *    return Permission::check('ARTICLE_DELETE');
61
 *  }
62
 *  function canCreate() {
63
 *    return Permission::check('ARTICLE_CREATE');
64
 *  }
65
 *  function providePermissions() {
66
 *    return array(
67
 *      'ARTICLE_VIEW' => 'Read an article object',
68
 *      'ARTICLE_EDIT' => 'Edit an article object',
69
 *      'ARTICLE_DELETE' => 'Delete an article object',
70
 *      'ARTICLE_CREATE' => 'Create an article object',
71
 *    );
72
 *  }
73
 * }
74
 * </code>
75
 *
76
 * Object-level access control by {@link Group} membership:
77
 * <code>
78
 * class Article extends DataObject {
79
 *   static $api_access = true;
80
 *
81
 *   function canView($member = false) {
82
 *     if(!$member) $member = Security::getCurrentUser();
83
 *     return $member->inGroup('Subscribers');
84
 *   }
85
 *   function canEdit($member = false) {
86
 *     if(!$member) $member = Security::getCurrentUser();
87
 *     return $member->inGroup('Editors');
88
 *   }
89
 *
90
 *   // ...
91
 * }
92
 * </code>
93
 *
94
 * If any public method on this class is prefixed with an underscore,
95
 * the results are cached in memory through {@link cachedCall()}.
96
 *
97
 *
98
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
99
 *  and defineMethods()
100
 *
101
 * @property int $ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
102
 * @property int $OldID ID of object, if deleted
103
 * @property string $Title
104
 * @property string $ClassName Class name of the DataObject
105
 * @property string $LastEdited Date and time of DataObject's last modification.
106
 * @property string $Created Date and time of DataObject creation.
107
 */
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..5.0
143
     * @var bool
144
     */
145
    public $destroyed = false;
146
147
    /**
148
     * Data stored in this objects database record. An array indexed by fieldname.
149
     *
150
     * Use {@link toMap()} if you want an array representation
151
     * of this object, as the $record array might contain lazy loaded field aliases.
152
     *
153
     * @var array
154
     */
155
    protected $record;
156
157
    /**
158
     * If selected through a many_many through relation, this is the instance of the through record
159
     *
160
     * @var DataObject
161
     */
162
    protected $joinRecord;
163
164
    /**
165
     * Represents a field that hasn't changed (before === after, thus before == after)
166
     */
167
    const CHANGE_NONE = 0;
168
169
    /**
170
     * Represents a field that has changed type, although not the loosely defined value.
171
     * (before !== after && before == after)
172
     * E.g. change 1 to true or "true" to true, but not true to 0.
173
     * Value changes are by nature also considered strict changes.
174
     */
175
    const CHANGE_STRICT = 1;
176
177
    /**
178
     * Represents a field that has changed the loosely defined value
179
     * (before != after, thus, before !== after))
180
     * E.g. change false to true, but not false to 0
181
     */
182
    const CHANGE_VALUE = 2;
183
184
    /**
185
     * An array indexed by fieldname, true if the field has been changed.
186
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
187
     * the changed state.
188
     *
189
     * @var array
190
     */
191
    private $changed;
192
193
    /**
194
     * The database record (in the same format as $record), before
195
     * any changes.
196
     * @var array
197
     */
198
    protected $original;
199
200
    /**
201
     * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
202
     * @var boolean
203
     */
204
    protected $brokenOnDelete = false;
205
206
    /**
207
     * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
208
     * @var boolean
209
     */
210
    protected $brokenOnWrite = false;
211
212
    /**
213
     * @config
214
     * @var boolean Should dataobjects be validated before they are written?
215
     * Caution: Validation can contain safeguards against invalid/malicious data,
216
     * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
217
     * to only disable validation for very specific use cases.
218
     */
219
    private static $validation_enabled = true;
220
221
    /**
222
     * Static caches used by relevant functions.
223
     *
224
     * @var array
225
     */
226
    protected static $_cache_get_one;
227
228
    /**
229
     * Cache of field labels
230
     *
231
     * @var array
232
     */
233
    protected static $_cache_field_labels = array();
234
235
    /**
236
     * Base fields which are not defined in static $db
237
     *
238
     * @config
239
     * @var array
240
     */
241
    private static $fixed_fields = array(
242
        'ID' => 'PrimaryKey',
243
        'ClassName' => 'DBClassName',
244
        'LastEdited' => 'DBDatetime',
245
        'Created' => 'DBDatetime',
246
    );
247
248
    /**
249
     * Override table name for this class. If ignored will default to FQN of class.
250
     * This option is not inheritable, and must be set on each class.
251
     * If left blank naming will default to the legacy (3.x) behaviour.
252
     *
253
     * @var string
254
     */
255
    private static $table_name = null;
256
257
    /**
258
     * Non-static relationship cache, indexed by component name.
259
     *
260
     * @var DataObject[]
261
     */
262
    protected $components = [];
263
264
    /**
265
     * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
266
     *
267
     * @var UnsavedRelationList[]
268
     */
269
    protected $unsavedRelations;
270
271
    /**
272
     * List of relations that should be cascade deleted, similar to `owns`
273
     * Note: This will trigger delete on many_many objects, not only the mapping table.
274
     * For many_many through you can specify the components you want to delete separately
275
     * (many_many or has_many sub-component)
276
     *
277
     * @config
278
     * @var array
279
     */
280
    private static $cascade_deletes = [];
281
282
    /**
283
     * List of relations that should be cascade duplicate.
284
     * many_many duplications are shallow only.
285
     *
286
     * Note: If duplicating a many_many through you should refer to the
287
     * has_many intermediary relation instead, otherwise extra fields
288
     * will be omitted from the duplicated relation.
289
     *
290
     * @var array
291
     */
292
    private static $cascade_duplicates = [];
293
294
    /**
295
     * Get schema object
296
     *
297
     * @return DataObjectSchema
298
     */
299
    public static function getSchema()
300
    {
301
        return Injector::inst()->get(DataObjectSchema::class);
302
    }
303
304
    /**
305
     * Construct a new DataObject.
306
     *
307
     * @param array|null $record Used internally for rehydrating an object from database content.
308
     *                           Bypasses setters on this class, and hence should not be used
309
     *                           for populating data on new records.
310
     * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
311
     *                             Singletons don't have their defaults set.
312
     * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
313
     */
314
    public function __construct($record = null, $isSingleton = false, $queryParams = array())
315
    {
316
        parent::__construct();
317
318
        // Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
319
        $this->setSourceQueryParams($queryParams);
320
321
        // Set the fields data.
322
        if (!$record) {
323
            $record = array(
324
                'ID' => 0,
325
                'ClassName' => static::class,
326
                'RecordClassName' => static::class
327
            );
328
        }
329
330
        if ($record instanceof stdClass) {
331
            $record = (array)$record;
332
        }
333
334
        if (!is_array($record)) {
335
            if (is_object($record)) {
336
                $passed = "an object of type '" . get_class($record) . "'";
337
            } else {
338
                $passed = "The value '$record'";
339
            }
340
341
            user_error(
342
                "DataObject::__construct passed $passed.  It's supposed to be passed an array,"
343
                . " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
344
                E_USER_WARNING
345
            );
346
            $record = null;
347
        }
348
349
        // Set $this->record to $record, but ignore NULLs
350
        $this->record = array();
351
        foreach ($record as $k => $v) {
352
            // Ensure that ID is stored as a number and not a string
353
            // To do: this kind of clean-up should be done on all numeric fields, in some relatively
354
            // performant manner
355
            if ($v !== null) {
356
                if ($k == 'ID' && is_numeric($v)) {
357
                    $this->record[$k] = (int)$v;
358
                } else {
359
                    $this->record[$k] = $v;
360
                }
361
            }
362
        }
363
364
        // Identify fields that should be lazy loaded, but only on existing records
365
        if (!empty($record['ID'])) {
366
            // Get all field specs scoped to class for later lazy loading
367
            $fields = static::getSchema()->fieldSpecs(
368
                static::class,
369
                DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
370
            );
371
            foreach ($fields as $field => $fieldSpec) {
372
                $fieldClass = strtok($fieldSpec, ".");
373
                if (!array_key_exists($field, $record)) {
374
                    $this->record[$field . '_Lazy'] = $fieldClass;
375
                }
376
            }
377
        }
378
379
        $this->original = $this->record;
380
381
        // Keep track of the modification date of all the data sourced to make this page
382
        // From this we create a Last-Modified HTTP header
383
        if (isset($record['LastEdited'])) {
384
            HTTP::register_modification_date($record['LastEdited']);
385
        }
386
387
        // Must be called after parent constructor
388
        if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
389
            $this->populateDefaults();
390
        }
391
392
        // prevent populateDefaults() and setField() from marking overwritten defaults as changed
393
        $this->changed = array();
394
    }
395
396
    /**
397
     * Destroy all of this objects dependant objects and local caches.
398
     * You'll need to call this to get the memory of an object that has components or extensions freed.
399
     */
400
    public function destroy()
401
    {
402
        $this->flushCache(false);
403
    }
404
405
    /**
406
     * Create a duplicate of this node. Can duplicate many_many relations
407
     *
408
     * @param bool $doWrite Perform a write() operation before returning the object.
409
     * If this is true, it will create the duplicate in the database.
410
     * @param array|null|false $relations List of relations to duplicate.
411
     * Will default to `cascade_duplicates` if null.
412
     * Set to 'false' to force none.
413
     * Set to specific array of names to duplicate to override these.
414
     * Note: If using versioned, this will additionally failover to `owns` config.
415
     * @return static A duplicate of this node. The exact type will be the type of this node.
416
     */
417
    public function duplicate($doWrite = true, $relations = null)
418
    {
419
        // Handle legacy behaviour
420
        if (is_string($relations) || $relations === true) {
421
            if ($relations === true) {
422
                $relations = 'many_many';
423
            }
424
            Deprecation::notice('5.0', 'Use cascade_duplicates config instead of providing a string to duplicate()');
425
            $relations = array_keys($this->config()->get($relations)) ?: [];
426
        }
427
428
        // Get duplicates
429
        if ($relations === null) {
430
            $relations = $this->config()->get('cascade_duplicates');
431
        }
432
433
        // Create unsaved raw duplicate
434
        $map = $this->toMap();
435
        unset($map['Created']);
436
        /** @var static $clone */
437
        $clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams());
438
        $clone->ID = 0;
439
440
        // Note: Extensions such as versioned may update $relations here
441
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $relations);
442
        if ($relations) {
443
            $this->duplicateRelations($this, $clone, $relations);
444
        }
445
        if ($doWrite) {
446
            $clone->write();
447
        }
448
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $relations);
449
450
        return $clone;
451
    }
452
453
    /**
454
     * Copies the given relations from this object to the destination
455
     *
456
     * @param DataObject $sourceObject the source object to duplicate from
457
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
458
     * @param array $relations List of relations
459
     */
460
    protected function duplicateRelations($sourceObject, $destinationObject, $relations)
461
    {
462
        // Get list of duplicable relation types
463
        $manyMany = $sourceObject->manyMany();
464
        $hasMany = $sourceObject->hasMany();
465
        $hasOne = $sourceObject->hasOne();
466
        $belongsTo = $sourceObject->belongsTo();
467
468
        // Duplicate each relation based on type
469
        foreach ($relations as $relation) {
470
            switch (true) {
471
                case array_key_exists($relation, $manyMany): {
472
                    $this->duplicateManyManyRelation($sourceObject, $destinationObject, $relation);
473
                    break;
474
                }
475
                case array_key_exists($relation, $hasMany): {
476
                    $this->duplicateHasManyRelation($sourceObject, $destinationObject, $relation);
477
                    break;
478
                }
479
                case array_key_exists($relation, $hasOne): {
480
                    $this->duplicateHasOneRelation($sourceObject, $destinationObject, $relation);
481
                    break;
482
                }
483
                case array_key_exists($relation, $belongsTo): {
484
                    $this->duplicateBelongsToRelation($sourceObject, $destinationObject, $relation);
485
                    break;
486
                }
487
                default: {
488
                    $sourceType = get_class($sourceObject);
489
                    throw new InvalidArgumentException(
490
                        "Cannot duplicate unknown relation {$relation} on parent type {$sourceType}"
491
                    );
492
                }
493
            }
494
        }
495
    }
496
497
    /**
498
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
499
     *
500
     * @deprecated 4.1...5.0 Use duplicateRelations() instead
501
     * @param DataObject $sourceObject the source object to duplicate from
502
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
503
     * @param bool|string $filter
504
     */
505
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
506
    {
507
        Deprecation::notice('5.0', 'Use duplicateRelations() instead');
508
509
        // Get list of relations to duplicate
510
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
511
            $relations = $sourceObject->config()->get($filter);
512
        } elseif ($filter === true) {
513
            $relations = $sourceObject->manyMany();
514
        } else {
515
            throw new InvalidArgumentException("Invalid many_many duplication filter");
516
        }
517
        foreach ($relations as $manyManyName => $type) {
518
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
519
        }
520
    }
521
522
    /**
523
     * Duplicates a single many_many relation from one object to another.
524
     *
525
     * @param DataObject $sourceObject
526
     * @param DataObject $destinationObject
527
     * @param string $relation
528
     */
529
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $relation)
530
    {
531
        // Copy all components from source to destination
532
        $source = $sourceObject->getManyManyComponents($relation);
533
        $dest = $destinationObject->getManyManyComponents($relation);
534
        $extraFieldNames = $source->getExtraFields();
535
        foreach ($source as $item) {
536
            // Merge extra fields
537
            $extraFields = [];
538
            foreach ($extraFieldNames as $fieldName => $fieldType) {
539
                $extraFields[$fieldName] = $item->getField($fieldName);
540
            }
541
            $dest->add($item, $extraFields);
542
        }
543
    }
544
545
    /**
546
     * Duplicates a single many_many relation from one object to another.
547
     *
548
     * @param DataObject $sourceObject
549
     * @param DataObject $destinationObject
550
     * @param string $relation
551
     */
552
    protected function duplicateHasManyRelation($sourceObject, $destinationObject, $relation)
553
    {
554
        // Copy all components from source to destination
555
        $source = $sourceObject->getComponents($relation);
556
        $dest = $destinationObject->getComponents($relation);
557
558
        /** @var DataObject $item */
559
        foreach ($source as $item) {
560
            // Don't write on duplicate; Wait until ParentID is available later.
561
            // writeRelations() will eventually write these records when converting
562
            // from UnsavedRelationList
563
            $clonedItem = $item->duplicate(false);
564
            $dest->add($clonedItem);
565
        }
566
    }
567
568
    /**
569
     * Duplicates a single has_one relation from one object to another.
570
     * Note: Child object will be force written.
571
     *
572
     * @param DataObject $sourceObject
573
     * @param DataObject $destinationObject
574
     * @param string $relation
575
     */
576
    protected function duplicateHasOneRelation($sourceObject, $destinationObject, $relation)
577
    {
578
        // Check if original object exists
579
        $item = $sourceObject->getComponent($relation);
580
        if (!$item->isInDB()) {
581
            return;
582
        }
583
584
        $clonedItem = $item->duplicate(false);
585
        $destinationObject->setComponent($relation, $clonedItem);
586
    }
587
588
    /**
589
     * Duplicates a single belongs_to relation from one object to another.
590
     * Note: This will force a write on both parent / child objects.
591
     *
592
     * @param DataObject $sourceObject
593
     * @param DataObject $destinationObject
594
     * @param string $relation
595
     */
596
    protected function duplicateBelongsToRelation($sourceObject, $destinationObject, $relation)
597
    {
598
        // Check if original object exists
599
        $item = $sourceObject->getComponent($relation);
600
        if (!$item->isInDB()) {
601
            return;
602
        }
603
604
        $clonedItem = $item->duplicate(false);
605
        $destinationObject->setComponent($relation, $clonedItem);
606
        // After $clonedItem is assigned the appropriate FieldID / FieldClass, force write
607
        // @todo Write this component in onAfterWrite instead, assigning the FieldID then
608
        // https://github.com/silverstripe/silverstripe-framework/issues/7818
609
        $clonedItem->write();
610
    }
611
612
    /**
613
     * Return obsolete class name, if this is no longer a valid class
614
     *
615
     * @return string
616
     */
617
    public function getObsoleteClassName()
618
    {
619
        $className = $this->getField("ClassName");
620
        if (!ClassInfo::exists($className)) {
621
            return $className;
622
        }
623
        return null;
624
    }
625
626
    /**
627
     * Gets name of this class
628
     *
629
     * @return string
630
     */
631
    public function getClassName()
632
    {
633
        $className = $this->getField("ClassName");
634
        if (!ClassInfo::exists($className)) {
635
            return static::class;
636
        }
637
        return $className;
638
    }
639
640
    /**
641
     * Set the ClassName attribute. {@link $class} is also updated.
642
     * Warning: This will produce an inconsistent record, as the object
643
     * instance will not automatically switch to the new subclass.
644
     * Please use {@link newClassInstance()} for this purpose,
645
     * or destroy and reinstanciate the record.
646
     *
647
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
648
     * @return $this
649
     */
650
    public function setClassName($className)
651
    {
652
        $className = trim($className);
653
        if (!$className || !is_subclass_of($className, self::class)) {
654
            return $this;
655
        }
656
657
        $this->setField("ClassName", $className);
658
        $this->setField('RecordClassName', $className);
659
        return $this;
660
    }
661
662
    /**
663
     * Create a new instance of a different class from this object's record.
664
     * This is useful when dynamically changing the type of an instance. Specifically,
665
     * it ensures that the instance of the class is a match for the className of the
666
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
667
     * property manually before calling this method, as it will confuse change detection.
668
     *
669
     * If the new class is different to the original class, defaults are populated again
670
     * because this will only occur automatically on instantiation of a DataObject if
671
     * there is no record, or the record has no ID. In this case, we do have an ID but
672
     * we still need to repopulate the defaults.
673
     *
674
     * @param string $newClassName The name of the new class
675
     *
676
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
677
     */
678
    public function newClassInstance($newClassName)
679
    {
680
        if (!is_subclass_of($newClassName, self::class)) {
681
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
682
        }
683
684
        $originalClass = $this->ClassName;
685
686
        /** @var DataObject $newInstance */
687
        $newInstance = Injector::inst()->create($newClassName, $this->record, false);
688
689
        // Modify ClassName
690
        if ($newClassName != $originalClass) {
691
            $newInstance->setClassName($newClassName);
692
            $newInstance->populateDefaults();
693
            $newInstance->forceChange();
694
        }
695
696
        return $newInstance;
697
    }
698
699
    /**
700
     * Adds methods from the extensions.
701
     * Called by Object::__construct() once per class.
702
     */
703
    public function defineMethods()
704
    {
705
        parent::defineMethods();
706
707
        if (static::class === self::class) {
708
            return;
709
        }
710
711
        // Set up accessors for joined items
712
        if ($manyMany = $this->manyMany()) {
713
            foreach ($manyMany as $relationship => $class) {
714
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
715
            }
716
        }
717
        if ($hasMany = $this->hasMany()) {
718
            foreach ($hasMany as $relationship => $class) {
719
                $this->addWrapperMethod($relationship, 'getComponents');
720
            }
721
        }
722
        if ($hasOne = $this->hasOne()) {
723
            foreach ($hasOne as $relationship => $class) {
724
                $this->addWrapperMethod($relationship, 'getComponent');
725
            }
726
        }
727
        if ($belongsTo = $this->belongsTo()) {
728
            foreach (array_keys($belongsTo) as $relationship) {
729
                $this->addWrapperMethod($relationship, 'getComponent');
730
            }
731
        }
732
    }
733
734
    /**
735
     * Returns true if this object "exists", i.e., has a sensible value.
736
     * The default behaviour for a DataObject is to return true if
737
     * the object exists in the database, you can override this in subclasses.
738
     *
739
     * @return boolean true if this object exists
740
     */
741
    public function exists()
742
    {
743
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
744
    }
745
746
    /**
747
     * Returns TRUE if all values (other than "ID") are
748
     * considered empty (by weak boolean comparison).
749
     *
750
     * @return boolean
751
     */
752
    public function isEmpty()
753
    {
754
        $fixed = DataObject::config()->uninherited('fixed_fields');
755
        foreach ($this->toMap() as $field => $value) {
756
            // only look at custom fields
757
            if (isset($fixed[$field])) {
758
                continue;
759
            }
760
761
            $dbObject = $this->dbObject($field);
762
            if (!$dbObject) {
763
                continue;
764
            }
765
            if ($dbObject->exists()) {
766
                return false;
767
            }
768
        }
769
        return true;
770
    }
771
772
    /**
773
     * Pluralise this item given a specific count.
774
     *
775
     * E.g. "0 Pages", "1 File", "3 Images"
776
     *
777
     * @param string $count
778
     * @return string
779
     */
780
    public function i18n_pluralise($count)
781
    {
782
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
783
        return i18n::_t(
784
            static::class . '.PLURALS',
785
            $default,
786
            ['count' => $count]
787
        );
788
    }
789
790
    /**
791
     * Get the user friendly singular name of this DataObject.
792
     * If the name is not defined (by redefining $singular_name in the subclass),
793
     * this returns the class name.
794
     *
795
     * @return string User friendly singular name of this DataObject
796
     */
797
    public function singular_name()
798
    {
799
        $name = $this->config()->get('singular_name');
800
        if ($name) {
801
            return $name;
802
        }
803
        return ucwords(trim(strtolower(preg_replace(
804
            '/_?([A-Z])/',
805
            ' $1',
806
            ClassInfo::shortName($this)
807
        ))));
808
    }
809
810
    /**
811
     * Get the translated user friendly singular name of this DataObject
812
     * same as singular_name() but runs it through the translating function
813
     *
814
     * Translating string is in the form:
815
     *     $this->class.SINGULARNAME
816
     * Example:
817
     *     Page.SINGULARNAME
818
     *
819
     * @return string User friendly translated singular name of this DataObject
820
     */
821
    public function i18n_singular_name()
822
    {
823
        return _t(static::class . '.SINGULARNAME', $this->singular_name());
824
    }
825
826
    /**
827
     * Get the user friendly plural name of this DataObject
828
     * If the name is not defined (by renaming $plural_name in the subclass),
829
     * this returns a pluralised version of the class name.
830
     *
831
     * @return string User friendly plural name of this DataObject
832
     */
833
    public function plural_name()
834
    {
835
        if ($name = $this->config()->get('plural_name')) {
836
            return $name;
837
        }
838
        $name = $this->singular_name();
839
        //if the penultimate character is not a vowel, replace "y" with "ies"
840
        if (preg_match('/[^aeiou]y$/i', $name)) {
841
            $name = substr($name, 0, -1) . 'ie';
842
        }
843
        return ucfirst($name . 's');
844
    }
845
846
    /**
847
     * Get the translated user friendly plural name of this DataObject
848
     * Same as plural_name but runs it through the translation function
849
     * Translation string is in the form:
850
     *      $this->class.PLURALNAME
851
     * Example:
852
     *      Page.PLURALNAME
853
     *
854
     * @return string User friendly translated plural name of this DataObject
855
     */
856
    public function i18n_plural_name()
857
    {
858
        return _t(static::class . '.PLURALNAME', $this->plural_name());
859
    }
860
861
    /**
862
     * Standard implementation of a title/label for a specific
863
     * record. Tries to find properties 'Title' or 'Name',
864
     * and falls back to the 'ID'. Useful to provide
865
     * user-friendly identification of a record, e.g. in errormessages
866
     * or UI-selections.
867
     *
868
     * Overload this method to have a more specialized implementation,
869
     * e.g. for an Address record this could be:
870
     * <code>
871
     * function getTitle() {
872
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
873
     * }
874
     * </code>
875
     *
876
     * @return string
877
     */
878
    public function getTitle()
879
    {
880
        $schema = static::getSchema();
881
        if ($schema->fieldSpec($this, 'Title')) {
882
            return $this->getField('Title');
883
        }
884
        if ($schema->fieldSpec($this, 'Name')) {
885
            return $this->getField('Name');
886
        }
887
888
        return "#{$this->ID}";
889
    }
890
891
    /**
892
     * Returns the associated database record - in this case, the object itself.
893
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
894
     *
895
     * @return DataObject Associated database record
896
     */
897
    public function data()
898
    {
899
        return $this;
900
    }
901
902
    /**
903
     * Convert this object to a map.
904
     *
905
     * @return array The data as a map.
906
     */
907
    public function toMap()
908
    {
909
        $this->loadLazyFields();
910
        return $this->record;
911
    }
912
913
    /**
914
     * Return all currently fetched database fields.
915
     *
916
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
917
     * Obviously, this makes it a lot faster.
918
     *
919
     * @return array The data as a map.
920
     */
921
    public function getQueriedDatabaseFields()
922
    {
923
        return $this->record;
924
    }
925
926
    /**
927
     * Update a number of fields on this object, given a map of the desired changes.
928
     *
929
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
930
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
931
     *
932
     * update() doesn't write the main object, but if you use the dot syntax, it will write()
933
     * the related objects that it alters.
934
     *
935
     * @param array $data A map of field name to data values to update.
936
     * @return DataObject $this
937
     */
938
    public function update($data)
939
    {
940
        foreach ($data as $key => $value) {
941
            // Implement dot syntax for updates
942
            if (strpos($key, '.') !== false) {
943
                $relations = explode('.', $key);
944
                $fieldName = array_pop($relations);
945
                /** @var static $relObj */
946
                $relObj = $this;
947
                $relation = null;
948
                foreach ($relations as $i => $relation) {
949
                    // no support for has_many or many_many relationships,
950
                    // as the updater wouldn't know which object to write to (or create)
951
                    if ($relObj->$relation() instanceof DataObject) {
952
                        $parentObj = $relObj;
953
                        $relObj = $relObj->$relation();
954
                        // If the intermediate relationship objects haven't been created, then write them
955
                        if ($i < sizeof($relations) - 1 && !$relObj->ID || (!$relObj->ID && $parentObj !== $this)) {
956
                            $relObj->write();
957
                            $relatedFieldName = $relation . "ID";
958
                            $parentObj->$relatedFieldName = $relObj->ID;
959
                            $parentObj->write();
960
                        }
961
                    } else {
962
                        user_error(
963
                            "DataObject::update(): Can't traverse relationship '$relation'," .
964
                            "it has to be a has_one relationship or return a single DataObject",
965
                            E_USER_NOTICE
966
                        );
967
                        // unset relation object so we don't write properties to the wrong object
968
                        $relObj = null;
969
                        break;
970
                    }
971
                }
972
973
                if ($relObj) {
974
                    $relObj->$fieldName = $value;
975
                    $relObj->write();
976
                    $relatedFieldName = $relation . "ID";
977
                    $this->$relatedFieldName = $relObj->ID;
978
                    $relObj->flushCache();
979
                } else {
980
                    $class = static::class;
981
                    user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
982
                }
983
            } else {
984
                $this->$key = $value;
985
            }
986
        }
987
        return $this;
988
    }
989
990
    /**
991
     * Pass changes as a map, and try to
992
     * get automatic casting for these fields.
993
     * Doesn't write to the database. To write the data,
994
     * use the write() method.
995
     *
996
     * @param array $data A map of field name to data values to update.
997
     * @return DataObject $this
998
     */
999
    public function castedUpdate($data)
1000
    {
1001
        foreach ($data as $k => $v) {
1002
            $this->setCastedField($k, $v);
1003
        }
1004
        return $this;
1005
    }
1006
1007
    /**
1008
     * Merges data and relations from another object of same class,
1009
     * without conflict resolution. Allows to specify which
1010
     * dataset takes priority in case its not empty.
1011
     * has_one-relations are just transferred with priority 'right'.
1012
     * has_many and many_many-relations are added regardless of priority.
1013
     *
1014
     * Caution: has_many/many_many relations are moved rather than duplicated,
1015
     * meaning they are not connected to the merged object any longer.
1016
     * Caution: Just saves updated has_many/many_many relations to the database,
1017
     * doesn't write the updated object itself (just writes the object-properties).
1018
     * Caution: Does not delete the merged object.
1019
     * Caution: Does now overwrite Created date on the original object.
1020
     *
1021
     * @param DataObject $rightObj
1022
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
1023
     * @param bool $includeRelations Merge any existing relations (optional)
1024
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
1025
     *                            Only applicable with $priority='right'. (optional)
1026
     * @return Boolean
1027
     */
1028
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
1029
    {
1030
        $leftObj = $this;
1031
1032
        if ($leftObj->ClassName != $rightObj->ClassName) {
1033
            // we can't merge similiar subclasses because they might have additional relations
1034
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
1035
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
1036
            return false;
1037
        }
1038
1039
        if (!$rightObj->ID) {
1040
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
1041
				to make sure all relations are transferred properly.').", E_USER_WARNING);
1042
            return false;
1043
        }
1044
1045
        // makes sure we don't merge data like ID or ClassName
1046
        $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
1047
        foreach ($rightData as $key => $rightSpec) {
1048
            // Don't merge ID
1049
            if ($key === 'ID') {
1050
                continue;
1051
            }
1052
1053
            // Only merge relations if allowed
1054
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
1055
                continue;
1056
            }
1057
1058
            // don't merge conflicting values if priority is 'left'
1059
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
1060
                continue;
1061
            }
1062
1063
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
1064
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
1065
                continue;
1066
            }
1067
1068
            // TODO remove redundant merge of has_one fields
1069
            $leftObj->{$key} = $rightObj->{$key};
1070
        }
1071
1072
        // merge relations
1073
        if ($includeRelations) {
1074
            if ($manyMany = $this->manyMany()) {
1075
                foreach ($manyMany as $relationship => $class) {
1076
                    /** @var DataObject $leftComponents */
1077
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
1078
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
1079
                    if ($rightComponents && $rightComponents->exists()) {
1080
                        $leftComponents->addMany($rightComponents->column('ID'));
1081
                    }
1082
                    $leftComponents->write();
1083
                }
1084
            }
1085
1086
            if ($hasMany = $this->hasMany()) {
1087
                foreach ($hasMany as $relationship => $class) {
1088
                    $leftComponents = $leftObj->getComponents($relationship);
1089
                    $rightComponents = $rightObj->getComponents($relationship);
1090
                    if ($rightComponents && $rightComponents->exists()) {
1091
                        $leftComponents->addMany($rightComponents->column('ID'));
1092
                    }
1093
                    $leftComponents->write();
1094
                }
1095
            }
1096
        }
1097
1098
        return true;
1099
    }
1100
1101
    /**
1102
     * Forces the record to think that all its data has changed.
1103
     * Doesn't write to the database. Only sets fields as changed
1104
     * if they are not already marked as changed.
1105
     *
1106
     * @return $this
1107
     */
1108
    public function forceChange()
1109
    {
1110
        // Ensure lazy fields loaded
1111
        $this->loadLazyFields();
1112
        $fields = static::getSchema()->fieldSpecs(static::class);
1113
1114
        // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
1115
        $fieldNames = array_unique(array_merge(
1116
            array_keys($this->record),
1117
            array_keys($fields)
1118
        ));
1119
1120
        foreach ($fieldNames as $fieldName) {
1121
            if (!isset($this->changed[$fieldName])) {
1122
                $this->changed[$fieldName] = self::CHANGE_STRICT;
1123
            }
1124
            // Populate the null values in record so that they actually get written
1125
            if (!isset($this->record[$fieldName])) {
1126
                $this->record[$fieldName] = null;
1127
            }
1128
        }
1129
1130
        // @todo Find better way to allow versioned to write a new version after forceChange
1131
        if ($this->isChanged('Version')) {
1132
            unset($this->changed['Version']);
1133
        }
1134
        return $this;
1135
    }
1136
1137
    /**
1138
     * Validate the current object.
1139
     *
1140
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1141
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1142
     *
1143
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1144
     * and onAfterWrite() won't get called either.
1145
     *
1146
     * It is expected that you call validate() in your own application to test that an object is valid before
1147
     * attempting a write, and respond appropriately if it isn't.
1148
     *
1149
     * @see {@link ValidationResult}
1150
     * @return ValidationResult
1151
     */
1152
    public function validate()
1153
    {
1154
        $result = ValidationResult::create();
1155
        $this->extend('validate', $result);
1156
        return $result;
1157
    }
1158
1159
    /**
1160
     * Public accessor for {@see DataObject::validate()}
1161
     *
1162
     * @return ValidationResult
1163
     */
1164
    public function doValidate()
1165
    {
1166
        Deprecation::notice('5.0', 'Use validate');
1167
        return $this->validate();
1168
    }
1169
1170
    /**
1171
     * Event handler called before writing to the database.
1172
     * You can overload this to clean up or otherwise process data before writing it to the
1173
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1174
     *
1175
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1176
     *
1177
     * @uses DataExtension->onBeforeWrite()
1178
     */
1179
    protected function onBeforeWrite()
1180
    {
1181
        $this->brokenOnWrite = false;
1182
1183
        $dummy = null;
1184
        $this->extend('onBeforeWrite', $dummy);
1185
    }
1186
1187
    /**
1188
     * Event handler called after writing to the database.
1189
     * You can overload this to act upon changes made to the data after it is written.
1190
     * $this->changed will have a record
1191
     * database.  Don't forget to call parent::onAfterWrite(), though!
1192
     *
1193
     * @uses DataExtension->onAfterWrite()
1194
     */
1195
    protected function onAfterWrite()
1196
    {
1197
        $dummy = null;
1198
        $this->extend('onAfterWrite', $dummy);
1199
    }
1200
1201
    /**
1202
     * Find all objects that will be cascade deleted if this object is deleted
1203
     *
1204
     * Notes:
1205
     *   - If this object is versioned, objects will only be searched in the same stage as the given record.
1206
     *   - This will only be useful prior to deletion, as post-deletion this record will no longer exist.
1207
     *
1208
     * @param bool $recursive True if recursive
1209
     * @param ArrayList $list Optional list to add items to
1210
     * @return ArrayList list of objects
1211
     */
1212
    public function findCascadeDeletes($recursive = true, $list = null)
1213
    {
1214
        // Find objects in these relationships
1215
        return $this->findRelatedObjects('cascade_deletes', $recursive, $list);
1216
    }
1217
1218
    /**
1219
     * Event handler called before deleting from the database.
1220
     * You can overload this to clean up or otherwise process data before delete this
1221
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1222
     *
1223
     * @uses DataExtension->onBeforeDelete()
1224
     */
1225
    protected function onBeforeDelete()
1226
    {
1227
        $this->brokenOnDelete = false;
1228
1229
        $dummy = null;
1230
        $this->extend('onBeforeDelete', $dummy);
1231
1232
        // Cascade deletes
1233
        $deletes = $this->findCascadeDeletes(false);
1234
        foreach ($deletes as $delete) {
1235
            $delete->delete();
1236
        }
1237
    }
1238
1239
    protected function onAfterDelete()
1240
    {
1241
        $this->extend('onAfterDelete');
1242
    }
1243
1244
    /**
1245
     * Load the default values in from the self::$defaults array.
1246
     * Will traverse the defaults of the current class and all its parent classes.
1247
     * Called by the constructor when creating new records.
1248
     *
1249
     * @uses DataExtension->populateDefaults()
1250
     * @return DataObject $this
1251
     */
1252
    public function populateDefaults()
1253
    {
1254
        $classes = array_reverse(ClassInfo::ancestry($this));
1255
1256
        foreach ($classes as $class) {
1257
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1258
1259
            if ($defaults && !is_array($defaults)) {
1260
                user_error(
1261
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1262
                    E_USER_WARNING
1263
                );
1264
                $defaults = null;
1265
            }
1266
1267
            if ($defaults) {
1268
                foreach ($defaults as $fieldName => $fieldValue) {
1269
                    // SRM 2007-03-06: Stricter check
1270
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1271
                        $this->$fieldName = $fieldValue;
1272
                    }
1273
                    // Set many-many defaults with an array of ids
1274
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1275
                        /** @var ManyManyList $manyManyJoin */
1276
                        $manyManyJoin = $this->$fieldName();
1277
                        $manyManyJoin->setByIDList($fieldValue);
1278
                    }
1279
                }
1280
            }
1281
            if ($class == self::class) {
1282
                break;
1283
            }
1284
        }
1285
1286
        $this->extend('populateDefaults');
1287
        return $this;
1288
    }
1289
1290
    /**
1291
     * Determine validation of this object prior to write
1292
     *
1293
     * @return ValidationException Exception generated by this write, or null if valid
1294
     */
1295
    protected function validateWrite()
1296
    {
1297
        if ($this->ObsoleteClassName) {
1298
            return new ValidationException(
1299
                "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - " .
1300
                "you need to change the ClassName before you can write it"
1301
            );
1302
        }
1303
1304
        // Note: Validation can only be disabled at the global level, not per-model
1305
        if (DataObject::config()->uninherited('validation_enabled')) {
1306
            $result = $this->validate();
1307
            if (!$result->isValid()) {
1308
                return new ValidationException($result);
1309
            }
1310
        }
1311
        return null;
1312
    }
1313
1314
    /**
1315
     * Prepare an object prior to write
1316
     *
1317
     * @throws ValidationException
1318
     */
1319
    protected function preWrite()
1320
    {
1321
        // Validate this object
1322
        if ($writeException = $this->validateWrite()) {
1323
            // Used by DODs to clean up after themselves, eg, Versioned
1324
            $this->invokeWithExtensions('onAfterSkippedWrite');
1325
            throw $writeException;
1326
        }
1327
1328
        // Check onBeforeWrite
1329
        $this->brokenOnWrite = true;
1330
        $this->onBeforeWrite();
1331
        if ($this->brokenOnWrite) {
1332
            user_error(static::class . " has a broken onBeforeWrite() function."
1333
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1334
        }
1335
    }
1336
1337
    /**
1338
     * Detects and updates all changes made to this object
1339
     *
1340
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1341
     * @return bool True if any changes are detected
1342
     */
1343
    protected function updateChanges($forceChanges = false)
1344
    {
1345
        if ($forceChanges) {
1346
            // Force changes, but only for loaded fields
1347
            foreach ($this->record as $field => $value) {
1348
                $this->changed[$field] = static::CHANGE_VALUE;
1349
            }
1350
            return true;
1351
        }
1352
        return $this->isChanged();
1353
    }
1354
1355
    /**
1356
     * Writes a subset of changes for a specific table to the given manipulation
1357
     *
1358
     * @param string $baseTable Base table
1359
     * @param string $now Timestamp to use for the current time
1360
     * @param bool $isNewRecord Whether this should be treated as a new record write
1361
     * @param array $manipulation Manipulation to write to
1362
     * @param string $class Class of table to manipulate
1363
     */
1364
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1365
    {
1366
        $schema = $this->getSchema();
1367
        $table = $schema->tableName($class);
1368
        $manipulation[$table] = array();
1369
1370
        // Extract records for this table
1371
        foreach ($this->record as $fieldName => $fieldValue) {
1372
            // we're not attempting to reset the BaseTable->ID
1373
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1374
            if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1375
                continue;
1376
            }
1377
1378
            // Ensure this field pertains to this table
1379
            $specification = $schema->fieldSpec(
1380
                $class,
1381
                $fieldName,
1382
                DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
1383
            );
1384
            if (!$specification) {
1385
                continue;
1386
            }
1387
1388
            // if database column doesn't correlate to a DBField instance...
1389
            $fieldObj = $this->dbObject($fieldName);
1390
            if (!$fieldObj) {
1391
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1392
            }
1393
1394
            // Write to manipulation
1395
            $fieldObj->writeToManipulation($manipulation[$table]);
1396
        }
1397
1398
        // Ensure update of Created and LastEdited columns
1399
        if ($baseTable === $table) {
1400
            $manipulation[$table]['fields']['LastEdited'] = $now;
1401
            if ($isNewRecord) {
1402
                $manipulation[$table]['fields']['Created'] = empty($this->record['Created'])
1403
                    ? $now
1404
                    : $this->record['Created'];
1405
                $manipulation[$table]['fields']['ClassName'] = static::class;
1406
            }
1407
        }
1408
1409
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1410
        // attempt an update, as though it were a normal update.
1411
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1412
        $manipulation[$table]['id'] = $this->record['ID'];
1413
        $manipulation[$table]['class'] = $class;
1414
    }
1415
1416
    /**
1417
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1418
     *
1419
     * Does nothing if an ID is already assigned for this record
1420
     *
1421
     * @param string $baseTable Base table
1422
     * @param string $now Timestamp to use for the current time
1423
     */
1424
    protected function writeBaseRecord($baseTable, $now)
1425
    {
1426
        // Generate new ID if not specified
1427
        if ($this->isInDB()) {
1428
            return;
1429
        }
1430
1431
        // Perform an insert on the base table
1432
        $insert = new SQLInsert('"' . $baseTable . '"');
1433
        $insert
1434
            ->assign('"Created"', $now)
1435
            ->execute();
1436
        $this->changed['ID'] = self::CHANGE_VALUE;
1437
        $this->record['ID'] = DB::get_generated_id($baseTable);
1438
    }
1439
1440
    /**
1441
     * Generate and write the database manipulation for all changed fields
1442
     *
1443
     * @param string $baseTable Base table
1444
     * @param string $now Timestamp to use for the current time
1445
     * @param bool $isNewRecord If this is a new record
1446
     */
1447
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1448
    {
1449
        // Generate database manipulations for each class
1450
        $manipulation = array();
1451
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1452
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1453
        }
1454
1455
        // Allow extensions to extend this manipulation
1456
        $this->extend('augmentWrite', $manipulation);
1457
1458
        // New records have their insert into the base data table done first, so that they can pass the
1459
        // generated ID on to the rest of the manipulation
1460
        if ($isNewRecord) {
1461
            $manipulation[$baseTable]['command'] = 'update';
1462
        }
1463
1464
        // Perform the manipulation
1465
        DB::manipulate($manipulation);
1466
    }
1467
1468
    /**
1469
     * Writes all changes to this object to the database.
1470
     *  - It will insert a record whenever ID isn't set, otherwise update.
1471
     *  - All relevant tables will be updated.
1472
     *  - $this->onBeforeWrite() gets called beforehand.
1473
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1474
     *
1475
     * @uses DataExtension->augmentWrite()
1476
     *
1477
     * @param boolean $showDebug Show debugging information
1478
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1479
     * @param boolean $forceWrite Write to database even if there are no changes
1480
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1481
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1482
     *                                 {@link getManyManyComponents()} (Default: false)
1483
     * @return int The ID of the record
1484
     * @throws ValidationException Exception that can be caught and handled by the calling function
1485
     */
1486
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1487
    {
1488
        $now = DBDatetime::now()->Rfc2822();
1489
1490
        // Execute pre-write tasks
1491
        $this->preWrite();
1492
1493
        // Check if we are doing an update or an insert
1494
        $isNewRecord = !$this->isInDB() || $forceInsert;
1495
1496
        // Check changes exist, abort if there are none
1497
        $hasChanges = $this->updateChanges($isNewRecord);
1498
        if ($hasChanges || $forceWrite || $isNewRecord) {
1499
            // Ensure Created and LastEdited are populated
1500
            if (!isset($this->record['Created'])) {
1501
                $this->record['Created'] = $now;
1502
            }
1503
            $this->record['LastEdited'] = $now;
1504
1505
            // New records have their insert into the base data table done first, so that they can pass the
1506
            // generated primary key on to the rest of the manipulation
1507
            $baseTable = $this->baseTable();
1508
            $this->writeBaseRecord($baseTable, $now);
1509
1510
            // Write the DB manipulation for all changed fields
1511
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1512
1513
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1514
            $this->writeRelations();
1515
            $this->onAfterWrite();
1516
            $this->changed = array();
1517
        } else {
1518
            if ($showDebug) {
1519
                Debug::message("no changes for DataObject");
1520
            }
1521
1522
            // Used by DODs to clean up after themselves, eg, Versioned
1523
            $this->invokeWithExtensions('onAfterSkippedWrite');
1524
        }
1525
1526
        // Write relations as necessary
1527
        if ($writeComponents) {
1528
            $this->writeComponents(true);
1529
        }
1530
1531
        // Clears the cache for this object so get_one returns the correct object.
1532
        $this->flushCache();
1533
1534
        return $this->record['ID'];
1535
    }
1536
1537
    /**
1538
     * Writes cached relation lists to the database, if possible
1539
     */
1540
    public function writeRelations()
1541
    {
1542
        if (!$this->isInDB()) {
1543
            return;
1544
        }
1545
1546
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1547
        if ($this->unsavedRelations) {
1548
            foreach ($this->unsavedRelations as $name => $list) {
1549
                $list->changeToList($this->$name());
1550
            }
1551
            $this->unsavedRelations = array();
1552
        }
1553
    }
1554
1555
    /**
1556
     * Write the cached components to the database. Cached components could refer to two different instances of the
1557
     * same record.
1558
     *
1559
     * @param bool $recursive Recursively write components
1560
     * @return DataObject $this
1561
     */
1562
    public function writeComponents($recursive = false)
1563
    {
1564
        foreach ($this->components as $component) {
1565
            $component->write(false, false, false, $recursive);
1566
        }
1567
1568
        if ($join = $this->getJoin()) {
1569
            $join->write(false, false, false, $recursive);
1570
        }
1571
1572
        return $this;
1573
    }
1574
1575
    /**
1576
     * Delete this data object.
1577
     * $this->onBeforeDelete() gets called.
1578
     * Note that in Versioned objects, both Stage and Live will be deleted.
1579
     * @uses DataExtension->augmentSQL()
1580
     */
1581
    public function delete()
1582
    {
1583
        $this->brokenOnDelete = true;
1584
        $this->onBeforeDelete();
1585
        if ($this->brokenOnDelete) {
1586
            user_error(static::class . " has a broken onBeforeDelete() function."
1587
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1588
        }
1589
1590
        // Deleting a record without an ID shouldn't do anything
1591
        if (!$this->ID) {
1592
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1593
        }
1594
1595
        // TODO: This is quite ugly.  To improve:
1596
        //  - move the details of the delete code in the DataQuery system
1597
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1598
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1599
        $srcQuery = DataList::create(static::class)
1600
            ->filter('ID', $this->ID)
1601
            ->dataQuery()
1602
            ->query();
1603
        $queriedTables = $srcQuery->queriedTables();
1604
        $this->extend('updateDeleteTables', $queriedTables, $srcQuery);
1605
        foreach ($queriedTables as $table) {
1606
            $delete = SQLDelete::create("\"$table\"", array('"ID"' => $this->ID));
1607
            $this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
1608
            $delete->execute();
1609
        }
1610
        // Remove this item out of any caches
1611
        $this->flushCache();
1612
1613
        $this->onAfterDelete();
1614
1615
        $this->OldID = $this->ID;
1616
        $this->ID = 0;
1617
    }
1618
1619
    /**
1620
     * Delete the record with the given ID.
1621
     *
1622
     * @param string $className The class name of the record to be deleted
1623
     * @param int $id ID of record to be deleted
1624
     */
1625
    public static function delete_by_id($className, $id)
1626
    {
1627
        $obj = DataObject::get_by_id($className, $id);
1628
        if ($obj) {
1629
            $obj->delete();
1630
        } else {
1631
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1632
        }
1633
    }
1634
1635
    /**
1636
     * Get the class ancestry, including the current class name.
1637
     * The ancestry will be returned as an array of class names, where the 0th element
1638
     * will be the class that inherits directly from DataObject, and the last element
1639
     * will be the current class.
1640
     *
1641
     * @return array Class ancestry
1642
     */
1643
    public function getClassAncestry()
1644
    {
1645
        return ClassInfo::ancestry(static::class);
1646
    }
1647
1648
    /**
1649
     * Return a unary component object from a one to one relationship, as a DataObject.
1650
     * If no component is available, an 'empty component' will be returned for
1651
     * non-polymorphic relations, or for polymorphic relations with a class set.
1652
     *
1653
     * @param string $componentName Name of the component
1654
     * @return DataObject The component object. It's exact type will be that of the component.
1655
     * @throws Exception
1656
     */
1657
    public function getComponent($componentName)
1658
    {
1659
        if (isset($this->components[$componentName])) {
1660
            return $this->components[$componentName];
1661
        }
1662
1663
        $schema = static::getSchema();
1664
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1665
            $joinField = $componentName . 'ID';
1666
            $joinID = $this->getField($joinField);
1667
1668
            // Extract class name for polymorphic relations
1669
            if ($class === self::class) {
1670
                $class = $this->getField($componentName . 'Class');
1671
                if (empty($class)) {
1672
                    return null;
1673
                }
1674
            }
1675
1676
            if ($joinID) {
1677
                // Ensure that the selected object originates from the same stage, subsite, etc
1678
                $component = DataObject::get($class)
1679
                    ->filter('ID', $joinID)
1680
                    ->setDataQueryParam($this->getInheritableQueryParams())
1681
                    ->first();
1682
            }
1683
1684
            if (empty($component)) {
1685
                $component = Injector::inst()->create($class);
1686
            }
1687
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1688
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1689
            $joinID = $this->ID;
1690
1691
            if ($joinID) {
1692
                // Prepare filter for appropriate join type
1693
                if ($polymorphic) {
1694
                    $filter = array(
1695
                        "{$joinField}ID" => $joinID,
1696
                        "{$joinField}Class" => static::class,
1697
                    );
1698
                } else {
1699
                    $filter = array(
1700
                        $joinField => $joinID
1701
                    );
1702
                }
1703
1704
                // Ensure that the selected object originates from the same stage, subsite, etc
1705
                $component = DataObject::get($class)
1706
                    ->filter($filter)
1707
                    ->setDataQueryParam($this->getInheritableQueryParams())
1708
                    ->first();
1709
            }
1710
1711
            if (empty($component)) {
1712
                $component = Injector::inst()->create($class);
1713
                if ($polymorphic) {
1714
                    $component->{$joinField . 'ID'} = $this->ID;
1715
                    $component->{$joinField . 'Class'} = static::class;
1716
                } else {
1717
                    $component->$joinField = $this->ID;
1718
                }
1719
            }
1720
        } else {
1721
            throw new InvalidArgumentException(
1722
                "DataObject->getComponent(): Could not find component '$componentName'."
1723
            );
1724
        }
1725
1726
        $this->components[$componentName] = $component;
1727
        return $component;
1728
    }
1729
1730
    /**
1731
     * Assign an item to the given component
1732
     *
1733
     * @param string $componentName
1734
     * @param DataObject|null $item
1735
     * @return $this
1736
     */
1737
    public function setComponent($componentName, $item)
1738
    {
1739
        // Validate component
1740
        $schema = static::getSchema();
1741
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1742
            // Force item to be written if not by this point
1743
            // @todo This could be lazy-written in a beforeWrite hook, but force write here for simplicity
1744
            // https://github.com/silverstripe/silverstripe-framework/issues/7818
1745
            if ($item && !$item->isInDB()) {
1746
                $item->write();
1747
            }
1748
1749
            // Update local ID
1750
            $joinField = $componentName . 'ID';
1751
            $this->setField($joinField, $item ? $item->ID : null);
1752
            // Update Class (Polymorphic has_one)
1753
            // Extract class name for polymorphic relations
1754
            if ($class === self::class) {
1755
                $this->setField($componentName . 'Class', $item ? get_class($item) : null);
1756
            }
1757
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1758
            if ($item) {
1759
                // For belongs_to, add to has_one on other component
1760
                $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1761
                if (!$polymorphic) {
1762
                    $joinField = substr($joinField, 0, -2);
1763
                }
1764
                $item->setComponent($joinField, $this);
1765
            }
1766
        } else {
1767
            throw new InvalidArgumentException(
1768
                "DataObject->setComponent(): Could not find component '$componentName'."
1769
            );
1770
        }
1771
1772
        $this->components[$componentName] = $item;
1773
        return $this;
1774
    }
1775
1776
    /**
1777
     * Returns a one-to-many relation as a HasManyList
1778
     *
1779
     * @param string $componentName Name of the component
1780
     * @param int|array $id Optional ID(s) for parent of this relation, if not the current record
1781
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1782
     */
1783
    public function getComponents($componentName, $id = null)
1784
    {
1785
        if (!isset($id)) {
1786
            $id = $this->ID;
1787
        }
1788
        $result = null;
1789
1790
        $schema = $this->getSchema();
1791
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1792
        if (!$componentClass) {
1793
            throw new InvalidArgumentException(sprintf(
1794
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1795
                $componentName,
1796
                static::class
1797
            ));
1798
        }
1799
1800
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1801
        if (!$id) {
1802
            if (!isset($this->unsavedRelations[$componentName])) {
1803
                $this->unsavedRelations[$componentName] =
1804
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1805
            }
1806
            return $this->unsavedRelations[$componentName];
1807
        }
1808
1809
        // Determine type and nature of foreign relation
1810
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1811
        /** @var HasManyList $result */
1812
        if ($polymorphic) {
1813
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
1814
        } else {
1815
            $result = HasManyList::create($componentClass, $joinField);
1816
        }
1817
1818
        return $result
1819
            ->setDataQueryParam($this->getInheritableQueryParams())
1820
            ->forForeignID($id);
1821
    }
1822
1823
    /**
1824
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1825
     *
1826
     * @param string $relationName Relation name.
1827
     * @return string Class name, or null if not found.
1828
     */
1829
    public function getRelationClass($relationName)
1830
    {
1831
        // Parse many_many
1832
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1833
        if ($manyManyComponent) {
1834
            return $manyManyComponent['childClass'];
1835
        }
1836
1837
        // Go through all relationship configuration fields.
1838
        $config = $this->config();
1839
        $candidates = array_merge(
1840
            ($relations = $config->get('has_one')) ? $relations : array(),
1841
            ($relations = $config->get('has_many')) ? $relations : array(),
1842
            ($relations = $config->get('belongs_to')) ? $relations : array()
1843
        );
1844
1845
        if (isset($candidates[$relationName])) {
1846
            $remoteClass = $candidates[$relationName];
1847
1848
            // If dot notation is present, extract just the first part that contains the class.
1849
            if (($fieldPos = strpos($remoteClass, '.')) !== false) {
1850
                return substr($remoteClass, 0, $fieldPos);
1851
            }
1852
1853
            // Otherwise just return the class
1854
            return $remoteClass;
1855
        }
1856
1857
        return null;
1858
    }
1859
1860
    /**
1861
     * Given a relation name, determine the relation type
1862
     *
1863
     * @param string $component Name of component
1864
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1865
     */
1866
    public function getRelationType($component)
1867
    {
1868
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1869
        $config = $this->config();
1870
        foreach ($types as $type) {
1871
            $relations = $config->get($type);
1872
            if ($relations && isset($relations[$component])) {
1873
                return $type;
1874
            }
1875
        }
1876
        return null;
1877
    }
1878
1879
    /**
1880
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1881
     * side of the relation.
1882
     *
1883
     * Notes on behaviour:
1884
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1885
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1886
     *  - Cannot be used on polymorphic relationships
1887
     *  - Cannot be used on unsaved objects.
1888
     *
1889
     * @param string $remoteClass
1890
     * @param string $remoteRelation
1891
     * @return DataList|DataObject The component, either as a list or single object
1892
     * @throws BadMethodCallException
1893
     * @throws InvalidArgumentException
1894
     */
1895
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1896
    {
1897
        $remote = DataObject::singleton($remoteClass);
1898
        $class = $remote->getRelationClass($remoteRelation);
1899
        $schema = static::getSchema();
1900
1901
        // Validate arguments
1902
        if (!$this->isInDB()) {
1903
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1904
        }
1905
        if (empty($class)) {
1906
            throw new InvalidArgumentException(sprintf(
1907
                "%s invoked with invalid relation %s.%s",
1908
                __METHOD__,
1909
                $remoteClass,
1910
                $remoteRelation
1911
            ));
1912
        }
1913
        if ($class === self::class) {
1914
            throw new InvalidArgumentException(sprintf(
1915
                "%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1916
                "This method does not support polymorphic relationships",
1917
                __METHOD__,
1918
                $remoteClass,
1919
                $remoteRelation
1920
            ));
1921
        }
1922
        if (!is_a($this, $class, true)) {
1923
            throw new InvalidArgumentException(sprintf(
1924
                "Relation %s on %s does not refer to objects of type %s",
1925
                $remoteRelation,
1926
                $remoteClass,
1927
                static::class
1928
            ));
1929
        }
1930
1931
        // Check the relation type to mock
1932
        $relationType = $remote->getRelationType($remoteRelation);
1933
        switch ($relationType) {
1934
            case 'has_one': {
1935
                // Mock has_many
1936
                $joinField = "{$remoteRelation}ID";
1937
                $componentClass = $schema->classForField($remoteClass, $joinField);
1938
                $result = HasManyList::create($componentClass, $joinField);
1939
                return $result
1940
                    ->setDataQueryParam($this->getInheritableQueryParams())
1941
                    ->forForeignID($this->ID);
1942
            }
1943
            case 'belongs_to':
1944
            case 'has_many': {
1945
                // These relations must have a has_one on the other end, so find it
1946
                $joinField = $schema->getRemoteJoinField(
1947
                    $remoteClass,
1948
                    $remoteRelation,
1949
                    $relationType,
1950
                    $polymorphic
1951
                );
1952
                if ($polymorphic) {
1953
                    throw new InvalidArgumentException(sprintf(
1954
                        "%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1955
                        "to be a has_one polymorphic. This method does not support polymorphic relationships",
1956
                        __METHOD__,
1957
                        $remoteClass,
1958
                        $remoteRelation
1959
                    ));
1960
                }
1961
                $joinID = $this->getField($joinField);
1962
                if (empty($joinID)) {
1963
                    return null;
1964
                }
1965
                // Get object by joined ID
1966
                return DataObject::get($remoteClass)
1967
                    ->filter('ID', $joinID)
1968
                    ->setDataQueryParam($this->getInheritableQueryParams())
1969
                    ->first();
1970
            }
1971
            case 'many_many':
1972
            case 'belongs_many_many': {
1973
                // Get components and extra fields from parent
1974
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1975
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1976
1977
                // Reverse parent and component fields and create an inverse ManyManyList
1978
                /** @var RelationList $result */
1979
                $result = Injector::inst()->create(
1980
                    $manyMany['relationClass'],
1981
                    $manyMany['parentClass'], // Substitute parent class for dataClass
1982
                    $manyMany['join'],
1983
                    $manyMany['parentField'], // Reversed parent / child field
1984
                    $manyMany['childField'], // Reversed parent / child field
1985
                    $extraFields
1986
                );
1987
                $this->extend('updateManyManyComponents', $result);
1988
1989
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1990
                // foreignID set elsewhere.
1991
                return $result
1992
                    ->setDataQueryParam($this->getInheritableQueryParams())
1993
                    ->forForeignID($this->ID);
1994
            }
1995
            default: {
1996
                return null;
1997
            }
1998
        }
1999
    }
2000
2001
    /**
2002
     * Returns a many-to-many component, as a ManyManyList.
2003
     * @param string $componentName Name of the many-many component
2004
     * @param int|array $id Optional ID for parent of this relation, if not the current record
2005
     * @return ManyManyList|UnsavedRelationList The set of components
2006
     */
2007
    public function getManyManyComponents($componentName, $id = null)
2008
    {
2009
        if (!isset($id)) {
2010
            $id = $this->ID;
2011
        }
2012
        $schema = static::getSchema();
2013
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
2014
        if (!$manyManyComponent) {
2015
            throw new InvalidArgumentException(sprintf(
2016
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
2017
                $componentName,
2018
                static::class
2019
            ));
2020
        }
2021
2022
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
2023
        if (!$id) {
2024
            if (!isset($this->unsavedRelations[$componentName])) {
2025
                $this->unsavedRelations[$componentName] =
2026
                    new UnsavedRelationList(
2027
                        $manyManyComponent['parentClass'],
2028
                        $componentName,
2029
                        $manyManyComponent['childClass']
2030
                    );
2031
            }
2032
            return $this->unsavedRelations[$componentName];
2033
        }
2034
2035
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
2036
        /** @var RelationList $result */
2037
        $result = Injector::inst()->create(
2038
            $manyManyComponent['relationClass'],
2039
            $manyManyComponent['childClass'],
2040
            $manyManyComponent['join'],
2041
            $manyManyComponent['childField'],
2042
            $manyManyComponent['parentField'],
2043
            $extraFields
2044
        );
2045
2046
2047
        // Store component data in query meta-data
2048
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
2049
            /** @var DataQuery $query */
2050
            $query->setQueryParam('Component.ExtraFields', $extraFields);
2051
        });
2052
2053
        $this->extend('updateManyManyComponents', $result);
2054
2055
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
2056
        // foreignID set elsewhere.
2057
        return $result
2058
            ->setDataQueryParam($this->getInheritableQueryParams())
2059
            ->forForeignID($id);
2060
    }
2061
2062
    /**
2063
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
2064
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
2065
     *
2066
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
2067
     *                          their classes.
2068
     */
2069
    public function hasOne()
2070
    {
2071
        return (array)$this->config()->get('has_one');
2072
    }
2073
2074
    /**
2075
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
2076
     * their class name will be returned.
2077
     *
2078
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2079
     *        the field data stripped off. It defaults to TRUE.
2080
     * @return string|array
2081
     */
2082
    public function belongsTo($classOnly = true)
2083
    {
2084
        $belongsTo = (array)$this->config()->get('belongs_to');
2085
        if ($belongsTo && $classOnly) {
2086
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
2087
        } else {
2088
            return $belongsTo ? $belongsTo : array();
2089
        }
2090
    }
2091
2092
    /**
2093
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
2094
     * relationships and their classes will be returned.
2095
     *
2096
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
2097
     *        the field data stripped off. It defaults to TRUE.
2098
     * @return string|array|false
2099
     */
2100
    public function hasMany($classOnly = true)
2101
    {
2102
        $hasMany = (array)$this->config()->get('has_many');
2103
        if ($hasMany && $classOnly) {
2104
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
2105
        } else {
2106
            return $hasMany ? $hasMany : array();
2107
        }
2108
    }
2109
2110
    /**
2111
     * Return the many-to-many extra fields specification.
2112
     *
2113
     * If you don't specify a component name, it returns all
2114
     * extra fields for all components available.
2115
     *
2116
     * @return array|null
2117
     */
2118
    public function manyManyExtraFields()
2119
    {
2120
        return $this->config()->get('many_many_extraFields');
2121
    }
2122
2123
    /**
2124
     * Return information about a many-to-many component.
2125
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
2126
     * components are returned.
2127
     *
2128
     * @see DataObjectSchema::manyManyComponent()
2129
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
2130
     */
2131
    public function manyMany()
2132
    {
2133
        $config = $this->config();
2134
        $manyManys = (array)$config->get('many_many');
2135
        $belongsManyManys = (array)$config->get('belongs_many_many');
2136
        $items = array_merge($manyManys, $belongsManyManys);
2137
        return $items;
2138
    }
2139
2140
    /**
2141
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
2142
     *
2143
     * This is experimental, and is currently only a Postgres-specific enhancement.
2144
     *
2145
     * @param string $class
2146
     * @return array|false
2147
     */
2148
    public function database_extensions($class)
2149
    {
2150
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
2151
        if ($extensions) {
2152
            return $extensions;
2153
        } else {
2154
            return false;
2155
        }
2156
    }
2157
2158
    /**
2159
     * Generates a SearchContext to be used for building and processing
2160
     * a generic search form for properties on this object.
2161
     *
2162
     * @return SearchContext
2163
     */
2164
    public function getDefaultSearchContext()
2165
    {
2166
        return new SearchContext(
2167
            static::class,
2168
            $this->scaffoldSearchFields(),
2169
            $this->defaultSearchFilters()
2170
        );
2171
    }
2172
2173
    /**
2174
     * Determine which properties on the DataObject are
2175
     * searchable, and map them to their default {@link FormField}
2176
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2177
     *
2178
     * Some additional logic is included for switching field labels, based on
2179
     * how generic or specific the field type is.
2180
     *
2181
     * Used by {@link SearchContext}.
2182
     *
2183
     * @param array $_params
2184
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2185
     *   'restrictFields': Numeric array of a field name whitelist
2186
     * @return FieldList
2187
     */
2188
    public function scaffoldSearchFields($_params = null)
2189
    {
2190
        $params = array_merge(
2191
            array(
2192
                'fieldClasses' => false,
2193
                'restrictFields' => false
2194
            ),
2195
            (array)$_params
2196
        );
2197
        $fields = new FieldList();
2198
        foreach ($this->searchableFields() as $fieldName => $spec) {
2199
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
2200
                continue;
2201
            }
2202
2203
            // If a custom fieldclass is provided as a string, use it
2204
            $field = null;
2205
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2206
                $fieldClass = $params['fieldClasses'][$fieldName];
2207
                $field = new $fieldClass($fieldName);
2208
            // If we explicitly set a field, then construct that
2209
            } elseif (isset($spec['field'])) {
2210
                // If it's a string, use it as a class name and construct
2211
                if (is_string($spec['field'])) {
2212
                    $fieldClass = $spec['field'];
2213
                    $field = new $fieldClass($fieldName);
2214
2215
                // If it's a FormField object, then just use that object directly.
2216
                } elseif ($spec['field'] instanceof FormField) {
2217
                    $field = $spec['field'];
2218
2219
                // Otherwise we have a bug
2220
                } else {
2221
                    user_error("Bad value for searchable_fields, 'field' value: "
2222
                        . var_export($spec['field'], true), E_USER_WARNING);
2223
                }
2224
2225
            // Otherwise, use the database field's scaffolder
2226
            } elseif ($object = $this->relObject($fieldName)) {
2227
                $field = $object->scaffoldSearchField();
2228
            }
2229
2230
            // Allow fields to opt out of search
2231
            if (!$field) {
2232
                continue;
2233
            }
2234
2235
            if (strstr($fieldName, '.')) {
2236
                $field->setName(str_replace('.', '__', $fieldName));
2237
            }
2238
            $field->setTitle($spec['title']);
2239
2240
            $fields->push($field);
2241
        }
2242
        return $fields;
2243
    }
2244
2245
    /**
2246
     * Scaffold a simple edit form for all properties on this dataobject,
2247
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2248
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2249
     *
2250
     * @uses FormScaffolder
2251
     *
2252
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2253
     * @return FieldList
2254
     */
2255
    public function scaffoldFormFields($_params = null)
2256
    {
2257
        $params = array_merge(
2258
            array(
2259
                'tabbed' => false,
2260
                'includeRelations' => false,
2261
                'restrictFields' => false,
2262
                'fieldClasses' => false,
2263
                'ajaxSafe' => false
2264
            ),
2265
            (array)$_params
2266
        );
2267
2268
        $fs = FormScaffolder::create($this);
2269
        $fs->tabbed = $params['tabbed'];
2270
        $fs->includeRelations = $params['includeRelations'];
2271
        $fs->restrictFields = $params['restrictFields'];
2272
        $fs->fieldClasses = $params['fieldClasses'];
2273
        $fs->ajaxSafe = $params['ajaxSafe'];
2274
2275
        return $fs->getFieldList();
2276
    }
2277
2278
    /**
2279
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2280
     * being called on extensions
2281
     *
2282
     * @param callable $callback The callback to execute
2283
     */
2284
    protected function beforeUpdateCMSFields($callback)
2285
    {
2286
        $this->beforeExtending('updateCMSFields', $callback);
2287
    }
2288
2289
    /**
2290
     * Centerpiece of every data administration interface in Silverstripe,
2291
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2292
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2293
     * generate this set. To customize, overload this method in a subclass
2294
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2295
     *
2296
     * <code>
2297
     * class MyCustomClass extends DataObject {
2298
     *  static $db = array('CustomProperty'=>'Boolean');
2299
     *
2300
     *  function getCMSFields() {
2301
     *    $fields = parent::getCMSFields();
2302
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2303
     *    return $fields;
2304
     *  }
2305
     * }
2306
     * </code>
2307
     *
2308
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2309
     *
2310
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2311
     */
2312
    public function getCMSFields()
2313
    {
2314
        $tabbedFields = $this->scaffoldFormFields(array(
2315
            // Don't allow has_many/many_many relationship editing before the record is first saved
2316
            'includeRelations' => ($this->ID > 0),
2317
            'tabbed' => true,
2318
            'ajaxSafe' => true
2319
        ));
2320
2321
        $this->extend('updateCMSFields', $tabbedFields);
2322
2323
        return $tabbedFields;
2324
    }
2325
2326
    /**
2327
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2328
     * including that dataobject's extensions customised actions could be added to the EditForm.
2329
     *
2330
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2331
     */
2332
    public function getCMSActions()
2333
    {
2334
        $actions = new FieldList();
2335
        $this->extend('updateCMSActions', $actions);
2336
        return $actions;
2337
    }
2338
2339
2340
    /**
2341
     * Used for simple frontend forms without relation editing
2342
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2343
     * by default. To customize, either overload this method in your
2344
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2345
     *
2346
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2347
     *
2348
     * @param array $params See {@link scaffoldFormFields()}
2349
     * @return FieldList Always returns a simple field collection without TabSet.
2350
     */
2351
    public function getFrontEndFields($params = null)
2352
    {
2353
        $untabbedFields = $this->scaffoldFormFields($params);
2354
        $this->extend('updateFrontEndFields', $untabbedFields);
2355
2356
        return $untabbedFields;
2357
    }
2358
2359
    public function getViewerTemplates($suffix = '')
2360
    {
2361
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2362
    }
2363
2364
    /**
2365
     * Gets the value of a field.
2366
     * Called by {@link __get()} and any getFieldName() methods you might create.
2367
     *
2368
     * @param string $field The name of the field
2369
     * @return mixed The field value
2370
     */
2371
    public function getField($field)
2372
    {
2373
        // If we already have a value in $this->record, then we should just return that
2374
        if (isset($this->record[$field])) {
2375
            return $this->record[$field];
2376
        }
2377
2378
        // Do we have a field that needs to be lazy loaded?
2379
        if (isset($this->record[$field . '_Lazy'])) {
2380
            $tableClass = $this->record[$field . '_Lazy'];
2381
            $this->loadLazyFields($tableClass);
2382
        }
2383
        $schema = static::getSchema();
2384
2385
        // Support unary relations as fields
2386
        if ($schema->unaryComponent(static::class, $field)) {
2387
            return $this->getComponent($field);
2388
        }
2389
2390
        // In case of complex fields, return the DBField object
2391
        if ($schema->compositeField(static::class, $field)) {
2392
            $this->record[$field] = $this->dbObject($field);
2393
        }
2394
2395
        return isset($this->record[$field]) ? $this->record[$field] : null;
2396
    }
2397
2398
    /**
2399
     * Loads all the stub fields that an initial lazy load didn't load fully.
2400
     *
2401
     * @param string $class Class to load the values from. Others are joined as required.
2402
     * Not specifying a tableClass will load all lazy fields from all tables.
2403
     * @return bool Flag if lazy loading succeeded
2404
     */
2405
    protected function loadLazyFields($class = null)
2406
    {
2407
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2408
            return false;
2409
        }
2410
2411
        if (!$class) {
2412
            $loaded = array();
2413
2414
            foreach ($this->record as $key => $value) {
2415
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2416
                    $this->loadLazyFields($value);
2417
                    $loaded[$value] = $value;
2418
                }
2419
            }
2420
2421
            return false;
2422
        }
2423
2424
        $dataQuery = new DataQuery($class);
2425
2426
        // Reset query parameter context to that of this DataObject
2427
        if ($params = $this->getSourceQueryParams()) {
2428
            foreach ($params as $key => $value) {
2429
                $dataQuery->setQueryParam($key, $value);
2430
            }
2431
        }
2432
2433
        // Limit query to the current record, unless it has the Versioned extension,
2434
        // in which case it requires special handling through augmentLoadLazyFields()
2435
        $schema = static::getSchema();
2436
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2437
        $dataQuery->where([
2438
            $baseIDColumn => $this->record['ID']
2439
        ])->limit(1);
2440
2441
        $columns = array();
2442
2443
        // Add SQL for fields, both simple & multi-value
2444
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2445
        $databaseFields = $schema->databaseFields($class, false);
2446
        foreach ($databaseFields as $k => $v) {
2447
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2448
                $columns[] = $k;
2449
            }
2450
        }
2451
2452
        if ($columns) {
2453
            $query = $dataQuery->query();
2454
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2455
            $this->extend('augmentSQL', $query, $dataQuery);
2456
2457
            $dataQuery->setQueriedColumns($columns);
2458
            $newData = $dataQuery->execute()->record();
2459
2460
            // Load the data into record
2461
            if ($newData) {
2462
                foreach ($newData as $k => $v) {
2463
                    if (in_array($k, $columns)) {
2464
                        $this->record[$k] = $v;
2465
                        $this->original[$k] = $v;
2466
                        unset($this->record[$k . '_Lazy']);
2467
                    }
2468
                }
2469
2470
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2471
            } else {
2472
                foreach ($columns as $k) {
2473
                    $this->record[$k] = null;
2474
                    $this->original[$k] = null;
2475
                    unset($this->record[$k . '_Lazy']);
2476
                }
2477
            }
2478
        }
2479
        return true;
2480
    }
2481
2482
    /**
2483
     * Return the fields that have changed.
2484
     *
2485
     * The change level affects what the functions defines as "changed":
2486
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2487
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2488
     *   for example a change from 0 to null would not be included.
2489
     *
2490
     * Example return:
2491
     * <code>
2492
     * array(
2493
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2494
     * )
2495
     * </code>
2496
     *
2497
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2498
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2499
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2500
     * @return array
2501
     */
2502
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2503
    {
2504
        $changedFields = array();
2505
2506
        // Update the changed array with references to changed obj-fields
2507
        foreach ($this->record as $k => $v) {
2508
            // Prevents DBComposite infinite looping on isChanged
2509
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2510
                continue;
2511
            }
2512
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2513
                $this->changed[$k] = self::CHANGE_VALUE;
2514
            }
2515
        }
2516
2517
        if (is_array($databaseFieldsOnly)) {
2518
            $fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2519
        } elseif ($databaseFieldsOnly) {
2520
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2521
            $fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
2522
        } else {
2523
            $fields = $this->changed;
2524
        }
2525
2526
        // Filter the list to those of a certain change level
2527
        if ($changeLevel > self::CHANGE_STRICT) {
2528
            if ($fields) {
2529
                foreach ($fields as $name => $level) {
2530
                    if ($level < $changeLevel) {
2531
                        unset($fields[$name]);
2532
                    }
2533
                }
2534
            }
2535
        }
2536
2537
        if ($fields) {
2538
            foreach ($fields as $name => $level) {
2539
                $changedFields[$name] = array(
2540
                    'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2541
                    'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2542
                    'level' => $level
2543
                );
2544
            }
2545
        }
2546
2547
        return $changedFields;
2548
    }
2549
2550
    /**
2551
     * Uses {@link getChangedFields()} to determine if fields have been changed
2552
     * since loading them from the database.
2553
     *
2554
     * @param string $fieldName Name of the database field to check, will check for any if not given
2555
     * @param int $changeLevel See {@link getChangedFields()}
2556
     * @return boolean
2557
     */
2558
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2559
    {
2560
        $fields = $fieldName ? array($fieldName) : true;
2561
        $changed = $this->getChangedFields($fields, $changeLevel);
2562
        if (!isset($fieldName)) {
2563
            return !empty($changed);
2564
        } else {
2565
            return array_key_exists($fieldName, $changed);
2566
        }
2567
    }
2568
2569
    /**
2570
     * Set the value of the field
2571
     * Called by {@link __set()} and any setFieldName() methods you might create.
2572
     *
2573
     * @param string $fieldName Name of the field
2574
     * @param mixed $val New field value
2575
     * @return $this
2576
     */
2577
    public function setField($fieldName, $val)
2578
    {
2579
        $this->objCacheClear();
2580
        //if it's a has_one component, destroy the cache
2581
        if (substr($fieldName, -2) == 'ID') {
2582
            unset($this->components[substr($fieldName, 0, -2)]);
2583
        }
2584
2585
        // If we've just lazy-loaded the column, then we need to populate the $original array
2586
        if (isset($this->record[$fieldName . '_Lazy'])) {
2587
            $tableClass = $this->record[$fieldName . '_Lazy'];
2588
            $this->loadLazyFields($tableClass);
2589
        }
2590
2591
        // Support component assignent via field setter
2592
        $schema = static::getSchema();
2593
        if ($schema->unaryComponent(static::class, $fieldName)) {
2594
            unset($this->components[$fieldName]);
2595
            // Assign component directly
2596
            if (is_null($val) || $val instanceof DataObject) {
2597
                return $this->setComponent($fieldName, $val);
2598
            }
2599
            // Assign by ID instead of object
2600
            if (is_numeric($val)) {
2601
                $fieldName .= 'ID';
2602
            }
2603
        }
2604
2605
        // Situation 1: Passing an DBField
2606
        if ($val instanceof DBField) {
2607
            $val->setName($fieldName);
2608
            $val->saveInto($this);
2609
2610
            // Situation 1a: Composite fields should remain bound in case they are
2611
            // later referenced to update the parent dataobject
2612
            if ($val instanceof DBComposite) {
2613
                $val->bindTo($this);
2614
                $this->record[$fieldName] = $val;
2615
            }
2616
        // Situation 2: Passing a literal or non-DBField object
2617
        } else {
2618
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2619
            if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) {
2620
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2621
            }
2622
2623
            // if a field is not existing or has strictly changed
2624
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2625
                // TODO Add check for php-level defaults which are not set in the db
2626
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2627
                // At the very least, the type has changed
2628
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2629
2630
                if ((!isset($this->record[$fieldName]) && $val)
2631
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2632
                ) {
2633
                    // Value has changed as well, not just the type
2634
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2635
                }
2636
2637
                // Value is always saved back when strict check succeeds.
2638
                $this->record[$fieldName] = $val;
2639
            }
2640
        }
2641
        return $this;
2642
    }
2643
2644
    /**
2645
     * Set the value of the field, using a casting object.
2646
     * This is useful when you aren't sure that a date is in SQL format, for example.
2647
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2648
     * can be saved into the Image table.
2649
     *
2650
     * @param string $fieldName Name of the field
2651
     * @param mixed $value New field value
2652
     * @return $this
2653
     */
2654
    public function setCastedField($fieldName, $value)
2655
    {
2656
        if (!$fieldName) {
2657
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2658
        }
2659
        $fieldObj = $this->dbObject($fieldName);
2660
        if ($fieldObj) {
2661
            $fieldObj->setValue($value);
2662
            $fieldObj->saveInto($this);
2663
        } else {
2664
            $this->$fieldName = $value;
2665
        }
2666
        return $this;
2667
    }
2668
2669
    /**
2670
     * {@inheritdoc}
2671
     */
2672
    public function castingHelper($field)
2673
    {
2674
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2675
        if ($fieldSpec) {
2676
            return $fieldSpec;
2677
        }
2678
2679
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2680
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2681
        $queryParams = $this->getSourceQueryParams();
2682
        if (!empty($queryParams['Component.ExtraFields'])) {
2683
            $extraFields = $queryParams['Component.ExtraFields'];
2684
2685
            if (isset($extraFields[$field])) {
2686
                return $extraFields[$field];
2687
            }
2688
        }
2689
2690
        return parent::castingHelper($field);
2691
    }
2692
2693
    /**
2694
     * Returns true if the given field exists in a database column on any of
2695
     * the objects tables and optionally look up a dynamic getter with
2696
     * get<fieldName>().
2697
     *
2698
     * @param string $field Name of the field
2699
     * @return boolean True if the given field exists
2700
     */
2701
    public function hasField($field)
2702
    {
2703
        $schema = static::getSchema();
2704
        return (
2705
            array_key_exists($field, $this->record)
2706
            || array_key_exists($field, $this->components)
2707
            || $schema->fieldSpec(static::class, $field)
2708
            || $schema->unaryComponent(static::class, $field)
2709
            || $this->hasMethod("get{$field}")
2710
        );
2711
    }
2712
2713
    /**
2714
     * Returns true if the given field exists as a database column
2715
     *
2716
     * @param string $field Name of the field
2717
     *
2718
     * @return boolean
2719
     */
2720
    public function hasDatabaseField($field)
2721
    {
2722
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2723
        return !empty($spec);
2724
    }
2725
2726
    /**
2727
     * Returns true if the member is allowed to do the given action.
2728
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2729
     *
2730
     * @param string $perm The permission to be checked, such as 'View'.
2731
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2732
     * in user.
2733
     * @param array $context Additional $context to pass to extendedCan()
2734
     *
2735
     * @return boolean True if the the member is allowed to do the given action
2736
     */
2737
    public function can($perm, $member = null, $context = array())
2738
    {
2739
        if (!$member) {
2740
            $member = Security::getCurrentUser();
2741
        }
2742
2743
        if ($member && Permission::checkMember($member, "ADMIN")) {
2744
            return true;
2745
        }
2746
2747
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2748
            $method = 'can' . ucfirst($perm);
2749
            return $this->$method($member);
2750
        }
2751
2752
        $results = $this->extendedCan('can', $member);
2753
        if (isset($results)) {
2754
            return $results;
2755
        }
2756
2757
        return ($member && Permission::checkMember($member, $perm));
2758
    }
2759
2760
    /**
2761
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2762
     * expected to return one of three values:
2763
     *
2764
     *  - false: Disallow this permission, regardless of what other extensions say
2765
     *  - true: Allow this permission, as long as no other extensions return false
2766
     *  - NULL: Don't affect the outcome
2767
     *
2768
     * This method itself returns a tri-state value, and is designed to be used like this:
2769
     *
2770
     * <code>
2771
     * $extended = $this->extendedCan('canDoSomething', $member);
2772
     * if($extended !== null) return $extended;
2773
     * else return $normalValue;
2774
     * </code>
2775
     *
2776
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2777
     * @param Member|int $member
2778
     * @param array $context Optional context
2779
     * @return boolean|null
2780
     */
2781
    public function extendedCan($methodName, $member, $context = array())
2782
    {
2783
        $results = $this->extend($methodName, $member, $context);
2784
        if ($results && is_array($results)) {
2785
            // Remove NULLs
2786
            $results = array_filter($results, function ($v) {
2787
                return !is_null($v);
2788
            });
2789
            // If there are any non-NULL responses, then return the lowest one of them.
2790
            // If any explicitly deny the permission, then we don't get access
2791
            if ($results) {
2792
                return min($results);
2793
            }
2794
        }
2795
        return null;
2796
    }
2797
2798
    /**
2799
     * @param Member $member
2800
     * @return boolean
2801
     */
2802
    public function canView($member = null)
2803
    {
2804
        $extended = $this->extendedCan(__FUNCTION__, $member);
2805
        if ($extended !== null) {
2806
            return $extended;
2807
        }
2808
        return Permission::check('ADMIN', 'any', $member);
2809
    }
2810
2811
    /**
2812
     * @param Member $member
2813
     * @return boolean
2814
     */
2815
    public function canEdit($member = null)
2816
    {
2817
        $extended = $this->extendedCan(__FUNCTION__, $member);
2818
        if ($extended !== null) {
2819
            return $extended;
2820
        }
2821
        return Permission::check('ADMIN', 'any', $member);
2822
    }
2823
2824
    /**
2825
     * @param Member $member
2826
     * @return boolean
2827
     */
2828
    public function canDelete($member = null)
2829
    {
2830
        $extended = $this->extendedCan(__FUNCTION__, $member);
2831
        if ($extended !== null) {
2832
            return $extended;
2833
        }
2834
        return Permission::check('ADMIN', 'any', $member);
2835
    }
2836
2837
    /**
2838
     * @param Member $member
2839
     * @param array $context Additional context-specific data which might
2840
     * affect whether (or where) this object could be created.
2841
     * @return boolean
2842
     */
2843
    public function canCreate($member = null, $context = array())
2844
    {
2845
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2846
        if ($extended !== null) {
2847
            return $extended;
2848
        }
2849
        return Permission::check('ADMIN', 'any', $member);
2850
    }
2851
2852
    /**
2853
     * Debugging used by Debug::show()
2854
     *
2855
     * @return string HTML data representing this object
2856
     */
2857
    public function debug()
2858
    {
2859
        $class = static::class;
2860
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2861
        if ($this->record) {
2862
            foreach ($this->record as $fieldName => $fieldVal) {
2863
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2864
            }
2865
        }
2866
        $val .= "</ul>\n";
2867
        return $val;
2868
    }
2869
2870
    /**
2871
     * Return the DBField object that represents the given field.
2872
     * This works similarly to obj() with 2 key differences:
2873
     *   - it still returns an object even when the field has no value.
2874
     *   - it only matches fields and not methods
2875
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2876
     *
2877
     * @param string $fieldName Name of the field
2878
     * @return DBField The field as a DBField object
2879
     */
2880
    public function dbObject($fieldName)
2881
    {
2882
        // Check for field in DB
2883
        $schema = static::getSchema();
2884
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2885
        if (!$helper) {
2886
            return null;
2887
        }
2888
2889
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2890
            $tableClass = $this->record[$fieldName . '_Lazy'];
2891
            $this->loadLazyFields($tableClass);
2892
        }
2893
2894
        $value = isset($this->record[$fieldName])
2895
            ? $this->record[$fieldName]
2896
            : null;
2897
2898
        // If we have a DBField object in $this->record, then return that
2899
        if ($value instanceof DBField) {
2900
            return $value;
2901
        }
2902
2903
        list($class, $spec) = explode('.', $helper);
2904
        /** @var DBField $obj */
2905
        $table = $schema->tableName($class);
2906
        $obj = Injector::inst()->create($spec, $fieldName);
2907
        $obj->setTable($table);
2908
        $obj->setValue($value, $this, false);
2909
        return $obj;
2910
    }
2911
2912
    /**
2913
     * Traverses to a DBField referenced by relationships between data objects.
2914
     *
2915
     * The path to the related field is specified with dot separated syntax
2916
     * (eg: Parent.Child.Child.FieldName).
2917
     *
2918
     * If a relation is blank, this will return null instead.
2919
     * If a relation name is invalid (e.g. non-relation on a parent) this
2920
     * can throw a LogicException.
2921
     *
2922
     * @param string $fieldPath List of paths on this object. All items in this path
2923
     * must be ViewableData implementors
2924
     *
2925
     * @return mixed DBField of the field on the object or a DataList instance.
2926
     * @throws LogicException If accessing invalid relations
2927
     */
2928
    public function relObject($fieldPath)
2929
    {
2930
        $object = null;
2931
        $component = $this;
2932
2933
        // Parse all relations
2934
        foreach (explode('.', $fieldPath) as $relation) {
2935
            if (!$component) {
2936
                return null;
2937
            }
2938
2939
            // Inspect relation type
2940
            if (ClassInfo::hasMethod($component, $relation)) {
2941
                $component = $component->$relation();
2942
            } elseif ($component instanceof Relation || $component instanceof DataList) {
2943
                // $relation could either be a field (aggregate), or another relation
2944
                $singleton = DataObject::singleton($component->dataClass());
2945
                $component = $singleton->dbObject($relation) ?: $component->relation($relation);
2946
            } elseif ($component instanceof DataObject && ($dbObject = $component->dbObject($relation))) {
2947
                $component = $dbObject;
2948
            } elseif ($component instanceof ViewableData && $component->hasField($relation)) {
2949
                $component = $component->obj($relation);
2950
            } else {
2951
                throw new LogicException(
2952
                    "$relation is not a relation/field on " . get_class($component)
2953
                );
2954
            }
2955
        }
2956
        return $component;
2957
    }
2958
2959
    /**
2960
     * Traverses to a field referenced by relationships between data objects, returning the value
2961
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2962
     *
2963
     * @param string $fieldName string
2964
     * @return mixed Will return null on a missing value
2965
     */
2966
    public function relField($fieldName)
2967
    {
2968
        // Navigate to relative parent using relObject() if needed
2969
        $component = $this;
2970
        if (($pos = strrpos($fieldName, '.')) !== false) {
2971
            $relation = substr($fieldName, 0, $pos);
2972
            $fieldName = substr($fieldName, $pos + 1);
2973
            $component = $this->relObject($relation);
2974
        }
2975
2976
        // Bail if the component is null
2977
        if (!$component) {
2978
            return null;
2979
        }
2980
        if (ClassInfo::hasMethod($component, $fieldName)) {
2981
            return $component->$fieldName();
2982
        }
2983
        return $component->$fieldName;
2984
    }
2985
2986
    /**
2987
     * Temporary hack to return an association name, based on class, to get around the mangle
2988
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2989
     *
2990
     * @param string $className
2991
     * @return string
2992
     */
2993
    public function getReverseAssociation($className)
2994
    {
2995
        if (is_array($this->manyMany())) {
2996
            $many_many = array_flip($this->manyMany());
2997
            if (array_key_exists($className, $many_many)) {
2998
                return $many_many[$className];
2999
            }
3000
        }
3001
        if (is_array($this->hasMany())) {
3002
            $has_many = array_flip($this->hasMany());
3003
            if (array_key_exists($className, $has_many)) {
3004
                return $has_many[$className];
3005
            }
3006
        }
3007
        if (is_array($this->hasOne())) {
3008
            $has_one = array_flip($this->hasOne());
3009
            if (array_key_exists($className, $has_one)) {
3010
                return $has_one[$className];
3011
            }
3012
        }
3013
3014
        return false;
3015
    }
3016
3017
    /**
3018
     * Return all objects matching the filter
3019
     * sub-classes are automatically selected and included
3020
     *
3021
     * @param string $callerClass The class of objects to be returned
3022
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3023
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3024
     * @param string|array $sort A sort expression to be inserted into the ORDER
3025
     * BY clause.  If omitted, self::$default_sort will be used.
3026
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3027
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3028
     * @param string $containerClass The container class to return the results in.
3029
     *
3030
     * @todo $containerClass is Ignored, why?
3031
     *
3032
     * @return DataList The objects matching the filter, in the class specified by $containerClass
3033
     */
3034
    public static function get(
3035
        $callerClass = null,
3036
        $filter = "",
3037
        $sort = "",
3038
        $join = "",
3039
        $limit = null,
3040
        $containerClass = DataList::class
3041
    ) {
3042
3043
        if ($callerClass == null) {
3044
            $callerClass = get_called_class();
3045
            if ($callerClass == self::class) {
3046
                throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3047
            }
3048
3049
            if ($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
3050
                throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3051
                    . ' arguments');
3052
            }
3053
3054
            return DataList::create(get_called_class());
3055
        }
3056
3057
        if ($join) {
3058
            throw new \InvalidArgumentException(
3059
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3060
            );
3061
        }
3062
3063
        $result = DataList::create($callerClass)->where($filter)->sort($sort);
3064
3065
        if ($limit && strpos($limit, ',') !== false) {
3066
            $limitArguments = explode(',', $limit);
3067
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
3068
        } elseif ($limit) {
3069
            $result = $result->limit($limit);
3070
        }
3071
3072
        return $result;
3073
    }
3074
3075
3076
    /**
3077
     * Return the first item matching the given query.
3078
     * All calls to get_one() are cached.
3079
     *
3080
     * @param string $callerClass The class of objects to be returned
3081
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3082
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3083
     * @param boolean $cache Use caching
3084
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3085
     *
3086
     * @return DataObject|null The first item matching the query
3087
     */
3088
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
3089
    {
3090
        $SNG = singleton($callerClass);
3091
3092
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3093
        $cacheKey = md5(serialize($cacheComponents));
3094
3095
        $item = null;
3096
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3097
            $dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
3098
            $item = $dl->first();
3099
3100
            if ($cache) {
3101
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3102
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
3103
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
3104
                }
3105
            }
3106
        }
3107
3108
        if ($cache) {
3109
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
3110
        } else {
3111
            return $item;
3112
        }
3113
    }
3114
3115
    /**
3116
     * Flush the cached results for all relations (has_one, has_many, many_many)
3117
     * Also clears any cached aggregate data.
3118
     *
3119
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3120
     *                            When false will just clear session-local cached data
3121
     * @return DataObject $this
3122
     */
3123
    public function flushCache($persistent = true)
3124
    {
3125
        if (static::class == self::class) {
3126
            self::$_cache_get_one = array();
3127
            return $this;
3128
        }
3129
3130
        $classes = ClassInfo::ancestry(static::class);
3131
        foreach ($classes as $class) {
3132
            if (isset(self::$_cache_get_one[$class])) {
3133
                unset(self::$_cache_get_one[$class]);
3134
            }
3135
        }
3136
3137
        $this->extend('flushCache');
3138
3139
        $this->components = array();
3140
        return $this;
3141
    }
3142
3143
    /**
3144
     * Flush the get_one global cache and destroy associated objects.
3145
     */
3146
    public static function flush_and_destroy_cache()
3147
    {
3148
        if (self::$_cache_get_one) {
3149
            foreach (self::$_cache_get_one as $class => $items) {
3150
                if (is_array($items)) {
3151
                    foreach ($items as $item) {
3152
                        if ($item) {
3153
                            $item->destroy();
3154
                        }
3155
                    }
3156
                }
3157
            }
3158
        }
3159
        self::$_cache_get_one = array();
3160
    }
3161
3162
    /**
3163
     * Reset all global caches associated with DataObject.
3164
     */
3165
    public static function reset()
3166
    {
3167
        // @todo Decouple these
3168
        DBClassName::clear_classname_cache();
3169
        ClassInfo::reset_db_cache();
3170
        static::getSchema()->reset();
3171
        self::$_cache_get_one = array();
3172
        self::$_cache_field_labels = array();
3173
    }
3174
3175
    /**
3176
     * Return the given element, searching by ID
3177
     *
3178
     * @param string $callerClass The class of the object to be returned
3179
     * @param int $id The id of the element
3180
     * @param boolean $cache See {@link get_one()}
3181
     *
3182
     * @return DataObject The element
3183
     */
3184
    public static function get_by_id($callerClass, $id, $cache = true)
3185
    {
3186
        if (!is_numeric($id)) {
3187
            user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3188
        }
3189
3190
        // Pass to get_one
3191
        $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
3192
        return DataObject::get_one($callerClass, array($column => $id), $cache);
3193
    }
3194
3195
    /**
3196
     * Get the name of the base table for this object
3197
     *
3198
     * @return string
3199
     */
3200
    public function baseTable()
3201
    {
3202
        return static::getSchema()->baseDataTable($this);
3203
    }
3204
3205
    /**
3206
     * Get the base class for this object
3207
     *
3208
     * @return string
3209
     */
3210
    public function baseClass()
3211
    {
3212
        return static::getSchema()->baseDataClass($this);
3213
    }
3214
3215
    /**
3216
     * @var array Parameters used in the query that built this object.
3217
     * This can be used by decorators (e.g. lazy loading) to
3218
     * run additional queries using the same context.
3219
     */
3220
    protected $sourceQueryParams;
3221
3222
    /**
3223
     * @see $sourceQueryParams
3224
     * @return array
3225
     */
3226
    public function getSourceQueryParams()
3227
    {
3228
        return $this->sourceQueryParams;
3229
    }
3230
3231
    /**
3232
     * Get list of parameters that should be inherited to relations on this object
3233
     *
3234
     * @return array
3235
     */
3236
    public function getInheritableQueryParams()
3237
    {
3238
        $params = $this->getSourceQueryParams();
3239
        $this->extend('updateInheritableQueryParams', $params);
3240
        return $params;
3241
    }
3242
3243
    /**
3244
     * @see $sourceQueryParams
3245
     * @param array
3246
     */
3247
    public function setSourceQueryParams($array)
3248
    {
3249
        $this->sourceQueryParams = $array;
3250
    }
3251
3252
    /**
3253
     * @see $sourceQueryParams
3254
     * @param string $key
3255
     * @param string $value
3256
     */
3257
    public function setSourceQueryParam($key, $value)
3258
    {
3259
        $this->sourceQueryParams[$key] = $value;
3260
    }
3261
3262
    /**
3263
     * @see $sourceQueryParams
3264
     * @param string $key
3265
     * @return string
3266
     */
3267
    public function getSourceQueryParam($key)
3268
    {
3269
        if (isset($this->sourceQueryParams[$key])) {
3270
            return $this->sourceQueryParams[$key];
3271
        }
3272
        return null;
3273
    }
3274
3275
    //-------------------------------------------------------------------------------------------//
3276
3277
    /**
3278
     * Check the database schema and update it as necessary.
3279
     *
3280
     * @uses DataExtension->augmentDatabase()
3281
     */
3282
    public function requireTable()
3283
    {
3284
        // Only build the table if we've actually got fields
3285
        $schema = static::getSchema();
3286
        $table = $schema->tableName(static::class);
3287
        $fields = $schema->databaseFields(static::class, false);
3288
        $indexes = $schema->databaseIndexes(static::class, false);
3289
        $extensions = self::database_extensions(static::class);
3290
        $legacyTables = $schema->getLegacyTableNames(static::class);
3291
3292
        if (empty($table)) {
3293
            throw new LogicException(
3294
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3295
            );
3296
        }
3297
3298
        if ($legacyTables) {
3299
            $ignore = Config::inst()->get(static::class, 'ignored_legacy_tables') ?: [];
3300
            $renameTables = array_diff(
3301
                array_intersect($legacyTables, DB::table_list()),
3302
                $ignore
3303
            );
3304
            if (count($renameTables) > 1) {
3305
                $class = static::class;
3306
                $legacyList = implode(', ', $renameTables);
3307
                trigger_error(
3308
                    "Class $class has multiple legacy tables: $legacyList",
3309
                    E_USER_NOTICE
3310
                );
3311
            }
3312
            if (count($renameTables) === 1) {
3313
                $dbSchema = DB::get_schema();
3314
                $dbSchema->renameTable($renameTables[0], $table);
3315
            }
3316
        }
3317
3318
        if ($fields) {
3319
            $hasAutoIncPK = get_parent_class($this) === self::class;
3320
            DB::require_table(
3321
                $table,
3322
                $fields,
3323
                $indexes,
3324
                $hasAutoIncPK,
3325
                $this->config()->get('create_table_options'),
3326
                $extensions
3327
            );
3328
        } else {
3329
            DB::dont_require_table($table);
3330
        }
3331
3332
        // Build any child tables for many_many items
3333
        if ($manyMany = $this->uninherited('many_many')) {
3334
            $extras = $this->uninherited('many_many_extraFields');
3335
            foreach ($manyMany as $component => $spec) {
3336
                // Get many_many spec
3337
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3338
                $parentField = $manyManyComponent['parentField'];
3339
                $childField = $manyManyComponent['childField'];
3340
                $tableOrClass = $manyManyComponent['join'];
3341
3342
                // Skip if backed by actual class
3343
                if (class_exists($tableOrClass)) {
3344
                    continue;
3345
                }
3346
3347
                // Build fields
3348
                $manymanyFields = array(
3349
                    $parentField => "Int",
3350
                    $childField => "Int",
3351
                );
3352
                if (isset($extras[$component])) {
3353
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3354
                }
3355
3356
                // Build index list
3357
                $manymanyIndexes = [
3358
                    $parentField => [
3359
                        'type' => 'index',
3360
                        'name' => $parentField,
3361
                        'columns' => [$parentField],
3362
                    ],
3363
                    $childField => [
3364
                        'type' => 'index',
3365
                        'name' => $childField,
3366
                        'columns' => [$childField],
3367
                    ],
3368
                ];
3369
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
3370
            }
3371
        }
3372
3373
        // Let any extentions make their own database fields
3374
        $this->extend('augmentDatabase', $dummy);
3375
    }
3376
3377
    /**
3378
     * Add default records to database. This function is called whenever the
3379
     * database is built, after the database tables have all been created. Overload
3380
     * this to add default records when the database is built, but make sure you
3381
     * call parent::requireDefaultRecords().
3382
     *
3383
     * @uses DataExtension->requireDefaultRecords()
3384
     */
3385
    public function requireDefaultRecords()
3386
    {
3387
        $defaultRecords = $this->config()->uninherited('default_records');
3388
3389
        if (!empty($defaultRecords)) {
3390
            $hasData = DataObject::get_one(static::class);
3391
            if (!$hasData) {
3392
                $className = static::class;
3393
                foreach ($defaultRecords as $record) {
3394
                    $obj = Injector::inst()->create($className, $record);
3395
                    $obj->write();
3396
                }
3397
                DB::alteration_message("Added default records to $className table", "created");
3398
            }
3399
        }
3400
3401
        // Let any extentions make their own database default data
3402
        $this->extend('requireDefaultRecords', $dummy);
3403
    }
3404
3405
    /**
3406
     * Get the default searchable fields for this object, as defined in the
3407
     * $searchable_fields list. If searchable fields are not defined on the
3408
     * data object, uses a default selection of summary fields.
3409
     *
3410
     * @return array
3411
     */
3412
    public function searchableFields()
3413
    {
3414
        // can have mixed format, need to make consistent in most verbose form
3415
        $fields = $this->config()->get('searchable_fields');
3416
        $labels = $this->fieldLabels();
3417
3418
        // fallback to summary fields (unless empty array is explicitly specified)
3419
        if (!$fields && !is_array($fields)) {
3420
            $summaryFields = array_keys($this->summaryFields());
3421
            $fields = array();
3422
3423
            // remove the custom getters as the search should not include them
3424
            $schema = static::getSchema();
3425
            if ($summaryFields) {
3426
                foreach ($summaryFields as $key => $name) {
3427
                    $spec = $name;
3428
3429
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3430
                    if (($fieldPos = strpos($name, '.')) !== false) {
3431
                        $name = substr($name, 0, $fieldPos);
3432
                    }
3433
3434
                    if ($schema->fieldSpec($this, $name)) {
3435
                        $fields[] = $name;
3436
                    } elseif ($this->relObject($spec)) {
3437
                        $fields[] = $spec;
3438
                    }
3439
                }
3440
            }
3441
        }
3442
3443
        // we need to make sure the format is unified before
3444
        // augmenting fields, so extensions can apply consistent checks
3445
        // but also after augmenting fields, because the extension
3446
        // might use the shorthand notation as well
3447
3448
        // rewrite array, if it is using shorthand syntax
3449
        $rewrite = array();
3450
        foreach ($fields as $name => $specOrName) {
3451
            $identifer = (is_int($name)) ? $specOrName : $name;
3452
3453
            if (is_int($name)) {
3454
                // Format: array('MyFieldName')
3455
                $rewrite[$identifer] = array();
3456
            } elseif (is_array($specOrName) && ($relObject = $this->relObject($identifer))) {
3457
                // Format: array('MyFieldName' => array(
3458
                //   'filter => 'ExactMatchFilter',
3459
                //   'field' => 'NumericField', // optional
3460
                //   'title' => 'My Title', // optional
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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