Passed
Pull Request — 4 (#8453)
by Sam
09:08 queued 28s
created

DataObject::getComponent()   C

Complexity

Conditions 12
Paths 20

Size

Total Lines 71
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

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

1097
                        $leftComponents->/** @scrutinizer ignore-call */ 
1098
                                         addMany($rightComponents->column('ID'));
Loading history...
1098
                    }
1099
                    $leftComponents->write();
1100
                }
1101
            }
1102
1103
            if ($hasMany = $this->hasMany()) {
1104
                foreach ($hasMany as $relationship => $class) {
1105
                    $leftComponents = $leftObj->getComponents($relationship);
1106
                    $rightComponents = $rightObj->getComponents($relationship);
1107
                    if ($rightComponents && $rightComponents->exists()) {
1108
                        $leftComponents->addMany($rightComponents->column('ID'));
1109
                    }
1110
                    $leftComponents->write();
0 ignored issues
show
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

1110
                    $leftComponents->/** @scrutinizer ignore-call */ 
1111
                                     write();
Loading history...
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

1110
                    $leftComponents->/** @scrutinizer ignore-call */ 
1111
                                     write();
Loading history...
1111
                }
1112
            }
1113
        }
1114
1115
        return true;
1116
    }
1117
1118
    /**
1119
     * Forces the record to think that all its data has changed.
1120
     * Doesn't write to the database. Force-change preseved until
1121
     * next write. Existing CHANGE_VALUE or CHANGE_STRICT values
1122
     * are preserved.
1123
     *
1124
     * @return $this
1125
     */
1126
    public function forceChange()
1127
    {
1128
        // Ensure lazy fields loaded
1129
        $this->loadLazyFields();
1130
1131
        // Populate the null values in record so that they actually get written
1132
        foreach (array_keys(static::getSchema()->fieldSpecs(static::class)) as $fieldName) {
1133
            if (!isset($this->record[$fieldName])) {
1134
                $this->record[$fieldName] = null;
1135
            }
1136
        }
1137
1138
        $this->changeForced = true;
1139
1140
        return $this;
1141
    }
1142
1143
    /**
1144
     * Validate the current object.
1145
     *
1146
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1147
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1148
     *
1149
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1150
     * and onAfterWrite() won't get called either.
1151
     *
1152
     * It is expected that you call validate() in your own application to test that an object is valid before
1153
     * attempting a write, and respond appropriately if it isn't.
1154
     *
1155
     * @see {@link ValidationResult}
1156
     * @return ValidationResult
1157
     */
1158
    public function validate()
1159
    {
1160
        $result = ValidationResult::create();
1161
        $this->extend('validate', $result);
1162
        return $result;
1163
    }
1164
1165
    /**
1166
     * Public accessor for {@see DataObject::validate()}
1167
     *
1168
     * @return ValidationResult
1169
     */
1170
    public function doValidate()
1171
    {
1172
        Deprecation::notice('5.0', 'Use validate');
1173
        return $this->validate();
1174
    }
1175
1176
    /**
1177
     * Event handler called before writing to the database.
1178
     * You can overload this to clean up or otherwise process data before writing it to the
1179
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1180
     *
1181
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1182
     *
1183
     * @uses DataExtension->onBeforeWrite()
1184
     */
1185
    protected function onBeforeWrite()
1186
    {
1187
        $this->brokenOnWrite = false;
1188
1189
        $dummy = null;
1190
        $this->extend('onBeforeWrite', $dummy);
1191
    }
1192
1193
    /**
1194
     * Event handler called after writing to the database.
1195
     * You can overload this to act upon changes made to the data after it is written.
1196
     * $this->changed will have a record
1197
     * database.  Don't forget to call parent::onAfterWrite(), though!
1198
     *
1199
     * @uses DataExtension->onAfterWrite()
1200
     */
1201
    protected function onAfterWrite()
1202
    {
1203
        $dummy = null;
1204
        $this->extend('onAfterWrite', $dummy);
1205
    }
1206
1207
    /**
1208
     * Find all objects that will be cascade deleted if this object is deleted
1209
     *
1210
     * Notes:
1211
     *   - If this object is versioned, objects will only be searched in the same stage as the given record.
1212
     *   - This will only be useful prior to deletion, as post-deletion this record will no longer exist.
1213
     *
1214
     * @param bool $recursive True if recursive
1215
     * @param ArrayList $list Optional list to add items to
1216
     * @return ArrayList list of objects
1217
     */
1218
    public function findCascadeDeletes($recursive = true, $list = null)
1219
    {
1220
        // Find objects in these relationships
1221
        return $this->findRelatedObjects('cascade_deletes', $recursive, $list);
1222
    }
1223
1224
    /**
1225
     * Event handler called before deleting from the database.
1226
     * You can overload this to clean up or otherwise process data before delete this
1227
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1228
     *
1229
     * @uses DataExtension->onBeforeDelete()
1230
     */
1231
    protected function onBeforeDelete()
1232
    {
1233
        $this->brokenOnDelete = false;
1234
1235
        $dummy = null;
1236
        $this->extend('onBeforeDelete', $dummy);
1237
1238
        // Cascade deletes
1239
        $deletes = $this->findCascadeDeletes(false);
1240
        foreach ($deletes as $delete) {
1241
            $delete->delete();
1242
        }
1243
    }
1244
1245
    protected function onAfterDelete()
1246
    {
1247
        $this->extend('onAfterDelete');
1248
    }
1249
1250
    /**
1251
     * Load the default values in from the self::$defaults array.
1252
     * Will traverse the defaults of the current class and all its parent classes.
1253
     * Called by the constructor when creating new records.
1254
     *
1255
     * @uses DataExtension->populateDefaults()
1256
     * @return DataObject $this
1257
     */
1258
    public function populateDefaults()
