Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

DataObject::i18n_singular_name()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use BadMethodCallException;
6
use Exception;
7
use InvalidArgumentException;
8
use LogicException;
9
use SilverStripe\Control\HTTP;
10
use SilverStripe\Core\ClassInfo;
11
use SilverStripe\Core\Config\Config;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Core\Resettable;
14
use SilverStripe\Dev\Debug;
15
use SilverStripe\Dev\Deprecation;
16
use SilverStripe\Forms\FieldList;
17
use SilverStripe\Forms\FormField;
18
use SilverStripe\Forms\FormScaffolder;
19
use SilverStripe\i18n\i18n;
20
use SilverStripe\i18n\i18nEntityProvider;
21
use SilverStripe\ORM\Connect\MySQLSchemaManager;
22
use SilverStripe\ORM\FieldType\DBClassName;
23
use SilverStripe\ORM\FieldType\DBComposite;
24
use SilverStripe\ORM\FieldType\DBDatetime;
25
use SilverStripe\ORM\FieldType\DBField;
26
use SilverStripe\ORM\Filters\SearchFilter;
27
use SilverStripe\ORM\Queries\SQLDelete;
28
use SilverStripe\ORM\Queries\SQLInsert;
29
use SilverStripe\ORM\Search\SearchContext;
30
use SilverStripe\Security\Member;
31
use SilverStripe\Security\Permission;
32
use SilverStripe\Security\Security;
33
use SilverStripe\View\SSViewer;
34
use SilverStripe\View\ViewableData;
35
use stdClass;
36
37
/**
38
 * A single database record & abstract class for the data-access-model.
39
 *
40
 * <h2>Extensions</h2>
41
 *
42
 * See {@link Extension} and {@link DataExtension}.
43
 *
44
 * <h2>Permission Control</h2>
45
 *
46
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
47
 * strings which can be selected on a group-by-group basis.
48
 *
49
 * <code>
50
 * class Article extends DataObject implements PermissionProvider {
51
 *  static $api_access = true;
52
 *
53
 *  function canView($member = false) {
54
 *    return Permission::check('ARTICLE_VIEW');
55
 *  }
56
 *  function canEdit($member = false) {
57
 *    return Permission::check('ARTICLE_EDIT');
58
 *  }
59
 *  function canDelete() {
60
 *    return Permission::check('ARTICLE_DELETE');
61
 *  }
62
 *  function canCreate() {
63
 *    return Permission::check('ARTICLE_CREATE');
64
 *  }
65
 *  function providePermissions() {
66
 *    return array(
67
 *      'ARTICLE_VIEW' => 'Read an article object',
68
 *      'ARTICLE_EDIT' => 'Edit an article object',
69
 *      'ARTICLE_DELETE' => 'Delete an article object',
70
 *      'ARTICLE_CREATE' => 'Create an article object',
71
 *    );
72
 *  }
73
 * }
74
 * </code>
75
 *
76
 * Object-level access control by {@link Group} membership:
77
 * <code>
78
 * class Article extends DataObject {
79
 *   static $api_access = true;
80
 *
81
 *   function canView($member = false) {
82
 *     if(!$member) $member = Security::getCurrentUser();
83
 *     return $member->inGroup('Subscribers');
84
 *   }
85
 *   function canEdit($member = false) {
86
 *     if(!$member) $member = Security::getCurrentUser();
87
 *     return $member->inGroup('Editors');
88
 *   }
89
 *
90
 *   // ...
91
 * }
92
 * </code>
93
 *
94
 * If any public method on this class is prefixed with an underscore,
95
 * the results are cached in memory through {@link cachedCall()}.
96
 *
97
 *
98
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
99
 *  and defineMethods()
100
 *
101
 * @property int $ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
102
 * @property int $OldID ID of object, if deleted
103
 * @property string $Title
104
 * @property string $ClassName Class name of the DataObject
105
 * @property string $LastEdited Date and time of DataObject's last modification.
106
 * @property string $Created Date and time of DataObject creation.
107
 */
108
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable
109
{
110
111
    /**
112
     * Human-readable singular name.
113
     * @var string
114
     * @config
115
     */
116
    private static $singular_name = null;
117
118
    /**
119
     * Human-readable plural name
120
     * @var string
121
     * @config
122
     */
123
    private static $plural_name = null;
124
125
    /**
126
     * Allow API access to this object?
127
     * @todo Define the options that can be set here
128
     * @config
129
     */
130
    private static $api_access = false;
131
132
    /**
133
     * Allows specification of a default value for the ClassName field.
134
     * Configure this value only in subclasses of DataObject.
135
     *
136
     * @config
137
     * @var string
138
     */
139
    private static $default_classname = null;
140
141
    /**
142
     * @deprecated 4.0..5.0
143
     * @var bool
144
     */
145
    public $destroyed = false;
146
147
    /**
148
     * Data stored in this objects database record. An array indexed by fieldname.
149
     *
150
     * Use {@link toMap()} if you want an array representation
151
     * of this object, as the $record array might contain lazy loaded field aliases.
152
     *
153
     * @var array
154
     */
155
    protected $record;
156
157
    /**
158
     * If selected through a many_many through relation, this is the instance of the through record
159
     *
160
     * @var DataObject
161
     */
162
    protected $joinRecord;
163
164
    /**
165
     * Represents a field that hasn't changed (before === after, thus before == after)
166
     */
167
    const CHANGE_NONE = 0;
168
169
    /**
170
     * Represents a field that has changed type, although not the loosely defined value.
171
     * (before !== after && before == after)
172
     * E.g. change 1 to true or "true" to true, but not true to 0.
173
     * Value changes are by nature also considered strict changes.
174
     */
175
    const CHANGE_STRICT = 1;
176
177
    /**
178
     * Represents a field that has changed the loosely defined value
179
     * (before != after, thus, before !== after))
180
     * E.g. change false to true, but not false to 0
181
     */
182
    const CHANGE_VALUE = 2;
183
184
    /**
185
     * An array indexed by fieldname, true if the field has been changed.
186
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
187
     * the changed state.
188
     *
189
     * @var array
190
     */
191
    private $changed;
192
193
    /**
194
     * The database record (in the same format as $record), before
195
     * any changes.
196
     * @var array
197
     */
198
    protected $original;
199
200
    /**
201
     * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
202
     * @var boolean
203
     */
204
    protected $brokenOnDelete = false;
205
206
    /**
207
     * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
208
     * @var boolean
209
     */
210
    protected $brokenOnWrite = false;
211
212
    /**
213
     * @config
214
     * @var boolean Should dataobjects be validated before they are written?
215
     * Caution: Validation can contain safeguards against invalid/malicious data,
216
     * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
217
     * to only disable validation for very specific use cases.
218
     */
219
    private static $validation_enabled = true;
220
221
    /**
222
     * Static caches used by relevant functions.
223
     *
224
     * @var array
225
     */
226
    protected static $_cache_get_one;
227
228
    /**
229
     * Cache of field labels
230
     *
231
     * @var array
232
     */
233
    protected static $_cache_field_labels = array();
234
235
    /**
236
     * Base fields which are not defined in static $db
237
     *
238
     * @config
239
     * @var array
240
     */
241
    private static $fixed_fields = array(
242
        'ID' => 'PrimaryKey',
243
        'ClassName' => 'DBClassName',
244
        'LastEdited' => 'DBDatetime',
245
        'Created' => 'DBDatetime',
246
    );
247
248
    /**
249
     * Override table name for this class. If ignored will default to FQN of class.
250
     * This option is not inheritable, and must be set on each class.
251
     * If left blank naming will default to the legacy (3.x) behaviour.
252
     *
253
     * @var string
254
     */
255
    private static $table_name = null;
256
257
    /**
258
     * Non-static relationship cache, indexed by component name.
259
     *
260
     * @var DataObject[]
261
     */
262
    protected $components;
263
264
    /**
265
     * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
266
     *
267
     * @var UnsavedRelationList[]
268
     */
269
    protected $unsavedRelations;
270
271
    /**
272
     * List of relations that should be cascade deleted, similar to `owns`
273
     * Note: This will trigger delete on many_many objects, not only the mapping table.
274
     * For many_many through you can specify the components you want to delete separately
275
     * (many_many or has_many sub-component)
276
     *
277
     * @config
278
     * @var array
279
     */
280
    private static $cascade_deletes = [];
281
282
    /**
283
     * Get schema object
284
     *
285
     * @return DataObjectSchema
286
     */
287
    public static function getSchema()
288
    {
289
        return Injector::inst()->get(DataObjectSchema::class);
290
    }
291
292
    /**
293
     * Construct a new DataObject.
294
     *
295
296
     * @param array|null $record Used internally for rehydrating an object from database content.
297
     *                           Bypasses setters on this class, and hence should not be used
298
     *                           for populating data on new records.
299
     * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
300
     *                             Singletons don't have their defaults set.
301
     * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
302
     */
303
    public function __construct($record = null, $isSingleton = false, $queryParams = array())
304
    {
305
        parent::__construct();
306
307
        // Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
308
        $this->setSourceQueryParams($queryParams);
309
310
        // Set the fields data.
311
        if (!$record) {
312
            $record = array(
313
                'ID' => 0,
314
                'ClassName' => static::class,
315
                'RecordClassName' => static::class
316
            );
317
        }
318
319
        if ($record instanceof stdClass) {
320
            $record = (array)$record;
321
        }
322
323
        if (!is_array($record)) {
324
            if (is_object($record)) {
325
                $passed = "an object of type '".get_class($record)."'";
326
            } else {
327
                $passed = "The value '$record'";
328
            }
329
330
            user_error(
331
                "DataObject::__construct passed $passed.  It's supposed to be passed an array,"
332
                . " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
333
                E_USER_WARNING
334
            );
335
            $record = null;
336
        }
337
338
        // Set $this->record to $record, but ignore NULLs
339
        $this->record = array();
340
        foreach ($record as $k => $v) {
341
            // Ensure that ID is stored as a number and not a string
342
            // To do: this kind of clean-up should be done on all numeric fields, in some relatively
343
            // performant manner
344
            if ($v !== null) {
345
                if ($k == 'ID' && is_numeric($v)) {
346
                    $this->record[$k] = (int)$v;
347
                } else {
348
                    $this->record[$k] = $v;
349
                }
350
            }
351
        }
352
353
        // Identify fields that should be lazy loaded, but only on existing records
354
        if (!empty($record['ID'])) {
355
            // Get all field specs scoped to class for later lazy loading
356
            $fields = static::getSchema()->fieldSpecs(
357
                static::class,
358
                DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
359
            );
360
            foreach ($fields as $field => $fieldSpec) {
361
                $fieldClass = strtok($fieldSpec, ".");
362
                if (!array_key_exists($field, $record)) {
363
                    $this->record[$field.'_Lazy'] = $fieldClass;
364
                }
365
            }
366
        }
367
368
        $this->original = $this->record;
369
370
        // Keep track of the modification date of all the data sourced to make this page
371
        // From this we create a Last-Modified HTTP header
372
        if (isset($record['LastEdited'])) {
373
            HTTP::register_modification_date($record['LastEdited']);
374
        }
375
376
        // Must be called after parent constructor
377
        if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
378
            $this->populateDefaults();
379
        }
380
381
        // prevent populateDefaults() and setField() from marking overwritten defaults as changed
382
        $this->changed = array();
383
    }
384
385
    /**
386
     * Destroy all of this objects dependant objects and local caches.
387
     * You'll need to call this to get the memory of an object that has components or extensions freed.
388
     */
389
    public function destroy()
390
    {
391
        $this->flushCache(false);
392
    }
393
394
    /**
395
     * Create a duplicate of this node. Can duplicate many_many relations
396
     *
397
     * @param bool $doWrite Perform a write() operation before returning the object.
398
     * If this is true, it will create the duplicate in the database.
399
     * @param bool|string $manyMany Which many-many to duplicate. Set to true to duplicate all, false to duplicate none.
400
     * Alternatively set to the string of the relation config to duplicate
401
     * (supports 'many_many', or 'belongs_many_many')
402
     * @return static A duplicate of this node. The exact type will be the type of this node.
403
     */
404
    public function duplicate($doWrite = true, $manyMany = 'many_many')
405
    {
406
        $map = $this->toMap();
407
        unset($map['Created']);
408
        /** @var static $clone */
409
        $clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams());
410
        $clone->ID = 0;
411
412
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $manyMany);
413
        if ($manyMany) {
414
            $this->duplicateManyManyRelations($this, $clone, $manyMany);
415
        }
416
        if ($doWrite) {
417
            $clone->write();
418
        }
419
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $manyMany);
420
421
        return $clone;
422
    }
423
424
    /**
425
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
426
     *
427
     * @param DataObject $sourceObject the source object to duplicate from
428
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
429
     * @param bool|string $filter
430
     */
431
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
432
    {
433
        // Get list of relations to duplicate
434
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
435
            $relations = $sourceObject->config()->get($filter);
436
        } elseif ($filter === true) {
437
            $relations = $sourceObject->manyMany();
438
        } else {
439
            throw new InvalidArgumentException("Invalid many_many duplication filter");
440
        }
441
        foreach ($relations as $manyManyName => $type) {
442
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
443
        }
444
    }
445
446
    /**
447
     * Duplicates a single many_many relation from one object to another
448
     *
449
     * @param DataObject $sourceObject
450
     * @param DataObject $destinationObject
451
     * @param string $manyManyName
452
     */
453
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName)
454
    {
455
        // Ensure this component exists on the destination side as well
456
        if (!static::getSchema()->manyManyComponent(get_class($destinationObject), $manyManyName)) {
457
            return;
458
        }
459
460
        // Copy all components from source to destination
461
        $source = $sourceObject->getManyManyComponents($manyManyName);
462
        $dest = $destinationObject->getManyManyComponents($manyManyName);
463
        foreach ($source as $item) {
464
            $dest->add($item);
465
        }
466
    }
467
468
    /**
469
     * Return obsolete class name, if this is no longer a valid class
470
     *
471
     * @return string
472
     */
473
    public function getObsoleteClassName()
