Passed
Push — master ( e78c48...210134 )
by Damian
14:57 queued 06:02
created

DataObject::getViewerTemplates()   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 1
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 $ClassName Class name of the DataObject
104
 * @property string $LastEdited Date and time of DataObject's last modification.
105
 * @property string $Created Date and time of DataObject creation.
106
 */
107
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable
108
{
109
110
    /**
111
     * Human-readable singular name.
112
     * @var string
113
     * @config
114
     */
115
    private static $singular_name = null;
116
117
    /**
118
     * Human-readable plural name
119
     * @var string
120
     * @config
121
     */
122
    private static $plural_name = null;
123
124
    /**
125
     * Allow API access to this object?
126
     * @todo Define the options that can be set here
127
     * @config
128
     */
129
    private static $api_access = false;
130
131
    /**
132
     * Allows specification of a default value for the ClassName field.
133
     * Configure this value only in subclasses of DataObject.
134
     *
135
     * @config
136
     * @var string
137
     */
138
    private static $default_classname = null;
139
140
    /**
141
     * @deprecated 4.0..5.0
142
     * @var bool
143
     */
144
    public $destroyed = false;
145
146
    /**
147
     * Data stored in this objects database record. An array indexed by fieldname.
148
     *
149
     * Use {@link toMap()} if you want an array representation
150
     * of this object, as the $record array might contain lazy loaded field aliases.
151
     *
152
     * @var array
153
     */
154
    protected $record;
155
156
    /**
157
     * If selected through a many_many through relation, this is the instance of the through record
158
     *
159
     * @var DataObject
160
     */
161
    protected $joinRecord;
162
163
    /**
164
     * Represents a field that hasn't changed (before === after, thus before == after)
165
     */
166
    const CHANGE_NONE = 0;
167
168
    /**
169
     * Represents a field that has changed type, although not the loosely defined value.
170
     * (before !== after && before == after)
171
     * E.g. change 1 to true or "true" to true, but not true to 0.
172
     * Value changes are by nature also considered strict changes.
173
     */
174
    const CHANGE_STRICT = 1;
175
176
    /**
177
     * Represents a field that has changed the loosely defined value
178
     * (before != after, thus, before !== after))
179
     * E.g. change false to true, but not false to 0
180
     */
181
    const CHANGE_VALUE = 2;
182
183
    /**
184
     * An array indexed by fieldname, true if the field has been changed.
185
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
186
     * the changed state.
187
     *
188
     * @var array
189
     */
190
    private $changed;
191
192
    /**
193
     * The database record (in the same format as $record), before
194
     * any changes.
195
     * @var array
196
     */
197
    protected $original;
198
199
    /**
200
     * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
201
     * @var boolean
202
     */
203
    protected $brokenOnDelete = false;
204
205
    /**
206
     * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
207
     * @var boolean
208
     */
209
    protected $brokenOnWrite = false;
210
211
    /**
212
     * @config
213
     * @var boolean Should dataobjects be validated before they are written?
214
     * Caution: Validation can contain safeguards against invalid/malicious data,
215
     * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
216
     * to only disable validation for very specific use cases.
217
     */
218
    private static $validation_enabled = true;
219
220
    /**
221
     * Static caches used by relevant functions.
222
     *
223
     * @var array
224
     */
225
    protected static $_cache_get_one;
226
227
    /**
228
     * Cache of field labels
229
     *
230
     * @var array
231
     */
232
    protected static $_cache_field_labels = array();
233
234
    /**
235
     * Base fields which are not defined in static $db
236
     *
237
     * @config
238
     * @var array
239
     */
240
    private static $fixed_fields = array(
241
        'ID' => 'PrimaryKey',
242
        'ClassName' => 'DBClassName',
243
        'LastEdited' => 'DBDatetime',
244
        'Created' => 'DBDatetime',
245
    );
246
247
    /**
248
     * Override table name for this class. If ignored will default to FQN of class.
249
     * This option is not inheritable, and must be set on each class.
250
     * If left blank naming will default to the legacy (3.x) behaviour.
251
     *
252
     * @var string
253
     */
254
    private static $table_name = null;
255
256
    /**
257
     * Non-static relationship cache, indexed by component name.
258
     *
259
     * @var DataObject[]
260
     */
261
    protected $components;
262
263
    /**
264
     * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
265
     *
266
     * @var UnsavedRelationList[]
267
     */
268
    protected $unsavedRelations;
269
270
    /**
271
     * List of relations that should be cascade deleted, similar to `owns`
272
     * Note: This will trigger delete on many_many objects, not only the mapping table.
273
     * For many_many through you can specify the components you want to delete separately
274
     * (many_many or has_many sub-component)
275
     *
276
     * @config
277
     * @var array
278
     */
279
    private static $cascade_deletes = [];
280
281
    /**
282
     * Get schema object
283
     *
284
     * @return DataObjectSchema
285
     */
286
    public static function getSchema()
287
    {
288
        return Injector::inst()->get(DataObjectSchema::class);
289
    }
290
291
    /**
292
     * Construct a new DataObject.
293
     *
294
295
     * @param array|null $record Used internally for rehydrating an object from database content.
296
     *                           Bypasses setters on this class, and hence should not be used
297
     *                           for populating data on new records.
298
     * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
299
     *                             Singletons don't have their defaults set.
300
     * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
301
     */
302
    public function __construct($record = null, $isSingleton = false, $queryParams = array())
303
    {
304
        parent::__construct();
305
306
        // Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
307
        $this->setSourceQueryParams($queryParams);
308
309
        // Set the fields data.
310
        if (!$record) {
311
            $record = array(
312
                'ID' => 0,
313
                'ClassName' => static::class,
314
                'RecordClassName' => static::class
315
            );
316
        }
317
318
        if ($record instanceof stdClass) {
319
            $record = (array)$record;
320
        }
321
322
        if (!is_array($record)) {
323
            if (is_object($record)) {
324
                $passed = "an object of type '".get_class($record)."'";
325
            } else {
326
                $passed = "The value '$record'";
327
            }
328
329
            user_error(
330
                "DataObject::__construct passed $passed.  It's supposed to be passed an array,"
331
                . " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
332
                E_USER_WARNING
333
            );
334
            $record = null;
335
        }
336
337
        // Set $this->record to $record, but ignore NULLs
338
        $this->record = array();
339
        foreach ($record as $k => $v) {
340
            // Ensure that ID is stored as a number and not a string
341
            // To do: this kind of clean-up should be done on all numeric fields, in some relatively
342
            // performant manner
343
            if ($v !== null) {
344
                if ($k == 'ID' && is_numeric($v)) {
345
                    $this->record[$k] = (int)$v;
346
                } else {
347
                    $this->record[$k] = $v;
348
                }
349
            }
350
        }
351
352
        // Identify fields that should be lazy loaded, but only on existing records
353
        if (!empty($record['ID'])) {
354
            // Get all field specs scoped to class for later lazy loading
355
            $fields = static::getSchema()->fieldSpecs(
356
                static::class,
357
                DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
358
            );
359
            foreach ($fields as $field => $fieldSpec) {
360
                $fieldClass = strtok($fieldSpec, ".");
361
                if (!array_key_exists($field, $record)) {
362
                    $this->record[$field.'_Lazy'] = $fieldClass;
363
                }
364
            }
365
        }
366
367
        $this->original = $this->record;
368
369
        // Keep track of the modification date of all the data sourced to make this page
370
        // From this we create a Last-Modified HTTP header
371
        if (isset($record['LastEdited'])) {
372
            HTTP::register_modification_date($record['LastEdited']);
373
        }
374
375
        // Must be called after parent constructor
376
        if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
377
            $this->populateDefaults();
378
        }
379
380
        // prevent populateDefaults() and setField() from marking overwritten defaults as changed
381
        $this->changed = array();
382
    }
383
384
    /**
385
     * Destroy all of this objects dependant objects and local caches.
386
     * You'll need to call this to get the memory of an object that has components or extensions freed.
387
     */
388
    public function destroy()
389
    {
390
        $this->flushCache(false);
391
    }
392
393
    /**
394
     * Create a duplicate of this node. Can duplicate many_many relations
395
     *
396
     * @param bool $doWrite Perform a write() operation before returning the object.
397
     * If this is true, it will create the duplicate in the database.
398
     * @param bool|string $manyMany Which many-many to duplicate. Set to true to duplicate all, false to duplicate none.
399
     * Alternatively set to the string of the relation config to duplicate
400
     * (supports 'many_many', or 'belongs_many_many')
401
     * @return static A duplicate of this node. The exact type will be the type of this node.
402
     */
403
    public function duplicate($doWrite = true, $manyMany = 'many_many')
404
    {
405
        $map = $this->toMap();
406
        unset($map['Created']);
407
        /** @var static $clone */
408
        $clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams());
409
        $clone->ID = 0;
410
411
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $manyMany);
412
        if ($manyMany) {
413
            $this->duplicateManyManyRelations($this, $clone, $manyMany);
414
        }
415
        if ($doWrite) {
416
            $clone->write();
417
        }
418
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $manyMany);
419
420
        return $clone;
421
    }
422
423
    /**
424
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
425
     *
426
     * @param DataObject $sourceObject the source object to duplicate from
427
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
428
     * @param bool|string $filter
429
     */
430
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
431
    {
432
        // Get list of relations to duplicate
433
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
434
            $relations = $sourceObject->config()->get($filter);
435
        } elseif ($filter === true) {
436
            $relations = $sourceObject->manyMany();
437
        } else {
438
            throw new InvalidArgumentException("Invalid many_many duplication filter");
439
        }
440
        foreach ($relations as $manyManyName => $type) {
441
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
442
        }
443
    }
444
445
    /**
446
     * Duplicates a single many_many relation from one object to another
447
     *
448
     * @param DataObject $sourceObject
449
     * @param DataObject $destinationObject
450
     * @param string $manyManyName
451
     */
452
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName)
453
    {
454
        // Ensure this component exists on the destination side as well
455
        if (!static::getSchema()->manyManyComponent(get_class($destinationObject), $manyManyName)) {
456
            return;
457
        }
458
459
        // Copy all components from source to destination
460
        $source = $sourceObject->getManyManyComponents($manyManyName);
461
        $dest = $destinationObject->getManyManyComponents($manyManyName);
462
        foreach ($source as $item) {
463
            $dest->add($item);
464
        }
465
    }
466
467
    /**
468
     * Return obsolete class name, if this is no longer a valid class
469
     *
470
     * @return string
471
     */
472
    public function getObsoleteClassName()