1259
    {
1260
        $classes = array_reverse(ClassInfo::ancestry($this));
1261
1262
        foreach ($classes as $class) {
1263
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1264
1265
            if ($defaults && !is_array($defaults)) {
1266
                user_error(
1267
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1268
                    E_USER_WARNING
1269
                );
1270
                $defaults = null;
1271
            }
1272
1273
            if ($defaults) {
1274
                foreach ($defaults as $fieldName => $fieldValue) {
1275
                    // SRM 2007-03-06: Stricter check
1276
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1277
                        $this->$fieldName = $fieldValue;
1278
                    }
1279
                    // Set many-many defaults with an array of ids
1280
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1281
                        /** @var ManyManyList $manyManyJoin */
1282
                        $manyManyJoin = $this->$fieldName();
1283
                        $manyManyJoin->setByIDList($fieldValue);
1284
                    }
1285
                }
1286
            }
1287
            if ($class == self::class) {
1288
                break;
1289
            }
1290
        }
1291
1292
        $this->extend('populateDefaults');
1293
        return $this;
1294
    }
1295
1296
    /**
1297
     * Determine validation of this object prior to write
1298
     *
1299
     * @return ValidationException Exception generated by this write, or null if valid
1300
     */
1301
    protected function validateWrite()
1302
    {
1303
        if ($this->ObsoleteClassName) {
1304
            return new ValidationException(
1305
                "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - " .
1306
                "you need to change the ClassName before you can write it"
1307
            );
1308
        }
1309
1310
        // Note: Validation can only be disabled at the global level, not per-model
1311
        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...
1312
            $result = $this->validate();
1313
            if (!$result->isValid()) {
1314
                return new ValidationException($result);
1315
            }
1316
        }
1317
        return null;
1318
    }
1319
1320
    /**
1321
     * Prepare an object prior to write
1322
     *
1323
     * @throws ValidationException
1324
     */
1325
    protected function preWrite()
1326
    {
1327
        // Validate this object
1328
        if ($writeException = $this->validateWrite()) {
1329
            // Used by DODs to clean up after themselves, eg, Versioned
1330
            $this->invokeWithExtensions('onAfterSkippedWrite');
1331
            throw $writeException;
1332
        }
1333
1334
        // Check onBeforeWrite
1335
        $this->brokenOnWrite = true;
1336
        $this->onBeforeWrite();
1337
        if ($this->brokenOnWrite) {
1338
            user_error(static::class . " has a broken onBeforeWrite() function."
1339
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1340
        }
1341
    }
1342
1343
    /**
1344
     * Detects and updates all changes made to this object
1345
     *
1346
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1347
     * @return bool True if any changes are detected
1348
     */
1349
    protected function updateChanges($forceChanges = false)
1350
    {
1351
        if ($forceChanges) {
1352
            // Force changes, but only for loaded fields
1353
            foreach ($this->record as $field => $value) {
1354
                $this->changed[$field] = static::CHANGE_VALUE;
1355
            }
1356
            return true;
1357
        }
1358
        return $this->isChanged();
1359
    }
1360
1361
    /**
1362
     * Writes a subset of changes for a specific table to the given manipulation
1363
     *
1364
     * @param string $baseTable Base table
1365
     * @param string $now Timestamp to use for the current time
1366
     * @param bool $isNewRecord Whether this should be treated as a new record write
1367
     * @param array $manipulation Manipulation to write to
1368
     * @param string $class Class of table to manipulate
1369
     */
1370
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1371
    {
1372
        $schema = $this->getSchema();
1373
        $table = $schema->tableName($class);
1374
        $manipulation[$table] = array();
1375
1376
        $changed = $this->getChangedFields();
1377
1378
        // Extract records for this table
1379
        foreach ($this->record as $fieldName => $fieldValue) {
1380
            // we're not attempting to reset the BaseTable->ID
1381
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1382
            if (empty($changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1383
                continue;
1384
            }
1385
1386
            // Ensure this field pertains to this table
1387
            $specification = $schema->fieldSpec(
1388
                $class,
1389
                $fieldName,
1390
                DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED
1391
            );
1392
            if (!$specification) {
1393
                continue;
1394
            }
1395
1396
            // if database column doesn't correlate to a DBField instance...
1397
            $fieldObj = $this->dbObject($fieldName);
1398
            if (!$fieldObj) {
1399
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1400
            }
1401
1402
            // Write to manipulation
1403
            $fieldObj->writeToManipulation($manipulation[$table]);
1404
        }
1405
1406
        // Ensure update of Created and LastEdited columns
1407
        if ($baseTable === $table) {
1408
            $manipulation[$table]['fields']['LastEdited'] = $now;
1409
            if ($isNewRecord) {
1410
                $manipulation[$table]['fields']['Created'] = empty($this->record['Created'])
1411
                    ? $now
1412
                    : $this->record['Created'];
1413
                $manipulation[$table]['fields']['ClassName'] = static::class;
1414
            }
1415
        }
1416
1417
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1418
        // attempt an update, as though it were a normal update.
1419
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1420
        $manipulation[$table]['id'] = $this->record['ID'];
1421
        $manipulation[$table]['class'] = $class;
1422
    }
1423
1424
    /**
1425
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1426
     *
1427
     * Does nothing if an ID is already assigned for this record
1428
     *
1429
     * @param string $baseTable Base table
1430
     * @param string $now Timestamp to use for the current time
1431
     */
1432
    protected function writeBaseRecord($baseTable, $now)
1433
    {
1434
        // Generate new ID if not specified
1435
        if ($this->isInDB()) {
1436
            return;
1437
        }
1438
1439
        // Perform an insert on the base table
1440
        $insert = new SQLInsert('"' . $baseTable . '"');
1441
        $insert
1442
            ->assign('"Created"', $now)
1443
            ->execute();
1444
        $this->changed['ID'] = self::CHANGE_VALUE;
1445
        $this->record['ID'] = DB::get_generated_id($baseTable);
1446
    }
1447
1448
    /**
1449
     * Generate and write the database manipulation for all changed fields
1450
     *
1451
     * @param string $baseTable Base table
1452
     * @param string $now Timestamp to use for the current time
1453
     * @param bool $isNewRecord If this is a new record
1454
     */
1455
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1456
    {
1457
        // Generate database manipulations for each class
1458
        $manipulation = array();
1459
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1460
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1461
        }
1462
1463
        // Allow extensions to extend this manipulation
1464
        $this->extend('augmentWrite', $manipulation);
1465
1466
        // New records have their insert into the base data table done first, so that they can pass the
1467
        // generated ID on to the rest of the manipulation
1468
        if ($isNewRecord) {
1469
            $manipulation[$baseTable]['command'] = 'update';
1470
        }
1471
1472
        // Perform the manipulation
1473
        DB::manipulate($manipulation);
1474
    }
1475
1476
    /**
1477
     * Writes all changes to this object to the database.
1478
     *  - It will insert a record whenever ID isn't set, otherwise update.
1479
     *  - All relevant tables will be updated.
1480
     *  - $this->onBeforeWrite() gets called beforehand.
1481
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1482
     *
1483
     * @uses DataExtension->augmentWrite()
1484
     *
1485
     * @param boolean $showDebug Show debugging information
1486
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1487
     * @param boolean $forceWrite Write to database even if there are no changes
1488
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1489
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1490
     *                                 {@link getManyManyComponents()} (Default: false)
1491
     * @return int The ID of the record
1492
     * @throws ValidationException Exception that can be caught and handled by the calling function
1493
     */
1494
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1495
    {
1496
        $now = DBDatetime::now()->Rfc2822();
1497
1498
        // Execute pre-write tasks
1499
        $this->preWrite();
1500
1501
        // Check if we are doing an update or an insert
1502
        $isNewRecord = !$this->isInDB() || $forceInsert;
1503
1504
        // Check changes exist, abort if there are none
1505
        $hasChanges = $this->updateChanges($isNewRecord);
1506
        if ($hasChanges || $forceWrite || $isNewRecord) {
1507
            // Ensure Created and LastEdited are populated
1508
            if (!isset($this->record['Created'])) {
1509
                $this->record['Created'] = $now;
1510
            }
1511
            $this->record['LastEdited'] = $now;
1512
1513
            // New records have their insert into the base data table done first, so that they can pass the
1514
            // generated primary key on to the rest of the manipulation
1515
            $baseTable = $this->baseTable();
1516
            $this->writeBaseRecord($baseTable, $now);
1517
1518
            // Write the DB manipulation for all changed fields
1519
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1520
1521
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1522
            $this->writeRelations();
1523
            $this->onAfterWrite();
1524
1525
            // Reset isChanged data
1526
            // DBComposites properly bound to the parent record will also have their isChanged value reset
1527
            $this->changed = [];
1528
            $this->changeForced = false;
1529
            $this->original = $this->record;
1530
        } else {
1531
            if ($showDebug) {
1532
                Debug::message("no changes for DataObject");
1533
            }
1534
1535
            // Used by DODs to clean up after themselves, eg, Versioned
1536
            $this->invokeWithExtensions('onAfterSkippedWrite');
1537
        }
1538
1539
        // Write relations as necessary
1540
        if ($writeComponents) {
1541
            $this->writeComponents(true);
1542
        }
1543
1544
        // Clears the cache for this object so get_one returns the correct object.
1545
        $this->flushCache();
1546
1547
        return $this->record['ID'];
1548
    }
1549
1550
    /**
1551
     * Writes cached relation lists to the database, if possible
1552
     */
1553
    public function writeRelations()
1554
    {
1555
        if (!$this->isInDB()) {
1556
            return;
1557
        }
1558
1559
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1560
        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...
1561
            foreach ($this->unsavedRelations as $name => $list) {
1562
                $list->changeToList($this->$name());
1563
            }
1564
            $this->unsavedRelations = array();
1565
        }
1566
    }
