Passed
Push — 4.2 ( 0758cd...79e44b )
by Robbie
31:45 queued 25:18
created

DataObject::setField()   F

Complexity

Conditions 21
Paths 124

Size

Total Lines 77
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 21
eloc 36
nc 124
nop 2
dl 0
loc 77
rs 3.9666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use BadMethodCallException;
6
use Exception;
7
use InvalidArgumentException;
8
use LogicException;
9
use SilverStripe\Core\ClassInfo;
10
use SilverStripe\Core\Config\Config;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Core\Resettable;
13
use SilverStripe\Dev\Debug;
14
use SilverStripe\Dev\Deprecation;
15
use SilverStripe\Forms\FieldList;
16
use SilverStripe\Forms\FormField;
17
use SilverStripe\Forms\FormScaffolder;
18
use SilverStripe\i18n\i18n;
19
use SilverStripe\i18n\i18nEntityProvider;
20
use SilverStripe\ORM\Connect\MySQLSchemaManager;
21
use SilverStripe\ORM\FieldType\DBClassName;
22
use SilverStripe\ORM\FieldType\DBComposite;
23
use SilverStripe\ORM\FieldType\DBDatetime;
24
use SilverStripe\ORM\FieldType\DBField;
25
use SilverStripe\ORM\Filters\SearchFilter;
26
use SilverStripe\ORM\Queries\SQLDelete;
27
use SilverStripe\ORM\Queries\SQLInsert;
28
use SilverStripe\ORM\Search\SearchContext;
29
use SilverStripe\Security\Member;
30
use SilverStripe\Security\Permission;
31
use SilverStripe\Security\Security;
32
use SilverStripe\View\SSViewer;
33
use SilverStripe\View\ViewableData;
34
use stdClass;
35
36
/**
37
 * A single database record & abstract class for the data-access-model.
38
 *
39
 * <h2>Extensions</h2>
40
 *
41
 * See {@link Extension} and {@link DataExtension}.
42
 *
43
 * <h2>Permission Control</h2>
44
 *
45
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
46
 * strings which can be selected on a group-by-group basis.
47
 *
48
 * <code>
49
 * class Article extends DataObject implements PermissionProvider {
50
 *  static $api_access = true;
51
 *
52
 *  function canView($member = false) {
53
 *    return Permission::check('ARTICLE_VIEW');
54
 *  }
55
 *  function canEdit($member = false) {
56
 *    return Permission::check('ARTICLE_EDIT');
57
 *  }
58
 *  function canDelete() {
59
 *    return Permission::check('ARTICLE_DELETE');
60
 *  }
61
 *  function canCreate() {
62
 *    return Permission::check('ARTICLE_CREATE');
63
 *  }
64
 *  function providePermissions() {
65
 *    return array(
66
 *      'ARTICLE_VIEW' => 'Read an article object',
67
 *      'ARTICLE_EDIT' => 'Edit an article object',
68
 *      'ARTICLE_DELETE' => 'Delete an article object',
69
 *      'ARTICLE_CREATE' => 'Create an article object',
70
 *    );
71
 *  }
72
 * }
73
 * </code>
74
 *
75
 * Object-level access control by {@link Group} membership:
76
 * <code>
77
 * class Article extends DataObject {
78
 *   static $api_access = true;
79
 *
80
 *   function canView($member = false) {
81
 *     if(!$member) $member = Security::getCurrentUser();
82
 *     return $member->inGroup('Subscribers');
83
 *   }
84
 *   function canEdit($member = false) {
85
 *     if(!$member) $member = Security::getCurrentUser();
86
 *     return $member->inGroup('Editors');
87
 *   }
88
 *
89
 *   // ...
90
 * }
91
 * </code>
92
 *
93
 * If any public method on this class is prefixed with an underscore,
94
 * the results are cached in memory through {@link cachedCall()}.
95
 *
96
 *
97
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
98
 *  and defineMethods()
99
 *
100
 * @property int $ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
101
 * @property int $OldID ID of object, if deleted
102
 * @property string $Title
103
 * @property string $ClassName Class name of the DataObject
104
 * @property string $LastEdited Date and time of DataObject's last modification.
105
 * @property string $Created Date and time of DataObject creation.
106
 * @property string $ObsoleteClassName If ClassName no longer exists this will be set to the legacy value
107
 */
