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

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

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

Loading history...
3077
            $ignore = Config::inst()->get(static::class, 'ignored_legacy_tables') ?: [];
3078
            $renameTables = array_diff(
3079
                array_intersect($legacyTables, DB::table_list()),
3080
                $ignore
3081
            );
3082
            if (count($renameTables) > 1) {
3083
                $class = static::class;
3084
                $legacyList = implode(', ', $renameTables);
3085
                trigger_error(
3086
                    "Class $class has multiple legacy tables: $legacyList",
3087
                    E_USER_NOTICE
3088
                );
3089
            }
3090
            if (count($renameTables) === 1) {
3091
                $dbSchema = DB::get_schema();
3092
                $dbSchema->renameTable($renameTables[0], $table);
3093
            }
3094
        }
3095
3096
        if ($fields) {
3097
            $hasAutoIncPK = get_parent_class($this) === self::class;
3098
            DB::require_table(
3099
                $table,
3100
                $fields,
3101
                $indexes,
3102
                $hasAutoIncPK,
3103
                $this->config()->get('create_table_options'),
3104
                $extensions
3105
            );
3106
        } else {
3107
            DB::dont_require_table($table);
3108
        }
3109
3110
        // Build any child tables for many_many items
3111
        if ($manyMany = $this->uninherited('many_many')) {
3112
            $extras = $this->uninherited('many_many_extraFields');
3113
            foreach ($manyMany as $component => $spec) {
3114
                // Get many_many spec
3115
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3116
                $parentField = $manyManyComponent['parentField'];
3117
                $childField = $manyManyComponent['childField'];
3118
                $tableOrClass = $manyManyComponent['join'];
3119
3120
                // Skip if backed by actual class
3121
                if (class_exists($tableOrClass)) {
3122
                    continue;
3123
                }
3124
3125
                // Build fields
3126
                $manymanyFields = array(
3127
                    $parentField => "Int",
3128
                    $childField => "Int",
3129
                );
3130
                if (isset($extras[$component])) {
3131
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3132
                }
3133
3134
                // Build index list
3135
                $manymanyIndexes = [
3136
                    $parentField => [
3137
                        'type' => 'index',
3138
                        'name' => $parentField,
3139
                        'columns' => [$parentField],
3140
                    ],
3141
                    $childField => [
3142
                        'type' => 'index',
3143
                        'name' =>$childField,
3144
                        'columns' => [$childField],
3145
                    ],
3146
                ];
3147
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
3148
            }
3149
        }
3150
3151
        // Let any extentions make their own database fields
3152
        $this->extend('augmentDatabase', $dummy);
3153
    }
3154
3155
    /**
3156
     * Add default records to database. This function is called whenever the
3157
     * database is built, after the database tables have all been created. Overload
3158
     * this to add default records when the database is built, but make sure you
3159
     * call parent::requireDefaultRecords().
3160
     *
3161
     * @uses DataExtension->requireDefaultRecords()
3162
     */
3163
    public function requireDefaultRecords()
3164
    {
3165
        $defaultRecords = $this->config()->uninherited('default_records');
3166
3167
        if (!empty($defaultRecords)) {
3168
            $hasData = DataObject::get_one(static::class);
3169
            if (!$hasData) {
3170
                $className = static::class;
3171
                foreach ($defaultRecords as $record) {
3172
                    $obj = Injector::inst()->create($className, $record);
3173
                    $obj->write();
3174
                }
3175
                DB::alteration_message("Added default records to $className table", "created");
3176
            }
3177
        }
3178
3179
        // Let any extentions make their own database default data
3180
        $this->extend('requireDefaultRecords', $dummy);
3181
    }
3182
3183
    /**
3184
     * Get the default searchable fields for this object, as defined in the
3185
     * $searchable_fields list. If searchable fields are not defined on the
3186
     * data object, uses a default selection of summary fields.
3187
     *
3188
     * @return array
3189
     */
3190
    public function searchableFields()