473
    {
474
        $className = $this->getField("ClassName");
475
        if (!ClassInfo::exists($className)) {
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

475
        if (!ClassInfo::exists(/** @scrutinizer ignore-type */ $className)) {
Loading history...
476
            return $className;
477
        }
478
        return null;
479
    }
480
481
    /**
482
     * Gets name of this class
483
     *
484
     * @return string
485
     */
486
    public function getClassName()
487
    {
488
        $className = $this->getField("ClassName");
489
        if (!ClassInfo::exists($className)) {
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

489
        if (!ClassInfo::exists(/** @scrutinizer ignore-type */ $className)) {
Loading history...
490
            return static::class;
491
        }
492
        return $className;
493
    }
494
495
    /**
496
     * Set the ClassName attribute. {@link $class} is also updated.
497
     * Warning: This will produce an inconsistent record, as the object
498
     * instance will not automatically switch to the new subclass.
499
     * Please use {@link newClassInstance()} for this purpose,
500
     * or destroy and reinstanciate the record.
501
     *
502
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
503
     * @return $this
504
     */
505
    public function setClassName($className)
506
    {
507
        $className = trim($className);
508
        if (!$className || !is_subclass_of($className, self::class)) {
509
            return $this;
510
        }
511
512
        $this->setField("ClassName", $className);
513
        $this->setField('RecordClassName', $className);
514
        return $this;
515
    }
516
517
    /**
518
     * Create a new instance of a different class from this object's record.
519
     * This is useful when dynamically changing the type of an instance. Specifically,
520
     * it ensures that the instance of the class is a match for the className of the
521
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
522
     * property manually before calling this method, as it will confuse change detection.
523
     *
524
     * If the new class is different to the original class, defaults are populated again
525
     * because this will only occur automatically on instantiation of a DataObject if
526
     * there is no record, or the record has no ID. In this case, we do have an ID but
527
     * we still need to repopulate the defaults.
528
     *
529
     * @param string $newClassName The name of the new class
530
     *
531
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
532
     */
533
    public function newClassInstance($newClassName)
534
    {
535
        if (!is_subclass_of($newClassName, self::class)) {
536
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
537
        }
538
539
        $originalClass = $this->ClassName;
540
541
        /** @var DataObject $newInstance */
542
        $newInstance = Injector::inst()->create($newClassName, $this->record, false);
543
544
        // Modify ClassName
545
        if ($newClassName != $originalClass) {
546
            $newInstance->setClassName($newClassName);
547
            $newInstance->populateDefaults();
548
            $newInstance->forceChange();
549
        }
550
551
        return $newInstance;
552
    }
553
554
    /**
555
     * Adds methods from the extensions.
556
     * Called by Object::__construct() once per class.
557
     */
558
    public function defineMethods()
559
    {
560
        parent::defineMethods();
561
562
        if (static::class === self::class) {
563
             return;
564
        }
565
566
        // Set up accessors for joined items
567
        if ($manyMany = $this->manyMany()) {
568
            foreach ($manyMany as $relationship => $class) {
569
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
570
            }
571
        }
572
        if ($hasMany = $this->hasMany()) {
573
            foreach ($hasMany as $relationship => $class) {
574
                $this->addWrapperMethod($relationship, 'getComponents');
575
            }
576
        }
577
        if ($hasOne = $this->hasOne()) {
578
            foreach ($hasOne as $relationship => $class) {
579
                $this->addWrapperMethod($relationship, 'getComponent');
580
            }
581
        }
582
        if ($belongsTo = $this->belongsTo()) {
583
            foreach (array_keys($belongsTo) as $relationship) {
584
                $this->addWrapperMethod($relationship, 'getComponent');
585
            }
586
        }
587
    }
588
589
    /**
590
     * Returns true if this object "exists", i.e., has a sensible value.
591
     * The default behaviour for a DataObject is to return true if
592
     * the object exists in the database, you can override this in subclasses.
593
     *
594
     * @return boolean true if this object exists
595
     */
596
    public function exists()
597
    {
598
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
599
    }
600
601
    /**
602
     * Returns TRUE if all values (other than "ID") are
603
     * considered empty (by weak boolean comparison).
604
     *
605
     * @return boolean
606
     */
607
    public function isEmpty()
608
    {
609
        $fixed = DataObject::config()->uninherited('fixed_fields');
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...
610
        foreach ($this->toMap() as $field => $value) {
611
            // only look at custom fields
612
            if (isset($fixed[$field])) {
613
                continue;
614
            }
615
616
            $dbObject = $this->dbObject($field);
617
            if (!$dbObject) {
618
                continue;
619
            }
620
            if ($dbObject->exists()) {
621
                return false;
622
            }
623
        }
624
        return true;
625
    }
626
627
    /**
628
     * Pluralise this item given a specific count.
629
     *
630
     * E.g. "0 Pages", "1 File", "3 Images"
631
     *
632
     * @param string $count
633
     * @return string
634
     */
635
    public function i18n_pluralise($count)
636
    {
637
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
638
        return i18n::_t(
639
            static::class.'.PLURALS',
640
            $default,
641
            [ 'count' => $count ]
642
        );
643
    }
644
645
    /**
646
     * Get the user friendly singular name of this DataObject.
647
     * If the name is not defined (by redefining $singular_name in the subclass),
648
     * this returns the class name.
649
     *
650
     * @return string User friendly singular name of this DataObject
651
     */
652
    public function singular_name()
653
    {
654
        $name = $this->config()->get('singular_name');
655
        if ($name) {
656
            return $name;
657
        }
658
        return ucwords(trim(strtolower(preg_replace(
659
            '/_?([A-Z])/',
660
            ' $1',
661
            ClassInfo::shortName($this)
662
        ))));
663
    }
664
665
    /**
666
     * Get the translated user friendly singular name of this DataObject
667
     * same as singular_name() but runs it through the translating function
668
     *
669
     * Translating string is in the form:
670
     *     $this->class.SINGULARNAME
671
     * Example:
672
     *     Page.SINGULARNAME
673
     *
674
     * @return string User friendly translated singular name of this DataObject
675
     */
676
    public function i18n_singular_name()
677
    {
678
        return _t(static::class.'.SINGULARNAME', $this->singular_name());
679
    }
680
681
    /**
682
     * Get the user friendly plural name of this DataObject
683
     * If the name is not defined (by renaming $plural_name in the subclass),
684
     * this returns a pluralised version of the class name.
685
     *
686
     * @return string User friendly plural name of this DataObject
687
     */
688
    public function plural_name()
689
    {
690
        if ($name = $this->config()->get('plural_name')) {
691
            return $name;
692
        }
693
        $name = $this->singular_name();
694
        //if the penultimate character is not a vowel, replace "y" with "ies"
695
        if (preg_match('/[^aeiou]y$/i', $name)) {
696
            $name = substr($name, 0, -1) . 'ie';
0 ignored issues
show
Bug introduced by
Are you sure substr($name, 0, -1) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

696
            $name = /** @scrutinizer ignore-type */ substr($name, 0, -1) . 'ie';
Loading history...
697
        }
698
        return ucfirst($name . 's');
699
    }
700
701
    /**
702
     * Get the translated user friendly plural name of this DataObject
703
     * Same as plural_name but runs it through the translation function
704
     * Translation string is in the form:
705
     *      $this->class.PLURALNAME
706
     * Example:
707
     *      Page.PLURALNAME
708
     *
709
     * @return string User friendly translated plural name of this DataObject
710
     */
711
    public function i18n_plural_name()
712
    {
713
        return _t(static::class.'.PLURALNAME', $this->plural_name());
714
    }
715
716
    /**
717
     * Standard implementation of a title/label for a specific
718
     * record. Tries to find properties 'Title' or 'Name',
719
     * and falls back to the 'ID'. Useful to provide
720
     * user-friendly identification of a record, e.g. in errormessages
721
     * or UI-selections.
722
     *
723
     * Overload this method to have a more specialized implementation,
724
     * e.g. for an Address record this could be:
725
     * <code>
726
     * function getTitle() {
727
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
728
     * }
729
     * </code>
730
     *
731
     * @return string
732
     */
733
    public function getTitle()
734
    {
735
        $schema = static::getSchema();
736
        if ($schema->fieldSpec($this, 'Title')) {
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...
737
            return $this->getField('Title');
738
        }
739
        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...
740
            return $this->getField('Name');
741
        }
742
743
        return "#{$this->ID}";
744
    }
745
746
    /**
747
     * Returns the associated database record - in this case, the object itself.
748
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
749
     *
750
     * @return DataObject Associated database record
751
     */
752
    public function data()
753
    {
754
        return $this;
755
    }
756
757
    /**
758
     * Convert this object to a map.
759
     *
760
     * @return array The data as a map.
761
     */
762
    public function toMap()
763
    {
764
        $this->loadLazyFields();
765
        return $this->record;
766
    }
767
768
    /**
769
     * Return all currently fetched database fields.
770
     *
771
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
772
     * Obviously, this makes it a lot faster.
773
     *
774
     * @return array The data as a map.
775
     */
776
    public function getQueriedDatabaseFields()
777
    {
778
        return $this->record;
779
    }
780
781
    /**
782
     * Update a number of fields on this object, given a map of the desired changes.
783
     *
784
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
785
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
786
     *
787
     * update() doesn't write the main object, but if you use the dot syntax, it will write()
788
     * the related objects that it alters.
789
     *
790
     * @param array $data A map of field name to data values to update.
791
     * @return DataObject $this
792
     */
793
    public function update($data)
794
    {
795
        foreach ($data as $key => $value) {
796
            // Implement dot syntax for updates
797
            if (strpos($key, '.') !== false) {
798
                $relations = explode('.', $key);
799
                $fieldName = array_pop($relations);
800
                /** @var static $relObj */
801
                $relObj = $this;
802
                $relation = null;
803
                foreach ($relations as $i => $relation) {
804
                    // no support for has_many or many_many relationships,
805
                    // as the updater wouldn't know which object to write to (or create)
806
                    if ($relObj->$relation() instanceof DataObject) {
807
                        $parentObj = $relObj;
808
                        $relObj = $relObj->$relation();
809
                        // If the intermediate relationship objects haven't been created, then write them
810
                        if ($i<sizeof($relations)-1 && !$relObj->ID || (!$relObj->ID && $parentObj !== $this)) {
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

810
                        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...
811
                            $relObj->write();
812
                            $relatedFieldName = $relation."ID";
813
                            $parentObj->$relatedFieldName = $relObj->ID;
814
                            $parentObj->write();
815
                        }
816
                    } else {
817
                        user_error(
818
                            "DataObject::update(): Can't traverse relationship '$relation'," .
819
                            "it has to be a has_one relationship or return a single DataObject",
820
                            E_USER_NOTICE
821
                        );
822
                        // unset relation object so we don't write properties to the wrong object
823
                        $relObj = null;
824
                        break;
825
                    }
826
                }
827
828
                if ($relObj) {
829
                    $relObj->$fieldName = $value;
830
                    $relObj->write();
831
                    $relatedFieldName = $relation."ID";
832
                    $this->$relatedFieldName = $relObj->ID;
833
                    $relObj->flushCache();
834
                } else {
835
                    $class = static::class;
836
                    user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
837
                }
838
            } else {
839
                $this->$key = $value;
840
            }
841
        }
842
        return $this;
843
    }
844
845
    /**
846
     * Pass changes as a map, and try to
847
     * get automatic casting for these fields.
848
     * Doesn't write to the database. To write the data,
849
     * use the write() method.
850
     *
851
     * @param array $data A map of field name to data values to update.
852
     * @return DataObject $this
853
     */
854
    public function castedUpdate($data)
855
    {
856
        foreach ($data as $k => $v) {
857
            $this->setCastedField($k, $v);
858
        }
859
        return $this;
860
    }
861
862
    /**
863
     * Merges data and relations from another object of same class,
864
     * without conflict resolution. Allows to specify which
865
     * dataset takes priority in case its not empty.
866
     * has_one-relations are just transferred with priority 'right'.
867
     * has_many and many_many-relations are added regardless of priority.
868
     *
869
     * Caution: has_many/many_many relations are moved rather than duplicated,
870
     * meaning they are not connected to the merged object any longer.
871
     * Caution: Just saves updated has_many/many_many relations to the database,
872
     * doesn't write the updated object itself (just writes the object-properties).
873
     * Caution: Does not delete the merged object.
874
     * Caution: Does now overwrite Created date on the original object.
875
     *
876
     * @param DataObject $rightObj
877
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
878
     * @param bool $includeRelations Merge any existing relations (optional)
879
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
880
     *                            Only applicable with $priority='right'. (optional)
881
     * @return Boolean
882
     */
883
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
884
    {
885
        $leftObj = $this;
886
887
        if ($leftObj->ClassName != $rightObj->ClassName) {
888
            // we can't merge similiar subclasses because they might have additional relations
889
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
890
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
891
            return false;
892
        }
893
894
        if (!$rightObj->ID) {
895
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
896
				to make sure all relations are transferred properly.').", E_USER_WARNING);
897
            return false;
898
        }
899
900
        // makes sure we don't merge data like ID or ClassName
901
        $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
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...
902
        foreach ($rightData as $key => $rightSpec) {
903
            // Don't merge ID
904
            if ($key === 'ID') {
905
                continue;
906
            }
907
908
            // Only merge relations if allowed
909
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
910
                continue;
911
            }
912
913
            // don't merge conflicting values if priority is 'left'
914
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
915
                continue;
916
            }
917
918
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
919
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
920
                continue;
921
            }