474
    {
475
        $className = $this->getField("ClassName");
476
        if (!ClassInfo::exists($className)) {
0 ignored issues
show
Bug introduced by
It seems like $className can also be of type object; however, parameter $class of SilverStripe\Core\ClassInfo::exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

476
        if (!ClassInfo::exists(/** @scrutinizer ignore-type */ $className)) {
Loading history...
477
            return $className;
478
        }
479
        return null;
480
    }
481
482
    /**
483
     * Gets name of this class
484
     *
485
     * @return string
486
     */
487
    public function getClassName()
488
    {
489
        $className = $this->getField("ClassName");
490
        if (!ClassInfo::exists($className)) {
0 ignored issues
show
Bug introduced by
It seems like $className can also be of type object; however, parameter $class of SilverStripe\Core\ClassInfo::exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

490
        if (!ClassInfo::exists(/** @scrutinizer ignore-type */ $className)) {
Loading history...
491
            return static::class;
492
        }
493
        return $className;
494
    }
495
496
    /**
497
     * Set the ClassName attribute. {@link $class} is also updated.
498
     * Warning: This will produce an inconsistent record, as the object
499
     * instance will not automatically switch to the new subclass.
500
     * Please use {@link newClassInstance()} for this purpose,
501
     * or destroy and reinstanciate the record.
502
     *
503
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
504
     * @return $this
505
     */
506
    public function setClassName($className)
507
    {
508
        $className = trim($className);
509
        if (!$className || !is_subclass_of($className, self::class)) {
510
            return $this;
511
        }
512
513
        $this->setField("ClassName", $className);
514
        $this->setField('RecordClassName', $className);
515
        return $this;
516
    }
517
518
    /**
519
     * Create a new instance of a different class from this object's record.
520
     * This is useful when dynamically changing the type of an instance. Specifically,
521
     * it ensures that the instance of the class is a match for the className of the
522
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
523
     * property manually before calling this method, as it will confuse change detection.
524
     *
525
     * If the new class is different to the original class, defaults are populated again
526
     * because this will only occur automatically on instantiation of a DataObject if
527
     * there is no record, or the record has no ID. In this case, we do have an ID but
528
     * we still need to repopulate the defaults.
529
     *
530
     * @param string $newClassName The name of the new class
531
     *
532
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
533
     */
534
    public function newClassInstance($newClassName)
535
    {
536
        if (!is_subclass_of($newClassName, self::class)) {
537
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
538
        }
539
540
        $originalClass = $this->ClassName;
541
542
        /** @var DataObject $newInstance */
543
        $newInstance = Injector::inst()->create($newClassName, $this->record, false);
544
545
        // Modify ClassName
546
        if ($newClassName != $originalClass) {
547
            $newInstance->setClassName($newClassName);
548
            $newInstance->populateDefaults();
549
            $newInstance->forceChange();
550
        }
551
552
        return $newInstance;
553
    }
554
555
    /**
556
     * Adds methods from the extensions.
557
     * Called by Object::__construct() once per class.
558
     */
559
    public function defineMethods()
560
    {
561
        parent::defineMethods();
562
563
        if (static::class === self::class) {
564
             return;
565
        }
566
567
        // Set up accessors for joined items
568
        if ($manyMany = $this->manyMany()) {
569
            foreach ($manyMany as $relationship => $class) {
570
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
571
            }
572
        }
573
        if ($hasMany = $this->hasMany()) {
574
            foreach ($hasMany as $relationship => $class) {
575
                $this->addWrapperMethod($relationship, 'getComponents');
576
            }
577
        }
578
        if ($hasOne = $this->hasOne()) {
579
            foreach ($hasOne as $relationship => $class) {
580
                $this->addWrapperMethod($relationship, 'getComponent');
581
            }
582
        }
583
        if ($belongsTo = $this->belongsTo()) {
584
            foreach (array_keys($belongsTo) as $relationship) {
585
                $this->addWrapperMethod($relationship, 'getComponent');
586
            }
587
        }
588
    }
589
590
    /**
591
     * Returns true if this object "exists", i.e., has a sensible value.
592
     * The default behaviour for a DataObject is to return true if
593
     * the object exists in the database, you can override this in subclasses.
594
     *
595
     * @return boolean true if this object exists
596
     */
597
    public function exists()
598
    {
599
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
600
    }
601
602
    /**
603
     * Returns TRUE if all values (other than "ID") are
604
     * considered empty (by weak boolean comparison).
605
     *
606
     * @return boolean
607
     */
608
    public function isEmpty()
609
    {
610
        $fixed = DataObject::config()->uninherited('fixed_fields');
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
611
        foreach ($this->toMap() as $field => $value) {
612
            // only look at custom fields
613
            if (isset($fixed[$field])) {
614
                continue;
615
            }
616
617
            $dbObject = $this->dbObject($field);
618
            if (!$dbObject) {
619
                continue;
620
            }
621
            if ($dbObject->exists()) {
622
                return false;
623
            }
624
        }
625
        return true;
626
    }
627
628
    /**
629
     * Pluralise this item given a specific count.
630
     *
631
     * E.g. "0 Pages", "1 File", "3 Images"
632
     *
633
     * @param string $count
634
     * @return string
635
     */
636
    public function i18n_pluralise($count)
637
    {
638
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
639
        return i18n::_t(
640
            static::class.'.PLURALS',
641
            $default,
642
            [ 'count' => $count ]
643
        );
644
    }
645
646
    /**
647
     * Get the user friendly singular name of this DataObject.
648
     * If the name is not defined (by redefining $singular_name in the subclass),
649
     * this returns the class name.
650
     *
651
     * @return string User friendly singular name of this DataObject
652
     */
653
    public function singular_name()
654
    {
655
        $name = $this->config()->get('singular_name');
656
        if ($name) {
657
            return $name;
658
        }
659
        return ucwords(trim(strtolower(preg_replace(
660
            '/_?([A-Z])/',
661
            ' $1',
662
            ClassInfo::shortName($this)
663
        ))));
664
    }
665
666
    /**
667
     * Get the translated user friendly singular name of this DataObject
668
     * same as singular_name() but runs it through the translating function
669
     *
670
     * Translating string is in the form:
671
     *     $this->class.SINGULARNAME
672
     * Example:
673
     *     Page.SINGULARNAME
674
     *
675
     * @return string User friendly translated singular name of this DataObject
676
     */
677
    public function i18n_singular_name()
678
    {
679
        return _t(static::class.'.SINGULARNAME', $this->singular_name());
680
    }
681
682
    /**
683
     * Get the user friendly plural name of this DataObject
684
     * If the name is not defined (by renaming $plural_name in the subclass),
685
     * this returns a pluralised version of the class name.
686
     *
687
     * @return string User friendly plural name of this DataObject
688
     */
689
    public function plural_name()
690
    {
691
        if ($name = $this->config()->get('plural_name')) {
692
            return $name;
693
        }
694
        $name = $this->singular_name();
695
        //if the penultimate character is not a vowel, replace "y" with "ies"
696
        if (preg_match('/[^aeiou]y$/i', $name)) {
697
            $name = substr($name, 0, -1) . 'ie';
698
        }
699
        return ucfirst($name . 's');
700
    }
701
702
    /**
703
     * Get the translated user friendly plural name of this DataObject
704
     * Same as plural_name but runs it through the translation function
705
     * Translation string is in the form:
706
     *      $this->class.PLURALNAME
707
     * Example:
708
     *      Page.PLURALNAME
709
     *
710
     * @return string User friendly translated plural name of this DataObject
711
     */
712
    public function i18n_plural_name()
713
    {
714
        return _t(static::class.'.PLURALNAME', $this->plural_name());
715
    }
716
717
    /**
718
     * Standard implementation of a title/label for a specific
719
     * record. Tries to find properties 'Title' or 'Name',
720
     * and falls back to the 'ID'. Useful to provide
721
     * user-friendly identification of a record, e.g. in errormessages
722
     * or UI-selections.
723
     *
724
     * Overload this method to have a more specialized implementation,
725
     * e.g. for an Address record this could be:
726
     * <code>
727
     * function getTitle() {
728
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
729
     * }
730
     * </code>
731
     *
732
     * @return string
733
     */
734
    public function getTitle()
735
    {
736
        $schema = static::getSchema();
737
        if ($schema->fieldSpec($this, 'Title')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema->fieldSpec($this, 'Title') of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
738
            return $this->getField('Title');
739
        }
740
        if ($schema->fieldSpec($this, 'Name')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema->fieldSpec($this, 'Name') of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
741
            return $this->getField('Name');
742
        }
743
744
        return "#{$this->ID}";
745
    }
746
747
    /**
748
     * Returns the associated database record - in this case, the object itself.
749
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
750
     *
751
     * @return DataObject Associated database record
752
     */
753
    public function data()
754
    {
755
        return $this;
756
    }
757
758
    /**
759
     * Convert this object to a map.
760
     *
761
     * @return array The data as a map.
762
     */
763
    public function toMap()
764
    {
765
        $this->loadLazyFields();
766
        return $this->record;
767
    }
768
769
    /**
770
     * Return all currently fetched database fields.
771
     *
772
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
773
     * Obviously, this makes it a lot faster.
774
     *
775
     * @return array The data as a map.
776
     */
777
    public function getQueriedDatabaseFields()
778
    {
779
        return $this->record;
780
    }
781
782
    /**
783
     * Update a number of fields on this object, given a map of the desired changes.
784
     *
785
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
786
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
787
     *
788
     * update() doesn't write the main object, but if you use the dot syntax, it will write()
789
     * the related objects that it alters.
790
     *
791
     * @param array $data A map of field name to data values to update.
792
     * @return DataObject $this
793
     */
794
    public function update($data)
795
    {
796
        foreach ($data as $key => $value) {
797
            // Implement dot syntax for updates
798
            if (strpos($key, '.') !== false) {
799
                $relations = explode('.', $key);
800
                $fieldName = array_pop($relations);
801
                /** @var static $relObj */
802
                $relObj = $this;
803
                $relation = null;
804
                foreach ($relations as $i => $relation) {
805
                    // no support for has_many or many_many relationships,
806
                    // as the updater wouldn't know which object to write to (or create)
807
                    if ($relObj->$relation() instanceof DataObject) {
808
                        $parentObj = $relObj;
809
                        $relObj = $relObj->$relation();
810
                        // If the intermediate relationship objects haven't been created, then write them
811
                        if ($i<sizeof($relations)-1 && !$relObj->ID || (!$relObj->ID && $parentObj !== $this)) {
0 ignored issues
show
Bug introduced by
The call to sizeof() has too few arguments starting with mode. ( Ignorable by Annotation )

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

811
                        if ($i</** @scrutinizer ignore-call */ sizeof($relations)-1 && !$relObj->ID || (!$relObj->ID && $parentObj !== $this)) {

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
812
                            $relObj->write();
813
                            $relatedFieldName = $relation."ID";
814
                            $parentObj->$relatedFieldName = $relObj->ID;
815
                            $parentObj->write();
816
                        }
817
                    } else {
818
                        user_error(
819
                            "DataObject::update(): Can't traverse relationship '$relation'," .
820
                            "it has to be a has_one relationship or return a single DataObject",
821
                            E_USER_NOTICE
822
                        );
823
                        // unset relation object so we don't write properties to the wrong object
824
                        $relObj = null;
825
                        break;
826
                    }
827
                }
828
829
                if ($relObj) {
830
                    $relObj->$fieldName = $value;
831
                    $relObj->write();
832
                    $relatedFieldName = $relation."ID";
833
                    $this->$relatedFieldName = $relObj->ID;
834
                    $relObj->flushCache();
835
                } else {
836
                    $class = static::class;
837
                    user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
838
                }
839
            } else {
840
                $this->$key = $value;
841
            }
842
        }
843
        return $this;
844
    }
845
846
    /**
847
     * Pass changes as a map, and try to
848
     * get automatic casting for these fields.
849
     * Doesn't write to the database. To write the data,
850
     * use the write() method.
851
     *
852
     * @param array $data A map of field name to data values to update.
853
     * @return DataObject $this
854
     */
855
    public function castedUpdate($data)
856
    {
857
        foreach ($data as $k => $v) {
858
            $this->setCastedField($k, $v);
859
        }
860
        return $this;
861
    }
862
863
    /**
864
     * Merges data and relations from another object of same class,
865
     * without conflict resolution. Allows to specify which
866
     * dataset takes priority in case its not empty.
867
     * has_one-relations are just transferred with priority 'right'.
868
     * has_many and many_many-relations are added regardless of priority.
869
     *
870
     * Caution: has_many/many_many relations are moved rather than duplicated,
871
     * meaning they are not connected to the merged object any longer.
872
     * Caution: Just saves updated has_many/many_many relations to the database,
873
     * doesn't write the updated object itself (just writes the object-properties).
874
     * Caution: Does not delete the merged object.
875
     * Caution: Does now overwrite Created date on the original object.
876
     *
877
     * @param DataObject $rightObj
878
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
879
     * @param bool $includeRelations Merge any existing relations (optional)
880
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
881
     *                            Only applicable with $priority='right'. (optional)
882
     * @return Boolean
883
     */
884
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
885
    {
886
        $leftObj = $this;
887
888
        if ($leftObj->ClassName != $rightObj->ClassName) {
889
            // we can't merge similiar subclasses because they might have additional relations
890
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
891
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
892
            return false;
893
        }
894
895
        if (!$rightObj->ID) {
896
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
897
				to make sure all relations are transferred properly.').", E_USER_WARNING);
898
            return false;
899
        }
900
901
        // makes sure we don't merge data like ID or ClassName
902
        $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
903
        foreach ($rightData as $key => $rightSpec) {
904
            // Don't merge ID
905
            if ($key === 'ID') {
906
                continue;
907
            }
908
909
            // Only merge relations if allowed
910
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
911
                continue;
912
            }
913
914
            // don't merge conflicting values if priority is 'left'
915
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
916
                continue;
917
            }
918
919
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
920
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
921
                continue;
922
            }
923
924
            // TODO remove redundant merge of has_one fields
925
            $leftObj->{$key} = $rightObj->{$key};
926
        }
927
928
        // merge relations
929
        if ($includeRelations) {
930
            if ($manyMany = $this->manyMany()) {
931
                foreach ($manyMany as $relationship => $class) {
932
                    /** @var DataObject $leftComponents */
933
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
934
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
935
                    if ($rightComponents && $rightComponents->exists()) {
936
                        $leftComponents->addMany($rightComponents->column('ID'));
937
                    }
938
                    $leftComponents->write();
939
                }
940
            }
941
942
            if ($hasMany = $this->hasMany()) {
943
                foreach ($hasMany as $relationship => $class) {
944
                    $leftComponents = $leftObj->getComponents($relationship);
945
                    $rightComponents = $rightObj->getComponents($relationship);
946
                    if ($rightComponents && $rightComponents->exists()) {
947
                        $leftComponents->addMany($rightComponents->column('ID'));
948
                    }
949
                    $leftComponents->write();
0 ignored issues
show
Bug introduced by
The method write() does not exist on SilverStripe\ORM\UnsavedRelationList. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

949
                    $leftComponents->/** @scrutinizer ignore-call */ 
950
                                     write();
Loading history...
950
                }
951
            }
952
        }
953
954
        return true;
955
    }
956
957
    /**
958
     * Forces the record to think that all its data has changed.
959
     * Doesn't write to the database. Only sets fields as changed
960
     * if they are not already marked as changed.
961
     *
962
     * @return $this
963
     */
964
    public function forceChange()
965
    {
966
        // Ensure lazy fields loaded
967
        $this->loadLazyFields();
968
        $fields = static::getSchema()->fieldSpecs(static::class);
969
970
        // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
971
        $fieldNames = array_unique(array_merge(
972
            array_keys($this->record),
973
            array_keys($fields)
974
        ));
975
976
        foreach ($fieldNames as $fieldName) {
977
            if (!isset($this->changed[$fieldName])) {
978
                $this->changed[$fieldName] = self::CHANGE_STRICT;
979
            }
980
            // Populate the null values in record so that they actually get written
981
            if (!isset($this->record[$fieldName])) {
982
                $this->record[$fieldName] = null;
983
            }
984
        }
985
986
        // @todo Find better way to allow versioned to write a new version after forceChange
987
        if ($this->isChanged('Version')) {
988
            unset($this->changed['Version']);
989
        }
990
        return $this;
991
    }