3191
    {
3192
        // can have mixed format, need to make consistent in most verbose form
3193
        $fields = $this->config()->get('searchable_fields');
3194
        $labels = $this->fieldLabels();
3195
3196
        // fallback to summary fields (unless empty array is explicitly specified)
3197
        if (! $fields && ! is_array($fields)) {
3198
            $summaryFields = array_keys($this->summaryFields());
3199
            $fields = array();
3200
3201
            // remove the custom getters as the search should not include them
3202
            $schema = static::getSchema();
3203
            if ($summaryFields) {
3204
                foreach ($summaryFields as $key => $name) {
3205
                    $spec = $name;
3206
3207
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3208
                    if (($fieldPos = strpos($name, '.')) !== false) {
3209
                        $name = substr($name, 0, $fieldPos);
3210
                    }
3211
3212
                    if ($schema->fieldSpec($this, $name)) {
3213
                        $fields[] = $name;
3214
                    } elseif ($this->relObject($spec)) {
3215
                        $fields[] = $spec;
3216
                    }
3217
                }
3218
            }
3219
        }
3220
3221
        // we need to make sure the format is unified before
3222
        // augmenting fields, so extensions can apply consistent checks
3223
        // but also after augmenting fields, because the extension
3224
        // might use the shorthand notation as well
3225
3226
        // rewrite array, if it is using shorthand syntax
3227
        $rewrite = array();
3228
        foreach ($fields as $name => $specOrName) {
3229
            $identifer = (is_int($name)) ? $specOrName : $name;
3230
3231
            if (is_int($name)) {
3232
                // Format: array('MyFieldName')
3233
                $rewrite[$identifer] = array();
3234
            } elseif (is_array($specOrName) && ($relObject = $this->relObject($identifer))) {
3235
                // Format: array('MyFieldName' => array(
3236
                //   'filter => 'ExactMatchFilter',
3237
                //   'field' => 'NumericField', // optional
3238
                //   'title' => 'My Title', // optional
3239
                // ))
3240
                $rewrite[$identifer] = array_merge(
3241
                    array('filter' => $relObject->config()->get('default_search_filter_class')),
3242
                    (array)$specOrName
3243
                );
3244
            } else {
3245
                // Format: array('MyFieldName' => 'ExactMatchFilter')
3246
                $rewrite[$identifer] = array(
3247
                    'filter' => $specOrName,
3248
                );
3249
            }
3250
            if (!isset($rewrite[$identifer]['title'])) {
3251
                $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3252
                    ? $labels[$identifer] : FormField::name_to_label($identifer);
3253
            }
3254
            if (!isset($rewrite[$identifer]['filter'])) {
3255
                /** @skipUpgrade */
3256
                $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3257
            }
3258
        }
3259
3260
        $fields = $rewrite;
3261
3262
        // apply DataExtensions if present
3263
        $this->extend('updateSearchableFields', $fields);
3264
3265
        return $fields;
3266
    }
3267
3268
    /**
3269
     * Get any user defined searchable fields labels that
3270
     * exist. Allows overriding of default field names in the form
3271
     * interface actually presented to the user.
3272
     *
3273
     * The reason for keeping this separate from searchable_fields,
3274
     * which would be a logical place for this functionality, is to
3275
     * avoid bloating and complicating the configuration array. Currently
3276
     * much of this system is based on sensible defaults, and this property
3277
     * would generally only be set in the case of more complex relationships
3278
     * between data object being required in the search interface.
3279
     *
3280
     * Generates labels based on name of the field itself, if no static property
3281
     * {@link self::field_labels} exists.
3282
     *
3283
     * @uses $field_labels
3284
     * @uses FormField::name_to_label()
3285
     *
3286
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3287
     *
3288
     * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3289
     */
3290
    public function fieldLabels($includerelations = true)
3291
    {
3292
        $cacheKey = static::class . '_' . $includerelations;
3293
3294
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
3295
            $customLabels = $this->config()->get('field_labels');
3296
            $autoLabels = array();
3297
3298
            // get all translated static properties as defined in i18nCollectStatics()
3299
            $ancestry = ClassInfo::ancestry(static::class);
3300
            $ancestry = array_reverse($ancestry);
3301
            if ($ancestry) {
3302
                foreach ($ancestry as $ancestorClass) {
3303
                    if ($ancestorClass === ViewableData::class) {
3304
                        break;
3305
                    }
3306
                    $types = [
3307
                        'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3308
                    ];
3309
                    if ($includerelations) {
3310
                        $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3311
                        $types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3312
                        $types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3313
                        $types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3314
                    }
3315
                    foreach ($types as $type => $attrs) {
3316
                        foreach ($attrs as $name => $spec) {
3317
                            $autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}", FormField::name_to_label($name));
3318
                        }
3319
                    }
3320
                }
3321
            }