1567
1568
    /**
1569
     * Write the cached components to the database. Cached components could refer to two different instances of the
1570
     * same record.
1571
     *
1572
     * @param bool $recursive Recursively write components
1573
     * @return DataObject $this
1574
     */
1575
    public function writeComponents($recursive = false)
1576
    {
1577
        foreach ($this->components as $component) {
1578
            $component->write(false, false, false, $recursive);
1579
        }
1580
1581
        if ($join = $this->getJoin()) {
1582
            $join->write(false, false, false, $recursive);
1583
        }
1584
1585
        return $this;
1586
    }
1587
1588
    /**
1589
     * Delete this data object.
1590
     * $this->onBeforeDelete() gets called.
1591
     * Note that in Versioned objects, both Stage and Live will be deleted.
1592
     * @uses DataExtension->augmentSQL()
1593
     */
1594
    public function delete()
1595
    {
1596
        $this->brokenOnDelete = true;
1597
        $this->onBeforeDelete();
1598
        if ($this->brokenOnDelete) {
1599
            user_error(static::class . " has a broken onBeforeDelete() function."
1600
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1601
        }
1602
1603
        // Deleting a record without an ID shouldn't do anything
1604
        if (!$this->ID) {
1605
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1606
        }
1607
1608
        // TODO: This is quite ugly.  To improve:
1609
        //  - move the details of the delete code in the DataQuery system
1610
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1611
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1612
        $srcQuery = DataList::create(static::class)
1613
            ->filter('ID', $this->ID)
1614
            ->dataQuery()
1615
            ->query();
1616
        $queriedTables = $srcQuery->queriedTables();
1617
        $this->extend('updateDeleteTables', $queriedTables, $srcQuery);
1618
        foreach ($queriedTables as $table) {
1619
            $delete = SQLDelete::create("\"$table\"", array('"ID"' => $this->ID));
1620
            $this->extend('updateDeleteTable', $delete, $table, $queriedTables, $srcQuery);
1621
            $delete->execute();
1622
        }
1623
        // Remove this item out of any caches
1624
        $this->flushCache();
1625
1626
        $this->onAfterDelete();
1627
1628
        $this->OldID = $this->ID;
1629
        $this->ID = 0;
1630
    }
1631
1632
    /**
1633
     * Delete the record with the given ID.
1634
     *
1635
     * @param string $className The class name of the record to be deleted
1636
     * @param int $id ID of record to be deleted
1637
     */
1638
    public static function delete_by_id($className, $id)
1639
    {
1640
        $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...
1641
        if ($obj) {
0 ignored issues
show
introduced by
$obj is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1642
            $obj->delete();
1643
        } else {
1644
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1645
        }
1646
    }
1647
1648
    /**
1649
     * Get the class ancestry, including the current class name.
1650
     * The ancestry will be returned as an array of class names, where the 0th element
1651
     * will be the class that inherits directly from DataObject, and the last element
1652
     * will be the current class.
1653
     *
1654
     * @return array Class ancestry
1655
     */
1656
    public function getClassAncestry()
1657
    {
1658
        return ClassInfo::ancestry(static::class);
1659
    }
1660
1661
    /**
1662
     * Return a unary component object from a one to one relationship, as a DataObject.
1663
     * If no component is available, an 'empty component' will be returned for
1664
     * non-polymorphic relations, or for polymorphic relations with a class set.
1665
     *
1666
     * @param string $componentName Name of the component
1667
     * @return DataObject The component object. It's exact type will be that of the component.
1668
     * @throws Exception
1669
     */
1670
    public function getComponent($componentName)
1671
    {
1672
        if (isset($this->components[$componentName])) {
1673
            return $this->components[$componentName];
1674
        }
1675
1676
        $schema = static::getSchema();
1677
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1678
            $joinField = $componentName . 'ID';
1679
            $joinID = $this->getField($joinField);
1680
1681
            // Extract class name for polymorphic relations
1682
            if ($class === self::class) {
1683
                $class = $this->getField($componentName . 'Class');
1684
                if (empty($class)) {
1685
                    return null;
1686
                }
1687
            }
1688
1689
            if ($joinID) {
1690
                // Ensure that the selected object originates from the same stage, subsite, etc
1691
                $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...
1692
                    ->filter('ID', $joinID)
1693
                    ->setDataQueryParam($this->getInheritableQueryParams())
1694
                    ->first();
1695
            }
1696
1697
            if (empty($component)) {
1698
                $component = Injector::inst()->create($class);
1699
            }
1700
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1701
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1702
            $joinID = $this->ID;
1703
1704
            if ($joinID) {
1705
                // Prepare filter for appropriate join type
1706
                if ($polymorphic) {
1707
                    $filter = array(
1708
                        "{$joinField}ID" => $joinID,
1709
                        "{$joinField}Class" => static::class,
1710
                    );
1711
                } else {
1712
                    $filter = array(
1713
                        $joinField => $joinID
1714
                    );
1715
                }
1716
1717
                // Ensure that the selected object originates from the same stage, subsite, etc
1718
                $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...
1719
                    ->filter($filter)
1720
                    ->setDataQueryParam($this->getInheritableQueryParams())
1721
                    ->first();
1722
            }
1723
1724
            if (empty($component)) {
1725
                $component = Injector::inst()->create($class);
1726
                if ($polymorphic) {
1727
                    $component->{$joinField . 'ID'} = $this->ID;
1728
                    $component->{$joinField . 'Class'} = static::class;
1729
                } else {
1730
                    $component->$joinField = $this->ID;
1731
                }
1732
            }
1733
        } else {
1734
            throw new InvalidArgumentException(
1735
                "DataObject->getComponent(): Could not find component '$componentName'."
1736
            );
1737
        }
1738
1739
        $this->components[$componentName] = $component;
1740
        return $component;
1741
    }