992
993
    /**
994
     * Validate the current object.
995
     *
996
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
997
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
998
     *
999
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1000
     * and onAfterWrite() won't get called either.
1001
     *
1002
     * It is expected that you call validate() in your own application to test that an object is valid before
1003
     * attempting a write, and respond appropriately if it isn't.
1004
     *
1005
     * @see {@link ValidationResult}
1006
     * @return ValidationResult
1007
     */
1008
    public function validate()
1009
    {
1010
        $result = ValidationResult::create();
1011
        $this->extend('validate', $result);
1012
        return $result;
1013
    }
1014
1015
    /**
1016
     * Public accessor for {@see DataObject::validate()}
1017
     *
1018
     * @return ValidationResult
1019
     */
1020
    public function doValidate()
1021
    {
1022
        Deprecation::notice('5.0', 'Use validate');
1023
        return $this->validate();
1024
    }
1025
1026
    /**
1027
     * Event handler called before writing to the database.
1028
     * You can overload this to clean up or otherwise process data before writing it to the
1029
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1030
     *
1031
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1032
     *
1033
     * @uses DataExtension->onBeforeWrite()
1034
     */
1035
    protected function onBeforeWrite()
1036
    {
1037
        $this->brokenOnWrite = false;
1038
1039
        $dummy = null;
1040
        $this->extend('onBeforeWrite', $dummy);
1041
    }
1042
1043
    /**
1044
     * Event handler called after writing to the database.
1045
     * You can overload this to act upon changes made to the data after it is written.
1046
     * $this->changed will have a record
1047
     * database.  Don't forget to call parent::onAfterWrite(), though!
1048
     *
1049
     * @uses DataExtension->onAfterWrite()
1050
     */
1051
    protected function onAfterWrite()
1052
    {
1053
        $dummy = null;
1054
        $this->extend('onAfterWrite', $dummy);
1055
    }
1056
1057
    /**
1058
     * Find all objects that will be cascade deleted if this object is deleted
1059
     *
1060
     * Notes:
1061
     *   - If this object is versioned, objects will only be searched in the same stage as the given record.
1062
     *   - This will only be useful prior to deletion, as post-deletion this record will no longer exist.
1063
     *
1064
     * @param bool $recursive True if recursive
1065
     * @param ArrayList $list Optional list to add items to
1066
     * @return ArrayList list of objects
1067
     */
1068
    public function findCascadeDeletes($recursive = true, $list = null)
1069
    {
1070
        // Find objects in these relationships
1071
        return $this->findRelatedObjects('cascade_deletes', $recursive, $list);
1072
    }
1073
1074
    /**
1075
     * Event handler called before deleting from the database.
1076
     * You can overload this to clean up or otherwise process data before delete this
1077
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1078
     *
1079
     * @uses DataExtension->onBeforeDelete()
1080
     */
1081
    protected function onBeforeDelete()
1082
    {
1083
        $this->brokenOnDelete = false;
1084
1085
        $dummy = null;
1086
        $this->extend('onBeforeDelete', $dummy);
1087
1088
        // Cascade deletes
1089
        $deletes = $this->findCascadeDeletes(false);
1090
        foreach ($deletes as $delete) {
1091
            $delete->delete();
1092
        }
1093
    }
1094
1095
    protected function onAfterDelete()
1096
    {
1097
        $this->extend('onAfterDelete');
1098
    }
1099
1100
    /**
1101
     * Load the default values in from the self::$defaults array.
1102
     * Will traverse the defaults of the current class and all its parent classes.
1103
     * Called by the constructor when creating new records.
1104
     *
1105
     * @uses DataExtension->populateDefaults()
1106
     * @return DataObject $this
1107
     */
1108
    public function populateDefaults()
1109
    {
1110
        $classes = array_reverse(ClassInfo::ancestry($this));
1111
1112
        foreach ($classes as $class) {
1113
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1114
1115
            if ($defaults && !is_array($defaults)) {
1116
                user_error(
1117
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1118
                    E_USER_WARNING
1119
                );
1120
                $defaults = null;
1121
            }
1122
1123
            if ($defaults) {
1124
                foreach ($defaults as $fieldName => $fieldValue) {
1125
                // SRM 2007-03-06: Stricter check
1126
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1127
                        $this->$fieldName = $fieldValue;
1128
                    }
1129
                // Set many-many defaults with an array of ids
1130
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1131
                        /** @var ManyManyList $manyManyJoin */
1132
                        $manyManyJoin = $this->$fieldName();
1133
                        $manyManyJoin->setByIDList($fieldValue);
1134
                    }
1135
                }
1136
            }
1137
            if ($class == self::class) {
1138
                break;
1139
            }
1140
        }
1141
1142
        $this->extend('populateDefaults');
1143
        return $this;
1144
    }
1145
1146
    /**
1147
     * Determine validation of this object prior to write
1148
     *
1149
     * @return ValidationException Exception generated by this write, or null if valid
1150
     */
1151
    protected function validateWrite()
1152
    {
1153
        if ($this->ObsoleteClassName) {
0 ignored issues
show
Bug Best Practice introduced by
The property ObsoleteClassName does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
1154
            return new ValidationException(
1155
                "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - ".
1156
                "you need to change the ClassName before you can write it"
1157
            );
1158
        }
1159
1160
        // Note: Validation can only be disabled at the global level, not per-model
1161
        if (DataObject::config()->uninherited('validation_enabled')) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1162
            $result = $this->validate();
1163
            if (!$result->isValid()) {
1164
                return new ValidationException($result);
1165
            }
1166
        }
1167
        return null;
1168
    }
1169
1170
    /**
1171
     * Prepare an object prior to write
1172
     *
1173
     * @throws ValidationException
1174
     */
1175
    protected function preWrite()
1176
    {
1177
        // Validate this object
1178
        if ($writeException = $this->validateWrite()) {
1179
            // Used by DODs to clean up after themselves, eg, Versioned
1180
            $this->invokeWithExtensions('onAfterSkippedWrite');
1181
            throw $writeException;
1182
        }
1183
1184
        // Check onBeforeWrite
1185
        $this->brokenOnWrite = true;
1186
        $this->onBeforeWrite();
1187
        if ($this->brokenOnWrite) {
1188
            user_error(static::class . " has a broken onBeforeWrite() function."
1189
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1190
        }
1191
    }
1192
1193
    /**
1194
     * Detects and updates all changes made to this object
1195
     *
1196
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1197
     * @return bool True if any changes are detected
1198
     */
1199
    protected function updateChanges($forceChanges = false)
1200
    {
1201
        if ($forceChanges) {
1202
            // Force changes, but only for loaded fields
1203
            foreach ($this->record as $field => $value) {
1204
                $this->changed[$field] = static::CHANGE_VALUE;
1205
            }
1206
            return true;
1207
        }
1208
        return $this->isChanged();
1209
    }
1210
1211
    /**
1212
     * Writes a subset of changes for a specific table to the given manipulation
1213
     *
1214
     * @param string $baseTable Base table
1215
     * @param string $now Timestamp to use for the current time
1216
     * @param bool $isNewRecord Whether this should be treated as a new record write
1217
     * @param array $manipulation Manipulation to write to
1218
     * @param string $class Class of table to manipulate
1219
     */
1220
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1221
    {
1222
        $schema = $this->getSchema();
1223
        $table = $schema->tableName($class);
1224
        $manipulation[$table] = array();
1225
1226
        // Extract records for this table
1227
        foreach ($this->record as $fieldName => $fieldValue) {
1228
            // we're not attempting to reset the BaseTable->ID
1229
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1230
            if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1231
                continue;
1232
            }
1233
1234
            // Ensure this field pertains to this table
1235
            $specification = $schema->fieldSpec($class, $fieldName, DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED);
1236
            if (!$specification) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $specification of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1237
                continue;
1238
            }
1239
1240
            // if database column doesn't correlate to a DBField instance...
1241
            $fieldObj = $this->dbObject($fieldName);
1242
            if (!$fieldObj) {
1243
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1244
            }
1245
1246
            // Write to manipulation
1247
            $fieldObj->writeToManipulation($manipulation[$table]);
1248
        }
1249
1250
        // Ensure update of Created and LastEdited columns
1251
        if ($baseTable === $table) {
1252
            $manipulation[$table]['fields']['LastEdited'] = $now;
1253
            if ($isNewRecord) {
1254
                $manipulation[$table]['fields']['Created']
1255
                    = empty($this->record['Created'])
1256
                        ? $now
1257
                        : $this->record['Created'];
1258
                $manipulation[$table]['fields']['ClassName'] = static::class;
1259
            }
1260
        }
1261
1262
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1263
        // attempt an update, as though it were a normal update.
1264
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1265
        $manipulation[$table]['id'] = $this->record['ID'];
1266
        $manipulation[$table]['class'] = $class;
1267
    }
1268
1269
    /**
1270
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1271
     *
1272
     * Does nothing if an ID is already assigned for this record
1273
     *
1274
     * @param string $baseTable Base table
1275
     * @param string $now Timestamp to use for the current time
1276
     */
1277
    protected function writeBaseRecord($baseTable, $now)
1278
    {
1279
        // Generate new ID if not specified
1280
        if ($this->isInDB()) {
1281
            return;
1282
        }
1283
1284
        // Perform an insert on the base table
1285
        $insert = new SQLInsert('"'.$baseTable.'"');
1286
        $insert
1287
            ->assign('"Created"', $now)
1288
            ->execute();
1289
        $this->changed['ID'] = self::CHANGE_VALUE;
1290
        $this->record['ID'] = DB::get_generated_id($baseTable);
1291
    }
1292
1293
    /**
1294
     * Generate and write the database manipulation for all changed fields
1295
     *
1296
     * @param string $baseTable Base table
1297
     * @param string $now Timestamp to use for the current time
1298
     * @param bool $isNewRecord If this is a new record
1299
     */
1300
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1301
    {
1302
        // Generate database manipulations for each class
1303
        $manipulation = array();
1304
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1305
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1306
        }
1307
1308
        // Allow extensions to extend this manipulation
1309
        $this->extend('augmentWrite', $manipulation);
1310
1311
        // New records have their insert into the base data table done first, so that they can pass the
1312
        // generated ID on to the rest of the manipulation
1313
        if ($isNewRecord) {
1314
            $manipulation[$baseTable]['command'] = 'update';
1315
        }
1316
1317
        // Perform the manipulation
1318
        DB::manipulate($manipulation);
1319
    }
1320
1321
    /**
1322
     * Writes all changes to this object to the database.
1323
     *  - It will insert a record whenever ID isn't set, otherwise update.
1324
     *  - All relevant tables will be updated.
1325
     *  - $this->onBeforeWrite() gets called beforehand.
1326
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1327
     *
1328
     *  @uses DataExtension->augmentWrite()
1329
     *
1330
     * @param boolean $showDebug Show debugging information
1331
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1332
     * @param boolean $forceWrite Write to database even if there are no changes
1333
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1334
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1335
     *                                 {@link getManyManyComponents()} (Default: false)
1336
     * @return int The ID of the record
1337
     * @throws ValidationException Exception that can be caught and handled by the calling function
1338
     */
1339
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1340
    {
1341
        $now = DBDatetime::now()->Rfc2822();
1342
1343
        // Execute pre-write tasks
1344
        $this->preWrite();
1345
1346
        // Check if we are doing an update or an insert
1347
        $isNewRecord = !$this->isInDB() || $forceInsert;
1348
1349
        // Check changes exist, abort if there are none
1350
        $hasChanges = $this->updateChanges($isNewRecord);
1351
        if ($hasChanges || $forceWrite || $isNewRecord) {
1352
            // Ensure Created and LastEdited are populated
1353
            if (!isset($this->record['Created'])) {
1354
                $this->record['Created'] = $now;
1355
            }
1356
            $this->record['LastEdited'] = $now;
1357
1358
            // New records have their insert into the base data table done first, so that they can pass the
1359
            // generated primary key on to the rest of the manipulation
1360
            $baseTable = $this->baseTable();
1361
            $this->writeBaseRecord($baseTable, $now);
1362
1363
            // Write the DB manipulation for all changed fields
1364
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1365
1366
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1367
            $this->writeRelations();
1368
            $this->onAfterWrite();
1369
            $this->changed = array();
1370
        } else {
1371
            if ($showDebug) {
1372
                Debug::message("no changes for DataObject");
1373
            }
1374
1375
            // Used by DODs to clean up after themselves, eg, Versioned
1376
            $this->invokeWithExtensions('onAfterSkippedWrite');
1377
        }
1378
1379
        // Write relations as necessary
1380
        if ($writeComponents) {
1381
            $this->writeComponents(true);
1382
        }
1383
1384
        // Clears the cache for this object so get_one returns the correct object.
1385
        $this->flushCache();
1386
1387
        return $this->record['ID'];
1388
    }
1389
1390
    /**
1391
     * Writes cached relation lists to the database, if possible
1392
     */
1393
    public function writeRelations()
1394
    {
1395
        if (!$this->isInDB()) {
1396
            return;
1397
        }
1398
1399
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1400
        if ($this->unsavedRelations) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->unsavedRelations of type SilverStripe\ORM\UnsavedRelationList[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1401
            foreach ($this->unsavedRelations as $name => $list) {
1402
                $list->changeToList($this->$name());
1403
            }
1404
            $this->unsavedRelations = array();
1405
        }
1406
    }
1407
1408
    /**
1409
     * Write the cached components to the database. Cached components could refer to two different instances of the
1410
     * same record.
1411
     *
1412
     * @param bool $recursive Recursively write components
1413
     * @return DataObject $this
1414
     */
1415
    public function writeComponents($recursive = false)
1416
    {
1417
        if ($this->components) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->components of type SilverStripe\ORM\DataObject[] 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...
1418
            foreach ($this->components as $component) {
1419
                $component->write(false, false, false, $recursive);
1420
            }
1421
        }
1422
1423
        if ($join = $this->getJoin()) {
1424
            $join->write(false, false, false, $recursive);
1425
        }
1426
1427
        return $this;
1428
    }
1429
1430
    /**
1431
     * Delete this data object.
1432
     * $this->onBeforeDelete() gets called.
1433
     * Note that in Versioned objects, both Stage and Live will be deleted.
1434
     *  @uses DataExtension->augmentSQL()
1435
     */
1436
    public function delete()