922
923
            // TODO remove redundant merge of has_one fields
924
            $leftObj->{$key} = $rightObj->{$key};
925
        }
926
927
        // merge relations
928
        if ($includeRelations) {
929
            if ($manyMany = $this->manyMany()) {
930
                foreach ($manyMany as $relationship => $class) {
931
                    /** @var DataObject $leftComponents */
932
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
933
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
934
                    if ($rightComponents && $rightComponents->exists()) {
935
                        $leftComponents->addMany($rightComponents->column('ID'));
936
                    }
937
                    $leftComponents->write();
938
                }
939
            }
940
941
            if ($hasMany = $this->hasMany()) {
942
                foreach ($hasMany as $relationship => $class) {
943
                    $leftComponents = $leftObj->getComponents($relationship);
944
                    $rightComponents = $rightObj->getComponents($relationship);
945
                    if ($rightComponents && $rightComponents->exists()) {
946
                        $leftComponents->addMany($rightComponents->column('ID'));
947
                    }
948
                    $leftComponents->write();
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

948
                    $leftComponents->/** @scrutinizer ignore-call */ 
949
                                     write();
Loading history...
949
                }
950
            }
951
        }
952
953
        return true;
954
    }
955
956
    /**
957
     * Forces the record to think that all its data has changed.
958
     * Doesn't write to the database. Only sets fields as changed
959
     * if they are not already marked as changed.
960
     *
961
     * @return $this
962
     */
963
    public function forceChange()
964
    {
965
        // Ensure lazy fields loaded
966
        $this->loadLazyFields();
967
        $fields = static::getSchema()->fieldSpecs(static::class);
968
969
        // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
970
        $fieldNames = array_unique(array_merge(
971
            array_keys($this->record),
972
            array_keys($fields)
973
        ));
974
975
        foreach ($fieldNames as $fieldName) {
976
            if (!isset($this->changed[$fieldName])) {
977
                $this->changed[$fieldName] = self::CHANGE_STRICT;
978
            }
979
            // Populate the null values in record so that they actually get written
980
            if (!isset($this->record[$fieldName])) {
981
                $this->record[$fieldName] = null;
982
            }
983
        }
984
985
        // @todo Find better way to allow versioned to write a new version after forceChange
986
        if ($this->isChanged('Version')) {
987
            unset($this->changed['Version']);
988
        }
989
        return $this;
990
    }
991
992
    /**
993
     * Validate the current object.
994
     *
995
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
996
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
997
     *
998
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
999
     * and onAfterWrite() won't get called either.
1000
     *
1001
     * It is expected that you call validate() in your own application to test that an object is valid before
1002
     * attempting a write, and respond appropriately if it isn't.
1003
     *
1004
     * @see {@link ValidationResult}
1005
     * @return ValidationResult
1006
     */
1007
    public function validate()
1008
    {
1009
        $result = ValidationResult::create();
1010
        $this->extend('validate', $result);
1011
        return $result;
1012
    }
1013
1014
    /**
1015
     * Public accessor for {@see DataObject::validate()}
1016
     *
1017
     * @return ValidationResult
1018
     */
1019
    public function doValidate()
1020
    {
1021
        Deprecation::notice('5.0', 'Use validate');
1022
        return $this->validate();
1023
    }
1024
1025
    /**
1026
     * Event handler called before writing to the database.
1027
     * You can overload this to clean up or otherwise process data before writing it to the
1028
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1029
     *
1030
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1031
     *
1032
     * @uses DataExtension->onBeforeWrite()
1033
     */
1034
    protected function onBeforeWrite()
1035
    {
1036
        $this->brokenOnWrite = false;
1037
1038
        $dummy = null;
1039
        $this->extend('onBeforeWrite', $dummy);
1040
    }
1041
1042
    /**
1043
     * Event handler called after writing to the database.
1044
     * You can overload this to act upon changes made to the data after it is written.
1045
     * $this->changed will have a record
1046
     * database.  Don't forget to call parent::onAfterWrite(), though!
1047
     *
1048
     * @uses DataExtension->onAfterWrite()
1049
     */
1050
    protected function onAfterWrite()
1051
    {
1052
        $dummy = null;
1053
        $this->extend('onAfterWrite', $dummy);
1054
    }
1055
1056
    /**
1057
     * Find all objects that will be cascade deleted if this object is deleted
1058
     *
1059
     * Notes:
1060
     *   - If this object is versioned, objects will only be searched in the same stage as the given record.
1061
     *   - This will only be useful prior to deletion, as post-deletion this record will no longer exist.
1062
     *
1063
     * @param bool $recursive True if recursive
1064
     * @param ArrayList $list Optional list to add items to
1065
     * @return ArrayList list of objects
1066
     */
1067
    public function findCascadeDeletes($recursive = true, $list = null)
1068
    {
1069
        // Find objects in these relationships
1070
        return $this->findRelatedObjects('cascade_deletes', $recursive, $list);
1071
    }
1072
1073
    /**
1074
     * Event handler called before deleting from the database.
1075
     * You can overload this to clean up or otherwise process data before delete this
1076
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1077
     *
1078
     * @uses DataExtension->onBeforeDelete()
1079
     */
1080
    protected function onBeforeDelete()
1081
    {
1082
        $this->brokenOnDelete = false;
1083
1084
        $dummy = null;
1085
        $this->extend('onBeforeDelete', $dummy);
1086
1087
        // Cascade deletes
1088
        $deletes = $this->findCascadeDeletes(false);
1089
        foreach ($deletes as $delete) {
1090
            $delete->delete();
1091
        }
1092
    }
1093
1094
    protected function onAfterDelete()
1095
    {
1096
        $this->extend('onAfterDelete');
1097
    }
1098
1099
    /**
1100
     * Load the default values in from the self::$defaults array.
1101
     * Will traverse the defaults of the current class and all its parent classes.
1102
     * Called by the constructor when creating new records.
1103
     *
1104
     * @uses DataExtension->populateDefaults()
1105
     * @return DataObject $this
1106
     */
1107
    public function populateDefaults()
1108
    {
1109
        $classes = array_reverse(ClassInfo::ancestry($this));
1110
1111
        foreach ($classes as $class) {
1112
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1113
1114
            if ($defaults && !is_array($defaults)) {
1115
                user_error(
1116
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1117
                    E_USER_WARNING
1118
                );
1119
                $defaults = null;
1120
            }
1121
1122
            if ($defaults) {
1123
                foreach ($defaults as $fieldName => $fieldValue) {
1124
                // SRM 2007-03-06: Stricter check
1125
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1126
                        $this->$fieldName = $fieldValue;
1127
                    }
1128
                // Set many-many defaults with an array of ids
1129
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1130
                        /** @var ManyManyList $manyManyJoin */
1131
                        $manyManyJoin = $this->$fieldName();
1132
                        $manyManyJoin->setByIDList($fieldValue);
1133
                    }
1134
                }
1135
            }
1136
            if ($class == self::class) {
1137
                break;
1138
            }
1139
        }
1140
1141
        $this->extend('populateDefaults');
1142
        return $this;
1143
    }
1144
1145
    /**
1146
     * Determine validation of this object prior to write
1147
     *
1148
     * @return ValidationException Exception generated by this write, or null if valid
1149
     */
1150
    protected function validateWrite()
1151
    {
1152
        if ($this->ObsoleteClassName) {
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...
1153
            return new ValidationException(
1154
                "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - ".
1155
                "you need to change the ClassName before you can write it"
1156
            );
1157
        }
1158
1159
        // Note: Validation can only be disabled at the global level, not per-model
1160
        if (DataObject::config()->uninherited('validation_enabled')) {
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...
1161
            $result = $this->validate();
1162
            if (!$result->isValid()) {
1163
                return new ValidationException($result);
1164
            }
1165
        }
1166
        return null;
1167
    }
1168
1169
    /**
1170
     * Prepare an object prior to write
1171
     *
1172
     * @throws ValidationException
1173
     */
1174
    protected function preWrite()
1175
    {
1176
        // Validate this object
1177
        if ($writeException = $this->validateWrite()) {
1178
            // Used by DODs to clean up after themselves, eg, Versioned
1179
            $this->invokeWithExtensions('onAfterSkippedWrite');
1180
            throw $writeException;
1181
        }
1182
1183
        // Check onBeforeWrite
1184
        $this->brokenOnWrite = true;
1185
        $this->onBeforeWrite();
1186
        if ($this->brokenOnWrite) {
1187
            user_error(static::class . " has a broken onBeforeWrite() function."
1188
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1189
        }
1190
    }
1191
1192
    /**
1193
     * Detects and updates all changes made to this object
1194
     *
1195
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1196
     * @return bool True if any changes are detected
1197
     */
1198
    protected function updateChanges($forceChanges = false)
1199
    {
1200
        if ($forceChanges) {
1201
            // Force changes, but only for loaded fields
1202
            foreach ($this->record as $field => $value) {
1203
                $this->changed[$field] = static::CHANGE_VALUE;
1204
            }
1205
            return true;
1206
        }
1207
        return $this->isChanged();
1208
    }
1209
1210
    /**
1211
     * Writes a subset of changes for a specific table to the given manipulation
1212
     *
1213
     * @param string $baseTable Base table
1214
     * @param string $now Timestamp to use for the current time
1215
     * @param bool $isNewRecord Whether this should be treated as a new record write
1216
     * @param array $manipulation Manipulation to write to
1217
     * @param string $class Class of table to manipulate
1218
     */
1219
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1220
    {
1221
        $schema = $this->getSchema();
1222
        $table = $schema->tableName($class);
1223
        $manipulation[$table] = array();
1224
1225
        // Extract records for this table
1226
        foreach ($this->record as $fieldName => $fieldValue) {
1227
            // we're not attempting to reset the BaseTable->ID
1228
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1229
            if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1230
                continue;
1231
            }
1232
1233
            // Ensure this field pertains to this table
1234
            $specification = $schema->fieldSpec($class, $fieldName, DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED);
1235
            if (!$specification) {
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...
1236
                continue;
1237
            }
1238
1239
            // if database column doesn't correlate to a DBField instance...
1240
            $fieldObj = $this->dbObject($fieldName);
1241
            if (!$fieldObj) {
1242
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1243
            }
1244
1245
            // Write to manipulation
1246
            $fieldObj->writeToManipulation($manipulation[$table]);
1247
        }
1248
1249
        // Ensure update of Created and LastEdited columns
1250
        if ($baseTable === $table) {
1251
            $manipulation[$table]['fields']['LastEdited'] = $now;
1252
            if ($isNewRecord) {
1253
                $manipulation[$table]['fields']['Created']
1254
                    = empty($this->record['Created'])
1255
                        ? $now
1256
                        : $this->record['Created'];
1257
                $manipulation[$table]['fields']['ClassName'] = static::class;
1258
            }
1259
        }
1260
1261
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1262
        // attempt an update, as though it were a normal update.
1263
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1264
        $manipulation[$table]['id'] = $this->record['ID'];
1265
        $manipulation[$table]['class'] = $class;
1266
    }
1267
1268
    /**
1269
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1270
     *
1271
     * Does nothing if an ID is already assigned for this record
1272
     *
1273
     * @param string $baseTable Base table
1274
     * @param string $now Timestamp to use for the current time
1275
     */