1742
1743
    /**
1744
     * Assign an item to the given component
1745
     *
1746
     * @param string $componentName
1747
     * @param DataObject|null $item
1748
     * @return $this
1749
     */
1750
    public function setComponent($componentName, $item)
1751
    {
1752
        // Validate component
1753
        $schema = static::getSchema();
1754
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1755
            // Force item to be written if not by this point
1756
            // @todo This could be lazy-written in a beforeWrite hook, but force write here for simplicity
1757
            // https://github.com/silverstripe/silverstripe-framework/issues/7818
1758
            if ($item && !$item->isInDB()) {
1759
                $item->write();
1760
            }
1761
1762
            // Update local ID
1763
            $joinField = $componentName . 'ID';
1764
            $this->setField($joinField, $item ? $item->ID : null);
1765
            // Update Class (Polymorphic has_one)
1766
            // Extract class name for polymorphic relations
1767
            if ($class === self::class) {
1768
                $this->setField($componentName . 'Class', $item ? get_class($item) : null);
1769
            }
1770
        } 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...
1771
            if ($item) {
1772
                // For belongs_to, add to has_one on other component
1773
                $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1774
                if (!$polymorphic) {
1775
                    $joinField = substr($joinField, 0, -2);
1776
                }
1777
                $item->setComponent($joinField, $this);
1778
            }
1779
        } else {
1780
            throw new InvalidArgumentException(
1781
                "DataObject->setComponent(): Could not find component '$componentName'."
1782
            );
1783
        }
1784
1785
        $this->components[$componentName] = $item;
1786
        return $this;
1787
    }
1788
1789
    /**
1790
     * Returns a one-to-many relation as a HasManyList
1791
     *
1792
     * @param string $componentName Name of the component
1793
     * @param int|array $id Optional ID(s) for parent of this relation, if not the current record
1794
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1795
     */
1796
    public function getComponents($componentName, $id = null)
1797
    {
1798
        if (!isset($id)) {
1799
            $id = $this->ID;
1800
        }
1801
        $result = null;
1802
1803
        $schema = $this->getSchema();
1804
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1805
        if (!$componentClass) {
1806
            throw new InvalidArgumentException(sprintf(
1807
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1808
                $componentName,
1809
                static::class
1810
            ));
1811
        }
1812
1813
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1814
        if (!$id) {
1815
            if (!isset($this->unsavedRelations[$componentName])) {
1816
                $this->unsavedRelations[$componentName] =
1817
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1818
            }
1819
            return $this->unsavedRelations[$componentName];
1820
        }
1821
1822
        // Determine type and nature of foreign relation
1823
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1824
        /** @var HasManyList $result */
1825
        if ($polymorphic) {
1826
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
1827
        } else {
1828
            $result = HasManyList::create($componentClass, $joinField);
1829
        }
1830
1831
        return $result
1832
            ->setDataQueryParam($this->getInheritableQueryParams())
1833
            ->forForeignID($id);
1834
    }