1437
    {
1438
        $this->brokenOnDelete = true;
1439
        $this->onBeforeDelete();
1440
        if ($this->brokenOnDelete) {
1441
            user_error(static::class . " has a broken onBeforeDelete() function."
1442
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1443
        }
1444
1445
        // Deleting a record without an ID shouldn't do anything
1446
        if (!$this->ID) {
1447
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1448
        }
1449
1450
        // TODO: This is quite ugly.  To improve:
1451
        //  - move the details of the delete code in the DataQuery system
1452
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1453
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1454
        $srcQuery = DataList::create(static::class)
0 ignored issues
show
Bug introduced by
static::class of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

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

1454
        $srcQuery = DataList::create(/** @scrutinizer ignore-type */ static::class)
Loading history...
1455
            ->filter('ID', $this->ID)
1456
            ->dataQuery()
1457
            ->query();
1458
        $queriedTables = $srcQuery->queriedTables();
1459
        $this->extend('updateDeleteTables', $queriedTables, $srcQuery);
1460
        foreach ($queriedTables as $table) {
1461
            $delete = SQLDelete::create("\"$table\"", array('"ID"' => $this->ID));
1462
            $this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
1463
            $delete->execute();
1464
        }
1465
        // Remove this item out of any caches
1466
        $this->flushCache();
1467
1468
        $this->onAfterDelete();
1469
1470
        $this->OldID = $this->ID;
1471
        $this->ID = 0;
1472
    }
1473
1474
    /**
1475
     * Delete the record with the given ID.
1476
     *
1477
     * @param string $className The class name of the record to be deleted
1478
     * @param int $id ID of record to be deleted
1479
     */
1480
    public static function delete_by_id($className, $id)
1481
    {
1482
        $obj = DataObject::get_by_id($className, $id);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1483
        if ($obj) {
1484
            $obj->delete();
1485
        } else {
1486
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1487
        }
1488
    }
1489
1490
    /**
1491
     * Get the class ancestry, including the current class name.
1492
     * The ancestry will be returned as an array of class names, where the 0th element
1493
     * will be the class that inherits directly from DataObject, and the last element
1494
     * will be the current class.
1495
     *
1496
     * @return array Class ancestry
1497
     */
1498
    public function getClassAncestry()
1499
    {
1500
        return ClassInfo::ancestry(static::class);
1501
    }
1502
1503
    /**
1504
     * Return a component object from a one to one relationship, as a DataObject.
1505
     * If no component is available, an 'empty component' will be returned for
1506
     * non-polymorphic relations, or for polymorphic relations with a class set.
1507
     *
1508
     * @param string $componentName Name of the component
1509
     * @return DataObject The component object. It's exact type will be that of the component.
1510
     * @throws Exception
1511
     */
1512
    public function getComponent($componentName)
1513
    {
1514
        if (isset($this->components[$componentName])) {
1515
            return $this->components[$componentName];
1516
        }
1517
1518
        $schema = static::getSchema();
1519
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1520
            $joinField = $componentName . 'ID';
1521
            $joinID    = $this->getField($joinField);
1522
1523
            // Extract class name for polymorphic relations
1524
            if ($class === self::class) {
1525
                $class = $this->getField($componentName . 'Class');
1526
                if (empty($class)) {
1527
                    return null;
1528
                }
1529
            }
1530
1531
            if ($joinID) {
1532
                // Ensure that the selected object originates from the same stage, subsite, etc
1533
                $component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1534
                    ->filter('ID', $joinID)
1535
                    ->setDataQueryParam($this->getInheritableQueryParams())
1536
                    ->first();
1537
            }
1538
1539
            if (empty($component)) {
1540
                $component = Injector::inst()->create($class);
1541
            }
1542
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1543
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1544
            $joinID = $this->ID;
1545
1546
            if ($joinID) {
1547
                // Prepare filter for appropriate join type
1548
                if ($polymorphic) {
1549
                    $filter = array(
1550
                        "{$joinField}ID" => $joinID,
1551
                        "{$joinField}Class" => static::class,
1552
                    );
1553
                } else {
1554
                    $filter = array(
1555
                        $joinField => $joinID
1556
                    );
1557
                }
1558
1559
                // Ensure that the selected object originates from the same stage, subsite, etc
1560
                $component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1561
                    ->filter($filter)
1562
                    ->setDataQueryParam($this->getInheritableQueryParams())
1563
                    ->first();
1564
            }
1565
1566
            if (empty($component)) {
1567
                $component = Injector::inst()->create($class);
1568
                if ($polymorphic) {
1569
                    $component->{$joinField.'ID'} = $this->ID;
1570
                    $component->{$joinField.'Class'} = static::class;
1571
                } else {
1572
                    $component->$joinField = $this->ID;
1573
                }
1574
            }
1575
        } else {
1576
            throw new InvalidArgumentException(
1577
                "DataObject->getComponent(): Could not find component '$componentName'."
1578
            );
1579
        }
1580
1581
        $this->components[$componentName] = $component;
1582
        return $component;
1583
    }
1584
1585
    /**
1586
     * Returns a one-to-many relation as a HasManyList
1587
     *
1588
     * @param string $componentName Name of the component
1589
     * @param int|array $id Optional ID(s) for parent of this relation, if not the current record
1590
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1591
     */
1592
    public function getComponents($componentName, $id = null)
1593
    {
1594
        if (!isset($id)) {
1595
            $id = $this->ID;
1596
        }
1597
        $result = null;
1598
1599
        $schema = $this->getSchema();
1600
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1601
        if (!$componentClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $componentClass of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1602
            throw new InvalidArgumentException(sprintf(
1603
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1604
                $componentName,
1605
                static::class
1606
            ));
1607
        }
1608
1609
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1610
        if (!$id) {
1611
            if (!isset($this->unsavedRelations[$componentName])) {
1612
                $this->unsavedRelations[$componentName] =
1613
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1614
            }
1615
            return $this->unsavedRelations[$componentName];
1616
        }
1617
1618
        // Determine type and nature of foreign relation
1619
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1620
        /** @var HasManyList $result */
1621
        if ($polymorphic) {
1622
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
0 ignored issues
show
Bug introduced by
$componentClass of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

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

1622
            $result = PolymorphicHasManyList::create(/** @scrutinizer ignore-type */ $componentClass, $joinField, static::class);
Loading history...
1623
        } else {
1624
            $result = HasManyList::create($componentClass, $joinField);
1625
        }
1626
1627
        return $result
1628
            ->setDataQueryParam($this->getInheritableQueryParams())
1629
            ->forForeignID($id);
1630
    }
1631
1632
    /**
1633
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1634
     *
1635
     * @param string $relationName Relation name.
1636
     * @return string Class name, or null if not found.
1637
     */
1638
    public function getRelationClass($relationName)
1639
    {
1640
        // Parse many_many
1641
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1642
        if ($manyManyComponent) {
1643
            return $manyManyComponent['childClass'];
1644
        }
1645
1646
        // Go through all relationship configuration fields.
1647
        $config = $this->config();
1648
        $candidates = array_merge(
1649
            ($relations = $config->get('has_one')) ? $relations : array(),
1650
            ($relations = $config->get('has_many')) ? $relations : array(),
1651
            ($relations = $config->get('belongs_to')) ? $relations : array()
1652
        );
1653
1654
        if (isset($candidates[$relationName])) {
1655
            $remoteClass = $candidates[$relationName];
1656
1657
            // If dot notation is present, extract just the first part that contains the class.
1658
            if (($fieldPos = strpos($remoteClass, '.'))!==false) {
1659
                return substr($remoteClass, 0, $fieldPos);
1660
            }
1661
1662
            // Otherwise just return the class
1663
            return $remoteClass;
1664
        }
1665
1666
        return null;
1667
    }
1668
1669
    /**
1670
     * Given a relation name, determine the relation type
1671
     *
1672
     * @param string $component Name of component
1673
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1674
     */
1675
    public function getRelationType($component)
1676
    {
1677
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1678
        $config = $this->config();
1679
        foreach ($types as $type) {
1680
            $relations = $config->get($type);
1681
            if ($relations && isset($relations[$component])) {
1682
                return $type;
1683
            }
1684
        }
1685
        return null;
1686
    }
1687
1688
    /**
1689
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1690
     * side of the relation.
1691
     *
1692
     * Notes on behaviour:
1693
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1694
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1695
     *  - Cannot be used on polymorphic relationships
1696
     *  - Cannot be used on unsaved objects.
1697
     *
1698
     * @param string $remoteClass
1699
     * @param string $remoteRelation
1700
     * @return DataList|DataObject The component, either as a list or single object
1701
     * @throws BadMethodCallException
1702
     * @throws InvalidArgumentException
1703
     */
1704
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1705
    {
1706
        $remote = DataObject::singleton($remoteClass);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1707
        $class = $remote->getRelationClass($remoteRelation);
1708
        $schema = static::getSchema();
1709
1710
        // Validate arguments
1711
        if (!$this->isInDB()) {
1712
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1713
        }
1714
        if (empty($class)) {
1715
            throw new InvalidArgumentException(sprintf(
1716
                "%s invoked with invalid relation %s.%s",
1717
                __METHOD__,
1718
                $remoteClass,
1719
                $remoteRelation
1720
            ));
1721
        }
1722
        if ($class === self::class) {
1723
            throw new InvalidArgumentException(sprintf(
1724
                "%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1725
                "This method does not support polymorphic relationships",
1726
                __METHOD__,
1727
                $remoteClass,
1728
                $remoteRelation
1729
            ));
1730
        }
1731
        if (!is_a($this, $class, true)) {
1732
            throw new InvalidArgumentException(sprintf(
1733
                "Relation %s on %s does not refer to objects of type %s",
1734
                $remoteRelation,
1735
                $remoteClass,
1736
                static::class
1737
            ));
1738
        }
1739
1740
        // Check the relation type to mock
1741
        $relationType = $remote->getRelationType($remoteRelation);
1742
        switch ($relationType) {
1743
            case 'has_one': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1744
                // Mock has_many
1745
                $joinField = "{$remoteRelation}ID";
1746
                $componentClass = $schema->classForField($remoteClass, $joinField);
1747
                $result = HasManyList::create($componentClass, $joinField);
0 ignored issues
show
Bug introduced by
$componentClass of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

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

1747
                $result = HasManyList::create(/** @scrutinizer ignore-type */ $componentClass, $joinField);
Loading history...
1748
                return $result
1749
                    ->setDataQueryParam($this->getInheritableQueryParams())
1750
                    ->forForeignID($this->ID);
1751
            }
1752
            case 'belongs_to':
1753
            case 'has_many': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1754
                // These relations must have a has_one on the other end, so find it
1755
                $joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic);
1756
                if ($polymorphic) {
1757
                    throw new InvalidArgumentException(sprintf(
1758
                        "%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1759
                        "to be a has_one polymorphic. This method does not support polymorphic relationships",
1760
                        __METHOD__,
1761
                        $remoteClass,
1762
                        $remoteRelation
1763
                    ));
1764
                }
1765
                $joinID = $this->getField($joinField);
1766
                if (empty($joinID)) {
1767
                    return null;
1768
                }
1769
                // Get object by joined ID
1770
                return DataObject::get($remoteClass)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1771
                    ->filter('ID', $joinID)
1772
                    ->setDataQueryParam($this->getInheritableQueryParams())
1773
                    ->first();
1774
            }
1775
            case 'many_many':
1776
            case 'belongs_many_many': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1777
                // Get components and extra fields from parent
1778
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1779
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1780
1781
                // Reverse parent and component fields and create an inverse ManyManyList
1782
                /** @var RelationList $result */
1783
                $result = Injector::inst()->create(
1784
                    $manyMany['relationClass'],
1785
                    $manyMany['parentClass'], // Substitute parent class for dataClass
1786
                    $manyMany['join'],
1787
                    $manyMany['parentField'], // Reversed parent / child field
1788
                    $manyMany['childField'], // Reversed parent / child field
1789
                    $extraFields
1790
                );
1791
                $this->extend('updateManyManyComponents', $result);
1792
1793
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1794
                // foreignID set elsewhere.
1795
                return $result
1796
                    ->setDataQueryParam($this->getInheritableQueryParams())
1797
                    ->forForeignID($this->ID);
1798
            }
1799
            default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1800
                return null;
1801
            }
1802
        }
1803
    }
1804
1805
    /**
1806
     * Returns a many-to-many component, as a ManyManyList.
1807
     * @param string $componentName Name of the many-many component
1808
     * @param int|array $id Optional ID for parent of this relation, if not the current record
1809
     * @return RelationList|UnsavedRelationList The set of components
1810
     */
1811
    public function getManyManyComponents($componentName, $id = null)
1812
    {
1813
        if (!isset($id)) {
1814
            $id = $this->ID;
1815
        }
1816
        $schema = static::getSchema();
1817
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
1818
        if (!$manyManyComponent) {
1819
            throw new InvalidArgumentException(sprintf(
1820
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1821
                $componentName,
1822
                static::class
1823
            ));
1824
        }
1825
1826
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1827
        if (!$id) {
1828
            if (!isset($this->unsavedRelations[$componentName])) {
1829
                $this->unsavedRelations[$componentName] =
1830
                    new UnsavedRelationList($manyManyComponent['parentClass'], $componentName, $manyManyComponent['childClass']);
1831
            }
1832
            return $this->unsavedRelations[$componentName];
1833
        }
1834
1835
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
1836
        /** @var RelationList $result */
1837
        $result = Injector::inst()->create(
1838
            $manyManyComponent['relationClass'],
1839
            $manyManyComponent['childClass'],
1840
            $manyManyComponent['join'],
1841
            $manyManyComponent['childField'],
1842
            $manyManyComponent['parentField'],
1843
            $extraFields
1844
        );
1845
1846
1847
        // Store component data in query meta-data
1848
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
1849
            /** @var DataQuery $query */
1850
            $query->setQueryParam('Component.ExtraFields', $extraFields);
1851
        });
1852
1853
        $this->extend('updateManyManyComponents', $result);
1854
1855
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1856
        // foreignID set elsewhere.
1857
        return $result
1858
            ->setDataQueryParam($this->getInheritableQueryParams())
1859
            ->forForeignID($id);
1860
    }
1861
1862
    /**
1863
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1864
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1865
     *
1866
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1867
     *                          their classes.
1868
     */
1869
    public function hasOne()
1870
    {
1871
        return (array)$this->config()->get('has_one');
1872
    }
1873
1874
    /**
1875
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1876
     * their class name will be returned.
1877
     *
1878
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1879
     *        the field data stripped off. It defaults to TRUE.
1880
     * @return string|array
1881
     */
1882
    public function belongsTo($classOnly = true)
1883
    {
1884
        $belongsTo = (array)$this->config()->get('belongs_to');
1885
        if ($belongsTo && $classOnly) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $belongsTo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1886
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1887
        } else {
1888
            return $belongsTo ? $belongsTo : array();
1889
        }
1890
    }
1891
1892
    /**
1893
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1894
     * relationships and their classes will be returned.
1895
     *
1896
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1897
     *        the field data stripped off. It defaults to TRUE.
1898
     * @return string|array|false
1899
     */
1900
    public function hasMany($classOnly = true)
1901
    {
1902
        $hasMany = (array)$this->config()->get('has_many');
1903
        if ($hasMany && $classOnly) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasMany of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1904
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1905
        } else {
1906
            return $hasMany ? $hasMany : array();
1907
        }
1908
    }
1909
1910
    /**
1911
     * Return the many-to-many extra fields specification.
1912
     *
1913
     * If you don't specify a component name, it returns all
1914
     * extra fields for all components available.
1915
     *
1916
     * @return array|null
1917
     */
1918
    public function manyManyExtraFields()
1919
    {
1920
        return $this->config()->get('many_many_extraFields');
1921
    }
1922
1923
    /**
1924
     * Return information about a many-to-many component.
1925
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
1926
     * components are returned.
1927
     *
1928
     * @see DataObjectSchema::manyManyComponent()
1929
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
1930
     */
1931
    public function manyMany()
1932
    {
1933
        $config = $this->config();
1934
        $manyManys = (array)$config->get('many_many');
1935
        $belongsManyManys = (array)$config->get('belongs_many_many');
1936
        $items = array_merge($manyManys, $belongsManyManys);
1937
        return $items;
1938
    }