1276
    protected function writeBaseRecord($baseTable, $now)
1277
    {
1278
        // Generate new ID if not specified
1279
        if ($this->isInDB()) {
1280
            return;
1281
        }
1282
1283
        // Perform an insert on the base table
1284
        $insert = new SQLInsert('"'.$baseTable.'"');
1285
        $insert
1286
            ->assign('"Created"', $now)
1287
            ->execute();
1288
        $this->changed['ID'] = self::CHANGE_VALUE;
1289
        $this->record['ID'] = DB::get_generated_id($baseTable);
1290
    }
1291
1292
    /**
1293
     * Generate and write the database manipulation for all changed fields
1294
     *
1295
     * @param string $baseTable Base table
1296
     * @param string $now Timestamp to use for the current time
1297
     * @param bool $isNewRecord If this is a new record
1298
     */
1299
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1300
    {
1301
        // Generate database manipulations for each class
1302
        $manipulation = array();
1303
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1304
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1305
        }
1306
1307
        // Allow extensions to extend this manipulation
1308
        $this->extend('augmentWrite', $manipulation);
1309
1310
        // New records have their insert into the base data table done first, so that they can pass the
1311
        // generated ID on to the rest of the manipulation
1312
        if ($isNewRecord) {
1313
            $manipulation[$baseTable]['command'] = 'update';
1314
        }
1315
1316
        // Perform the manipulation
1317
        DB::manipulate($manipulation);
1318
    }
1319
1320
    /**
1321
     * Writes all changes to this object to the database.
1322
     *  - It will insert a record whenever ID isn't set, otherwise update.
1323
     *  - All relevant tables will be updated.
1324
     *  - $this->onBeforeWrite() gets called beforehand.
1325
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1326
     *
1327
     *  @uses DataExtension->augmentWrite()
1328
     *
1329
     * @param boolean $showDebug Show debugging information
1330
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1331
     * @param boolean $forceWrite Write to database even if there are no changes
1332
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1333
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1334
     *                                 {@link getManyManyComponents()} (Default: false)
1335
     * @return int The ID of the record
1336
     * @throws ValidationException Exception that can be caught and handled by the calling function
1337
     */
1338
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1339
    {
1340
        $now = DBDatetime::now()->Rfc2822();
1341
1342
        // Execute pre-write tasks
1343
        $this->preWrite();
1344
1345
        // Check if we are doing an update or an insert
1346
        $isNewRecord = !$this->isInDB() || $forceInsert;
1347
1348
        // Check changes exist, abort if there are none
1349
        $hasChanges = $this->updateChanges($isNewRecord);
1350
        if ($hasChanges || $forceWrite || $isNewRecord) {
1351
            // Ensure Created and LastEdited are populated
1352
            if (!isset($this->record['Created'])) {
1353
                $this->record['Created'] = $now;
1354
            }
1355
            $this->record['LastEdited'] = $now;
1356
1357
            // New records have their insert into the base data table done first, so that they can pass the
1358
            // generated primary key on to the rest of the manipulation
1359
            $baseTable = $this->baseTable();
1360
            $this->writeBaseRecord($baseTable, $now);
1361
1362
            // Write the DB manipulation for all changed fields
1363
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1364
1365
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1366
            $this->writeRelations();
1367
            $this->onAfterWrite();
1368
            $this->changed = array();
1369
        } else {
1370
            if ($showDebug) {
1371
                Debug::message("no changes for DataObject");
1372
            }
1373
1374
            // Used by DODs to clean up after themselves, eg, Versioned
1375
            $this->invokeWithExtensions('onAfterSkippedWrite');
1376
        }
1377
1378
        // Write relations as necessary
1379
        if ($writeComponents) {
1380
            $this->writeComponents(true);
1381
        }
1382
1383
        // Clears the cache for this object so get_one returns the correct object.
1384
        $this->flushCache();
1385
1386
        return $this->record['ID'];
1387
    }
1388
1389
    /**
1390
     * Writes cached relation lists to the database, if possible
1391
     */
1392
    public function writeRelations()
1393
    {
1394
        if (!$this->isInDB()) {
1395
            return;
1396
        }
1397
1398
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1399
        if ($this->unsavedRelations) {
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...
1400
            foreach ($this->unsavedRelations as $name => $list) {
1401
                $list->changeToList($this->$name());
1402
            }
1403
            $this->unsavedRelations = array();
1404
        }
1405
    }
1406
1407
    /**
1408
     * Write the cached components to the database. Cached components could refer to two different instances of the
1409
     * same record.
1410
     *
1411
     * @param bool $recursive Recursively write components
1412
     * @return DataObject $this
1413
     */
1414
    public function writeComponents($recursive = false)
1415
    {
1416
        if ($this->components) {
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...
1417
            foreach ($this->components as $component) {
1418
                $component->write(false, false, false, $recursive);
1419
            }
1420
        }
1421
1422
        if ($join = $this->getJoin()) {
1423
            $join->write(false, false, false, $recursive);
1424
        }
1425
1426
        return $this;
1427
    }
1428
1429
    /**
1430
     * Delete this data object.
1431
     * $this->onBeforeDelete() gets called.
1432
     * Note that in Versioned objects, both Stage and Live will be deleted.
1433
     *  @uses DataExtension->augmentSQL()
1434
     */
1435
    public function delete()
1436
    {
1437
        $this->brokenOnDelete = true;
1438
        $this->onBeforeDelete();
1439
        if ($this->brokenOnDelete) {
1440
            user_error(static::class . " has a broken onBeforeDelete() function."
1441
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1442
        }
1443
1444
        // Deleting a record without an ID shouldn't do anything
1445
        if (!$this->ID) {
1446
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1447
        }
1448
1449
        // TODO: This is quite ugly.  To improve:
1450
        //  - move the details of the delete code in the DataQuery system
1451
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1452
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1453
        $srcQuery = DataList::create(static::class)
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

1453
        $srcQuery = DataList::create(/** @scrutinizer ignore-type */ static::class)
Loading history...
1454
            ->filter('ID', $this->ID)
1455
            ->dataQuery()
1456
            ->query();
1457
        $queriedTables = $srcQuery->queriedTables();
1458
        $this->extend('updateDeleteTables', $queriedTables, $srcQuery);
1459
        foreach ($queriedTables as $table) {
1460
            $delete = SQLDelete::create("\"$table\"", array('"ID"' => $this->ID));
1461
            $this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
1462
            $delete->execute();
1463
        }
1464
        // Remove this item out of any caches
1465
        $this->flushCache();
1466
1467
        $this->onAfterDelete();
1468
1469
        $this->OldID = $this->ID;
1470
        $this->ID = 0;
1471
    }
1472
1473
    /**
1474
     * Delete the record with the given ID.
1475
     *
1476
     * @param string $className The class name of the record to be deleted
1477
     * @param int $id ID of record to be deleted
1478
     */
1479
    public static function delete_by_id($className, $id)
1480
    {
1481
        $obj = DataObject::get_by_id($className, $id);
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...
1482
        if ($obj) {
1483
            $obj->delete();
1484
        } else {
1485
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1486
        }
1487
    }
1488
1489
    /**
1490
     * Get the class ancestry, including the current class name.
1491
     * The ancestry will be returned as an array of class names, where the 0th element
1492
     * will be the class that inherits directly from DataObject, and the last element
1493
     * will be the current class.
1494
     *
1495
     * @return array Class ancestry
1496
     */
1497
    public function getClassAncestry()
1498
    {
1499
        return ClassInfo::ancestry(static::class);
1500
    }
1501
1502
    /**
1503
     * Return a component object from a one to one relationship, as a DataObject.
1504
     * If no component is available, an 'empty component' will be returned for
1505
     * non-polymorphic relations, or for polymorphic relations with a class set.
1506
     *
1507
     * @param string $componentName Name of the component
1508
     * @return DataObject The component object. It's exact type will be that of the component.
1509
     * @throws Exception
1510
     */
1511
    public function getComponent($componentName)
1512
    {
1513
        if (isset($this->components[$componentName])) {
1514
            return $this->components[$componentName];
1515
        }
1516
1517
        $schema = static::getSchema();
1518
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1519
            $joinField = $componentName . 'ID';
1520
            $joinID    = $this->getField($joinField);
1521
1522
            // Extract class name for polymorphic relations
1523
            if ($class === self::class) {
1524
                $class = $this->getField($componentName . 'Class');
1525
                if (empty($class)) {
1526
                    return null;
1527
                }
1528
            }
1529
1530
            if ($joinID) {
1531
                // Ensure that the selected object originates from the same stage, subsite, etc
1532
                $component = DataObject::get($class)
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type object; however, parameter $callerClass of SilverStripe\ORM\DataObject::get() 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

1532
                $component = DataObject::get(/** @scrutinizer ignore-type */ $class)
Loading history...
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...
1533
                    ->filter('ID', $joinID)
1534
                    ->setDataQueryParam($this->getInheritableQueryParams())
1535
                    ->first();
1536
            }
1537
1538
            if (empty($component)) {
1539
                $component = Injector::inst()->create($class);
1540
            }
1541
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1542
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1543
            $joinID = $this->ID;
1544
1545
            if ($joinID) {
1546
                // Prepare filter for appropriate join type
1547
                if ($polymorphic) {
1548
                    $filter = array(
1549
                        "{$joinField}ID" => $joinID,
1550
                        "{$joinField}Class" => static::class,
1551
                    );
1552
                } else {
1553
                    $filter = array(
1554
                        $joinField => $joinID
1555
                    );
1556
                }
1557
1558
                // Ensure that the selected object originates from the same stage, subsite, etc
1559
                $component = DataObject::get($class)
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...
1560
                    ->filter($filter)
1561
                    ->setDataQueryParam($this->getInheritableQueryParams())
1562
                    ->first();
1563
            }
1564
1565
            if (empty($component)) {
1566
                $component = Injector::inst()->create($class);
1567
                if ($polymorphic) {
1568
                    $component->{$joinField.'ID'} = $this->ID;
1569
                    $component->{$joinField.'Class'} = static::class;
1570
                } else {
1571
                    $component->$joinField = $this->ID;
1572
                }
1573
            }
1574
        } else {
1575
            throw new InvalidArgumentException(
1576
                "DataObject->getComponent(): Could not find component '$componentName'."
1577
            );
1578
        }
1579
1580
        $this->components[$componentName] = $component;
1581
        return $component;
1582
    }
1583
1584
    /**
1585
     * Returns a one-to-many relation as a HasManyList
1586
     *
1587
     * @param string $componentName Name of the component
1588
     * @param int|array $id Optional ID(s) for parent of this relation, if not the current record
1589
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1590
     */
1591
    public function getComponents($componentName, $id = null)
1592
    {
1593
        if (!isset($id)) {
1594
            $id = $this->ID;
1595
        }
1596
        $result = null;
1597
1598
        $schema = $this->getSchema();
1599
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1600
        if (!$componentClass) {
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...
1601
            throw new InvalidArgumentException(sprintf(
1602
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1603
                $componentName,
1604
                static::class
1605
            ));
1606
        }
1607
1608
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1609
        if (!$id) {
1610
            if (!isset($this->unsavedRelations[$componentName])) {
1611
                $this->unsavedRelations[$componentName] =
1612
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1613
            }
1614
            return $this->unsavedRelations[$componentName];
1615
        }
1616
1617
        // Determine type and nature of foreign relation
1618
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1619
        /** @var HasManyList $result */
1620
        if ($polymorphic) {
1621
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
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

1621
            $result = PolymorphicHasManyList::create(/** @scrutinizer ignore-type */ $componentClass, $joinField, static::class);
Loading history...
1622
        } else {
1623
            $result = HasManyList::create($componentClass, $joinField);
1624
        }
1625
1626
        return $result
1627
            ->setDataQueryParam($this->getInheritableQueryParams())
1628
            ->forForeignID($id);
1629
    }