1835
1836
    /**
1837
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1838
     *
1839
     * @param string $relationName Relation name.
1840
     * @return string Class name, or null if not found.
1841
     */
1842
    public function getRelationClass($relationName)
1843
    {
1844
        // Parse many_many
1845
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1846
        if ($manyManyComponent) {
1847
            return $manyManyComponent['childClass'];
1848
        }
1849
1850
        // Go through all relationship configuration fields.
1851
        $config = $this->config();
1852
        $candidates = array_merge(
1853
            ($relations = $config->get('has_one')) ? $relations : array(),
1854
            ($relations = $config->get('has_many')) ? $relations : array(),
1855
            ($relations = $config->get('belongs_to')) ? $relations : array()
1856
        );
1857
1858
        if (isset($candidates[$relationName])) {
1859
            $remoteClass = $candidates[$relationName];
1860
1861
            // If dot notation is present, extract just the first part that contains the class.
1862
            if (($fieldPos = strpos($remoteClass, '.')) !== false) {
1863
                return substr($remoteClass, 0, $fieldPos);
1864
            }
1865
1866
            // Otherwise just return the class
1867
            return $remoteClass;
1868
        }
1869
1870
        return null;
1871
    }
1872
1873
    /**
1874
     * Given a relation name, determine the relation type
1875
     *
1876
     * @param string $component Name of component
1877
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1878
     */
1879
    public function getRelationType($component)
1880
    {
1881
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1882
        $config = $this->config();
1883
        foreach ($types as $type) {
1884
            $relations = $config->get($type);
1885
            if ($relations && isset($relations[$component])) {
1886
                return $type;
1887
            }
1888
        }
1889
        return null;
1890
    }
1891
1892
    /**
1893
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1894
     * side of the relation.
1895
     *
1896
     * Notes on behaviour:
1897
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1898
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1899
     *  - Polymorphic relationships do not have two natural endpoints (only on one side)
1900
     *   and thus attempting to infer them will return nothing.
1901
     *  - Cannot be used on unsaved objects.
1902
     *
1903
     * @param string $remoteClass
1904
     * @param string $remoteRelation
1905
     * @return DataList|DataObject The component, either as a list or single object
1906
     * @throws BadMethodCallException
1907
     * @throws InvalidArgumentException
1908
     */
1909
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1910
    {
1911
        $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...
1912
        $class = $remote->getRelationClass($remoteRelation);
1913
        $schema = static::getSchema();
1914
1915
        // Validate arguments
1916
        if (!$this->isInDB()) {
1917
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1918
        }
1919
        if (empty($class)) {
1920
            throw new InvalidArgumentException(sprintf(
1921
                "%s invoked with invalid relation %s.%s",
1922
                __METHOD__,
1923
                $remoteClass,
1924
                $remoteRelation
1925
            ));
1926
        }
1927
        // If relation is polymorphic, do not infer recriprocal relationship
1928
        if ($class === self::class) {
1929
            return null;
1930
        }
1931
        if (!is_a($this, $class, true)) {
1932
            throw new InvalidArgumentException(sprintf(
1933
                "Relation %s on %s does not refer to objects of type %s",
1934
                $remoteRelation,
1935
                $remoteClass,
1936
                static::class
1937
            ));
1938
        }
1939
1940
        // Check the relation type to mock
1941
        $relationType = $remote->getRelationType($remoteRelation);
1942
        switch ($relationType) {
1943
            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...
1944
                // Mock has_many
1945
                $joinField = "{$remoteRelation}ID";
1946
                $componentClass = $schema->classForField($remoteClass, $joinField);
1947
                $result = HasManyList::create($componentClass, $joinField);
1948
                return $result
1949
                    ->setDataQueryParam($this->getInheritableQueryParams())
1950
                    ->forForeignID($this->ID);
1951
            }
1952
            case 'belongs_to':
1953
            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...
1954
                // These relations must have a has_one on the other end, so find it
1955
                $joinField = $schema->getRemoteJoinField(
1956
                    $remoteClass,
1957
                    $remoteRelation,
1958
                    $relationType,
1959
                    $polymorphic
1960
                );
1961
                // If relation is polymorphic, do not infer recriprocal relationship automatically
1962
                if ($polymorphic) {
1963
                    return null;
1964
                }
1965
                $joinID = $this->getField($joinField);
1966
                if (empty($joinID)) {
1967
                    return null;
1968
                }
1969
                // Get object by joined ID
1970
                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...
1971
                    ->filter('ID', $joinID)
1972
                    ->setDataQueryParam($this->getInheritableQueryParams())
1973
                    ->first();
1974
            }
1975
            case 'many_many':
1976
            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...
1977
                // Get components and extra fields from parent
1978
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1979
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1980
1981
                // Reverse parent and component fields and create an inverse ManyManyList
1982
                /** @var RelationList $result */
1983
                $result = Injector::inst()->create(
1984
                    $manyMany['relationClass'],
1985
                    $manyMany['parentClass'], // Substitute parent class for dataClass
1986
                    $manyMany['join'],
1987
                    $manyMany['parentField'], // Reversed parent / child field
1988
                    $manyMany['childField'], // Reversed parent / child field
1989
                    $extraFields,
1990
                    $manyMany['childClass'], // substitute child class for parentClass
1991
                    $remoteClass // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
1992
                );
1993
                $this->extend('updateManyManyComponents', $result);
1994
1995
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1996
                // foreignID set elsewhere.
1997
                return $result
1998
                    ->setDataQueryParam($this->getInheritableQueryParams())
1999
                    ->forForeignID($this->ID);
2000
            }
2001
            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...
2002
                return null;
2003
            }
2004
        }
2005
    }
2006
2007
    /**
2008
     * Returns a many-to-many component, as a ManyManyList.
2009
     * @param string $componentName Name of the many-many component
2010
     * @param int|array $id Optional ID for parent of this relation, if not the current record
2011
     * @return ManyManyList|UnsavedRelationList The set of components
2012
     */