1939
1940
    /**
1941
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
1942
     *
1943
     * This is experimental, and is currently only a Postgres-specific enhancement.
1944
     *
1945
     * @param string $class
1946
     * @return array|false
1947
     */
1948
    public function database_extensions($class)
1949
    {
1950
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
1951
        if ($extensions) {
1952
            return $extensions;
1953
        } else {
1954
            return false;
1955
        }
1956
    }
1957
1958
    /**
1959
     * Generates a SearchContext to be used for building and processing
1960
     * a generic search form for properties on this object.
1961
     *
1962
     * @return SearchContext
1963
     */
1964
    public function getDefaultSearchContext()
1965
    {
1966
        return new SearchContext(
1967
            static::class,
1968
            $this->scaffoldSearchFields(),
1969
            $this->defaultSearchFilters()
1970
        );
1971
    }
1972
1973
    /**
1974
     * Determine which properties on the DataObject are
1975
     * searchable, and map them to their default {@link FormField}
1976
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
1977
     *
1978
     * Some additional logic is included for switching field labels, based on
1979
     * how generic or specific the field type is.
1980
     *
1981
     * Used by {@link SearchContext}.
1982
     *
1983
     * @param array $_params
1984
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
1985
     *   'restrictFields': Numeric array of a field name whitelist
1986
     * @return FieldList
1987
     */
1988
    public function scaffoldSearchFields($_params = null)
1989
    {
1990
        $params = array_merge(
1991
            array(
1992
                'fieldClasses' => false,
1993
                'restrictFields' => false
1994
            ),
1995
            (array)$_params
1996
        );
1997
        $fields = new FieldList();
1998
        foreach ($this->searchableFields() as $fieldName => $spec) {
1999
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
2000
                continue;
2001
            }
2002
2003
            // If a custom fieldclass is provided as a string, use it
2004
            $field = null;
2005
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2006
                $fieldClass = $params['fieldClasses'][$fieldName];
2007
                $field = new $fieldClass($fieldName);
2008
            // If we explicitly set a field, then construct that
2009
            } elseif (isset($spec['field'])) {
2010
                // If it's a string, use it as a class name and construct
2011
                if (is_string($spec['field'])) {
2012
                    $fieldClass = $spec['field'];
2013
                    $field = new $fieldClass($fieldName);
2014
2015
                // If it's a FormField object, then just use that object directly.
2016
                } elseif ($spec['field'] instanceof FormField) {
2017
                    $field = $spec['field'];
2018
2019
                // Otherwise we have a bug
2020
                } else {
2021
                    user_error("Bad value for searchable_fields, 'field' value: "
2022
                        . var_export($spec['field'], true), E_USER_WARNING);
2023
                }
2024
2025
            // Otherwise, use the database field's scaffolder
2026
            } elseif ($object = $this->relObject($fieldName)) {
2027
                $field = $object->scaffoldSearchField();
0 ignored issues
show
Bug introduced by
The method scaffoldSearchField() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

2027
                /** @scrutinizer ignore-call */ 
2028
                $field = $object->scaffoldSearchField();
Loading history...
2028
            }
2029
2030
            // Allow fields to opt out of search
2031
            if (!$field) {
2032
                continue;
2033
            }
2034
2035
            if (strstr($fieldName, '.')) {
2036
                $field->setName(str_replace('.', '__', $fieldName));
2037
            }
2038
            $field->setTitle($spec['title']);
2039
2040
            $fields->push($field);
2041
        }
2042
        return $fields;
2043
    }
2044
2045
    /**
2046
     * Scaffold a simple edit form for all properties on this dataobject,
2047
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2048
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2049
     *
2050
     * @uses FormScaffolder
2051
     *
2052
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2053
     * @return FieldList
2054
     */
2055
    public function scaffoldFormFields($_params = null)
2056
    {
2057
        $params = array_merge(
2058
            array(
2059
                'tabbed' => false,
2060
                'includeRelations' => false,
2061
                'restrictFields' => false,
2062
                'fieldClasses' => false,
2063
                'ajaxSafe' => false
2064
            ),
2065
            (array)$_params
2066
        );
2067
2068
        $fs = FormScaffolder::create($this);
0 ignored issues
show
Bug introduced by
$this of type SilverStripe\ORM\DataObject is incompatible with the type array expected by parameter $args of SilverStripe\Forms\FormScaffolder::create(). ( Ignorable by Annotation )

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

2068
        $fs = FormScaffolder::create(/** @scrutinizer ignore-type */ $this);
Loading history...
2069
        $fs->tabbed = $params['tabbed'];
2070
        $fs->includeRelations = $params['includeRelations'];
2071
        $fs->restrictFields = $params['restrictFields'];
2072
        $fs->fieldClasses = $params['fieldClasses'];
2073
        $fs->ajaxSafe = $params['ajaxSafe'];
2074
2075
        return $fs->getFieldList();
2076
    }
2077
2078
    /**
2079
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2080
     * being called on extensions
2081
     *
2082
     * @param callable $callback The callback to execute
2083
     */
2084
    protected function beforeUpdateCMSFields($callback)
2085
    {
2086
        $this->beforeExtending('updateCMSFields', $callback);
2087
    }
2088
2089
    /**
2090
     * Centerpiece of every data administration interface in Silverstripe,
2091
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2092
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2093
     * generate this set. To customize, overload this method in a subclass
2094
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2095
     *
2096
     * <code>
2097
     * class MyCustomClass extends DataObject {
2098
     *  static $db = array('CustomProperty'=>'Boolean');
2099
     *
2100
     *  function getCMSFields() {
2101
     *    $fields = parent::getCMSFields();
2102
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2103
     *    return $fields;
2104
     *  }
2105
     * }
2106
     * </code>
2107
     *
2108
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2109
     *
2110
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2111
     */
2112
    public function getCMSFields()
2113
    {
2114
        $tabbedFields = $this->scaffoldFormFields(array(
2115
            // Don't allow has_many/many_many relationship editing before the record is first saved
2116
            'includeRelations' => ($this->ID > 0),
2117
            'tabbed' => true,
2118
            'ajaxSafe' => true
2119
        ));
2120
2121
        $this->extend('updateCMSFields', $tabbedFields);
2122
2123
        return $tabbedFields;
2124
    }
2125
2126
    /**
2127
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2128
     * including that dataobject's extensions customised actions could be added to the EditForm.
2129
     *
2130
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2131
     */
2132
    public function getCMSActions()
2133
    {
2134
        $actions = new FieldList();
2135
        $this->extend('updateCMSActions', $actions);
2136
        return $actions;
2137
    }
2138
2139
2140
    /**
2141
     * Used for simple frontend forms without relation editing
2142
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2143
     * by default. To customize, either overload this method in your
2144
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2145
     *
2146
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2147
     *
2148
     * @param array $params See {@link scaffoldFormFields()}
2149
     * @return FieldList Always returns a simple field collection without TabSet.
2150
     */
2151
    public function getFrontEndFields($params = null)
2152
    {
2153
        $untabbedFields = $this->scaffoldFormFields($params);
2154
        $this->extend('updateFrontEndFields', $untabbedFields);
2155
2156
        return $untabbedFields;
2157
    }
2158
2159
    public function getViewerTemplates($suffix = '')
2160
    {
2161
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2162
    }
2163
2164
    /**
2165
     * Gets the value of a field.
2166
     * Called by {@link __get()} and any getFieldName() methods you might create.
2167
     *
2168
     * @param string $field The name of the field
2169
     * @return mixed The field value
2170
     */
2171
    public function getField($field)
2172
    {
2173
        // If we already have an object in $this->record, then we should just return that
2174
        if (isset($this->record[$field]) && is_object($this->record[$field])) {
2175
            return $this->record[$field];
2176
        }
2177
2178
        // Do we have a field that needs to be lazy loaded?
2179
        if (isset($this->record[$field.'_Lazy'])) {
2180
            $tableClass = $this->record[$field.'_Lazy'];
2181
            $this->loadLazyFields($tableClass);
2182
        }
2183
2184
        // In case of complex fields, return the DBField object
2185
        if (static::getSchema()->compositeField(static::class, $field)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::getSchema()->com...(static::class, $field) of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2186
            $this->record[$field] = $this->dbObject($field);
2187
        }
2188
2189
        return isset($this->record[$field]) ? $this->record[$field] : null;
2190
    }
2191
2192
    /**
2193
     * Loads all the stub fields that an initial lazy load didn't load fully.
2194
     *
2195
     * @param string $class Class to load the values from. Others are joined as required.
2196
     * Not specifying a tableClass will load all lazy fields from all tables.
2197
     * @return bool Flag if lazy loading succeeded
2198
     */
2199
    protected function loadLazyFields($class = null)
2200
    {
2201
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2202
            return false;
2203
        }
2204
2205
        if (!$class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2206
            $loaded = array();
2207
2208
            foreach ($this->record as $key => $value) {
2209
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2210
                    $this->loadLazyFields($value);
2211
                    $loaded[$value] = $value;
2212
                }
2213
            }
2214
2215
            return false;
2216
        }
2217
2218
        $dataQuery = new DataQuery($class);
2219
2220
        // Reset query parameter context to that of this DataObject
2221
        if ($params = $this->getSourceQueryParams()) {
2222
            foreach ($params as $key => $value) {
2223
                $dataQuery->setQueryParam($key, $value);
2224
            }
2225
        }
2226
2227
        // Limit query to the current record, unless it has the Versioned extension,
2228
        // in which case it requires special handling through augmentLoadLazyFields()
2229
        $schema = static::getSchema();
2230
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2231
        $dataQuery->where([
2232
            $baseIDColumn => $this->record['ID']
2233
        ])->limit(1);
2234
2235
        $columns = array();
2236
2237
        // Add SQL for fields, both simple & multi-value
2238
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2239
        $databaseFields = $schema->databaseFields($class, false);
2240
        foreach ($databaseFields as $k => $v) {
2241
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2242
                $columns[] = $k;
2243
            }
2244
        }
2245
2246
        if ($columns) {
2247
            $query = $dataQuery->query();
2248
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2249
            $this->extend('augmentSQL', $query, $dataQuery);
2250
2251
            $dataQuery->setQueriedColumns($columns);
2252
            $newData = $dataQuery->execute()->record();
2253
2254
            // Load the data into record
2255
            if ($newData) {
2256
                foreach ($newData as $k => $v) {
2257
                    if (in_array($k, $columns)) {
2258
                        $this->record[$k] = $v;
2259
                        $this->original[$k] = $v;
2260
                        unset($this->record[$k . '_Lazy']);
2261
                    }
2262
                }
2263
2264
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2265
            } else {
2266
                foreach ($columns as $k) {
2267
                    $this->record[$k] = null;
2268
                    $this->original[$k] = null;
2269
                    unset($this->record[$k . '_Lazy']);
2270
                }
2271
            }
2272
        }
2273
        return true;
2274
    }
2275
2276
    /**
2277
     * Return the fields that have changed.
2278
     *
2279
     * The change level affects what the functions defines as "changed":
2280
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2281
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2282
     *   for example a change from 0 to null would not be included.
2283
     *
2284
     * Example return:
2285
     * <code>
2286
     * array(
2287
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2288
     * )
2289
     * </code>
2290
     *
2291
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2292
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2293
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2294
     * @return array
2295
     */
2296
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2297
    {
2298
        $changedFields = array();
2299
2300
        // Update the changed array with references to changed obj-fields
2301
        foreach ($this->record as $k => $v) {
2302
            // Prevents DBComposite infinite looping on isChanged
2303
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2304
                continue;
2305
            }
2306
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2307
                $this->changed[$k] = self::CHANGE_VALUE;
2308
            }
2309
        }
2310
2311
        if (is_array($databaseFieldsOnly)) {
2312
            $fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2313
        } elseif ($databaseFieldsOnly) {
2314
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2315
            $fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
2316
        } else {
2317
            $fields = $this->changed;
2318
        }
2319
2320
        // Filter the list to those of a certain change level
2321
        if ($changeLevel > self::CHANGE_STRICT) {
2322
            if ($fields) {
2323
                foreach ($fields as $name => $level) {
2324
                    if ($level < $changeLevel) {
2325
                        unset($fields[$name]);
2326
                    }
2327
                }
2328
            }
2329
        }
2330
2331
        if ($fields) {
2332
            foreach ($fields as $name => $level) {
2333
                $changedFields[$name] = array(
2334
                'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2335
                'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2336
                'level' => $level
2337
                );
2338
            }
2339
        }
2340
2341
        return $changedFields;
2342
    }
2343
2344
    /**
2345
     * Uses {@link getChangedFields()} to determine if fields have been changed
2346
     * since loading them from the database.
2347
     *
2348
     * @param string $fieldName Name of the database field to check, will check for any if not given
2349
     * @param int $changeLevel See {@link getChangedFields()}
2350
     * @return boolean
2351
     */
2352
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2353
    {
2354
        $fields = $fieldName ? array($fieldName) : true;
2355
        $changed = $this->getChangedFields($fields, $changeLevel);
2356
        if (!isset($fieldName)) {
2357
            return !empty($changed);
2358
        } else {
2359
            return array_key_exists($fieldName, $changed);
2360
        }
2361
    }
2362
2363
    /**
2364
     * Set the value of the field
2365
     * Called by {@link __set()} and any setFieldName() methods you might create.
2366
     *
2367
     * @param string $fieldName Name of the field
2368
     * @param mixed $val New field value
2369
     * @return $this
2370
     */
2371
    public function setField($fieldName, $val)
2372
    {
2373
        $this->objCacheClear();
2374
        //if it's a has_one component, destroy the cache
2375
        if (substr($fieldName, -2) == 'ID') {
2376
            unset($this->components[substr($fieldName, 0, -2)]);
2377
        }
2378
2379
        // If we've just lazy-loaded the column, then we need to populate the $original array
2380
        if (isset($this->record[$fieldName.'_Lazy'])) {
2381
            $tableClass = $this->record[$fieldName.'_Lazy'];
2382
            $this->loadLazyFields($tableClass);
2383
        }
2384
2385
        // Situation 1: Passing an DBField
2386
        if ($val instanceof DBField) {
2387
            $val->setName($fieldName);
2388
            $val->saveInto($this);
2389
2390
            // Situation 1a: Composite fields should remain bound in case they are
2391
            // later referenced to update the parent dataobject
2392
            if ($val instanceof DBComposite) {
2393
                $val->bindTo($this);
2394
                $this->record[$fieldName] = $val;
2395
            }
2396
        // Situation 2: Passing a literal or non-DBField object
2397
        } else {
2398
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2399
            if (is_object($val) && static::getSchema()->fieldSpec(static::class, $fieldName)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::getSchema()->fie...tic::class, $fieldName) of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2400
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2401
            }
2402
2403
            // if a field is not existing or has strictly changed
2404
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2405
                // TODO Add check for php-level defaults which are not set in the db
2406
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2407
                // At the very least, the type has changed
2408
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2409
2410
                if ((!isset($this->record[$fieldName]) && $val)
2411
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2412
                ) {
2413
                    // Value has changed as well, not just the type
2414
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2415
                }
2416
2417
                // Value is always saved back when strict check succeeds.
2418
                $this->record[$fieldName] = $val;
2419
            }