1630
1631
    /**
1632
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1633
     *
1634
     * @param string $relationName Relation name.
1635
     * @return string Class name, or null if not found.
1636
     */
1637
    public function getRelationClass($relationName)
1638
    {
1639
        // Parse many_many
1640
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1641
        if ($manyManyComponent) {
1642
            return $manyManyComponent['childClass'];
1643
        }
1644
1645
        // Go through all relationship configuration fields.
1646
        $config = $this->config();
1647
        $candidates = array_merge(
1648
            ($relations = $config->get('has_one')) ? $relations : array(),
1649
            ($relations = $config->get('has_many')) ? $relations : array(),
1650
            ($relations = $config->get('belongs_to')) ? $relations : array()
1651
        );
1652
1653
        if (isset($candidates[$relationName])) {
1654
            $remoteClass = $candidates[$relationName];
1655
1656
            // If dot notation is present, extract just the first part that contains the class.
1657
            if (($fieldPos = strpos($remoteClass, '.'))!==false) {
1658
                return substr($remoteClass, 0, $fieldPos);
1659
            }
1660
1661
            // Otherwise just return the class
1662
            return $remoteClass;
1663
        }
1664
1665
        return null;
1666
    }
1667
1668
    /**
1669
     * Given a relation name, determine the relation type
1670
     *
1671
     * @param string $component Name of component
1672
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1673
     */
1674
    public function getRelationType($component)
1675
    {
1676
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1677
        $config = $this->config();
1678
        foreach ($types as $type) {
1679
            $relations = $config->get($type);
1680
            if ($relations && isset($relations[$component])) {
1681
                return $type;
1682
            }
1683
        }
1684
        return null;
1685
    }
1686
1687
    /**
1688
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1689
     * side of the relation.
1690
     *
1691
     * Notes on behaviour:
1692
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1693
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1694
     *  - Cannot be used on polymorphic relationships
1695
     *  - Cannot be used on unsaved objects.
1696
     *
1697
     * @param string $remoteClass
1698
     * @param string $remoteRelation
1699
     * @return DataList|DataObject The component, either as a list or single object
1700
     * @throws BadMethodCallException
1701
     * @throws InvalidArgumentException
1702
     */
1703
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1704
    {
1705
        $remote = DataObject::singleton($remoteClass);
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...
1706
        $class = $remote->getRelationClass($remoteRelation);
1707
        $schema = static::getSchema();
1708
1709
        // Validate arguments
1710
        if (!$this->isInDB()) {
1711
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1712
        }
1713
        if (empty($class)) {
1714
            throw new InvalidArgumentException(sprintf(
1715
                "%s invoked with invalid relation %s.%s",
1716
                __METHOD__,
1717
                $remoteClass,
1718
                $remoteRelation
1719
            ));
1720
        }
1721
        if ($class === self::class) {
1722
            throw new InvalidArgumentException(sprintf(
1723
                "%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1724
                "This method does not support polymorphic relationships",
1725
                __METHOD__,
1726
                $remoteClass,
1727
                $remoteRelation
1728
            ));
1729
        }
1730
        if (!is_a($this, $class, true)) {
1731
            throw new InvalidArgumentException(sprintf(
1732
                "Relation %s on %s does not refer to objects of type %s",
1733
                $remoteRelation,
1734
                $remoteClass,
1735
                static::class
1736
            ));
1737
        }
1738
1739
        // Check the relation type to mock
1740
        $relationType = $remote->getRelationType($remoteRelation);
1741
        switch ($relationType) {
1742
            case 'has_one': {
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...
1743
                // Mock has_many
1744
                $joinField = "{$remoteRelation}ID";
1745
                $componentClass = $schema->classForField($remoteClass, $joinField);
1746
                $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

1746
                $result = HasManyList::create(/** @scrutinizer ignore-type */ $componentClass, $joinField);
Loading history...
1747
                return $result
1748
                    ->setDataQueryParam($this->getInheritableQueryParams())
1749
                    ->forForeignID($this->ID);
1750
            }
1751
            case 'belongs_to':
1752
            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...
1753
                // These relations must have a has_one on the other end, so find it
1754
                $joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic);
1755
                if ($polymorphic) {
1756
                    throw new InvalidArgumentException(sprintf(
1757
                        "%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1758
                        "to be a has_one polymorphic. This method does not support polymorphic relationships",
1759
                        __METHOD__,
1760
                        $remoteClass,
1761
                        $remoteRelation
1762
                    ));
1763
                }
1764
                $joinID = $this->getField($joinField);
1765
                if (empty($joinID)) {
1766
                    return null;
1767
                }
1768
                // Get object by joined ID
1769
                return DataObject::get($remoteClass)
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...
1770
                    ->filter('ID', $joinID)
1771
                    ->setDataQueryParam($this->getInheritableQueryParams())
1772
                    ->first();
1773
            }
1774
            case 'many_many':
1775
            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...
1776
                // Get components and extra fields from parent
1777
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1778
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1779
1780
                // Reverse parent and component fields and create an inverse ManyManyList
1781
                /** @var RelationList $result */
1782
                $result = Injector::inst()->create(
1783
                    $manyMany['relationClass'],
1784
                    $manyMany['parentClass'], // Substitute parent class for dataClass
1785
                    $manyMany['join'],
1786
                    $manyMany['parentField'], // Reversed parent / child field
1787
                    $manyMany['childField'], // Reversed parent / child field
1788
                    $extraFields
1789
                );
1790
                $this->extend('updateManyManyComponents', $result);
1791
1792
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1793
                // foreignID set elsewhere.
1794
                return $result
1795
                    ->setDataQueryParam($this->getInheritableQueryParams())
1796
                    ->forForeignID($this->ID);
1797
            }
1798
            default: {
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...
1799
                return null;
1800
            }
1801
        }
1802
    }
1803
1804
    /**
1805
     * Returns a many-to-many component, as a ManyManyList.
1806
     * @param string $componentName Name of the many-many component
1807
     * @param int|array $id Optional ID for parent of this relation, if not the current record
1808
     * @return RelationList|UnsavedRelationList The set of components
1809
     */
1810
    public function getManyManyComponents($componentName, $id = null)
1811
    {
1812
        if (!isset($id)) {
1813
            $id = $this->ID;
1814
        }
1815
        $schema = static::getSchema();
1816
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
1817
        if (!$manyManyComponent) {
1818
            throw new InvalidArgumentException(sprintf(
1819
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1820
                $componentName,
1821
                static::class
1822
            ));
1823
        }
1824
1825
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1826
        if (!$id) {
1827
            if (!isset($this->unsavedRelations[$componentName])) {
1828
                $this->unsavedRelations[$componentName] =
1829
                    new UnsavedRelationList($manyManyComponent['parentClass'], $componentName, $manyManyComponent['childClass']);
1830
            }
1831
            return $this->unsavedRelations[$componentName];
1832
        }
1833
1834
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
1835
        /** @var RelationList $result */
1836
        $result = Injector::inst()->create(
1837
            $manyManyComponent['relationClass'],
1838
            $manyManyComponent['childClass'],
1839
            $manyManyComponent['join'],
1840
            $manyManyComponent['childField'],
1841
            $manyManyComponent['parentField'],
1842
            $extraFields
1843
        );
1844
1845
1846
        // Store component data in query meta-data
1847
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
1848
            /** @var DataQuery $query */
1849
            $query->setQueryParam('Component.ExtraFields', $extraFields);
1850
        });
1851
1852
        $this->extend('updateManyManyComponents', $result);
1853
1854
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1855
        // foreignID set elsewhere.
1856
        return $result
1857
            ->setDataQueryParam($this->getInheritableQueryParams())
1858
            ->forForeignID($id);
1859
    }
1860
1861
    /**
1862
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1863
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1864
     *
1865
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1866
     *                          their classes.
1867
     */
1868
    public function hasOne()
1869
    {
1870
        return (array)$this->config()->get('has_one');
1871
    }
1872
1873
    /**
1874
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1875
     * their class name will be returned.
1876
     *
1877
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1878
     *        the field data stripped off. It defaults to TRUE.
1879
     * @return string|array
1880
     */
1881
    public function belongsTo($classOnly = true)
1882
    {
1883
        $belongsTo = (array)$this->config()->get('belongs_to');
1884
        if ($belongsTo && $classOnly) {
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...
1885
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1886
        } else {
1887
            return $belongsTo ? $belongsTo : array();
1888
        }
1889
    }
1890
1891
    /**
1892
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1893
     * relationships and their classes will be returned.
1894
     *
1895
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1896
     *        the field data stripped off. It defaults to TRUE.
1897
     * @return string|array|false
1898
     */
1899
    public function hasMany($classOnly = true)
1900
    {
1901
        $hasMany = (array)$this->config()->get('has_many');
1902
        if ($hasMany && $classOnly) {
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...
1903
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1904
        } else {
1905
            return $hasMany ? $hasMany : array();
1906
        }
1907
    }
1908
1909
    /**
1910
     * Return the many-to-many extra fields specification.
1911
     *
1912
     * If you don't specify a component name, it returns all
1913
     * extra fields for all components available.
1914
     *
1915
     * @return array|null
1916
     */
1917
    public function manyManyExtraFields()
1918
    {
1919
        return $this->config()->get('many_many_extraFields');
1920
    }
1921
1922
    /**
1923
     * Return information about a many-to-many component.
1924
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
1925
     * components are returned.
1926
     *
1927
     * @see DataObjectSchema::manyManyComponent()
1928
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
1929
     */
1930
    public function manyMany()
1931
    {
1932
        $config = $this->config();
1933
        $manyManys = (array)$config->get('many_many');
1934
        $belongsManyManys = (array)$config->get('belongs_many_many');
1935
        $items = array_merge($manyManys, $belongsManyManys);
1936
        return $items;
1937
    }
1938
1939
    /**
1940
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
1941
     *
1942
     * This is experimental, and is currently only a Postgres-specific enhancement.
1943
     *
1944
     * @param string $class
1945
     * @return array|false
1946
     */
1947
    public function database_extensions($class)
1948
    {
1949
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
1950
        if ($extensions) {
1951
            return $extensions;
1952
        } else {
1953
            return false;
1954
        }
1955
    }
1956
1957
    /**
1958
     * Generates a SearchContext to be used for building and processing
1959
     * a generic search form for properties on this object.
1960
     *
1961
     * @return SearchContext
1962
     */
1963
    public function getDefaultSearchContext()
1964
    {
1965
        return new SearchContext(
1966
            static::class,
1967
            $this->scaffoldSearchFields(),
1968
            $this->defaultSearchFilters()
1969
        );
1970
    }
1971
1972
    /**
1973
     * Determine which properties on the DataObject are
1974
     * searchable, and map them to their default {@link FormField}
1975
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
1976
     *
1977
     * Some additional logic is included for switching field labels, based on
1978
     * how generic or specific the field type is.
1979
     *
1980
     * Used by {@link SearchContext}.
1981
     *
1982
     * @param array $_params
1983
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
1984
     *   'restrictFields': Numeric array of a field name whitelist
1985
     * @return FieldList
1986
     */
1987
    public function scaffoldSearchFields($_params = null)