108
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable
109
{
110
111
    /**
112
     * Human-readable singular name.
113
     * @var string
114
     * @config
115
     */
116
    private static $singular_name = null;
117
118
    /**
119
     * Human-readable plural name
120
     * @var string
121
     * @config
122
     */
123
    private static $plural_name = null;
124
125
    /**
126
     * Allow API access to this object?
127
     * @todo Define the options that can be set here
128
     * @config
129
     */
130
    private static $api_access = false;
131
132
    /**
133
     * Allows specification of a default value for the ClassName field.
134
     * Configure this value only in subclasses of DataObject.
135
     *
136
     * @config
137
     * @var string
138
     */
139
    private static $default_classname = null;
140
141
    /**
142
     * @deprecated 4.0..5.0
143
     * @var bool
144
     */
145
    public $destroyed = false;
146
147
    /**
148
     * Data stored in this objects database record. An array indexed by fieldname.
149
     *
150
     * Use {@link toMap()} if you want an array representation
151
     * of this object, as the $record array might contain lazy loaded field aliases.
152
     *
153
     * @var array
154
     */
155
    protected $record;
156
157
    /**
158
     * If selected through a many_many through relation, this is the instance of the through record
159
     *
160
     * @var DataObject
161
     */
162
    protected $joinRecord;
163
164
    /**
165
     * Represents a field that hasn't changed (before === after, thus before == after)
166
     */
167
    const CHANGE_NONE = 0;
168
169
    /**
170
     * Represents a field that has changed type, although not the loosely defined value.
171
     * (before !== after && before == after)
172
     * E.g. change 1 to true or "true" to true, but not true to 0.
173
     * Value changes are by nature also considered strict changes.
174
     */
175
    const CHANGE_STRICT = 1;
176
177
    /**
178
     * Represents a field that has changed the loosely defined value
179
     * (before != after, thus, before !== after))
180
     * E.g. change false to true, but not false to 0
181
     */
182
    const CHANGE_VALUE = 2;
183
184
    /**
185
     * An array indexed by fieldname, true if the field has been changed.
186
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
187
     * the changed state.
188
     *
189
     * @var array
190
     */
191
    private $changed;
192
193
    /**
194
     * The database record (in the same format as $record), before
195
     * any changes.
196
     * @var array
197
     */
198
    protected $original;
199
200
    /**
201
     * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
202
     * @var boolean
203
     */
204
    protected $brokenOnDelete = false;
205
206
    /**
207
     * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
208
     * @var boolean
209
     */
210
    protected $brokenOnWrite = false;
211
212
    /**
213
     * @config
214
     * @var boolean Should dataobjects be validated before they are written?
215
     * Caution: Validation can contain safeguards against invalid/malicious data,
216
     * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
217
     * to only disable validation for very specific use cases.
218
     */
219
    private static $validation_enabled = true;
220
221
    /**
222
     * Static caches used by relevant functions.
223
     *
224
     * @var array
225
     */
226
    protected static $_cache_get_one;
227
228
    /**
229
     * Cache of field labels
230
     *
231
     * @var array
232
     */
233
    protected static $_cache_field_labels = array();
234
235
    /**
236
     * Base fields which are not defined in static $db
237
     *
238
     * @config
239
     * @var array
240
     */
241
    private static $fixed_fields = array(
242
        'ID' => 'PrimaryKey',
243
        'ClassName' => 'DBClassName',
244
        'LastEdited' => 'DBDatetime',
245
        'Created' => 'DBDatetime',
246
    );
247
248
    /**
249
     * Override table name for this class. If ignored will default to FQN of class.
250
     * This option is not inheritable, and must be set on each class.
251
     * If left blank naming will default to the legacy (3.x) behaviour.
252
     *
253
     * @var string
254
     */
255
    private static $table_name = null;
256
257
    /**
258
     * Non-static relationship cache, indexed by component name.
259
     *
260
     * @var DataObject[]
261
     */
262
    protected $components = [];
263
264
    /**
265
     * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
266
     *
267
     * @var UnsavedRelationList[]
268
     */
269
    protected $unsavedRelations;
270
271
    /**
272
     * List of relations that should be cascade deleted, similar to `owns`
273
     * Note: This will trigger delete on many_many objects, not only the mapping table.
274
     * For many_many through you can specify the components you want to delete separately
275
     * (many_many or has_many sub-component)
276
     *
277
     * @config
278
     * @var array
279
     */
280
    private static $cascade_deletes = [];
281
282
    /**
283
     * List of relations that should be cascade duplicate.
284
     * many_many duplications are shallow only.
285
     *
286
     * Note: If duplicating a many_many through you should refer to the
287
     * has_many intermediary relation instead, otherwise extra fields
288
     * will be omitted from the duplicated relation.
289
     *
290
     * @var array
291
     */
292
    private static $cascade_duplicates = [];
293
294
    /**
295
     * Get schema object
296
     *
297
     * @return DataObjectSchema
298
     */
299
    public static function getSchema()
300
    {
301
        return Injector::inst()->get(DataObjectSchema::class);
302
    }
303
304
    /**
305
     * Construct a new DataObject.
306
     *
307
     * @param array|null $record Used internally for rehydrating an object from database content.
308
     *                           Bypasses setters on this class, and hence should not be used
309
     *                           for populating data on new records.
310
     * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
311
     *                             Singletons don't have their defaults set.
312
     * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
313
     */
314
    public function __construct($record = null, $isSingleton = false, $queryParams = array())
315
    {
316
        parent::__construct();
317
318
        // Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
319
        $this->setSourceQueryParams($queryParams);
320
321
        // Set the fields data.
322
        if (!$record) {
323
            $record = array(
324
                'ID' => 0,
325
                'ClassName' => static::class,
326
                'RecordClassName' => static::class
327
            );
328
        }
329
330
        if ($record instanceof stdClass) {
0 ignored issues
show
introduced by
$record is never a sub-type of stdClass.
Loading history...
331
            $record = (array)$record;
332
        }
333
334
        if (!is_array($record)) {
0 ignored issues
show
introduced by
The condition is_array($record) is always true.
Loading history...
335
            if (is_object($record)) {
336
                $passed = "an object of type '" . get_class($record) . "'";
337
            } else {
338
                $passed = "The value '$record'";
339
            }
340
341
            user_error(
342
                "DataObject::__construct passed $passed.  It's supposed to be passed an array,"
343
                . " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
344
                E_USER_WARNING
345
            );
346
            $record = null;
347
        }
348
349
        // Set $this->record to $record, but ignore NULLs
350
        $this->record = array();
351
        foreach ($record as $k => $v) {
352
            // Ensure that ID is stored as a number and not a string
353
            // To do: this kind of clean-up should be done on all numeric fields, in some relatively
354
            // performant manner
355
            if ($v !== null) {
356
                if ($k == 'ID' && is_numeric($v)) {
357
                    $this->record[$k] = (int)$v;
358
                } else {
359
                    $this->record[$k] = $v;
360
                }
361
            }
362
        }
363
364
        // Identify fields that should be lazy loaded, but only on existing records
365
        if (!empty($record['ID'])) {
366
            // Get all field specs scoped to class for later lazy loading
367
            $fields = static::getSchema()->fieldSpecs(
368
                static::class,
369
                DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
370
            );
371
            foreach ($fields as $field => $fieldSpec) {
372
                $fieldClass = strtok($fieldSpec, ".");
373
                if (!array_key_exists($field, $record)) {
374
                    $this->record[$field . '_Lazy'] = $fieldClass;
375
                }
376
            }
377
        }
378
379
        $this->original = $this->record;
380
381
        // Must be called after parent constructor
382
        if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
383
            $this->populateDefaults();
384
        }
385
386
        // prevent populateDefaults() and setField() from marking overwritten defaults as changed
387
        $this->changed = array();
388
    }