2420
        }
2421
        return $this;
2422
    }
2423
2424
    /**
2425
     * Set the value of the field, using a casting object.
2426
     * This is useful when you aren't sure that a date is in SQL format, for example.
2427
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2428
     * can be saved into the Image table.
2429
     *
2430
     * @param string $fieldName Name of the field
2431
     * @param mixed $value New field value
2432
     * @return $this
2433
     */
2434
    public function setCastedField($fieldName, $value)
2435
    {
2436
        if (!$fieldName) {
2437
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2438
        }
2439
        $fieldObj = $this->dbObject($fieldName);
2440
        if ($fieldObj) {
2441
            $fieldObj->setValue($value);
2442
            $fieldObj->saveInto($this);
2443
        } else {
2444
            $this->$fieldName = $value;
2445
        }
2446
        return $this;
2447
    }
2448
2449
    /**
2450
     * {@inheritdoc}
2451
     */
2452
    public function castingHelper($field)
2453
    {
2454
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2455
        if ($fieldSpec) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldSpec of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2456
            return $fieldSpec;
2457
        }
2458
2459
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2460
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2461
        $queryParams = $this->getSourceQueryParams();
2462
        if (!empty($queryParams['Component.ExtraFields'])) {
2463
            $extraFields = $queryParams['Component.ExtraFields'];
2464
2465
            if (isset($extraFields[$field])) {
2466
                return $extraFields[$field];
2467
            }
2468
        }
2469
2470
        return parent::castingHelper($field);
2471
    }
2472
2473
    /**
2474
     * Returns true if the given field exists in a database column on any of
2475
     * the objects tables and optionally look up a dynamic getter with
2476
     * get<fieldName>().
2477
     *
2478
     * @param string $field Name of the field
2479
     * @return boolean True if the given field exists
2480
     */
2481
    public function hasField($field)
2482
    {
2483
        $schema = static::getSchema();
2484
        return (
2485
            array_key_exists($field, $this->record)
2486
            || $schema->fieldSpec(static::class, $field)
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema->fieldSpec(static::class, $field) of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2487
            || (substr($field, -2) == 'ID') && $schema->hasOneComponent(static::class, substr($field, 0, -2))
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema->hasOneComponent... substr($field, 0, -2)) of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2488
            || $this->hasMethod("get{$field}")
2489
        );
2490
    }
2491
2492
    /**
2493
     * Returns true if the given field exists as a database column
2494
     *
2495
     * @param string $field Name of the field
2496
     *
2497
     * @return boolean
2498
     */
2499
    public function hasDatabaseField($field)
2500
    {
2501
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2502
        return !empty($spec);
2503
    }
2504
2505
    /**
2506
     * Returns true if the member is allowed to do the given action.
2507
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2508
     *
2509
     * @param string $perm The permission to be checked, such as 'View'.
2510
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2511
     * in user.
2512
     * @param array $context Additional $context to pass to extendedCan()
2513
     *
2514
     * @return boolean True if the the member is allowed to do the given action
2515
     */
2516
    public function can($perm, $member = null, $context = array())
2517
    {
2518
        if (!$member) {
2519
            $member = Security::getCurrentUser();
2520
        }
2521
2522
        if ($member && Permission::checkMember($member, "ADMIN")) {
2523
            return true;
2524
        }
2525
2526
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2527
            $method = 'can' . ucfirst($perm);
2528
            return $this->$method($member);
2529
        }
2530
2531
        $results = $this->extendedCan('can', $member);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $results is correct as $this->extendedCan('can', $member) targeting SilverStripe\ORM\DataObject::extendedCan() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2532
        if (isset($results)) {
2533
            return $results;
2534
        }
2535
2536
        return ($member && Permission::checkMember($member, $perm));
2537
    }
2538
2539
    /**
2540
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2541
     * expected to return one of three values:
2542
     *
2543
     *  - false: Disallow this permission, regardless of what other extensions say
2544
     *  - true: Allow this permission, as long as no other extensions return false
2545
     *  - NULL: Don't affect the outcome
2546
     *
2547
     * This method itself returns a tri-state value, and is designed to be used like this:
2548
     *
2549
     * <code>
2550
     * $extended = $this->extendedCan('canDoSomething', $member);
2551
     * if($extended !== null) return $extended;
2552
     * else return $normalValue;
2553
     * </code>
2554
     *
2555
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2556
     * @param Member|int $member
2557
     * @param array $context Optional context
2558
     * @return boolean|null
2559
     */
2560
    public function extendedCan($methodName, $member, $context = array())
2561
    {
2562
        $results = $this->extend($methodName, $member, $context);
2563
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2564
            // Remove NULLs
2565
            $results = array_filter($results, function ($v) {
2566
                return !is_null($v);
2567
            });
2568
            // If there are any non-NULL responses, then return the lowest one of them.
2569
            // If any explicitly deny the permission, then we don't get access
2570
            if ($results) {
2571
                return min($results);
2572
            }
2573
        }
2574
        return null;
2575
    }
2576
2577
    /**
2578
     * @param Member $member
2579
     * @return boolean
2580
     */
2581
    public function canView($member = null)
2582
    {
2583
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $extended is correct as $this->extendedCan(__FUNCTION__, $member) targeting SilverStripe\ORM\DataObject::extendedCan() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2584
        if ($extended !== null) {
2585
            return $extended;
2586
        }
2587
        return Permission::check('ADMIN', 'any', $member);
2588
    }
2589
2590
    /**
2591
     * @param Member $member
2592
     * @return boolean
2593
     */
2594
    public function canEdit($member = null)
2595
    {
2596
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $extended is correct as $this->extendedCan(__FUNCTION__, $member) targeting SilverStripe\ORM\DataObject::extendedCan() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2597
        if ($extended !== null) {
2598
            return $extended;
2599
        }
2600
        return Permission::check('ADMIN', 'any', $member);
2601
    }
2602
2603
    /**
2604
     * @param Member $member
2605
     * @return boolean
2606
     */
2607
    public function canDelete($member = null)
2608
    {
2609
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $extended is correct as $this->extendedCan(__FUNCTION__, $member) targeting SilverStripe\ORM\DataObject::extendedCan() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2610
        if ($extended !== null) {
2611
            return $extended;
2612
        }
2613
        return Permission::check('ADMIN', 'any', $member);
2614
    }
2615
2616
    /**
2617
     * @param Member $member
2618
     * @param array $context Additional context-specific data which might
2619
     * affect whether (or where) this object could be created.
2620
     * @return boolean
2621
     */
2622
    public function canCreate($member = null, $context = array())
2623
    {
2624
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $extended is correct as $this->extendedCan(__FUN...N__, $member, $context) targeting SilverStripe\ORM\DataObject::extendedCan() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2625
        if ($extended !== null) {
2626
            return $extended;
2627
        }
2628
        return Permission::check('ADMIN', 'any', $member);
2629
    }
2630
2631
    /**
2632
     * Debugging used by Debug::show()
2633
     *
2634
     * @return string HTML data representing this object
2635
     */
2636
    public function debug()
2637
    {
2638
        $class = static::class;
2639
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2640
        if ($this->record) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2641
            foreach ($this->record as $fieldName => $fieldVal) {
2642
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2643
            }
2644
        }
2645
        $val .= "</ul>\n";
2646
        return $val;
2647
    }
2648
2649
    /**
2650
     * Return the DBField object that represents the given field.
2651
     * This works similarly to obj() with 2 key differences:
2652
     *   - it still returns an object even when the field has no value.
2653
     *   - it only matches fields and not methods
2654
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2655
     *
2656
     * @param string $fieldName Name of the field
2657
     * @return DBField The field as a DBField object
2658
     */
2659
    public function dbObject($fieldName)
2660
    {
2661
        // Check for field in DB
2662
        $schema = static::getSchema();
2663
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2664
        if (!$helper) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $helper of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2665
            return null;
2666
        }
2667
2668
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2669
            $tableClass = $this->record[$fieldName . '_Lazy'];
2670
            $this->loadLazyFields($tableClass);
2671
        }
2672
2673
        $value = isset($this->record[$fieldName])
2674
            ? $this->record[$fieldName]
2675
            : null;
2676
2677
        // If we have a DBField object in $this->record, then return that
2678
        if ($value instanceof DBField) {
2679
            return $value;
2680
        }
2681
2682
        list($class, $spec) = explode('.', $helper);
2683
        /** @var DBField $obj */
2684
        $table = $schema->tableName($class);
2685
        $obj = Injector::inst()->create($spec, $fieldName);
2686
        $obj->setTable($table);
2687
        $obj->setValue($value, $this, false);
2688
        return $obj;
2689
    }
2690
2691
    /**
2692
     * Traverses to a DBField referenced by relationships between data objects.
2693
     *
2694
     * The path to the related field is specified with dot separated syntax
2695
     * (eg: Parent.Child.Child.FieldName).
2696
     *
2697
     * If a relation is blank, this will return null instead.
2698
     * If a relation name is invalid (e.g. non-relation on a parent) this
2699
     * can throw a LogicException.
2700
     *
2701
     * @param string $fieldPath List of paths on this object. All items in this path
2702
     * must be ViewableData implementors
2703
     *
2704
     * @return mixed DBField of the field on the object or a DataList instance.
2705
     * @throws LogicException If accessing invalid relations
2706
     */
2707
    public function relObject($fieldPath)
2708
    {
2709
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
2710
        $component = $this;
2711
2712
        // Parse all relations
2713
        foreach (explode('.', $fieldPath) as $relation) {
2714
            if (!$component) {
2715
                return null;
2716
            }
2717
2718
            // Inspect relation type
2719
            if (ClassInfo::hasMethod($component, $relation)) {
2720
                $component = $component->$relation();
2721
            } elseif ($component instanceof Relation || $component instanceof DataList) {
2722
                // $relation could either be a field (aggregate), or another relation
2723
                $singleton = DataObject::singleton($component->dataClass());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2724
                $component = $singleton->dbObject($relation) ?: $component->relation($relation);
2725
            } elseif ($component instanceof DataObject && ($dbObject = $component->dbObject($relation))) {
2726
                $component = $dbObject;
2727
            } elseif ($component instanceof ViewableData && $component->hasField($relation)) {
2728
                $component = $component->obj($relation);
2729
            } else {
2730
                throw new LogicException(
2731
                    "$relation is not a relation/field on ".get_class($component)
2732
                );
2733
            }
2734
        }
2735
        return $component;
2736
    }
2737
2738
    /**
2739
     * Traverses to a field referenced by relationships between data objects, returning the value
2740
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2741
     *
2742
     * @param string $fieldName string
2743
     * @return mixed Will return null on a missing value
2744
     */
2745
    public function relField($fieldName)
2746
    {
2747
        // Navigate to relative parent using relObject() if needed
2748
        $component = $this;
2749
        if (($pos = strrpos($fieldName, '.')) !== false) {
2750
            $relation = substr($fieldName, 0, $pos);
2751
            $fieldName = substr($fieldName, $pos + 1);
2752
            $component = $this->relObject($relation);
2753
        }
2754
2755
        // Bail if the component is null
2756
        if (!$component) {
2757
            return null;
2758
        }
2759
        if (ClassInfo::hasMethod($component, $fieldName)) {
2760
            return $component->$fieldName();
2761
        }
2762
        return $component->$fieldName;
2763
    }
2764
2765
    /**
2766
     * Temporary hack to return an association name, based on class, to get around the mangle
2767
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2768
     *
2769
     * @param string $className
2770
     * @return string
2771
     */
2772
    public function getReverseAssociation($className)
2773
    {
2774
        if (is_array($this->manyMany())) {
2775
            $many_many = array_flip($this->manyMany());
2776
            if (array_key_exists($className, $many_many)) {
2777
                return $many_many[$className];
2778
            }
2779
        }
2780
        if (is_array($this->hasMany())) {
2781
            $has_many = array_flip($this->hasMany());
2782
            if (array_key_exists($className, $has_many)) {
2783
                return $has_many[$className];
2784
            }
2785
        }
2786
        if (is_array($this->hasOne())) {
2787
            $has_one = array_flip($this->hasOne());
2788
            if (array_key_exists($className, $has_one)) {
2789
                return $has_one[$className];
2790
            }
2791
        }
2792
2793
        return false;
2794
    }
2795
2796
    /**
2797
     * Return all objects matching the filter
2798
     * sub-classes are automatically selected and included
2799
     *
2800
     * @param string $callerClass The class of objects to be returned
2801
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2802
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2803
     * @param string|array $sort A sort expression to be inserted into the ORDER
2804
     * BY clause.  If omitted, self::$default_sort will be used.
2805
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
2806
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2807
     * @param string $containerClass The container class to return the results in.
2808
     *
2809
     * @todo $containerClass is Ignored, why?
2810
     *
2811
     * @return DataList The objects matching the filter, in the class specified by $containerClass
2812
     */
2813
    public static function get(
2814
        $callerClass = null,
2815
        $filter = "",
2816
        $sort = "",
2817
        $join = "",
2818
        $limit = null,
2819
        $containerClass = DataList::class
2820
    ) {
2821
2822
        if ($callerClass == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $callerClass of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2823
            $callerClass = get_called_class();
2824
            if ($callerClass == self::class) {
2825
                throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
2826
            }
2827
2828
            if ($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
2829
                throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
2830
                    . ' arguments');
2831
            }
2832
2833
            return DataList::create(get_called_class());
0 ignored issues
show
Bug introduced by
get_called_class() of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

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

2833
            return DataList::create(/** @scrutinizer ignore-type */ get_called_class());
Loading history...
2834
        }
2835
2836
        if ($join) {
2837
            throw new \InvalidArgumentException(
2838
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
2839
            );
2840
        }
2841
2842
        $result = DataList::create($callerClass)->where($filter)->sort($sort);
2843
2844
        if ($limit && strpos($limit, ',') !== false) {
0 ignored issues
show
Bug introduced by
It seems like $limit can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2844
        if ($limit && strpos(/** @scrutinizer ignore-type */ $limit, ',') !== false) {
Loading history...
2845
            $limitArguments = explode(',', $limit);
0 ignored issues
show
Bug introduced by
It seems like $limit can also be of type array; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

2845
            $limitArguments = explode(',', /** @scrutinizer ignore-type */ $limit);
Loading history...
2846
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
2847
        } elseif ($limit) {
2848
            $result = $result->limit($limit);
2849
        }
2850
2851
        return $result;
2852
    }
2853
2854
2855
    /**
2856
     * Return the first item matching the given query.
2857
     * All calls to get_one() are cached.
2858
     *
2859
     * @param string $callerClass The class of objects to be returned
2860
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2861
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2862
     * @param boolean $cache Use caching
2863
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
2864
     *
2865
     * @return DataObject|null The first item matching the query
2866
     */
2867
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
2868
    {
2869
        $SNG = singleton($callerClass);
2870
2871
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
2872
        $cacheKey = md5(serialize($cacheComponents));
2873
2874
        $item = null;
2875
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
2876
            $dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2877
            $item = $dl->first();
2878
2879
            if ($cache) {
2880
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
2881
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
2882
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
2883
                }
2884
            }
2885
        }