3322
3323
            $labels = array_merge((array)$autoLabels, (array)$customLabels);
3324
            $this->extend('updateFieldLabels', $labels);
3325
            self::$_cache_field_labels[$cacheKey] = $labels;
3326
        }
3327
3328
        return self::$_cache_field_labels[$cacheKey];
3329
    }
3330
3331
    /**
3332
     * Get a human-readable label for a single field,
3333
     * see {@link fieldLabels()} for more details.
3334
     *
3335
     * @uses fieldLabels()
3336
     * @uses FormField::name_to_label()
3337
     *
3338
     * @param string $name Name of the field
3339
     * @return string Label of the field
3340
     */
3341
    public function fieldLabel($name)
3342
    {
3343
        $labels = $this->fieldLabels();
3344
        return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3345
    }
3346
3347
    /**
3348
     * Get the default summary fields for this object.
3349
     *
3350
     * @todo use the translation apparatus to return a default field selection for the language
3351
     *
3352
     * @return array
3353
     */
3354
    public function summaryFields()
3355
    {
3356
        $fields = $this->config()->get('summary_fields');
3357
3358
        // if fields were passed in numeric array,
3359
        // convert to an associative array
3360
        if ($fields && array_key_exists(0, $fields)) {
3361
            $fields = array_combine(array_values($fields), array_values($fields));
3362
        }
3363
3364
        if (!$fields) {
3365
            $fields = array();
3366
            // try to scaffold a couple of usual suspects
3367
            if ($this->hasField('Name')) {
3368
                $fields['Name'] = 'Name';
3369
            }
3370
            if (static::getSchema()->fieldSpec($this, 'Title')) {
3371
                $fields['Title'] = 'Title';
3372
            }
3373
            if ($this->hasField('Description')) {
3374
                $fields['Description'] = 'Description';
3375
            }
3376
            if ($this->hasField('FirstName')) {
3377
                $fields['FirstName'] = 'First Name';
3378
            }
3379
        }
3380
        $this->extend("updateSummaryFields", $fields);
3381
3382
        // Final fail-over, just list ID field
3383
        if (!$fields) {
3384
            $fields['ID'] = 'ID';
3385
        }
3386
3387
        // Localize fields (if possible)
3388
        foreach ($this->fieldLabels(false) as $name => $label) {
3389
            // only attempt to localize if the label definition is the same as the field name.
3390
            // this will preserve any custom labels set in the summary_fields configuration
3391
            if (isset($fields[$name]) && $name === $fields[$name]) {
3392
                $fields[$name] = $label;
3393
            }
3394
        }
3395
3396
        return $fields;
3397
    }
3398
3399
    /**
3400
     * Defines a default list of filters for the search context.
3401
     *
3402
     * If a filter class mapping is defined on the data object,
3403
     * it is constructed here. Otherwise, the default filter specified in
3404
     * {@link DBField} is used.
3405
     *
3406
     * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3407
     *
3408
     * @return array
3409
     */
3410
    public function defaultSearchFilters()
3411
    {
3412
        $filters = array();
3413
3414
        foreach ($this->searchableFields() as $name => $spec) {
3415
            if (empty($spec['filter'])) {
3416
                /** @skipUpgrade */
3417
                $filters[$name] = 'PartialMatchFilter';
3418
            } elseif ($spec['filter'] instanceof SearchFilter) {
3419
                $filters[$name] = $spec['filter'];
3420
            } else {
3421
                $filters[$name] = Injector::inst()->create($spec['filter'], $name);
3422
            }
3423
        }
3424
3425
        return $filters;
3426
    }
3427
3428
    /**
3429
     * @return boolean True if the object is in the database
3430
     */
3431
    public function isInDB()