1988
    {
1989
        $params = array_merge(
1990
            array(
1991
                'fieldClasses' => false,
1992
                'restrictFields' => false
1993
            ),
1994
            (array)$_params
1995
        );
1996
        $fields = new FieldList();
1997
        foreach ($this->searchableFields() as $fieldName => $spec) {
1998
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
1999
                continue;
2000
            }
2001
2002
            // If a custom fieldclass is provided as a string, use it
2003
            $field = null;
2004
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2005
                $fieldClass = $params['fieldClasses'][$fieldName];
2006
                $field = new $fieldClass($fieldName);
2007
            // If we explicitly set a field, then construct that
2008
            } elseif (isset($spec['field'])) {
2009
                // If it's a string, use it as a class name and construct
2010
                if (is_string($spec['field'])) {
2011
                    $fieldClass = $spec['field'];
2012
                    $field = new $fieldClass($fieldName);
2013
2014
                // If it's a FormField object, then just use that object directly.
2015
                } elseif ($spec['field'] instanceof FormField) {
2016
                    $field = $spec['field'];
2017
2018
                // Otherwise we have a bug
2019
                } else {
2020
                    user_error("Bad value for searchable_fields, 'field' value: "
2021
                        . var_export($spec['field'], true), E_USER_WARNING);
2022
                }
2023
2024
            // Otherwise, use the database field's scaffolder
2025
            } elseif ($object = $this->relObject($fieldName)) {
2026
                $field = $object->scaffoldSearchField();
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

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

2067
        $fs = FormScaffolder::create(/** @scrutinizer ignore-type */ $this);
Loading history...
2068
        $fs->tabbed = $params['tabbed'];
2069
        $fs->includeRelations = $params['includeRelations'];
2070
        $fs->restrictFields = $params['restrictFields'];
2071
        $fs->fieldClasses = $params['fieldClasses'];
2072
        $fs->ajaxSafe = $params['ajaxSafe'];
2073
2074
        return $fs->getFieldList();
2075
    }
2076
2077
    /**
2078
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2079
     * being called on extensions
2080
     *
2081
     * @param callable $callback The callback to execute
2082
     */
2083
    protected function beforeUpdateCMSFields($callback)
2084
    {
2085
        $this->beforeExtending('updateCMSFields', $callback);
2086
    }
2087
2088
    /**
2089
     * Centerpiece of every data administration interface in Silverstripe,
2090
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2091
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2092
     * generate this set. To customize, overload this method in a subclass
2093
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2094
     *
2095
     * <code>
2096
     * class MyCustomClass extends DataObject {
2097
     *  static $db = array('CustomProperty'=>'Boolean');
2098
     *
2099
     *  function getCMSFields() {
2100
     *    $fields = parent::getCMSFields();
2101
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2102
     *    return $fields;
2103
     *  }
2104
     * }
2105
     * </code>
2106
     *
2107
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2108
     *
2109
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2110
     */
2111
    public function getCMSFields()
2112
    {
2113
        $tabbedFields = $this->scaffoldFormFields(array(
2114
            // Don't allow has_many/many_many relationship editing before the record is first saved
2115
            'includeRelations' => ($this->ID > 0),
2116
            'tabbed' => true,
2117
            'ajaxSafe' => true
2118
        ));
2119
2120
        $this->extend('updateCMSFields', $tabbedFields);
2121
2122
        return $tabbedFields;
2123
    }
2124
2125
    /**
2126
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2127
     * including that dataobject's extensions customised actions could be added to the EditForm.
2128
     *
2129
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2130
     */
2131
    public function getCMSActions()
2132
    {
2133
        $actions = new FieldList();
2134
        $this->extend('updateCMSActions', $actions);
2135
        return $actions;
2136
    }
2137
2138
2139
    /**
2140
     * Used for simple frontend forms without relation editing
2141
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2142
     * by default. To customize, either overload this method in your
2143
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2144
     *
2145
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2146
     *
2147
     * @param array $params See {@link scaffoldFormFields()}
2148
     * @return FieldList Always returns a simple field collection without TabSet.
2149
     */
2150
    public function getFrontEndFields($params = null)
2151
    {
2152
        $untabbedFields = $this->scaffoldFormFields($params);
2153
        $this->extend('updateFrontEndFields', $untabbedFields);
2154
2155
        return $untabbedFields;
2156
    }
2157
2158
    public function getViewerTemplates($suffix = '')
2159
    {
2160
        return SSViewer::get_templates_by_class(static::class, $suffix, $this->baseClass());
2161
    }
2162
2163
    /**
2164
     * Gets the value of a field.
2165
     * Called by {@link __get()} and any getFieldName() methods you might create.
2166
     *
2167
     * @param string $field The name of the field
2168
     * @return mixed The field value
2169
     */
2170
    public function getField($field)
2171
    {
2172
        // If we already have an object in $this->record, then we should just return that
2173
        if (isset($this->record[$field]) && is_object($this->record[$field])) {
2174
            return $this->record[$field];
2175
        }
2176
2177
        // Do we have a field that needs to be lazy loaded?
2178
        if (isset($this->record[$field.'_Lazy'])) {
2179
            $tableClass = $this->record[$field.'_Lazy'];
2180
            $this->loadLazyFields($tableClass);
2181
        }
2182
2183
        // In case of complex fields, return the DBField object
2184
        if (static::getSchema()->compositeField(static::class, $field)) {
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...
2185
            $this->record[$field] = $this->dbObject($field);
2186
        }
2187
2188
        return isset($this->record[$field]) ? $this->record[$field] : null;
2189
    }
2190
2191
    /**
2192
     * Loads all the stub fields that an initial lazy load didn't load fully.
2193
     *
2194
     * @param string $class Class to load the values from. Others are joined as required.
2195
     * Not specifying a tableClass will load all lazy fields from all tables.
2196
     * @return bool Flag if lazy loading succeeded
2197
     */
2198
    protected function loadLazyFields($class = null)
2199
    {
2200
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2201
            return false;
2202
        }
2203
2204
        if (!$class) {
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...
2205
            $loaded = array();
2206
2207
            foreach ($this->record as $key => $value) {
2208
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2209
                    $this->loadLazyFields($value);
2210
                    $loaded[$value] = $value;
2211
                }
2212
            }
2213
2214
            return false;
2215
        }
2216
2217
        $dataQuery = new DataQuery($class);
2218
2219
        // Reset query parameter context to that of this DataObject
2220
        if ($params = $this->getSourceQueryParams()) {
2221
            foreach ($params as $key => $value) {
2222
                $dataQuery->setQueryParam($key, $value);
2223
            }
2224
        }
2225
2226
        // Limit query to the current record, unless it has the Versioned extension,
2227
        // in which case it requires special handling through augmentLoadLazyFields()
2228
        $schema = static::getSchema();
2229
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2230
        $dataQuery->where([
2231
            $baseIDColumn => $this->record['ID']
2232
        ])->limit(1);
2233
2234
        $columns = array();
2235
2236
        // Add SQL for fields, both simple & multi-value
2237
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2238
        $databaseFields = $schema->databaseFields($class, false);
2239
        foreach ($databaseFields as $k => $v) {
2240
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2241
                $columns[] = $k;
2242
            }
2243
        }
2244
2245
        if ($columns) {
2246
            $query = $dataQuery->query();
2247
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2248
            $this->extend('augmentSQL', $query, $dataQuery);
2249
2250
            $dataQuery->setQueriedColumns($columns);
2251
            $newData = $dataQuery->execute()->record();
2252
2253
            // Load the data into record
2254
            if ($newData) {
2255
                foreach ($newData as $k => $v) {
2256
                    if (in_array($k, $columns)) {
2257
                        $this->record[$k] = $v;
2258
                        $this->original[$k] = $v;
2259
                        unset($this->record[$k . '_Lazy']);
2260
                    }
2261
                }
2262
2263
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2264
            } else {
2265
                foreach ($columns as $k) {
2266
                    $this->record[$k] = null;
2267
                    $this->original[$k] = null;
2268
                    unset($this->record[$k . '_Lazy']);
2269
                }
2270
            }
2271
        }
2272
        return true;
2273
    }
2274
2275
    /**
2276
     * Return the fields that have changed.
2277
     *
2278
     * The change level affects what the functions defines as "changed":
2279
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2280
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2281
     *   for example a change from 0 to null would not be included.
2282
     *
2283
     * Example return:
2284
     * <code>
2285
     * array(
2286
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2287
     * )
2288
     * </code>
2289
     *
2290
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2291
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2292
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2293
     * @return array
2294
     */
2295
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2296
    {
2297
        $changedFields = array();
2298
2299
        // Update the changed array with references to changed obj-fields
2300
        foreach ($this->record as $k => $v) {
2301
            // Prevents DBComposite infinite looping on isChanged
2302
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2303
                continue;
2304
            }
2305
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2306
                $this->changed[$k] = self::CHANGE_VALUE;
2307
            }
2308
        }
2309
2310
        if (is_array($databaseFieldsOnly)) {
2311
            $fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2312
        } elseif ($databaseFieldsOnly) {
2313
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2314
            $fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
2315
        } else {
2316
            $fields = $this->changed;
2317
        }
2318
2319
        // Filter the list to those of a certain change level
2320
        if ($changeLevel > self::CHANGE_STRICT) {
2321
            if ($fields) {
2322
                foreach ($fields as $name => $level) {
2323
                    if ($level < $changeLevel) {
2324
                        unset($fields[$name]);
2325
                    }
2326
                }
2327
            }
2328
        }
2329
2330
        if ($fields) {
2331
            foreach ($fields as $name => $level) {
2332
                $changedFields[$name] = array(
2333
                'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2334
                'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2335
                'level' => $level
2336
                );
2337
            }
2338
        }
2339
2340
        return $changedFields;
2341
    }
2342
2343
    /**
2344
     * Uses {@link getChangedFields()} to determine if fields have been changed
2345
     * since loading them from the database.
2346
     *
2347
     * @param string $fieldName Name of the database field to check, will check for any if not given
2348
     * @param int $changeLevel See {@link getChangedFields()}
2349
     * @return boolean
2350
     */
2351
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2352
    {
2353
        $fields = $fieldName ? array($fieldName) : true;
2354
        $changed = $this->getChangedFields($fields, $changeLevel);
2355
        if (!isset($fieldName)) {
2356
            return !empty($changed);
2357
        } else {
2358
            return array_key_exists($fieldName, $changed);
2359
        }
2360
    }
2361
2362
    /**
2363
     * Set the value of the field
2364
     * Called by {@link __set()} and any setFieldName() methods you might create.
2365
     *
2366
     * @param string $fieldName Name of the field
2367
     * @param mixed $val New field value
2368
     * @return $this
2369
     */
2370
    public function setField($fieldName, $val)
2371
    {
2372
        $this->objCacheClear();
2373
        //if it's a has_one component, destroy the cache
2374
        if (substr($fieldName, -2) == 'ID') {
2375
            unset($this->components[substr($fieldName, 0, -2)]);
2376
        }
2377
2378
        // If we've just lazy-loaded the column, then we need to populate the $original array
2379
        if (isset($this->record[$fieldName.'_Lazy'])) {
2380
            $tableClass = $this->record[$fieldName.'_Lazy'];
2381
            $this->loadLazyFields($tableClass);
2382
        }
2383
2384
        // Situation 1: Passing an DBField
2385
        if ($val instanceof DBField) {
2386
            $val->setName($fieldName);
2387
            $val->saveInto($this);
2388
2389
            // Situation 1a: Composite fields should remain bound in case they are
2390
            // later referenced to update the parent dataobject
2391
            if ($val instanceof DBComposite) {
2392
                $val->bindTo($this);
2393
                $this->record[$fieldName] = $val;
2394
            }
2395
        // Situation 2: Passing a literal or non-DBField object
2396
        } else {
2397
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2398
            if (is_object($val) && static::getSchema()->fieldSpec(static::class, $fieldName)) {
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...
2399
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2400
            }
2401
2402
            // if a field is not existing or has strictly changed
2403
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2404
                // TODO Add check for php-level defaults which are not set in the db
2405
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2406
                // At the very least, the type has changed
2407
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2408
2409
                if ((!isset($this->record[$fieldName]) && $val)
2410
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2411
                ) {
2412
                    // Value has changed as well, not just the type
2413
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2414
                }
2415
2416
                // Value is always saved back when strict check succeeds.
2417
                $this->record[$fieldName] = $val;
2418
            }
2419
        }