2886
2887
        if ($cache) {
2888
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
2889
        } else {
2890
            return $item;
2891
        }
2892
    }
2893
2894
    /**
2895
     * Flush the cached results for all relations (has_one, has_many, many_many)
2896
     * Also clears any cached aggregate data.
2897
     *
2898
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
2899
     *                            When false will just clear session-local cached data
2900
     * @return DataObject $this
2901
     */
2902
    public function flushCache($persistent = true)
2903
    {
2904
        if (static::class == self::class) {
2905
            self::$_cache_get_one = array();
2906
            return $this;
2907
        }
2908
2909
        $classes = ClassInfo::ancestry(static::class);
2910
        foreach ($classes as $class) {
2911
            if (isset(self::$_cache_get_one[$class])) {
2912
                unset(self::$_cache_get_one[$class]);
2913
            }
2914
        }
2915
2916
        $this->extend('flushCache');
2917
2918
        $this->components = array();
2919
        return $this;
2920
    }
2921
2922
    /**
2923
     * Flush the get_one global cache and destroy associated objects.
2924
     */
2925
    public static function flush_and_destroy_cache()
2926
    {
2927
        if (self::$_cache_get_one) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::_cache_get_one of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2928
            foreach (self::$_cache_get_one as $class => $items) {
2929
                if (is_array($items)) {
2930
                    foreach ($items as $item) {
2931
                        if ($item) {
2932
                            $item->destroy();
2933
                        }
2934
                    }
2935
                }
2936
            }
2937
        }
2938
        self::$_cache_get_one = array();
2939
    }
2940
2941
    /**
2942
     * Reset all global caches associated with DataObject.
2943
     */
2944
    public static function reset()
2945
    {
2946
        // @todo Decouple these
2947
        DBClassName::clear_classname_cache();
2948
        ClassInfo::reset_db_cache();
2949
        static::getSchema()->reset();
2950
        self::$_cache_get_one = array();
2951
        self::$_cache_field_labels = array();
2952
    }
2953
2954
    /**
2955
     * Return the given element, searching by ID
2956
     *
2957
     * @param string $callerClass The class of the object to be returned
2958
     * @param int $id The id of the element
2959
     * @param boolean $cache See {@link get_one()}
2960
     *
2961
     * @return DataObject The element
2962
     */
2963
    public static function get_by_id($callerClass, $id, $cache = true)
2964
    {
2965
        if (!is_numeric($id)) {
2966
            user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
2967
        }
2968
2969
        // Pass to get_one
2970
        $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
2971
        return DataObject::get_one($callerClass, array($column => $id), $cache);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2972
    }
2973
2974
    /**
2975
     * Get the name of the base table for this object
2976
     *
2977
     * @return string
2978
     */
2979
    public function baseTable()
2980
    {
2981
        return static::getSchema()->baseDataTable($this);
2982
    }
2983
2984
    /**
2985
     * Get the base class for this object
2986
     *
2987
     * @return string
2988
     */
2989
    public function baseClass()
2990
    {
2991
        return static::getSchema()->baseDataClass($this);
2992
    }
2993
2994
    /**
2995
     * @var array Parameters used in the query that built this object.
2996
     * This can be used by decorators (e.g. lazy loading) to
2997
     * run additional queries using the same context.
2998
     */
2999
    protected $sourceQueryParams;
3000
3001
    /**
3002
     * @see $sourceQueryParams
3003
     * @return array
3004
     */
3005
    public function getSourceQueryParams()
3006
    {
3007
        return $this->sourceQueryParams;
3008
    }
3009
3010
    /**
3011
     * Get list of parameters that should be inherited to relations on this object
3012
     *
3013
     * @return array
3014
     */
3015
    public function getInheritableQueryParams()
3016
    {
3017
        $params = $this->getSourceQueryParams();
3018
        $this->extend('updateInheritableQueryParams', $params);
3019
        return $params;
3020
    }
3021
3022
    /**
3023
     * @see $sourceQueryParams
3024
     * @param array
3025
     */
3026
    public function setSourceQueryParams($array)
3027
    {
3028
        $this->sourceQueryParams = $array;
3029
    }
3030
3031
    /**
3032
     * @see $sourceQueryParams
3033
     * @param string $key
3034
     * @param string $value
3035
     */
3036
    public function setSourceQueryParam($key, $value)
3037
    {
3038
        $this->sourceQueryParams[$key] = $value;
3039
    }
3040
3041
    /**
3042
     * @see $sourceQueryParams
3043
     * @param string $key
3044
     * @return string
3045
     */
3046
    public function getSourceQueryParam($key)
3047
    {
3048
        if (isset($this->sourceQueryParams[$key])) {
3049
            return $this->sourceQueryParams[$key];
3050
        }
3051
        return null;
3052
    }
3053
3054
    //-------------------------------------------------------------------------------------------//
3055
3056
    /**
3057
     * Check the database schema and update it as necessary.
3058
     *
3059
     * @uses DataExtension->augmentDatabase()
3060
     */
3061
    public function requireTable()
3062
    {
3063
        // Only build the table if we've actually got fields
3064
        $schema = static::getSchema();
3065
        $table = $schema->tableName(static::class);
3066
        $fields = $schema->databaseFields(static::class, false);
3067
        $indexes = $schema->databaseIndexes(static::class, false);
3068
        $extensions = self::database_extensions(static::class);
0 ignored issues
show
Bug Best Practice introduced by
The method SilverStripe\ORM\DataObject::database_extensions() is not static, but was called statically. ( Ignorable by Annotation )

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

3068
        /** @scrutinizer ignore-call */ 
3069
        $extensions = self::database_extensions(static::class);
Loading history...
3069
        $legacyTables = $schema->getLegacyTableNames(static::class);
3070
3071
        if (empty($table)) {
3072
            throw new LogicException(
3073
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3074
            );
3075
        }
3076
3077
        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...
3078
            $ignore = Config::inst()->get(static::class, 'ignored_legacy_tables') ?: [];
3079
            $renameTables = array_diff(
3080
                array_intersect($legacyTables, DB::table_list()),
3081
                $ignore
3082
            );
3083
            if (count($renameTables) > 1) {
3084
                $class = static::class;
3085
                $legacyList = implode(', ', $renameTables);
3086
                trigger_error(
3087
                    "Class $class has multiple legacy tables: $legacyList",
3088
                    E_USER_NOTICE
3089
                );
3090
            }
3091
            if (count($renameTables) === 1) {
3092
                $dbSchema = DB::get_schema();
3093
                $dbSchema->renameTable($renameTables[0], $table);
3094
            }
3095
        }
3096
3097
        if ($fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
3098
            $hasAutoIncPK = get_parent_class($this) === self::class;
3099
            DB::require_table(
3100
                $table,
3101
                $fields,
3102
                $indexes,
3103
                $hasAutoIncPK,
3104
                $this->config()->get('create_table_options'),
3105
                $extensions
3106
            );
3107
        } else {
3108
            DB::dont_require_table($table);
3109
        }
3110
3111
        // Build any child tables for many_many items
3112
        if ($manyMany = $this->uninherited('many_many')) {
3113
            $extras = $this->uninherited('many_many_extraFields');
3114
            foreach ($manyMany as $component => $spec) {
3115
                // Get many_many spec
3116
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3117
                $parentField = $manyManyComponent['parentField'];
3118
                $childField = $manyManyComponent['childField'];
3119
                $tableOrClass = $manyManyComponent['join'];
3120
3121
                // Skip if backed by actual class
3122
                if (class_exists($tableOrClass)) {
3123
                    continue;
3124
                }
3125
3126
                // Build fields
3127
                $manymanyFields = array(
3128
                    $parentField => "Int",
3129
                    $childField => "Int",
3130
                );
3131
                if (isset($extras[$component])) {
3132
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3133
                }
3134
3135
                // Build index list
3136
                $manymanyIndexes = [
3137
                    $parentField => [
3138
                        'type' => 'index',
3139
                        'name' => $parentField,
3140
                        'columns' => [$parentField],
3141
                    ],
3142
                    $childField => [
3143
                        'type' => 'index',
3144
                        'name' =>$childField,
3145
                        'columns' => [$childField],
3146
                    ],
3147
                ];
3148
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Bug introduced by
$manymanyIndexes of type array<mixed,array<string,mixed|string|array>> is incompatible with the type string expected by parameter $indexSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

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

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

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

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

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

3148
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, /** @scrutinizer ignore-type */ $extensions);
Loading history...
3149
            }
3150
        }
3151
3152
        // Let any extentions make their own database fields
3153
        $this->extend('augmentDatabase', $dummy);
3154
    }
3155
3156
    /**
3157
     * Add default records to database. This function is called whenever the
3158
     * database is built, after the database tables have all been created. Overload
3159
     * this to add default records when the database is built, but make sure you
3160
     * call parent::requireDefaultRecords().
3161
     *
3162
     * @uses DataExtension->requireDefaultRecords()
3163
     */
3164
    public function requireDefaultRecords()
3165
    {
3166
        $defaultRecords = $this->config()->uninherited('default_records');
3167
3168
        if (!empty($defaultRecords)) {
3169
            $hasData = DataObject::get_one(static::class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
3170
            if (!$hasData) {
3171
                $className = static::class;
3172
                foreach ($defaultRecords as $record) {
3173
                    $obj = Injector::inst()->create($className, $record);
3174
                    $obj->write();
3175
                }
3176
                DB::alteration_message("Added default records to $className table", "created");
3177
            }
3178
        }
3179
3180
        // Let any extentions make their own database default data
3181
        $this->extend('requireDefaultRecords', $dummy);
3182
    }
3183
3184
    /**
3185
     * Get the default searchable fields for this object, as defined in the
3186
     * $searchable_fields list. If searchable fields are not defined on the
3187
     * data object, uses a default selection of summary fields.
3188
     *
3189
     * @return array
3190
     */
3191
    public function searchableFields()
3192
    {
3193
        // can have mixed format, need to make consistent in most verbose form
3194
        $fields = $this->config()->get('searchable_fields');
3195
        $labels = $this->fieldLabels();
3196
3197
        // fallback to summary fields (unless empty array is explicitly specified)
3198
        if (! $fields && ! is_array($fields)) {
3199
            $summaryFields = array_keys($this->summaryFields());
3200
            $fields = array();
3201
3202
            // remove the custom getters as the search should not include them
3203
            $schema = static::getSchema();
3204
            if ($summaryFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $summaryFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
3205
                foreach ($summaryFields as $key => $name) {
3206
                    $spec = $name;
3207
3208
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3209
                    if (($fieldPos = strpos($name, '.')) !== false) {
3210
                        $name = substr($name, 0, $fieldPos);
3211
                    }
3212
3213
                    if ($schema->fieldSpec($this, $name)) {
3214
                        $fields[] = $name;
3215
                    } elseif ($this->relObject($spec)) {
3216
                        $fields[] = $spec;
3217
                    }
3218
                }
3219
            }
3220
        }
3221
3222
        // we need to make sure the format is unified before
3223
        // augmenting fields, so extensions can apply consistent checks
3224
        // but also after augmenting fields, because the extension
3225
        // might use the shorthand notation as well
3226
3227
        // rewrite array, if it is using shorthand syntax
3228
        $rewrite = array();
3229
        foreach ($fields as $name => $specOrName) {
3230
            $identifer = (is_int($name)) ? $specOrName : $name;
3231
3232
            if (is_int($name)) {
3233
                // Format: array('MyFieldName')
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3234
                $rewrite[$identifer] = array();
3235
            } elseif (is_array($specOrName) && ($relObject = $this->relObject($identifer))) {
3236
                // Format: array('MyFieldName' => array(
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3237
                //   'filter => 'ExactMatchFilter',
3238
                //   'field' => 'NumericField', // optional
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3239
                //   'title' => 'My Title', // optional
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3240
                // ))
3241
                $rewrite[$identifer] = array_merge(
3242
                    array('filter' => $relObject->config()->get('default_search_filter_class')),
3243
                    (array)$specOrName
3244
                );
3245
            } else {
3246
                // Format: array('MyFieldName' => 'ExactMatchFilter')
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
3247
                $rewrite[$identifer] = array(
3248
                    'filter' => $specOrName,
3249
                );
3250
            }
3251
            if (!isset($rewrite[$identifer]['title'])) {
3252
                $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3253
                    ? $labels[$identifer] : FormField::name_to_label($identifer);
3254
            }
3255
            if (!isset($rewrite[$identifer]['filter'])) {
3256
                /** @skipUpgrade */
3257
                $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3258
            }
3259
        }
3260
3261
        $fields = $rewrite;
3262
3263
        // apply DataExtensions if present
3264
        $this->extend('updateSearchableFields', $fields);
3265
3266
        return $fields;
3267
    }
3268
3269
    /**
3270
     * Get any user defined searchable fields labels that
3271
     * exist. Allows overriding of default field names in the form
3272
     * interface actually presented to the user.
3273
     *
3274
     * The reason for keeping this separate from searchable_fields,
3275
     * which would be a logical place for this functionality, is to
3276
     * avoid bloating and complicating the configuration array. Currently
3277
     * much of this system is based on sensible defaults, and this property
3278
     * would generally only be set in the case of more complex relationships
3279
     * between data object being required in the search interface.
3280
     *
3281
     * Generates labels based on name of the field itself, if no static property
3282
     * {@link self::field_labels} exists.
3283
     *
3284
     * @uses $field_labels
3285
     * @uses FormField::name_to_label()
3286
     *
3287
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3288
     *
3289
     * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3290
     */
3291
    public function fieldLabels($includerelations = true)
3292
    {
3293
        $cacheKey = static::class . '_' . $includerelations;
3294
3295
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
3296
            $customLabels = $this->config()->get('field_labels');
3297
            $autoLabels = array();
3298
3299
            // get all translated static properties as defined in i18nCollectStatics()
3300
            $ancestry = ClassInfo::ancestry(static::class);
3301
            $ancestry = array_reverse($ancestry);
3302
            if ($ancestry) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ancestry of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
3303
                foreach ($ancestry as $ancestorClass) {
3304
                    if ($ancestorClass === ViewableData::class) {
3305
                        break;
3306
                    }
3307
                    $types = [
3308
                        'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3309
                    ];
3310
                    if ($includerelations) {
3311
                        $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3312
                        $types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3313
                        $types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3314
                        $types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3315
                    }
3316
                    foreach ($types as $type => $attrs) {
3317
                        foreach ($attrs as $name => $spec) {
3318
                            $autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}", FormField::name_to_label($name));
3319
                        }
3320
                    }
3321
                }
3322
            }
3323
3324
            $labels = array_merge((array)$autoLabels, (array)$customLabels);
3325
            $this->extend('updateFieldLabels', $labels);
3326
            self::$_cache_field_labels[$cacheKey] = $labels;
3327
        }
3328
3329
        return self::$_cache_field_labels[$cacheKey];
3330
    }
3331
3332
    /**
3333
     * Get a human-readable label for a single field,
3334
     * see {@link fieldLabels()} for more details.
3335
     *
3336
     * @uses fieldLabels()
3337
     * @uses FormField::name_to_label()
3338
     *
3339
     * @param string $name Name of the field
3340
     * @return string Label of the field
3341
     */
3342
    public function fieldLabel($name)