3432
    {
3433
        return is_numeric($this->ID) && $this->ID > 0;
3434
    }
3435
3436
    /*
3437
	 * @ignore
3438
	 */
3439
    private static $subclass_access = true;
3440
3441
    /**
3442
     * Temporarily disable subclass access in data object qeur
3443
     */
3444
    public static function disable_subclass_access()
3445
    {
3446
        self::$subclass_access = false;
3447
    }
3448
    public static function enable_subclass_access()
3449
    {
3450
        self::$subclass_access = true;
3451
    }
3452
3453
    //-------------------------------------------------------------------------------------------//
3454
3455
    /**
3456
     * Database field definitions.
3457
     * This is a map from field names to field type. The field
3458
     * type should be a class that extends .
3459
     * @var array
3460
     * @config
3461
     */
3462
    private static $db = [];
3463
3464
    /**
3465
     * Use a casting object for a field. This is a map from
3466
     * field name to class name of the casting object.
3467
     *
3468
     * @var array
3469
     */
3470
    private static $casting = array(
3471
        "Title" => 'Text',
3472
    );
3473
3474
    /**
3475
     * Specify custom options for a CREATE TABLE call.
3476
     * Can be used to specify a custom storage engine for specific database table.
3477
     * All options have to be keyed for a specific database implementation,
3478
     * identified by their class name (extending from {@link SS_Database}).
3479
     *
3480
     * <code>
3481
     * array(
3482
     *  'MySQLDatabase' => 'ENGINE=MyISAM'
3483
     * )
3484
     * </code>
3485
     *
3486
     * Caution: This API is experimental, and might not be
3487
     * included in the next major release. Please use with care.
3488
     *
3489
     * @var array
3490
     * @config
3491
     */
3492
    private static $create_table_options = array(
3493
        MySQLSchemaManager::ID => 'ENGINE=InnoDB'
3494
    );
3495
3496
    /**
3497
     * If a field is in this array, then create a database index
3498
     * on that field. This is a map from fieldname to index type.
3499
     * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3500
     *
3501
     * @var array
3502
     * @config
3503
     */
3504
    private static $indexes = null;
3505
3506
    /**
3507
     * Inserts standard column-values when a DataObject
3508
     * is instanciated. Does not insert default records {@see $default_records}.
3509
     * This is a map from fieldname to default value.
3510
     *
3511
     *  - If you would like to change a default value in a sub-class, just specify it.
3512
     *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3513
     *    or false in your subclass.  Setting it to null won't work.
3514
     *
3515
     * @var array
3516
     * @config
3517
     */
3518
    private static $defaults = [];
3519
3520
    /**
3521
     * Multidimensional array which inserts default data into the database
3522
     * on a db/build-call as long as the database-table is empty. Please use this only
3523
     * for simple constructs, not for SiteTree-Objects etc. which need special
3524
     * behaviour such as publishing and ParentNodes.
3525
     *
3526
     * Example:
3527
     * array(
3528
     *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3529
     *  array('Title' => "DefaultPage2")
3530
     * ).
3531
     *
3532
     * @var array
3533
     * @config
3534
     */
3535
    private static $default_records = null;
3536
3537
    /**
3538
     * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3539
     * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3540
     *
3541
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3542
     *
3543
     *  @var array
3544
     * @config
3545
     */
3546
    private static $has_one = [];
3547
3548
    /**
3549
     * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3550
     *
3551
     * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3552
     * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3553
     * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3554
     *
3555
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3556
     *
3557
     * @var array
3558
     * @config
3559
     */
3560
    private static $belongs_to = [];
3561
3562
    /**
3563
     * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3564
     *
3565
     * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3566
     * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3567
     * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3568
     * which foreign key to use.
3569
     *
3570
     * @var array
3571
     * @config
3572
     */
3573
    private static $has_many = [];
3574
3575
    /**
3576
     * many-many relationship definitions.
3577
     * This is a map from component name to data type.
3578
     * @var array
3579
     * @config
3580
     */
3581
    private static $many_many = [];