389
390
    /**
391
     * Destroy all of this objects dependant objects and local caches.
392
     * You'll need to call this to get the memory of an object that has components or extensions freed.
393
     */
394
    public function destroy()
395
    {
396
        $this->flushCache(false);
397
    }
398
399
    /**
400
     * Create a duplicate of this node. Can duplicate many_many relations
401
     *
402
     * @param bool $doWrite Perform a write() operation before returning the object.
403
     * If this is true, it will create the duplicate in the database.
404
     * @param array|null|false $relations List of relations to duplicate.
405
     * Will default to `cascade_duplicates` if null.
406
     * Set to 'false' to force none.
407
     * Set to specific array of names to duplicate to override these.
408
     * Note: If using versioned, this will additionally failover to `owns` config.
409
     * @return static A duplicate of this node. The exact type will be the type of this node.
410
     */
411
    public function duplicate($doWrite = true, $relations = null)
412
    {
413
        // Handle legacy behaviour
414
        if (is_string($relations) || $relations === true) {
0 ignored issues
show
introduced by
The condition $relations === true is always false.
Loading history...
415
            if ($relations === true) {
416
                $relations = 'many_many';
417
            }
418
            Deprecation::notice('5.0', 'Use cascade_duplicates config instead of providing a string to duplicate()');
419
            $relations = array_keys($this->config()->get($relations)) ?: [];
420
        }
421
422
        // Get duplicates
423
        if ($relations === null) {
424
            $relations = $this->config()->get('cascade_duplicates');
425
            // Remove any duplicate entries before duplicating them
426
            if (is_array($relations)) {
427
                $relations = array_unique($relations);
428
            }
429
        }
430
431
        // Create unsaved raw duplicate
432
        $map = $this->toMap();
433
        unset($map['Created']);
434
        /** @var static $clone */
435
        $clone = Injector::inst()->create(static::class, $map, false, $this->getSourceQueryParams());
436
        $clone->ID = 0;
437
438
        // Note: Extensions such as versioned may update $relations here
439
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $relations);
440
        if ($relations) {
441
            $this->duplicateRelations($this, $clone, $relations);
442
        }
443
        if ($doWrite) {
444
            $clone->write();
445
        }
446
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $relations);
447
448
        return $clone;
449
    }
450
451
    /**
452
     * Copies the given relations from this object to the destination
453
     *
454
     * @param DataObject $sourceObject the source object to duplicate from
455
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
456
     * @param array $relations List of relations
457
     */
458
    protected function duplicateRelations($sourceObject, $destinationObject, $relations)
459
    {
460
        // Get list of duplicable relation types
461
        $manyMany = $sourceObject->manyMany();
462
        $hasMany = $sourceObject->hasMany();
463
        $hasOne = $sourceObject->hasOne();
464
        $belongsTo = $sourceObject->belongsTo();
465
466
        // Duplicate each relation based on type
467
        foreach ($relations as $relation) {
468
            switch (true) {
469
                case array_key_exists($relation, $manyMany): {
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...
470
                    $this->duplicateManyManyRelation($sourceObject, $destinationObject, $relation);
471
                    break;
472
                }
473
                case array_key_exists($relation, $hasMany): {
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...
474
                    $this->duplicateHasManyRelation($sourceObject, $destinationObject, $relation);
475
                    break;
476
                }
477
                case array_key_exists($relation, $hasOne): {
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...
478
                    $this->duplicateHasOneRelation($sourceObject, $destinationObject, $relation);
479
                    break;
480
                }
481
                case array_key_exists($relation, $belongsTo): {
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...
482
                    $this->duplicateBelongsToRelation($sourceObject, $destinationObject, $relation);
483
                    break;
484
                }
485
                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...
486
                    $sourceType = get_class($sourceObject);
487
                    throw new InvalidArgumentException(
488
                        "Cannot duplicate unknown relation {$relation} on parent type {$sourceType}"
489
                    );
490
                }
491
            }
492
        }
493
    }
494
495
    /**
496
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
497
     *
498
     * @deprecated 4.1...5.0 Use duplicateRelations() instead
499
     * @param DataObject $sourceObject the source object to duplicate from
500
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
501
     * @param bool|string $filter
502
     */
503
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
504
    {
505
        Deprecation::notice('5.0', 'Use duplicateRelations() instead');
506
507
        // Get list of relations to duplicate
508
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
509
            $relations = $sourceObject->config()->get($filter);
510
        } elseif ($filter === true) {
511
            $relations = $sourceObject->manyMany();
512
        } else {
513
            throw new InvalidArgumentException("Invalid many_many duplication filter");
514
        }
515
        foreach ($relations as $manyManyName => $type) {
516
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
517
        }
518
    }
519
520
    /**
521
     * Duplicates a single many_many relation from one object to another.
522
     *
523
     * @param DataObject $sourceObject
524
     * @param DataObject $destinationObject
525
     * @param string $relation
526
     */
527
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $relation)
528
    {
529
        // Copy all components from source to destination
530
        $source = $sourceObject->getManyManyComponents($relation);
531
        $dest = $destinationObject->getManyManyComponents($relation);
532
533
        if ($source instanceof ManyManyList) {
534
            $extraFieldNames = $source->getExtraFields();
535
        } else {
536
            $extraFieldNames = [];
537
        }
538
539
        foreach ($source as $item) {
540
            // Merge extra fields
541
            $extraFields = [];
542
            foreach ($extraFieldNames as $fieldName => $fieldType) {
543
                $extraFields[$fieldName] = $item->getField($fieldName);
544
            }
545
            $dest->add($item, $extraFields);
546
        }
547
    }
548
549
    /**
550
     * Duplicates a single many_many relation from one object to another.
551
     *
552
     * @param DataObject $sourceObject
553
     * @param DataObject $destinationObject
554
     * @param string $relation
555
     */