2420
        return $this;
2421
    }
2422
2423
    /**
2424
     * Set the value of the field, using a casting object.
2425
     * This is useful when you aren't sure that a date is in SQL format, for example.
2426
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2427
     * can be saved into the Image table.
2428
     *
2429
     * @param string $fieldName Name of the field
2430
     * @param mixed $value New field value
2431
     * @return $this
2432
     */
2433
    public function setCastedField($fieldName, $value)
2434
    {
2435
        if (!$fieldName) {
2436
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2437
        }
2438
        $fieldObj = $this->dbObject($fieldName);
2439
        if ($fieldObj) {
2440
            $fieldObj->setValue($value);
2441
            $fieldObj->saveInto($this);
2442
        } else {
2443
            $this->$fieldName = $value;
2444
        }
2445
        return $this;
2446
    }
2447
2448
    /**
2449
     * {@inheritdoc}
2450
     */
2451
    public function castingHelper($field)
2452
    {
2453
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2454
        if ($fieldSpec) {
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...
2455
            return $fieldSpec;
2456
        }
2457
2458
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2459
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2460
        $queryParams = $this->getSourceQueryParams();
2461
        if (!empty($queryParams['Component.ExtraFields'])) {
2462
            $extraFields = $queryParams['Component.ExtraFields'];
2463
2464
            if (isset($extraFields[$field])) {
2465
                return $extraFields[$field];
2466
            }
2467
        }
2468
2469
        return parent::castingHelper($field);
2470
    }
2471
2472
    /**
2473
     * Returns true if the given field exists in a database column on any of
2474
     * the objects tables and optionally look up a dynamic getter with
2475
     * get<fieldName>().
2476
     *
2477
     * @param string $field Name of the field
2478
     * @return boolean True if the given field exists
2479
     */
2480
    public function hasField($field)
2481
    {
2482
        $schema = static::getSchema();
2483
        return (
2484
            array_key_exists($field, $this->record)
2485
            || $schema->fieldSpec(static::class, $field)
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...
2486
            || (substr($field, -2) == 'ID') && $schema->hasOneComponent(static::class, substr($field, 0, -2))
0 ignored issues
show
Bug introduced by
It seems like substr($field, 0, -2) can also be of type false; however, parameter $component of SilverStripe\ORM\DataObj...hema::hasOneComponent() 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

2486
            || (substr($field, -2) == 'ID') && $schema->hasOneComponent(static::class, /** @scrutinizer ignore-type */ substr($field, 0, -2))
Loading history...
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...
2487
            || $this->hasMethod("get{$field}")
2488
        );
2489
    }
2490
2491
    /**
2492
     * Returns true if the given field exists as a database column
2493
     *
2494
     * @param string $field Name of the field
2495
     *
2496
     * @return boolean
2497
     */
2498
    public function hasDatabaseField($field)
2499
    {
2500
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2501
        return !empty($spec);
2502
    }
2503
2504
    /**
2505
     * Returns true if the member is allowed to do the given action.
2506
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2507
     *
2508
     * @param string $perm The permission to be checked, such as 'View'.
2509
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2510
     * in user.
2511
     * @param array $context Additional $context to pass to extendedCan()
2512
     *
2513
     * @return boolean True if the the member is allowed to do the given action
2514
     */
2515
    public function can($perm, $member = null, $context = array())
2516
    {
2517
        if (!$member) {
2518
            $member = Security::getCurrentUser();
2519
        }
2520
2521
        if ($member && Permission::checkMember($member, "ADMIN")) {
2522
            return true;
2523
        }
2524
2525
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2526
            $method = 'can' . ucfirst($perm);
2527
            return $this->$method($member);
2528
        }
2529
2530
        $results = $this->extendedCan('can', $member);
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...
2531
        if (isset($results)) {
2532
            return $results;
2533
        }
2534
2535
        return ($member && Permission::checkMember($member, $perm));
2536
    }
2537
2538
    /**
2539
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2540
     * expected to return one of three values:
2541
     *
2542
     *  - false: Disallow this permission, regardless of what other extensions say
2543
     *  - true: Allow this permission, as long as no other extensions return false
2544
     *  - NULL: Don't affect the outcome
2545
     *
2546
     * This method itself returns a tri-state value, and is designed to be used like this:
2547
     *
2548
     * <code>
2549
     * $extended = $this->extendedCan('canDoSomething', $member);
2550
     * if($extended !== null) return $extended;
2551
     * else return $normalValue;
2552
     * </code>
2553
     *
2554
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2555
     * @param Member|int $member
2556
     * @param array $context Optional context
2557
     * @return boolean|null
2558
     */
2559
    public function extendedCan($methodName, $member, $context = array())
2560
    {
2561
        $results = $this->extend($methodName, $member, $context);
2562
        if ($results && is_array($results)) {
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...
2563
            // Remove NULLs
2564
            $results = array_filter($results, function ($v) {
2565
                return !is_null($v);
2566
            });
2567
            // If there are any non-NULL responses, then return the lowest one of them.
2568
            // If any explicitly deny the permission, then we don't get access
2569
            if ($results) {
2570
                return min($results);
2571
            }
2572
        }
2573
        return null;
2574
    }
2575
2576
    /**
2577
     * @param Member $member
2578
     * @return boolean
2579
     */
2580
    public function canView($member = null)
2581
    {
2582
        $extended = $this->extendedCan(__FUNCTION__, $member);
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...
2583
        if ($extended !== null) {
2584
            return $extended;
2585
        }
2586
        return Permission::check('ADMIN', 'any', $member);
2587
    }
2588
2589
    /**
2590
     * @param Member $member
2591
     * @return boolean
2592
     */
2593
    public function canEdit($member = null)
2594
    {
2595
        $extended = $this->extendedCan(__FUNCTION__, $member);
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...
2596
        if ($extended !== null) {
2597
            return $extended;
2598
        }
2599
        return Permission::check('ADMIN', 'any', $member);
2600
    }
2601
2602
    /**
2603
     * @param Member $member
2604
     * @return boolean
2605
     */
2606
    public function canDelete($member = null)
2607
    {
2608
        $extended = $this->extendedCan(__FUNCTION__, $member);
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...
2609
        if ($extended !== null) {
2610
            return $extended;
2611
        }
2612
        return Permission::check('ADMIN', 'any', $member);
2613
    }
2614
2615
    /**
2616
     * @param Member $member
2617
     * @param array $context Additional context-specific data which might
2618
     * affect whether (or where) this object could be created.
2619
     * @return boolean
2620
     */
2621
    public function canCreate($member = null, $context = array())
2622
    {
2623
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
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...
2624
        if ($extended !== null) {
2625
            return $extended;
2626
        }
2627
        return Permission::check('ADMIN', 'any', $member);
2628
    }
2629
2630
    /**
2631
     * Debugging used by Debug::show()
2632
     *
2633
     * @return string HTML data representing this object
2634
     */
2635
    public function debug()
2636
    {
2637
        $class = static::class;
2638
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2639
        if ($this->record) {
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...
2640
            foreach ($this->record as $fieldName => $fieldVal) {
2641
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2642
            }
2643
        }
2644
        $val .= "</ul>\n";
2645
        return $val;
2646
    }
2647
2648
    /**
2649
     * Return the DBField object that represents the given field.
2650
     * This works similarly to obj() with 2 key differences:
2651
     *   - it still returns an object even when the field has no value.
2652
     *   - it only matches fields and not methods
2653
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2654
     *
2655
     * @param string $fieldName Name of the field
2656
     * @return DBField The field as a DBField object
2657
     */
2658
    public function dbObject($fieldName)
2659
    {
2660
        // Check for field in DB
2661
        $schema = static::getSchema();
2662
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2663
        if (!$helper) {
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...
2664
            return null;
2665
        }
2666
2667
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2668
            $tableClass = $this->record[$fieldName . '_Lazy'];
2669
            $this->loadLazyFields($tableClass);
2670
        }
2671
2672
        $value = isset($this->record[$fieldName])
2673
            ? $this->record[$fieldName]
2674
            : null;
2675
2676
        // If we have a DBField object in $this->record, then return that
2677
        if ($value instanceof DBField) {
2678
            return $value;
2679
        }
2680
2681
        list($class, $spec) = explode('.', $helper);
2682
        /** @var DBField $obj */
2683
        $table = $schema->tableName($class);
2684
        $obj = Injector::inst()->create($spec, $fieldName);
2685
        $obj->setTable($table);
2686
        $obj->setValue($value, $this, false);
2687
        return $obj;
2688
    }
2689
2690
    /**
2691
     * Traverses to a DBField referenced by relationships between data objects.
2692
     *
2693
     * The path to the related field is specified with dot separated syntax
2694
     * (eg: Parent.Child.Child.FieldName).
2695
     *
2696
     * If a relation is blank, this will return null instead.
2697
     * If a relation name is invalid (e.g. non-relation on a parent) this
2698
     * can throw a LogicException.
2699
     *
2700
     * @param string $fieldPath List of paths on this object. All items in this path
2701
     * must be ViewableData implementors
2702
     *
2703
     * @return mixed DBField of the field on the object or a DataList instance.
2704
     * @throws LogicException If accessing invalid relations
2705
     */
2706
    public function relObject($fieldPath)
2707
    {
2708
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
2709
        $component = $this;
2710
2711
        // Parse all relations
2712
        foreach (explode('.', $fieldPath) as $relation) {
2713
            if (!$component) {
2714
                return null;
2715
            }
2716
2717
            // Inspect relation type
2718
            if (ClassInfo::hasMethod($component, $relation)) {
2719
                $component = $component->$relation();
2720
            } elseif ($component instanceof Relation || $component instanceof DataList) {
2721
                // $relation could either be a field (aggregate), or another relation
2722
                $singleton = DataObject::singleton($component->dataClass());
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...
2723
                $component = $singleton->dbObject($relation) ?: $component->relation($relation);
2724
            } elseif ($component instanceof DataObject && ($dbObject = $component->dbObject($relation))) {
2725
                $component = $dbObject;
2726
            } elseif ($component instanceof ViewableData && $component->hasField($relation)) {
2727
                $component = $component->obj($relation);
2728
            } else {
2729
                throw new LogicException(
2730
                    "$relation is not a relation/field on ".get_class($component)
2731
                );
2732
            }
2733
        }
2734
        return $component;
2735
    }
2736
2737
    /**
2738
     * Traverses to a field referenced by relationships between data objects, returning the value
2739
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2740
     *
2741
     * @param string $fieldName string
2742
     * @return mixed Will return null on a missing value
2743
     */
2744
    public function relField($fieldName)