3582
3583
    /**
3584
     * Extra fields to include on the connecting many-many table.
3585
     * This is a map from field name to field type.
3586
     *
3587
     * Example code:
3588
     * <code>
3589
     * public static $many_many_extraFields = array(
3590
     *  'Members' => array(
3591
     *          'Role' => 'Varchar(100)'
3592
     *      )
3593
     * );
3594
     * </code>
3595
     *
3596
     * @var array
3597
     * @config
3598
     */
3599
    private static $many_many_extraFields = [];
3600
3601
    /**
3602
     * The inverse side of a many-many relationship.
3603
     * This is a map from component name to data type.
3604
     * @var array
3605
     * @config
3606
     */
3607
    private static $belongs_many_many = [];
3608
3609
    /**
3610
     * The default sort expression. This will be inserted in the ORDER BY
3611
     * clause of a SQL query if no other sort expression is provided.
3612
     * @var string
3613
     * @config
3614
     */
3615
    private static $default_sort = null;
3616
3617
    /**
3618
     * Default list of fields that can be scaffolded by the ModelAdmin
3619
     * search interface.
3620
     *
3621
     * Overriding the default filter, with a custom defined filter:
3622
     * <code>
3623
     *  static $searchable_fields = array(
3624
     *     "Name" => "PartialMatchFilter"
3625
     *  );
3626
     * </code>
3627
     *
3628
     * Overriding the default form fields, with a custom defined field.
3629
     * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3630
     * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3631
     * <code>
3632
     *  static $searchable_fields = array(
3633
     *    "Name" => array(
3634
     *      "field" => "TextField"
3635
     *    )
3636
     *  );
3637
     * </code>
3638
     *
3639
     * Overriding the default form field, filter and title:
3640
     * <code>
3641
     *  static $searchable_fields = array(
3642
     *    "Organisation.ZipCode" => array(
3643
     *      "field" => "TextField",
3644
     *      "filter" => "PartialMatchFilter",
3645
     *      "title" => 'Organisation ZIP'
3646
     *    )
3647
     *  );
3648
     * </code>
3649
     * @config
3650
     */
3651
    private static $searchable_fields = null;
3652
3653
    /**
3654
     * User defined labels for searchable_fields, used to override
3655
     * default display in the search form.
3656
     * @config
3657
     */
3658
    private static $field_labels = [];
3659
3660
    /**
3661
     * Provides a default list of fields to be used by a 'summary'
3662
     * view of this object.
3663
     * @config
3664
     */
3665
    private static $summary_fields = [];
3666
3667
    public function provideI18nEntities()
3668
    {
3669
        // Note: see http://guides.rubyonrails.org/i18n.html#pluralization for rules
3670
        // Best guess for a/an rule. Better guesses require overriding in subclasses
3671
        $pluralName = $this->plural_name();
3672
        $singularName = $this->singular_name();
3673
        $conjunction = preg_match('/^[aeiou]/i', $singularName) ? 'An ' : 'A ';
3674
        return [
3675
            static::class.'.SINGULARNAME' => $this->singular_name(),
3676
            static::class.'.PLURALNAME' => $pluralName,
3677
            static::class.'.PLURALS' => [
3678
                'one' => $conjunction . $singularName,
3679
                'other' => '{count} ' . $pluralName
3680
            ]
3681
        ];
3682
    }
3683
3684
    /**
3685
     * Returns true if the given method/parameter has a value
3686
     * (Uses the DBField::hasValue if the parameter is a database field)
3687
     *
3688
     * @param string $field The field name
3689
     * @param array $arguments
3690
     * @param bool $cache
3691
     * @return boolean
3692
     */
3693
    public function hasValue($field, $arguments = null, $cache = true)
3694
    {
3695
        // has_one fields should not use dbObject to check if a value is given
3696
        $hasOne = static::getSchema()->hasOneComponent(static::class, $field);
3697
        if (!$hasOne && ($obj = $this->dbObject($field))) {
3698
            return $obj->exists();
3699
        } else {
3700
            return parent::hasValue($field, $arguments, $cache);
3701
        }
3702
    }
3703
3704
    /**
3705
     * If selected through a many_many through relation, this is the instance of the joined record
3706
     *
3707
     * @return DataObject
3708
     */
3709
    public function getJoin()
3710
    {
3711
        return $this->joinRecord;
3712
    }