556
    protected function duplicateHasManyRelation($sourceObject, $destinationObject, $relation)
557
    {
558
        // Copy all components from source to destination
559
        $source = $sourceObject->getComponents($relation);
560
        $dest = $destinationObject->getComponents($relation);
561
562
        /** @var DataObject $item */
563
        foreach ($source as $item) {
564
            // Don't write on duplicate; Wait until ParentID is available later.
565
            // writeRelations() will eventually write these records when converting
566
            // from UnsavedRelationList
567
            $clonedItem = $item->duplicate(false);
568
            $dest->add($clonedItem);
569
        }
570
    }
571
572
    /**
573
     * Duplicates a single has_one relation from one object to another.
574
     * Note: Child object will be force written.
575
     *
576
     * @param DataObject $sourceObject
577
     * @param DataObject $destinationObject
578
     * @param string $relation
579
     */
580
    protected function duplicateHasOneRelation($sourceObject, $destinationObject, $relation)
581
    {
582
        // Check if original object exists
583
        $item = $sourceObject->getComponent($relation);
584
        if (!$item->isInDB()) {
585
            return;
586
        }
587
588
        $clonedItem = $item->duplicate(false);
589
        $destinationObject->setComponent($relation, $clonedItem);
590
    }
591
592
    /**
593
     * Duplicates a single belongs_to relation from one object to another.
594
     * Note: This will force a write on both parent / child objects.
595
     *
596
     * @param DataObject $sourceObject
597
     * @param DataObject $destinationObject
598
     * @param string $relation
599
     */
600
    protected function duplicateBelongsToRelation($sourceObject, $destinationObject, $relation)
601
    {
602
        // Check if original object exists
603
        $item = $sourceObject->getComponent($relation);
604
        if (!$item->isInDB()) {
605
            return;
606
        }
607
608
        $clonedItem = $item->duplicate(false);
609
        $destinationObject->setComponent($relation, $clonedItem);
610
        // After $clonedItem is assigned the appropriate FieldID / FieldClass, force write
611
        // @todo Write this component in onAfterWrite instead, assigning the FieldID then
612
        // https://github.com/silverstripe/silverstripe-framework/issues/7818
613
        $clonedItem->write();
614
    }
615
616
    /**
617
     * Return obsolete class name, if this is no longer a valid class
618
     *
619
     * @return string
620
     */
621
    public function getObsoleteClassName()
622
    {
623
        $className = $this->getField("ClassName");
624
        if (!ClassInfo::exists($className)) {
625
            return $className;
626
        }
627
        return null;
628
    }
629
630
    /**
631
     * Gets name of this class
632
     *
633
     * @return string
634
     */
635
    public function getClassName()
636
    {
637
        $className = $this->getField("ClassName");
638
        if (!ClassInfo::exists($className)) {
639
            return static::class;
640
        }
641
        return $className;
642
    }
643
644
    /**
645
     * Set the ClassName attribute. {@link $class} is also updated.
646
     * Warning: This will produce an inconsistent record, as the object
647
     * instance will not automatically switch to the new subclass.
648
     * Please use {@link newClassInstance()} for this purpose,
649
     * or destroy and reinstanciate the record.
650
     *
651
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
652
     * @return $this
653
     */
654
    public function setClassName($className)
655
    {
656
        $className = trim($className);
657
        if (!$className || !is_subclass_of($className, self::class)) {
658
            return $this;
659
        }
660
661
        $this->setField("ClassName", $className);
662
        $this->setField('RecordClassName', $className);
663
        return $this;
664
    }
665
666
    /**
667
     * Create a new instance of a different class from this object's record.
668
     * This is useful when dynamically changing the type of an instance. Specifically,
669
     * it ensures that the instance of the class is a match for the className of the
670
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
671
     * property manually before calling this method, as it will confuse change detection.
672
     *
673
     * If the new class is different to the original class, defaults are populated again
674
     * because this will only occur automatically on instantiation of a DataObject if
675
     * there is no record, or the record has no ID. In this case, we do have an ID but
676
     * we still need to repopulate the defaults.
677
     *
678
     * @param string $newClassName The name of the new class
679
     *
680
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
681
     */
682
    public function newClassInstance($newClassName)
683
    {
684
        if (!is_subclass_of($newClassName, self::class)) {
685
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
686
        }
687
688
        $originalClass = $this->ClassName;
689
690
        /** @var DataObject $newInstance */
691
        $newInstance = Injector::inst()->create($newClassName, $this->record, false);
692
693
        // Modify ClassName
694
        if ($newClassName != $originalClass) {
695
            $newInstance->setClassName($newClassName);
696
            $newInstance->populateDefaults();
697
            $newInstance->forceChange();
698
        }
699
700
        return $newInstance;
701
    }
702
703
    /**
704
     * Adds methods from the extensions.
705
     * Called by Object::__construct() once per class.
706
     */
707
    public function defineMethods()
708
    {
709
        parent::defineMethods();
710
711
        if (static::class === self::class) {
0 ignored issues
show
introduced by
The condition static::class === self::class is always true.
Loading history...
712
            return;
713
        }
714
715
        // Set up accessors for joined items
716
        if ($manyMany = $this->manyMany()) {
717
            foreach ($manyMany as $relationship => $class) {
718
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
719
            }
720
        }
721
        if ($hasMany = $this->hasMany()) {
722
            foreach ($hasMany as $relationship => $class) {
723
                $this->addWrapperMethod($relationship, 'getComponents');
724
            }
725
        }
726
        if ($hasOne = $this->hasOne()) {
727
            foreach ($hasOne as $relationship => $class) {
728
                $this->addWrapperMethod($relationship, 'getComponent');
729
            }
730
        }
731
        if ($belongsTo = $this->belongsTo()) {
732
            foreach (array_keys($belongsTo) as $relationship) {
733
                $this->addWrapperMethod($relationship, 'getComponent');
734
            }
735
        }
736
    }