2745
    {
2746
        // Navigate to relative parent using relObject() if needed
2747
        $component = $this;
2748
        if (($pos = strrpos($fieldName, '.')) !== false) {
2749
            $relation = substr($fieldName, 0, $pos);
2750
            $fieldName = substr($fieldName, $pos + 1);
2751
            $component = $this->relObject($relation);
0 ignored issues
show
Bug introduced by
It seems like $relation can also be of type false; however, parameter $fieldPath of SilverStripe\ORM\DataObject::relObject() 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

2751
            $component = $this->relObject(/** @scrutinizer ignore-type */ $relation);
Loading history...
2752
        }
2753
2754
        // Bail if the component is null
2755
        if (!$component) {
2756
            return null;
2757
        }
2758
        if (ClassInfo::hasMethod($component, $fieldName)) {
0 ignored issues
show
Bug introduced by
It seems like $fieldName can also be of type false; however, parameter $method of SilverStripe\Core\ClassInfo::hasMethod() 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

2758
        if (ClassInfo::hasMethod($component, /** @scrutinizer ignore-type */ $fieldName)) {
Loading history...
2759
            return $component->$fieldName();
2760
        }
2761
        return $component->$fieldName;
2762
    }
2763
2764
    /**
2765
     * Temporary hack to return an association name, based on class, to get around the mangle
2766
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2767
     *
2768
     * @param string $className
2769
     * @return string
2770
     */
2771
    public function getReverseAssociation($className)
2772
    {
2773
        if (is_array($this->manyMany())) {
2774
            $many_many = array_flip($this->manyMany());
2775
            if (array_key_exists($className, $many_many)) {
2776
                return $many_many[$className];
2777
            }
2778
        }
2779
        if (is_array($this->hasMany())) {
2780
            $has_many = array_flip($this->hasMany());
2781
            if (array_key_exists($className, $has_many)) {
2782
                return $has_many[$className];
2783
            }
2784
        }
2785
        if (is_array($this->hasOne())) {
2786
            $has_one = array_flip($this->hasOne());
2787
            if (array_key_exists($className, $has_one)) {
2788
                return $has_one[$className];
2789
            }
2790
        }
2791
2792
        return false;
2793
    }
2794
2795
    /**
2796
     * Return all objects matching the filter
2797
     * sub-classes are automatically selected and included
2798
     *
2799
     * @param string $callerClass The class of objects to be returned
2800
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2801
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2802
     * @param string|array $sort A sort expression to be inserted into the ORDER
2803
     * BY clause.  If omitted, self::$default_sort will be used.
2804
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
2805
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2806
     * @param string $containerClass The container class to return the results in.
2807
     *
2808
     * @todo $containerClass is Ignored, why?
2809
     *
2810
     * @return DataList The objects matching the filter, in the class specified by $containerClass
2811
     */
2812
    public static function get(
2813
        $callerClass = null,
2814
        $filter = "",
2815
        $sort = "",
2816
        $join = "",
2817
        $limit = null,
2818
        $containerClass = DataList::class
2819
    ) {
2820
2821
        if ($callerClass == null) {
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...
2822
            $callerClass = get_called_class();
2823
            if ($callerClass == self::class) {
2824
                throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
2825
            }
2826
2827
            if ($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
2828
                throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
2829
                    . ' arguments');
2830
            }
2831
2832
            return DataList::create(get_called_class());
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

2832
            return DataList::create(/** @scrutinizer ignore-type */ get_called_class());
Loading history...
2833
        }
2834
2835
        if ($join) {
2836
            throw new \InvalidArgumentException(
2837
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
2838
            );
2839
        }
2840
2841
        $result = DataList::create($callerClass)->where($filter)->sort($sort);
2842
2843
        if ($limit && strpos($limit, ',') !== false) {
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

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

2844
            $limitArguments = explode(',', /** @scrutinizer ignore-type */ $limit);
Loading history...
2845
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
2846
        } elseif ($limit) {
2847
            $result = $result->limit($limit);
2848
        }
2849
2850
        return $result;
2851
    }
2852
2853
2854
    /**
2855
     * Return the first item matching the given query.
2856
     * All calls to get_one() are cached.
2857
     *
2858
     * @param string $callerClass The class of objects to be returned
2859
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2860
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2861
     * @param boolean $cache Use caching
2862
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
2863
     *
2864
     * @return DataObject|null The first item matching the query
2865
     */
2866
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
2867
    {
2868
        $SNG = singleton($callerClass);
2869
2870
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
2871
        $cacheKey = md5(serialize($cacheComponents));
2872
2873
        $item = null;
2874
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
2875
            $dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
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...
2876
            $item = $dl->first();
2877
2878
            if ($cache) {
2879
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
2880
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
2881
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
2882
                }
2883
            }
2884
        }
2885
2886
        if ($cache) {
2887
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
2888
        } else {
2889
            return $item;
2890
        }
2891
    }
2892
2893
    /**
2894
     * Flush the cached results for all relations (has_one, has_many, many_many)
2895
     * Also clears any cached aggregate data.
2896
     *
2897
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
2898
     *                            When false will just clear session-local cached data
2899
     * @return DataObject $this
2900
     */
2901
    public function flushCache($persistent = true)
2902
    {
2903
        if (static::class == self::class) {
2904
            self::$_cache_get_one = array();
2905
            return $this;
2906
        }
2907
2908
        $classes = ClassInfo::ancestry(static::class);
2909
        foreach ($classes as $class) {
2910
            if (isset(self::$_cache_get_one[$class])) {
2911
                unset(self::$_cache_get_one[$class]);
2912
            }
2913
        }
2914
2915
        $this->extend('flushCache');
2916
2917
        $this->components = array();
2918
        return $this;
2919
    }
2920
2921
    /**
2922
     * Flush the get_one global cache and destroy associated objects.
2923
     */
2924
    public static function flush_and_destroy_cache()
2925
    {
2926
        if (self::$_cache_get_one) {
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...
2927
            foreach (self::$_cache_get_one as $class => $items) {
2928
                if (is_array($items)) {
2929
                    foreach ($items as $item) {
2930
                        if ($item) {
2931
                            $item->destroy();
2932
                        }
2933
                    }
2934
                }
2935
            }
2936
        }
2937
        self::$_cache_get_one = array();
2938
    }
2939
2940
    /**
2941
     * Reset all global caches associated with DataObject.
2942
     */
2943
    public static function reset()
2944
    {
2945
        // @todo Decouple these
2946
        DBClassName::clear_classname_cache();
2947
        ClassInfo::reset_db_cache();
2948
        static::getSchema()->reset();
2949
        self::$_cache_get_one = array();
2950
        self::$_cache_field_labels = array();
2951
    }
2952
2953
    /**
2954
     * Return the given element, searching by ID
2955
     *
2956
     * @param string $callerClass The class of the object to be returned
2957
     * @param int $id The id of the element
2958
     * @param boolean $cache See {@link get_one()}
2959
     *
2960
     * @return DataObject The element
2961
     */
2962
    public static function get_by_id($callerClass, $id, $cache = true)
2963
    {
2964
        if (!is_numeric($id)) {
2965
            user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
2966
        }
2967
2968
        // Pass to get_one
2969
        $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
2970
        return DataObject::get_one($callerClass, array($column => $id), $cache);
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...
2971
    }
2972
2973
    /**
2974
     * Get the name of the base table for this object
2975
     *
2976
     * @return string
2977
     */
2978
    public function baseTable()
2979
    {
2980
        return static::getSchema()->baseDataTable($this);
2981
    }
2982
2983
    /**
2984
     * Get the base class for this object
2985
     *
2986
     * @return string
2987
     */
2988
    public function baseClass()
2989
    {
2990
        return static::getSchema()->baseDataClass($this);
2991
    }
2992
2993
    /**
2994
     * @var array Parameters used in the query that built this object.
2995
     * This can be used by decorators (e.g. lazy loading) to
2996
     * run additional queries using the same context.
2997
     */
2998
    protected $sourceQueryParams;
2999
3000
    /**
3001
     * @see $sourceQueryParams
3002
     * @return array
3003
     */
3004
    public function getSourceQueryParams()
3005
    {
3006
        return $this->sourceQueryParams;
3007
    }
3008
3009
    /**
3010
     * Get list of parameters that should be inherited to relations on this object
3011
     *
3012
     * @return array
3013
     */
3014
    public function getInheritableQueryParams()
3015
    {
3016
        $params = $this->getSourceQueryParams();
3017
        $this->extend('updateInheritableQueryParams', $params);
3018
        return $params;
3019
    }
3020
3021
    /**
3022
     * @see $sourceQueryParams
3023
     * @param array
3024
     */
3025
    public function setSourceQueryParams($array)
3026
    {
3027
        $this->sourceQueryParams = $array;
3028
    }
3029
3030
    /**
3031
     * @see $sourceQueryParams
3032
     * @param string $key
3033
     * @param string $value
3034
     */
3035
    public function setSourceQueryParam($key, $value)
3036
    {
3037
        $this->sourceQueryParams[$key] = $value;
3038
    }
3039
3040
    /**
3041
     * @see $sourceQueryParams
3042
     * @param string $key
3043
     * @return string
3044
     */
3045
    public function getSourceQueryParam($key)
3046
    {
3047
        if (isset($this->sourceQueryParams[$key])) {
3048
            return $this->sourceQueryParams[$key];
3049
        }
3050
        return null;
3051
    }
3052
3053
    //-------------------------------------------------------------------------------------------//
3054
3055
    /**
3056
     * Check the database schema and update it as necessary.
3057
     *
3058
     * @uses DataExtension->augmentDatabase()
3059
     */
3060
    public function requireTable()
3061
    {
3062
        // Only build the table if we've actually got fields
3063
        $schema = static::getSchema();
3064
        $table = $schema->tableName(static::class);
3065
        $fields = $schema->databaseFields(static::class, false);
3066
        $indexes = $schema->databaseIndexes(static::class, false);
3067
        $extensions = self::database_extensions(static::class);
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

3067
        /** @scrutinizer ignore-call */ 
3068
        $extensions = self::database_extensions(static::class);
Loading history...
3068
        $legacyTables = $schema->getLegacyTableNames(static::class);
3069
3070
        if (empty($table)) {
3071
            throw new LogicException(
3072
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3073
            );
3074
        }
3075
3076
        if ($legacyTables) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $legacyTables of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
3077
            $ignore = Config::inst()->get(static::class, 'ignored_legacy_tables') ?: [];
3078
            $renameTables = array_diff(
3079
                array_intersect($legacyTables, DB::table_list()),
3080
                $ignore
3081
            );
3082
            if (count($renameTables) > 1) {
3083
                $class = static::class;
3084
                $legacyList = implode(', ', $renameTables);
3085
                trigger_error(
3086
                    "Class $class has multiple legacy tables: $legacyList",
3087
                    E_USER_NOTICE
3088
                );
3089
            }
3090
            if (count($renameTables) === 1) {
3091
                $dbSchema = DB::get_schema();
3092
                $dbSchema->renameTable($renameTables[0], $table);
3093
            }
3094
        }
3095
3096
        if ($fields) {
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...
3097
            $hasAutoIncPK = get_parent_class($this) === self::class;
3098
            DB::require_table(
3099
                $table,
3100
                $fields,
3101
                $indexes,
3102
                $hasAutoIncPK,
3103
                $this->config()->get('create_table_options'),
3104
                $extensions
3105
            );
3106
        } else {
3107
            DB::dont_require_table($table);
3108
        }
3109
3110
        // Build any child tables for many_many items
3111
        if ($manyMany = $this->uninherited('many_many')) {
3112
            $extras = $this->uninherited('many_many_extraFields');
3113
            foreach ($manyMany as $component => $spec) {
3114
                // Get many_many spec
3115
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3116
                $parentField = $manyManyComponent['parentField'];
3117
                $childField = $manyManyComponent['childField'];
3118
                $tableOrClass = $manyManyComponent['join'];
3119
3120
                // Skip if backed by actual class
3121
                if (class_exists($tableOrClass)) {
3122
                    continue;
3123
                }
3124
3125
                // Build fields
3126
                $manymanyFields = array(
3127
                    $parentField => "Int",
3128
                    $childField => "Int",
3129
                );
3130
                if (isset($extras[$component])) {
3131
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3132
                }
3133
3134
                // Build index list
3135
                $manymanyIndexes = [
3136
                    $parentField => [
3137
                        'type' => 'index',
3138
                        'name' => $parentField,
3139
                        'columns' => [$parentField],
3140
                    ],
3141
                    $childField => [
3142
                        'type' => 'index',
3143
                        'name' =>$childField,
3144
                        'columns' => [$childField],
3145
                    ],
3146
                ];
3147
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
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

3147
                DB::require_table($tableOrClass, $manymanyFields, /** @scrutinizer ignore-type */ $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

3147
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, /** @scrutinizer ignore-type */ $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

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