2013
    public function getManyManyComponents($componentName, $id = null)
2014
    {
2015
        if (!isset($id)) {
2016
            $id = $this->ID;
2017
        }
2018
        $schema = static::getSchema();
2019
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
2020
        if (!$manyManyComponent) {
2021
            throw new InvalidArgumentException(sprintf(
2022
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
2023
                $componentName,
2024
                static::class
2025
            ));
2026
        }
2027
2028
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
2029
        if (!$id) {
2030
            if (!isset($this->unsavedRelations[$componentName])) {
2031
                $this->unsavedRelations[$componentName] =
2032
                    new UnsavedRelationList(
2033
                        $manyManyComponent['parentClass'],
2034
                        $componentName,
2035
                        $manyManyComponent['childClass']
2036
                    );
2037
            }
2038
            return $this->unsavedRelations[$componentName];
2039
        }
2040
2041
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
2042
        /** @var RelationList $result */
2043
        $result = Injector::inst()->create(
2044
            $manyManyComponent['relationClass'],
2045
            $manyManyComponent['childClass'],
2046
            $manyManyComponent['join'],
2047
            $manyManyComponent['childField'],
2048
            $manyManyComponent['parentField'],
2049
            $extraFields,
2050
            $manyManyComponent['parentClass'],
2051
            static::class // In case ManyManyThroughList needs to use PolymorphicHasManyList internally
2052
        );
2053
2054
        // Store component data in query meta-data
2055
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
2056
            /** @var DataQuery $query */
2057
            $query->setQueryParam('Component.ExtraFields', $extraFields);
2058
        });
2059
2060
        // If we have a default sort set for our "join" then we should overwrite any default already set.
2061
        $joinSort = Config::inst()->get($manyManyComponent['join'], 'default_sort');
2062
        if (!empty($joinSort)) {
2063
            $result = $result->sort($joinSort);
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 since the last write.
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 change was forced, then derive change data from $this->record
2531
        if ($this->changeForced && $changeLevel <= self::CHANGE_STRICT) {
2532
            $changed = array_combine(
2533
                array_keys($this->record),
2534
                array_fill(0, count($this->record), self::CHANGE_STRICT)
2535
            );
2536
            // @todo Find better way to allow versioned to write a new version after forceChange
2537
            unset($changed['Version']);
2538
        } else {
2539
            $changed = $this->changed;
2540
        }
2541
2542
        if (is_array($databaseFieldsOnly)) {
2543
            $fields = array_intersect_key($changed, array_flip($databaseFieldsOnly));
2544
        } elseif ($databaseFieldsOnly) {
2545
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2546
            $fields = array_intersect_key($changed, $fieldsSpecs);
2547
        } else {
2548
            $fields = $changed;
2549
        }
2550
2551
        // Filter the list to those of a certain change level
2552
        if ($changeLevel > self::CHANGE_STRICT) {
2553
            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...
2554
                foreach ($fields as $name => $level) {
2555
                    if ($level < $changeLevel) {
2556
                        unset($fields[$name]);
2557
                    }
2558
                }
2559
            }
2560
        }
2561
2562
        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...
2563
            foreach ($fields as $name => $level) {
2564
                $changedFields[$name] = array(
2565
                    'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2566
                    'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2567
                    'level' => $level
2568
                );
2569
            }
2570
        }
2571
2572
        return $changedFields;
2573
    }
2574
2575
    /**
2576
     * Uses {@link getChangedFields()} to determine if fields have been changed
2577
     * since loading them from the database.
2578
     *
2579
     * @param string $fieldName Name of the database field to check, will check for any if not given
2580
     * @param int $changeLevel See {@link getChangedFields()}
2581
     * @return boolean
2582
     */
2583
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2584
    {
2585
        $fields = $fieldName ? array($fieldName) : true;
2586
        $changed = $this->getChangedFields($fields, $changeLevel);
2587
        if (!isset($fieldName)) {
2588
            return !empty($changed);
2589
        } else {
2590
            return array_key_exists($fieldName, $changed);
2591
        }
2592
    }
2593
2594
    /**
2595
     * Set the value of the field
2596
     * Called by {@link __set()} and any setFieldName() methods you might create.
2597
     *
2598
     * @param string $fieldName Name of the field
2599
     * @param mixed $val New field value
2600
     * @return $this
2601
     */
2602
    public function setField($fieldName, $val)
2603
    {
2604
        $this->objCacheClear();
2605
        //if it's a has_one component, destroy the cache
2606
        if (substr($fieldName, -2) == 'ID') {
2607
            unset($this->components[substr($fieldName, 0, -2)]);
2608
        }
2609
2610
        // If we've just lazy-loaded the column, then we need to populate the $original array
2611
        if (isset($this->record[$fieldName . '_Lazy'])) {
2612
            $tableClass = $this->record[$fieldName . '_Lazy'];
2613
            $this->loadLazyFields($tableClass);
2614
        }
2615
2616
        // Support component assignent via field setter
2617
        $schema = static::getSchema();
2618
        if ($schema->unaryComponent(static::class, $fieldName)) {
2619
            unset($this->components[$fieldName]);
2620
            // Assign component directly
2621
            if (is_null($val) || $val instanceof DataObject) {
2622
                return $this->setComponent($fieldName, $val);
2623
            }
2624
            // Assign by ID instead of object
2625
            if (is_numeric($val)) {
2626
                $fieldName .= 'ID';
2627
            }
2628
        }
2629
2630
        // Situation 1: Passing an DBField
2631
        if ($val instanceof DBField) {
2632
            $val->setName($fieldName);
2633
            $val->saveInto($this);
2634
2635
            // Situation 1a: Composite fields should remain bound in case they are
2636
            // later referenced to update the parent dataobject
2637
            if ($val instanceof DBComposite) {
2638
                $val->bindTo($this);
2639
                $this->record[$fieldName] = $val;
2640
            }
2641
        // Situation 2: Passing a literal or non-DBField object
2642
        } else {
2643
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2644
            if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) {
2645
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2646
            }
2647
2648
            // if a field is not existing or has strictly changed
2649
            if (!isset($this->original[$fieldName]) || $this->original[$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->original[$fieldName]) && $val)
2656
                    || (isset($this->original[$fieldName]) && $this->original[$fieldName] != $val)
2657
                ) {
2658
                    // Value has changed as well, not just the type
2659
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2660
                }