737
738
    /**
739
     * Returns true if this object "exists", i.e., has a sensible value.
740
     * The default behaviour for a DataObject is to return true if
741
     * the object exists in the database, you can override this in subclasses.
742
     *
743
     * @return boolean true if this object exists
744
     */
745
    public function exists()
746
    {
747
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
748
    }
749
750
    /**
751
     * Returns TRUE if all values (other than "ID") are
752
     * considered empty (by weak boolean comparison).
753
     *
754
     * @return boolean
755
     */
756
    public function isEmpty()
757
    {
758
        $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...
759
        foreach ($this->toMap() as $field => $value) {
760
            // only look at custom fields
761
            if (isset($fixed[$field])) {
762
                continue;
763
            }
764
765
            $dbObject = $this->dbObject($field);
766
            if (!$dbObject) {
767
                continue;
768
            }
769
            if ($dbObject->exists()) {
770
                return false;
771
            }
772
        }
773
        return true;
774
    }
775
776
    /**
777
     * Pluralise this item given a specific count.
778
     *
779
     * E.g. "0 Pages", "1 File", "3 Images"
780
     *
781
     * @param string $count
782
     * @return string
783
     */
784
    public function i18n_pluralise($count)
785
    {
786
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
787
        return i18n::_t(
788
            static::class . '.PLURALS',
789
            $default,
790
            ['count' => $count]
791
        );
792
    }
793
794
    /**
795
     * Get the user friendly singular name of this DataObject.
796
     * If the name is not defined (by redefining $singular_name in the subclass),
797
     * this returns the class name.
798
     *
799
     * @return string User friendly singular name of this DataObject
800
     */
801
    public function singular_name()
802
    {
803
        $name = $this->config()->get('singular_name');
804
        if ($name) {
805
            return $name;
806
        }
807
        return ucwords(trim(strtolower(preg_replace(
808
            '/_?([A-Z])/',
809
            ' $1',
810
            ClassInfo::shortName($this)
811
        ))));
812
    }
813
814
    /**
815
     * Get the translated user friendly singular name of this DataObject
816
     * same as singular_name() but runs it through the translating function
817
     *
818
     * Translating string is in the form:
819
     *     $this->class.SINGULARNAME
820
     * Example:
821
     *     Page.SINGULARNAME
822
     *
823
     * @return string User friendly translated singular name of this DataObject
824
     */
825
    public function i18n_singular_name()
826
    {
827
        return _t(static::class . '.SINGULARNAME', $this->singular_name());
828
    }
829
830
    /**
831
     * Get the user friendly plural name of this DataObject
832
     * If the name is not defined (by renaming $plural_name in the subclass),
833
     * this returns a pluralised version of the class name.
834
     *
835
     * @return string User friendly plural name of this DataObject
836
     */
837
    public function plural_name()
838
    {
839
        if ($name = $this->config()->get('plural_name')) {
840
            return $name;
841
        }
842
        $name = $this->singular_name();
843
        //if the penultimate character is not a vowel, replace "y" with "ies"
844
        if (preg_match('/[^aeiou]y$/i', $name)) {
845
            $name = substr($name, 0, -1) . 'ie';
846
        }
847
        return ucfirst($name . 's');
848
    }
849
850
    /**
851
     * Get the translated user friendly plural name of this DataObject
852
     * Same as plural_name but runs it through the translation function
853
     * Translation string is in the form:
854
     *      $this->class.PLURALNAME
855
     * Example:
856
     *      Page.PLURALNAME
857
     *
858
     * @return string User friendly translated plural name of this DataObject
859
     */
860
    public function i18n_plural_name()
861
    {
862
        return _t(static::class . '.PLURALNAME', $this->plural_name());
863
    }
864
865
    /**
866
     * Standard implementation of a title/label for a specific
867
     * record. Tries to find properties 'Title' or 'Name',
868
     * and falls back to the 'ID'. Useful to provide
869
     * user-friendly identification of a record, e.g. in errormessages
870
     * or UI-selections.
871
     *
872
     * Overload this method to have a more specialized implementation,
873
     * e.g. for an Address record this could be:
874
     * <code>
875
     * function getTitle() {
876
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
877
     * }
878
     * </code>
879
     *
880
     * @return string
881
     */
882
    public function getTitle()
883
    {
884
        $schema = static::getSchema();
885
        if ($schema->fieldSpec($this, 'Title')) {
886
            return $this->getField('Title');
887
        }
888
        if ($schema->fieldSpec($this, 'Name')) {
889
            return $this->getField('Name');
890
        }
891
892
        return "#{$this->ID}";
893
    }
894
895
    /**
896
     * Returns the associated database record - in this case, the object itself.
897
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
898
     *
899
     * @return DataObject Associated database record
900
     */
901
    public function data()
902
    {
903
        return $this;
904
    }
905
906
    /**
907
     * Convert this object to a map.
908
     *
909
     * @return array The data as a map.
910
     */
911
    public function toMap()
912
    {
913
        $this->loadLazyFields();
914
        return $this->record;
915
    }
916
917
    /**
918
     * Return all currently fetched database fields.
919
     *
920
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
921
     * Obviously, this makes it a lot faster.
922
     *
923
     * @return array The data as a map.
924
     */
925
    public function getQueriedDatabaseFields()
926
    {
927
        return $this->record;
928
    }
929
930
    /**
931
     * Update a number of fields on this object, given a map of the desired changes.
932
     *
933
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
934
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
935
     *
936
     * update() doesn't write the main object, but if you use the dot syntax, it will write()
937
     * the related objects that it alters.
938
     *
939
     * @param array $data A map of field name to data values to update.
940
     * @return DataObject $this
941
     */
942
    public function update($data)