3343
    {
3344
        $labels = $this->fieldLabels();
3345
        return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3346
    }
3347
3348
    /**
3349
     * Get the default summary fields for this object.
3350
     *
3351
     * @todo use the translation apparatus to return a default field selection for the language
3352
     *
3353
     * @return array
3354
     */
3355
    public function summaryFields()
3356
    {
3357
        $rawFields = $this->config()->get('summary_fields');
3358
3359
        // Merge associative / numeric keys
3360
        $fields = [];
3361
        foreach ($rawFields as $key => $value) {
3362
            if (is_int($key)) {
3363
                $key = $value;
3364
            }
3365
            $fields[$key] = $value;
3366
        }
3367
3368
        if (!$fields) {
3369
            $fields = array();
3370
            // try to scaffold a couple of usual suspects
3371
            if ($this->hasField('Name')) {
3372
                $fields['Name'] = 'Name';
3373
            }
3374
            if (static::getSchema()->fieldSpec($this, 'Title')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::getSchema()->fieldSpec($this, 'Title') of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3375
                $fields['Title'] = 'Title';
3376
            }
3377
            if ($this->hasField('Description')) {
3378
                $fields['Description'] = 'Description';
3379
            }
3380
            if ($this->hasField('FirstName')) {
3381
                $fields['FirstName'] = 'First Name';
3382
            }
3383
        }
3384
        $this->extend("updateSummaryFields", $fields);
3385
3386
        // Final fail-over, just list ID field
3387
        if (!$fields) {
3388
            $fields['ID'] = 'ID';
3389
        }
3390
3391
        // Localize fields (if possible)
3392
        foreach ($this->fieldLabels(false) as $name => $label) {
3393
            // only attempt to localize if the label definition is the same as the field name.
3394
            // this will preserve any custom labels set in the summary_fields configuration
3395
            if (isset($fields[$name]) && $name === $fields[$name]) {
3396
                $fields[$name] = $label;
3397
            }
3398
        }
3399
3400
        return $fields;
3401
    }
3402
3403
    /**
3404
     * Defines a default list of filters for the search context.
3405
     *
3406
     * If a filter class mapping is defined on the data object,
3407
     * it is constructed here. Otherwise, the default filter specified in
3408
     * {@link DBField} is used.
3409
     *
3410
     * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3411
     *
3412
     * @return array
3413
     */
3414
    public function defaultSearchFilters()
3415
    {
3416
        $filters = array();
3417
3418
        foreach ($this->searchableFields() as $name => $spec) {
3419
            if (empty($spec['filter'])) {
3420
                /** @skipUpgrade */
3421
                $filters[$name] = 'PartialMatchFilter';
3422
            } elseif ($spec['filter'] instanceof SearchFilter) {
3423
                $filters[$name] = $spec['filter'];
3424
            } else {
3425
                $filters[$name] = Injector::inst()->create($spec['filter'], $name);
3426
            }
3427
        }
3428
3429
        return $filters;
3430
    }
3431
3432
    /**
3433
     * @return boolean True if the object is in the database
3434
     */
3435
    public function isInDB()
3436
    {
3437
        return is_numeric($this->ID) && $this->ID > 0;
3438
    }
3439
3440
    /*
3441
     * @ignore
3442
     */
3443
    private static $subclass_access = true;
3444
3445
    /**
3446
     * Temporarily disable subclass access in data object qeur
3447
     */
3448
    public static function disable_subclass_access()
3449
    {
3450
        self::$subclass_access = false;
3451
    }
3452
    public static function enable_subclass_access()
3453
    {
3454
        self::$subclass_access = true;
3455
    }
3456
3457
    //-------------------------------------------------------------------------------------------//
3458
3459
    /**
3460
     * Database field definitions.
3461
     * This is a map from field names to field type. The field
3462
     * type should be a class that extends .
3463
     * @var array
3464
     * @config
3465
     */
3466
    private static $db = [];
3467
3468
    /**
3469
     * Use a casting object for a field. This is a map from
3470
     * field name to class name of the casting object.
3471
     *
3472
     * @var array
3473
     */
3474
    private static $casting = array(
3475
        "Title" => 'Text',
3476
    );
3477
3478
    /**
3479
     * Specify custom options for a CREATE TABLE call.
3480
     * Can be used to specify a custom storage engine for specific database table.
3481
     * All options have to be keyed for a specific database implementation,
3482
     * identified by their class name (extending from {@link SS_Database}).
3483
     *
3484
     * <code>
3485
     * array(
3486
     *  'MySQLDatabase' => 'ENGINE=MyISAM'
3487
     * )
3488
     * </code>
3489
     *
3490
     * Caution: This API is experimental, and might not be
3491
     * included in the next major release. Please use with care.
3492
     *
3493
     * @var array
3494
     * @config
3495
     */
3496
    private static $create_table_options = array(
3497
        MySQLSchemaManager::ID => 'ENGINE=InnoDB'
3498
    );
3499
3500
    /**
3501
     * If a field is in this array, then create a database index
3502
     * on that field. This is a map from fieldname to index type.
3503
     * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3504
     *
3505
     * @var array
3506
     * @config
3507
     */
3508
    private static $indexes = null;
3509
3510
    /**
3511
     * Inserts standard column-values when a DataObject
3512
     * is instanciated. Does not insert default records {@see $default_records}.
3513
     * This is a map from fieldname to default value.
3514
     *
3515
     *  - If you would like to change a default value in a sub-class, just specify it.
3516
     *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3517
     *    or false in your subclass.  Setting it to null won't work.
3518
     *
3519
     * @var array
3520
     * @config
3521
     */
3522
    private static $defaults = [];
3523
3524
    /**
3525
     * Multidimensional array which inserts default data into the database
3526
     * on a db/build-call as long as the database-table is empty. Please use this only
3527
     * for simple constructs, not for SiteTree-Objects etc. which need special
3528
     * behaviour such as publishing and ParentNodes.
3529
     *
3530
     * Example:
3531
     * array(
3532
     *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3533
     *  array('Title' => "DefaultPage2")
3534
     * ).
3535
     *
3536
     * @var array
3537
     * @config
3538
     */
3539
    private static $default_records = null;
3540
3541
    /**
3542
     * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3543
     * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3544
     *
3545
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3546
     *
3547
     *  @var array
3548
     * @config
3549
     */
3550
    private static $has_one = [];
3551
3552
    /**
3553
     * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3554
     *
3555
     * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3556
     * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3557
     * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3558
     *
3559
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3560
     *
3561
     * @var array
3562
     * @config
3563
     */
3564
    private static $belongs_to = [];
3565
3566
    /**
3567
     * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3568
     *
3569
     * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3570
     * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3571
     * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3572
     * which foreign key to use.
3573
     *
3574
     * @var array
3575
     * @config
3576
     */
3577
    private static $has_many = [];
3578
3579
    /**
3580
     * many-many relationship definitions.
3581
     * This is a map from component name to data type.
3582
     * @var array
3583
     * @config
3584
     */
3585
    private static $many_many = [];
3586
3587
    /**
3588
     * Extra fields to include on the connecting many-many table.
3589
     * This is a map from field name to field type.
3590
     *
3591
     * Example code:
3592
     * <code>
3593
     * public static $many_many_extraFields = array(
3594
     *  'Members' => array(
3595
     *          'Role' => 'Varchar(100)'
3596
     *      )
3597
     * );
3598
     * </code>
3599
     *
3600
     * @var array
3601
     * @config
3602
     */
3603
    private static $many_many_extraFields = [];
3604
3605
    /**
3606
     * The inverse side of a many-many relationship.
3607
     * This is a map from component name to data type.
3608
     * @var array
3609
     * @config
3610
     */
3611
    private static $belongs_many_many = [];
3612
3613
    /**
3614
     * The default sort expression. This will be inserted in the ORDER BY
3615
     * clause of a SQL query if no other sort expression is provided.
3616
     * @var string
3617
     * @config
3618
     */
3619
    private static $default_sort = null;
3620
3621
    /**
3622
     * Default list of fields that can be scaffolded by the ModelAdmin
3623
     * search interface.
3624
     *
3625
     * Overriding the default filter, with a custom defined filter:
3626
     * <code>
3627
     *  static $searchable_fields = array(
3628
     *     "Name" => "PartialMatchFilter"
3629
     *  );
3630
     * </code>
3631
     *
3632
     * Overriding the default form fields, with a custom defined field.
3633
     * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3634
     * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3635
     * <code>
3636
     *  static $searchable_fields = array(
3637
     *    "Name" => array(
3638
     *      "field" => "TextField"
3639
     *    )
3640
     *  );
3641
     * </code>
3642
     *
3643
     * Overriding the default form field, filter and title:
3644
     * <code>
3645
     *  static $searchable_fields = array(
3646
     *    "Organisation.ZipCode" => array(
3647
     *      "field" => "TextField",
3648
     *      "filter" => "PartialMatchFilter",
3649
     *      "title" => 'Organisation ZIP'
3650
     *    )
3651
     *  );
3652
     * </code>
3653
     * @config
3654
     */
3655
    private static $searchable_fields = null;
3656
3657
    /**
3658
     * User defined labels for searchable_fields, used to override
3659
     * default display in the search form.
3660
     * @config
3661
     */
3662
    private static $field_labels = [];
3663
3664
    /**
3665
     * Provides a default list of fields to be used by a 'summary'
3666
     * view of this object.
3667
     * @config
3668
     */
3669
    private static $summary_fields = [];
3670
3671
    public function provideI18nEntities()
3672
    {
3673
        // Note: see http://guides.rubyonrails.org/i18n.html#pluralization for rules
3674
        // Best guess for a/an rule. Better guesses require overriding in subclasses
3675
        $pluralName = $this->plural_name();
3676
        $singularName = $this->singular_name();
3677
        $conjunction = preg_match('/^[aeiou]/i', $singularName) ? 'An ' : 'A ';
3678
        return [
3679
            static::class.'.SINGULARNAME' => $this->singular_name(),
3680
            static::class.'.PLURALNAME' => $pluralName,
3681
            static::class.'.PLURALS' => [
3682
                'one' => $conjunction . $singularName,
3683
                'other' => '{count} ' . $pluralName
3684
            ]
3685
        ];
3686
    }
3687
3688
    /**
3689
     * Returns true if the given method/parameter has a value
3690
     * (Uses the DBField::hasValue if the parameter is a database field)
3691
     *
3692
     * @param string $field The field name
3693
     * @param array $arguments
3694
     * @param bool $cache
3695
     * @return boolean
3696
     */
3697
    public function hasValue($field, $arguments = null, $cache = true)
3698
    {
3699
        // has_one fields should not use dbObject to check if a value is given
3700
        $hasOne = static::getSchema()->hasOneComponent(static::class, $field);
3701
        if (!$hasOne && ($obj = $this->dbObject($field))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasOne of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3702
            return $obj->exists();
3703
        } else {
3704
            return parent::hasValue($field, $arguments, $cache);
3705
        }
3706
    }
3707
3708
    /**
3709
     * If selected through a many_many through relation, this is the instance of the joined record
3710
     *
3711
     * @return DataObject
3712
     */
3713
    public function getJoin()
3714
    {
3715
        return $this->joinRecord;
3716
    }
3717
3718
    /**
3719
     * Set joining object
3720
     *
3721
     * @param DataObject $object
3722
     * @param string $alias Alias
3723
     * @return $this
3724
     */
3725
    public function setJoin(DataObject $object, $alias = null)
3726
    {
3727
        $this->joinRecord = $object;
3728
        if ($alias) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $alias of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3729
            if (static::getSchema()->fieldSpec(static::class, $alias)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::getSchema()->fie...(static::class, $alias) of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3730
                throw new InvalidArgumentException(
3731
                    "Joined record $alias cannot also be a db field"
3732
                );
3733
            }
3734
            $this->record[$alias] = $object;
3735
        }
3736
        return $this;
3737
    }
3738
3739
    /**
3740
     * Find objects in the given relationships, merging them into the given list
3741
     *
3742
     * @param string $source Config property to extract relationships from
3743
     * @param bool $recursive True if recursive
3744
     * @param ArrayList $list If specified, items will be added to this list. If not, a new
3745
     * instance of ArrayList will be constructed and returned
3746
     * @return ArrayList The list of related objects
3747
     */
3748
    public function findRelatedObjects($source, $recursive = true, $list = null)
3749
    {
3750
        if (!$list) {
3751
            $list = new ArrayList();
3752
        }
3753
3754
        // Skip search for unsaved records
3755
        if (!$this->isInDB()) {
3756
            return $list;
3757
        }
3758
3759
        $relationships = $this->config()->get($source) ?: [];
3760
        foreach ($relationships as $relationship) {
3761
            // Warn if invalid config
3762
            if (!$this->hasMethod($relationship)) {
3763
                trigger_error(sprintf(
3764
                    "Invalid %s config value \"%s\" on object on class \"%s\"",
3765
                    $source,
3766
                    $relationship,
3767
                    get_class($this)
3768
                ), E_USER_WARNING);
3769
                continue;
3770
            }
3771
3772
            // Inspect value of this relationship
3773
            $items = $this->{$relationship}();
3774
3775
            // Merge any new item
3776
            $newItems = $this->mergeRelatedObjects($list, $items);
3777
3778
            // Recurse if necessary
3779
            if ($recursive) {
3780
                foreach ($newItems as $item) {
3781
                    /** @var DataObject $item */
3782
                    $item->findRelatedObjects($source, true, $list);
3783
                }
3784
            }
3785
        }
3786
        return $list;
3787
    }
3788
3789
    /**
3790
     * Helper method to merge owned/owning items into a list.
3791
     * Items already present in the list will be skipped.
3792
     *
3793
     * @param ArrayList $list Items to merge into
3794
     * @param mixed $items List of new items to merge
3795
     * @return ArrayList List of all newly added items that did not already exist in $list
3796
     */
3797
    public function mergeRelatedObjects($list, $items)
3798
    {
3799
        $added = new ArrayList();
3800
        if (!$items) {
3801
            return $added;
3802
        }
3803
        if ($items instanceof DataObject) {
3804
            $items = [$items];
3805
        }
3806
3807
        /** @var DataObject $item */
3808
        foreach ($items as $item) {
3809
            $this->mergeRelatedObject($list, $added, $item);
3810
        }
3811
        return $added;
3812
    }
3813
3814
    /**
3815
     * Merge single object into a list, but ensures that existing objects are not
3816
     * re-added.
3817
     *
3818
     * @param ArrayList $list Global list
3819
     * @param ArrayList $added Additional list to insert into
3820
     * @param DataObject $item Item to add
3821
     */
3822
    protected function mergeRelatedObject($list, $added, $item)
3823
    {
3824
        // Identify item
3825
        $itemKey = get_class($item) . '/' . $item->ID;
3826
3827
        // Write if saved, versioned, and not already added
3828
        if ($item->isInDB() && !isset($list[$itemKey])) {
3829
            $list[$itemKey] = $item;
3830
            $added[$itemKey] = $item;
3831
        }
3832
3833
        // Add joined record (from many_many through) automatically
3834
        $joined = $item->getJoin();
3835
        if ($joined) {
3836
            $this->mergeRelatedObject($list, $added, $joined);
3837
        }
3838
    }
3839
}
3840