2661
            // Value has been restored to its original, remove any record of the change
2662
            } elseif (isset($this->changed[$fieldName])) {
2663
                unset($this->changed[$fieldName]);
2664
            }
2665
2666
            // Value is saved regardless, since the change detection relates to the last write
2667
            $this->record[$fieldName] = $val;
2668
        }
2669
        return $this;
2670
    }
2671
2672
    /**
2673
     * Set the value of the field, using a casting object.
2674
     * This is useful when you aren't sure that a date is in SQL format, for example.
2675
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2676
     * can be saved into the Image table.
2677
     *
2678
     * @param string $fieldName Name of the field
2679
     * @param mixed $value New field value
2680
     * @return $this
2681
     */
2682
    public function setCastedField($fieldName, $value)
2683
    {
2684
        if (!$fieldName) {
2685
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2686
        }
2687
        $fieldObj = $this->dbObject($fieldName);
2688
        if ($fieldObj) {
0 ignored issues
show
introduced by
$fieldObj is of type SilverStripe\ORM\FieldType\DBField, thus it always evaluated to true.
Loading history...
2689
            $fieldObj->setValue($value);
2690
            $fieldObj->saveInto($this);
2691
        } else {
2692
            $this->$fieldName = $value;
2693
        }
2694
        return $this;
2695
    }
2696
2697
    /**
2698
     * {@inheritdoc}
2699
     */
2700
    public function castingHelper($field)
2701
    {
2702
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2703
        if ($fieldSpec) {
2704
            return $fieldSpec;
2705
        }
2706
2707
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2708
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2709
        $queryParams = $this->getSourceQueryParams();
2710
        if (!empty($queryParams['Component.ExtraFields'])) {
2711
            $extraFields = $queryParams['Component.ExtraFields'];
2712
2713
            if (isset($extraFields[$field])) {
2714
                return $extraFields[$field];
2715
            }
2716
        }
2717
2718
        return parent::castingHelper($field);
2719
    }
2720
2721
    /**
2722
     * Returns true if the given field exists in a database column on any of
2723
     * the objects tables and optionally look up a dynamic getter with
2724
     * get<fieldName>().
2725
     *
2726
     * @param string $field Name of the field
2727
     * @return boolean True if the given field exists
2728
     */
2729
    public function hasField($field)
2730
    {
2731
        $schema = static::getSchema();
2732
        return (
2733
            array_key_exists($field, $this->record)
2734
            || array_key_exists($field, $this->components)
2735
            || $schema->fieldSpec(static::class, $field)
2736
            || $schema->unaryComponent(static::class, $field)
2737
            || $this->hasMethod("get{$field}")
2738
        );
2739
    }
2740
2741
    /**
2742
     * Returns true if the given field exists as a database column
2743
     *
2744
     * @param string $field Name of the field
2745
     *
2746
     * @return boolean
2747
     */
2748
    public function hasDatabaseField($field)
2749
    {
2750
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2751
        return !empty($spec);
2752
    }
2753
2754
    /**
2755
     * Returns true if the member is allowed to do the given action.
2756
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2757
     *
2758
     * @param string $perm The permission to be checked, such as 'View'.
2759
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2760
     * in user.
2761
     * @param array $context Additional $context to pass to extendedCan()
2762
     *
2763
     * @return boolean True if the the member is allowed to do the given action
2764
     */
2765
    public function can($perm, $member = null, $context = array())
2766
    {
2767
        if (!$member) {
2768
            $member = Security::getCurrentUser();
2769
        }
2770
2771
        if ($member && Permission::checkMember($member, "ADMIN")) {
2772
            return true;
2773
        }
2774
2775
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2776
            $method = 'can' . ucfirst($perm);
2777
            return $this->$method($member);
2778
        }
2779
2780
        $results = $this->extendedCan('can', $member);
2781
        if (isset($results)) {
2782
            return $results;
2783
        }
2784
2785
        return ($member && Permission::checkMember($member, $perm));
2786
    }
2787
2788
    /**
2789
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2790
     * expected to return one of three values:
2791
     *
2792
     *  - false: Disallow this permission, regardless of what other extensions say
2793
     *  - true: Allow this permission, as long as no other extensions return false
2794
     *  - NULL: Don't affect the outcome
2795
     *
2796
     * This method itself returns a tri-state value, and is designed to be used like this:
2797
     *
2798
     * <code>
2799
     * $extended = $this->extendedCan('canDoSomething', $member);
2800
     * if($extended !== null) return $extended;
2801
     * else return $normalValue;
2802
     * </code>
2803
     *
2804
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2805
     * @param Member|int $member
2806
     * @param array $context Optional context
2807
     * @return boolean|null
2808
     */
2809
    public function extendedCan($methodName, $member, $context = array())
2810
    {
2811
        $results = $this->extend($methodName, $member, $context);
2812
        if ($results && is_array($results)) {
2813
            // Remove NULLs
2814
            $results = array_filter($results, function ($v) {
2815
                return !is_null($v);
2816
            });
2817
            // If there are any non-NULL responses, then return the lowest one of them.
2818
            // If any explicitly deny the permission, then we don't get access
2819
            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...
2820
                return min($results);
2821
            }
2822
        }
2823
        return null;
2824
    }
2825
2826
    /**
2827
     * @param Member $member
2828
     * @return boolean
2829
     */
2830
    public function canView($member = null)
2831
    {
2832
        $extended = $this->extendedCan(__FUNCTION__, $member);
2833
        if ($extended !== null) {
2834
            return $extended;
2835
        }
2836
        return Permission::check('ADMIN', 'any', $member);
2837
    }
2838
2839
    /**
2840
     * @param Member $member
2841
     * @return boolean
2842
     */
2843
    public function canEdit($member = null)