943
    {
944
        foreach ($data as $key => $value) {
945
            // Implement dot syntax for updates
946
            if (strpos($key, '.') !== false) {
947
                $relations = explode('.', $key);
948
                $fieldName = array_pop($relations);
949
                /** @var static $relObj */
950
                $relObj = $this;
951
                $relation = null;
952
                foreach ($relations as $i => $relation) {
953
                    // no support for has_many or many_many relationships,
954
                    // as the updater wouldn't know which object to write to (or create)
955
                    if ($relObj->$relation() instanceof DataObject) {
956
                        $parentObj = $relObj;
957
                        $relObj = $relObj->$relation();
958
                        // If the intermediate relationship objects haven't been created, then write them
959
                        if ($i < sizeof($relations) - 1 && !$relObj->ID || (!$relObj->ID && $parentObj !== $this)) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($i < sizeof($relations)...&& $parentObj !== $this, Probably Intended Meaning: $i < sizeof($relations) ...& $parentObj !== $this)
Loading history...
960
                            $relObj->write();
961
                            $relatedFieldName = $relation . "ID";
962
                            $parentObj->$relatedFieldName = $relObj->ID;
963
                            $parentObj->write();
964
                        }
965
                    } else {
966
                        user_error(
967
                            "DataObject::update(): Can't traverse relationship '$relation'," .
968
                            "it has to be a has_one relationship or return a single DataObject",
969
                            E_USER_NOTICE
970
                        );
971
                        // unset relation object so we don't write properties to the wrong object
972
                        $relObj = null;
973
                        break;
974
                    }
975
                }
976
977
                if ($relObj) {
978
                    $relObj->$fieldName = $value;
979
                    $relObj->write();
980
                    $relatedFieldName = $relation . "ID";
981
                    $this->$relatedFieldName = $relObj->ID;
982
                    $relObj->flushCache();
983
                } else {
984
                    $class = static::class;
985
                    user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
986
                }
987
            } else {
988
                $this->$key = $value;
989
            }
990
        }
991
        return $this;
992
    }
993
994
    /**
995
     * Pass changes as a map, and try to
996
     * get automatic casting for these fields.
997
     * Doesn't write to the database. To write the data,
998
     * use the write() method.
999
     *
1000
     * @param array $data A map of field name to data values to update.
1001
     * @return DataObject $this
1002
     */
1003
    public function castedUpdate($data)
1004
    {
1005
        foreach ($data as $k => $v) {
1006
            $this->setCastedField($k, $v);
1007
        }
1008
        return $this;
1009
    }
1010
1011
    /**
1012
     * Merges data and relations from another object of same class,
1013
     * without conflict resolution. Allows to specify which
1014
     * dataset takes priority in case its not empty.
1015
     * has_one-relations are just transferred with priority 'right'.
1016
     * has_many and many_many-relations are added regardless of priority.
1017
     *
1018
     * Caution: has_many/many_many relations are moved rather than duplicated,
1019
     * meaning they are not connected to the merged object any longer.
1020
     * Caution: Just saves updated has_many/many_many relations to the database,
1021
     * doesn't write the updated object itself (just writes the object-properties).
1022
     * Caution: Does not delete the merged object.
1023
     * Caution: Does now overwrite Created date on the original object.
1024
     *
1025
     * @param DataObject $rightObj
1026
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
1027
     * @param bool $includeRelations Merge any existing relations (optional)
1028
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
1029
     *                            Only applicable with $priority='right'. (optional)
1030
     * @return Boolean
1031
     */
1032
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
1033
    {
1034
        $leftObj = $this;
1035
1036
        if ($leftObj->ClassName != $rightObj->ClassName) {
1037
            // we can't merge similiar subclasses because they might have additional relations
1038
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
1039
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
1040
            return false;
1041
        }
1042
1043
        if (!$rightObj->ID) {
1044
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
1045
				to make sure all relations are transferred properly.').", E_USER_WARNING);
1046
            return false;
1047
        }
1048
1049
        // makes sure we don't merge data like ID or ClassName
1050
        $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...
1051
        foreach ($rightData as $key => $rightSpec) {
1052
            // Don't merge ID
1053
            if ($key === 'ID') {
1054
                continue;
1055
            }
1056
1057
            // Only merge relations if allowed
1058
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
1059
                continue;
1060
            }
1061
1062
            // don't merge conflicting values if priority is 'left'
1063
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
1064
                continue;
1065
            }
1066
1067
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
1068
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
1069
                continue;
1070
            }
1071
1072
            // TODO remove redundant merge of has_one fields
1073
            $leftObj->{$key} = $rightObj->{$key};
1074
        }
1075
1076
        // merge relations
1077
        if ($includeRelations) {
1078
            if ($manyMany = $this->manyMany()) {
1079
                foreach ($manyMany as $relationship => $class) {
1080
                    /** @var DataObject $leftComponents */
1081
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
1082
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
1083
                    if ($rightComponents && $rightComponents->exists()) {
1084
                        $leftComponents->addMany($rightComponents->column('ID'));
0 ignored issues
show
Bug introduced by
The method addMany() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

1084
                        $leftComponents->/** @scrutinizer ignore-call */ 
1085
                                         addMany($rightComponents->column('ID'));
Loading history...
1085
                    }
1086
                    $leftComponents->write();
1087
                }
1088
            }
1089
1090
            if ($hasMany = $this->hasMany()) {
1091
                foreach ($hasMany as $relationship => $class) {
1092
                    $leftComponents = $leftObj->getComponents($relationship);
1093
                    $rightComponents = $rightObj->getComponents($relationship);
1094
                    if ($rightComponents && $rightComponents->exists()) {
1095
                        $leftComponents->addMany($rightComponents->column('ID'));
1096
                    }
1097
                    $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

1097
                    $leftComponents->/** @scrutinizer ignore-call */ 
1098
                                     write();
Loading history...
Bug introduced by
The method write() does not exist on SilverStripe\ORM\HasManyList. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

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

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

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

Loading history...
2817
                return min($results);
2818
            }
2819
        }
2820
        return null;
2821
    }
2822
2823
    /**
2824
     * @param Member $member
2825
     * @return boolean
2826
     */
2827
    public function canView($member = null)