3713
3714
    /**
3715
     * Set joining object
3716
     *
3717
     * @param DataObject $object
3718
     * @param string $alias Alias
3719
     * @return $this
3720
     */
3721
    public function setJoin(DataObject $object, $alias = null)
3722
    {
3723
        $this->joinRecord = $object;
3724
        if ($alias) {
3725
            if (static::getSchema()->fieldSpec(static::class, $alias)) {
3726
                throw new InvalidArgumentException(
3727
                    "Joined record $alias cannot also be a db field"
3728
                );
3729
            }
3730
            $this->record[$alias] = $object;
3731
        }
3732
        return $this;
3733
    }
3734
3735
    /**
3736
     * Find objects in the given relationships, merging them into the given list
3737
     *
3738
     * @param string $source Config property to extract relationships from
3739
     * @param bool $recursive True if recursive
3740
     * @param ArrayList $list If specified, items will be added to this list. If not, a new
3741
     * instance of ArrayList will be constructed and returned
3742
     * @return ArrayList The list of related objects
3743
     */
3744
    public function findRelatedObjects($source, $recursive = true, $list = null)
3745
    {
3746
        if (!$list) {
3747
            $list = new ArrayList();
3748
        }
3749
3750
        // Skip search for unsaved records
3751
        if (!$this->isInDB()) {
3752
            return $list;
3753
        }
3754
3755
        $relationships = $this->config()->get($source) ?: [];
3756
        foreach ($relationships as $relationship) {
3757
            // Warn if invalid config
3758
            if (!$this->hasMethod($relationship)) {
3759
                trigger_error(sprintf(
3760
                    "Invalid %s config value \"%s\" on object on class \"%s\"",
3761
                    $source,
3762
                    $relationship,
3763
                    get_class($this)
3764
                ), E_USER_WARNING);
3765
                continue;
3766
            }
3767
3768
            // Inspect value of this relationship
3769
            $items = $this->{$relationship}();
3770
3771
            // Merge any new item
3772
            $newItems = $this->mergeRelatedObjects($list, $items);
3773
3774
            // Recurse if necessary
3775
            if ($recursive) {
3776
                foreach ($newItems as $item) {
3777
                    /** @var DataObject $item */
3778
                    $item->findRelatedObjects($source, true, $list);
3779
                }
3780
            }
3781
        }
3782
        return $list;
3783
    }
3784
3785
    /**
3786
     * Helper method to merge owned/owning items into a list.
3787
     * Items already present in the list will be skipped.
3788
     *
3789
     * @param ArrayList $list Items to merge into
3790
     * @param mixed $items List of new items to merge
3791
     * @return ArrayList List of all newly added items that did not already exist in $list
3792
     */
3793
    public function mergeRelatedObjects($list, $items)
3794
    {
3795
        $added = new ArrayList();
3796
        if (!$items) {
3797
            return $added;
3798
        }
3799
        if ($items instanceof DataObject) {
3800
            $items = [$items];
3801
        }
3802
3803
        /** @var DataObject $item */
3804
        foreach ($items as $item) {
3805
            $this->mergeRelatedObject($list, $added, $item);
3806
        }
3807
        return $added;
3808
    }
3809
3810
    /**
3811
     * Merge single object into a list, but ensures that existing objects are not
3812
     * re-added.
3813
     *
3814
     * @param ArrayList $list Global list
3815
     * @param ArrayList $added Additional list to insert into
3816
     * @param DataObject $item Item to add
3817
     */
3818
    protected function mergeRelatedObject($list, $added, $item)
3819
    {
3820
        // Identify item
3821
        $itemKey = get_class($item) . '/' . $item->ID;
3822
3823
        // Write if saved, versioned, and not already added
3824
        if ($item->isInDB() && !isset($list[$itemKey])) {
3825
            $list[$itemKey] = $item;
3826
            $added[$itemKey] = $item;
3827
        }
3828
3829
        // Add joined record (from many_many through) automatically
3830
        $joined = $item->getJoin();
3831
        if ($joined) {
3832
            $this->mergeRelatedObject($list, $added, $joined);
3833
        }
3834
    }
3835
}
3836