2844
    {
2845
        $extended = $this->extendedCan(__FUNCTION__, $member);
2846
        if ($extended !== null) {
2847
            return $extended;
2848
        }
2849
        return Permission::check('ADMIN', 'any', $member);
2850
    }
2851
2852
    /**
2853
     * @param Member $member
2854
     * @return boolean
2855
     */
2856
    public function canDelete($member = null)
2857
    {
2858
        $extended = $this->extendedCan(__FUNCTION__, $member);
2859
        if ($extended !== null) {
2860
            return $extended;
2861
        }
2862
        return Permission::check('ADMIN', 'any', $member);
2863
    }
2864
2865
    /**
2866
     * @param Member $member
2867
     * @param array $context Additional context-specific data which might
2868
     * affect whether (or where) this object could be created.
2869
     * @return boolean
2870
     */
2871
    public function canCreate($member = null, $context = array())
2872
    {
2873
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2874
        if ($extended !== null) {
2875
            return $extended;
2876
        }
2877
        return Permission::check('ADMIN', 'any', $member);
2878
    }
2879
2880
    /**
2881
     * Debugging used by Debug::show()
2882
     *
2883
     * @return string HTML data representing this object
2884
     */
2885
    public function debug()
2886
    {
2887
        $class = static::class;
2888
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2889
        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...
2890
            foreach ($this->record as $fieldName => $fieldVal) {
2891
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2892
            }
2893
        }
2894
        $val .= "</ul>\n";
2895
        return $val;
2896
    }
2897
2898
    /**
2899
     * Return the DBField object that represents the given field.
2900
     * This works similarly to obj() with 2 key differences:
2901
     *   - it still returns an object even when the field has no value.
2902
     *   - it only matches fields and not methods
2903
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2904
     *
2905
     * @param string $fieldName Name of the field
2906
     * @return DBField The field as a DBField object
2907
     */
2908
    public function dbObject($fieldName)
2909
    {
2910
        // Check for field in DB
2911
        $schema = static::getSchema();
2912
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2913
        if (!$helper) {
2914
            return null;
2915
        }
2916
2917
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2918
            $tableClass = $this->record[$fieldName . '_Lazy'];
2919
            $this->loadLazyFields($tableClass);
2920
        }
2921
2922
        $value = isset($this->record[$fieldName])
2923
            ? $this->record[$fieldName]
2924
            : null;
2925
2926
        // If we have a DBField object in $this->record, then return that
2927
        if ($value instanceof DBField) {
2928
            return $value;
2929
        }
2930
2931
        list($class, $spec) = explode('.', $helper);
2932
        /** @var DBField $obj */
2933
        $table = $schema->tableName($class);
2934
        $obj = Injector::inst()->create($spec, $fieldName);
2935
        $obj->setTable($table);
2936
        $obj->setValue($value, $this, false);
2937
        return $obj;
2938
    }
2939
2940
    /**
2941
     * Traverses to a DBField referenced by relationships between data objects.
2942
     *
2943
     * The path to the related field is specified with dot separated syntax
2944
     * (eg: Parent.Child.Child.FieldName).
2945
     *
2946
     * If a relation is blank, this will return null instead.
2947
     * If a relation name is invalid (e.g. non-relation on a parent) this
2948
     * can throw a LogicException.
2949
     *
2950
     * @param string $fieldPath List of paths on this object. All items in this path
2951
     * must be ViewableData implementors
2952
     *
2953
     * @return mixed DBField of the field on the object or a DataList instance.
2954
     * @throws LogicException If accessing invalid relations
2955
     */
2956
    public function relObject($fieldPath)
2957
    {
2958
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
2959
        $component = $this;
2960
2961
        // Parse all relations
2962
        foreach (explode('.', $fieldPath) as $relation) {
2963
            if (!$component) {
2964
                return null;
2965
            }
2966
2967
            // Inspect relation type
2968
            if (ClassInfo::hasMethod($component, $relation)) {
2969
                $component = $component->$relation();
2970
            } elseif ($component instanceof Relation || $component instanceof DataList) {
2971
                // $relation could either be a field (aggregate), or another relation
2972
                $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

2972
                $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...
2973
                $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

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

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

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

3330
        /** @scrutinizer ignore-call */ 
3331
        $extensions = self::database_extensions(static::class);
Loading history...
3331
3332
        if (empty($table)) {
3333
            throw new LogicException(
3334
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3335
            );
3336
        }
3337
3338
        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...
3339
            $hasAutoIncPK = get_parent_class($this) === self::class;
3340
            DB::require_table(
3341
                $table,
3342
                $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

3342
                /** @scrutinizer ignore-type */ $fields,
Loading history...
3343
                $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

3343
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3344
                $hasAutoIncPK,
3345
                $this->config()->get('create_table_options'),
3346
                $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

3346
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3347
            );
3348
        } else {
3349
            DB::dont_require_table($table);
3350
        }
3351
3352
        // Build any child tables for many_many items
3353
        if ($manyMany = $this->uninherited('many_many')) {
3354
            $extras = $this->uninherited('many_many_extraFields');
3355
            foreach ($manyMany as $component => $spec) {
3356
                // Get many_many spec
3357
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3358
                $parentField = $manyManyComponent['parentField'];
3359
                $childField = $manyManyComponent['childField'];
3360
                $tableOrClass = $manyManyComponent['join'];
3361
3362
                // Skip if backed by actual class
3363
                if (class_exists($tableOrClass)) {
3364
                    continue;
3365
                }
3366
3367
                // Build fields
3368
                $manymanyFields = array(
3369
                    $parentField => "Int",
3370
                    $childField => "Int",
3371
                );
3372
                if (isset($extras[$component])) {
3373
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3374
                }
3375
3376
                // Build index list
3377
                $manymanyIndexes = [
3378
                    $parentField => [
3379
                        'type' => 'index',
3380
                        'name' => $parentField,
3381
                        'columns' => [$parentField],
3382
                    ],
3383
                    $childField => [
3384
                        'type' => 'index',
3385
                        'name' => $childField,
3386
                        'columns' => [$childField],
3387
                    ],
3388
                ];
3389
                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

3389
                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

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