2828
    {
2829
        $extended = $this->extendedCan(__FUNCTION__, $member);
2830
        if ($extended !== null) {
2831
            return $extended;
2832
        }
2833
        return Permission::check('ADMIN', 'any', $member);
2834
    }
2835
2836
    /**
2837
     * @param Member $member
2838
     * @return boolean
2839
     */
2840
    public function canEdit($member = null)
2841
    {
2842
        $extended = $this->extendedCan(__FUNCTION__, $member);
2843
        if ($extended !== null) {
2844
            return $extended;
2845
        }
2846
        return Permission::check('ADMIN', 'any', $member);
2847
    }
2848
2849
    /**
2850
     * @param Member $member
2851
     * @return boolean
2852
     */
2853
    public function canDelete($member = null)
2854
    {
2855
        $extended = $this->extendedCan(__FUNCTION__, $member);
2856
        if ($extended !== null) {
2857
            return $extended;
2858
        }
2859
        return Permission::check('ADMIN', 'any', $member);
2860
    }
2861
2862
    /**
2863
     * @param Member $member
2864
     * @param array $context Additional context-specific data which might
2865
     * affect whether (or where) this object could be created.
2866
     * @return boolean
2867
     */
2868
    public function canCreate($member = null, $context = array())
2869
    {
2870
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2871
        if ($extended !== null) {
2872
            return $extended;
2873
        }
2874
        return Permission::check('ADMIN', 'any', $member);
2875
    }
2876
2877
    /**
2878
     * Debugging used by Debug::show()
2879
     *
2880
     * @return string HTML data representing this object
2881
     */
2882
    public function debug()
2883
    {
2884
        $class = static::class;
2885
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2886
        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...
2887
            foreach ($this->record as $fieldName => $fieldVal) {
2888
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2889
            }
2890
        }
2891
        $val .= "</ul>\n";
2892
        return $val;
2893
    }
2894
2895
    /**
2896
     * Return the DBField object that represents the given field.
2897
     * This works similarly to obj() with 2 key differences:
2898
     *   - it still returns an object even when the field has no value.
2899
     *   - it only matches fields and not methods
2900
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2901
     *
2902
     * @param string $fieldName Name of the field
2903
     * @return DBField The field as a DBField object
2904
     */
2905
    public function dbObject($fieldName)
2906
    {
2907
        // Check for field in DB
2908
        $schema = static::getSchema();
2909
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2910
        if (!$helper) {
2911
            return null;
2912
        }
2913
2914
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2915
            $tableClass = $this->record[$fieldName . '_Lazy'];
2916
            $this->loadLazyFields($tableClass);
2917
        }
2918
2919
        $value = isset($this->record[$fieldName])
2920
            ? $this->record[$fieldName]
2921
            : null;
2922
2923
        // If we have a DBField object in $this->record, then return that
2924
        if ($value instanceof DBField) {
2925
            return $value;
2926
        }
2927
2928
        list($class, $spec) = explode('.', $helper);
2929
        /** @var DBField $obj */
2930
        $table = $schema->tableName($class);
2931
        $obj = Injector::inst()->create($spec, $fieldName);
2932
        $obj->setTable($table);
2933
        $obj->setValue($value, $this, false);
2934
        return $obj;
2935
    }
2936
2937
    /**
2938
     * Traverses to a DBField referenced by relationships between data objects.
2939
     *
2940
     * The path to the related field is specified with dot separated syntax
2941
     * (eg: Parent.Child.Child.FieldName).
2942
     *
2943
     * If a relation is blank, this will return null instead.
2944
     * If a relation name is invalid (e.g. non-relation on a parent) this
2945
     * can throw a LogicException.
2946
     *
2947
     * @param string $fieldPath List of paths on this object. All items in this path
2948
     * must be ViewableData implementors
2949
     *
2950
     * @return mixed DBField of the field on the object or a DataList instance.
2951
     * @throws LogicException If accessing invalid relations
2952
     */
2953
    public function relObject($fieldPath)
2954
    {
2955
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
2956
        $component = $this;
2957
2958
        // Parse all relations
2959
        foreach (explode('.', $fieldPath) as $relation) {
2960
            if (!$component) {
2961
                return null;
2962
            }
2963
2964
            // Inspect relation type
2965
            if (ClassInfo::hasMethod($component, $relation)) {
2966
                $component = $component->$relation();
2967
            } elseif ($component instanceof Relation || $component instanceof DataList) {
2968
                // $relation could either be a field (aggregate), or another relation
2969
                $singleton = DataObject::singleton($component->dataClass());
0 ignored issues
show
Bug introduced by
The method dataClass() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

2969
                $singleton = DataObject::singleton($component->/** @scrutinizer ignore-call */ dataClass());
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...
2970
                $component = $singleton->dbObject($relation) ?: $component->relation($relation);
0 ignored issues
show
Bug introduced by
The method relation() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

2970
                $component = $singleton->dbObject($relation) ?: $component->/** @scrutinizer ignore-call */ relation($relation);
Loading history...
2971
            } elseif ($component instanceof DataObject && ($dbObject = $component->dbObject($relation))) {
2972
                $component = $dbObject;
2973
            } elseif ($component instanceof ViewableData && $component->hasField($relation)) {
2974
                $component = $component->obj($relation);
2975
            } else {
2976
                throw new LogicException(
2977
                    "$relation is not a relation/field on " . get_class($component)
2978
                );
2979
            }
2980
        }
2981
        return $component;
2982
    }
2983
2984
    /**
2985
     * Traverses to a field referenced by relationships between data objects, returning the value
2986
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2987
     *
2988
     * @param string $fieldName string
2989
     * @return mixed Will return null on a missing value
2990
     */
2991
    public function relField($fieldName)
2992
    {
2993
        // Navigate to relative parent using relObject() if needed
2994
        $component = $this;
2995
        if (($pos = strrpos($fieldName, '.')) !== false) {
2996
            $relation = substr($fieldName, 0, $pos);
2997
            $fieldName = substr($fieldName, $pos + 1);
2998
            $component = $this->relObject($relation);
2999
        }
3000
3001
        // Bail if the component is null
3002
        if (!$component) {
3003
            return null;
3004
        }
3005
        if (ClassInfo::hasMethod($component, $fieldName)) {
3006
            return $component->$fieldName();
3007
        }
3008
        return $component->$fieldName;
3009
    }
3010
3011
    /**
3012
     * Temporary hack to return an association name, based on class, to get around the mangle
3013
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
3014
     *
3015
     * @param string $className
3016
     * @return string
3017
     */
3018
    public function getReverseAssociation($className)
3019
    {
3020
        if (is_array($this->manyMany())) {
0 ignored issues
show
introduced by
The condition is_array($this->manyMany()) is always true.
Loading history...
3021
            $many_many = array_flip($this->manyMany());
3022
            if (array_key_exists($className, $many_many)) {
3023
                return $many_many[$className];
3024
            }
3025
        }
3026
        if (is_array($this->hasMany())) {
0 ignored issues
show
introduced by
The condition is_array($this->hasMany()) is always true.
Loading history...
3027
            $has_many = array_flip($this->hasMany());
3028
            if (array_key_exists($className, $has_many)) {
3029
                return $has_many[$className];
3030
            }
3031
        }
3032
        if (is_array($this->hasOne())) {
0 ignored issues
show
introduced by
The condition is_array($this->hasOne()) is always true.
Loading history...
3033
            $has_one = array_flip($this->hasOne());
3034
            if (array_key_exists($className, $has_one)) {
3035
                return $has_one[$className];
3036
            }
3037
        }
3038
3039
        return false;
3040
    }
3041
3042
    /**
3043
     * Return all objects matching the filter
3044
     * sub-classes are automatically selected and included
3045
     *
3046
     * @param string $callerClass The class of objects to be returned
3047
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3048
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3049
     * @param string|array $sort A sort expression to be inserted into the ORDER
3050
     * BY clause.  If omitted, self::$default_sort will be used.
3051
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
3052
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
3053
     * @param string $containerClass The container class to return the results in.
3054
     *
3055
     * @todo $containerClass is Ignored, why?
3056
     *
3057
     * @return DataList The objects matching the filter, in the class specified by $containerClass
3058
     */
3059
    public static function get(
3060
        $callerClass = null,
3061
        $filter = "",
3062
        $sort = "",
3063
        $join = "",
3064
        $limit = null,
3065
        $containerClass = DataList::class
3066
    ) {
3067
        // Validate arguments
3068
        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...
3069
            $callerClass = get_called_class();
3070
            if ($callerClass === self::class) {
3071
                throw new InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
3072
            }
3073
            if ($filter || $sort || $join || $limit || ($containerClass !== DataList::class)) {
3074
                throw new InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
3075
                    . ' arguments');
3076
            }
3077
        } elseif ($callerClass === self::class) {
3078
            throw new InvalidArgumentException('DataObject::get() cannot query non-subclass DataObject directly');
3079
        }
3080
        if ($join) {
3081
            throw new InvalidArgumentException(
3082
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
3083
            );
3084
        }
3085
3086
        // Build and decorate with args
3087
        $result = DataList::create($callerClass);
3088
        if ($filter) {
3089
            $result = $result->where($filter);
3090
        }
3091
        if ($sort) {
3092
            $result = $result->sort($sort);
3093
        }
3094
        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

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

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

3327
        /** @scrutinizer ignore-call */ 
3328
        $extensions = self::database_extensions(static::class);
Loading history...
3328
3329
        if (empty($table)) {
3330
            throw new LogicException(
3331
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3332
            );
3333
        }
3334
3335
        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...
3336
            $hasAutoIncPK = get_parent_class($this) === self::class;
3337
            DB::require_table(
3338
                $table,
3339
                $fields,
0 ignored issues
show
Bug introduced by
$fields of type array is incompatible with the type string expected by parameter $fieldSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

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

3339
                /** @scrutinizer ignore-type */ $fields,
Loading history...
3340
                $indexes,
0 ignored issues
show
Bug introduced by
$indexes of type array is incompatible with the type string expected by parameter $indexSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

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

3340
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3341
                $hasAutoIncPK,
3342
                $this->config()->get('create_table_options'),
3343
                $extensions
0 ignored issues
show
Bug introduced by
It seems like $extensions can also be of type false; however, parameter $extensions of SilverStripe\ORM\DB::require_table() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

3343
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3344
            );
3345
        } else {
3346
            DB::dont_require_table($table);
3347
        }
3348
3349
        // Build any child tables for many_many items
3350
        if ($manyMany = $this->uninherited('many_many')) {
3351
            $extras = $this->uninherited('many_many_extraFields');
3352
            foreach ($manyMany as $component => $spec) {
3353
                // Get many_many spec
3354
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3355
                $parentField = $manyManyComponent['parentField'];
3356
                $childField = $manyManyComponent['childField'];
3357
                $tableOrClass = $manyManyComponent['join'];
3358
3359
                // Skip if backed by actual class
3360
                if (class_exists($tableOrClass)) {
3361
                    continue;
3362
                }
3363
3364
                // Build fields
3365
                $manymanyFields = array(
3366
                    $parentField => "Int",
3367
                    $childField => "Int",
3368
                );
3369
                if (isset($extras[$component])) {
3370
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3371
                }
3372
3373
                // Build index list
3374
                $manymanyIndexes = [
3375
                    $parentField => [
3376
                        'type' => 'index',
3377
                        'name' => $parentField,
3378
                        'columns' => [$parentField],
3379
                    ],
3380
                    $childField => [
3381
                        'type' => 'index',
3382
                        'name' => $childField,
3383
                        'columns' => [$childField],
3384
                    ],
3385
                ];
3386
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Bug introduced by
$manymanyIndexes of type array<mixed,array<string,array|mixed|string>> is incompatible with the type string expected by parameter $indexSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

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

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

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

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