Completed
Push — 4 ( 5fbfd8...bd8494 )
by Ingo
09:20
created

DataObject::__construct()   D

Complexity

Conditions 15
Paths 192

Size

Total Lines 74
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

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

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

1095
                    $leftComponents->/** @scrutinizer ignore-call */ 
1096
                                     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

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

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

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

Loading history...
2781
            // Remove NULLs
2782
            $results = array_filter($results, function ($v) {
2783
                return !is_null($v);
2784
            });
2785
            // If there are any non-NULL responses, then return the lowest one of them.
2786
            // If any explicitly deny the permission, then we don't get access
2787
            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...
2788
                return min($results);
2789
            }
2790
        }
2791
        return null;
2792
    }
2793
2794
    /**
2795
     * @param Member $member
2796
     * @return boolean
2797
     */
2798
    public function canView($member = null)
2799
    {
2800
        $extended = $this->extendedCan(__FUNCTION__, $member);
2801
        if ($extended !== null) {
2802
            return $extended;
2803
        }
2804
        return Permission::check('ADMIN', 'any', $member);
2805
    }
2806
2807
    /**
2808
     * @param Member $member
2809
     * @return boolean
2810
     */
2811
    public function canEdit($member = null)
2812
    {
2813
        $extended = $this->extendedCan(__FUNCTION__, $member);
2814
        if ($extended !== null) {
2815
            return $extended;
2816
        }
2817
        return Permission::check('ADMIN', 'any', $member);
2818
    }
2819
2820
    /**
2821
     * @param Member $member
2822
     * @return boolean
2823
     */
2824
    public function canDelete($member = null)
2825
    {
2826
        $extended = $this->extendedCan(__FUNCTION__, $member);
2827
        if ($extended !== null) {
2828
            return $extended;
2829
        }
2830
        return Permission::check('ADMIN', 'any', $member);
2831
    }
2832
2833
    /**
2834
     * @param Member $member
2835
     * @param array $context Additional context-specific data which might
2836
     * affect whether (or where) this object could be created.
2837
     * @return boolean
2838
     */
2839
    public function canCreate($member = null, $context = array())
2840
    {
2841
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2842
        if ($extended !== null) {
2843
            return $extended;
2844
        }
2845
        return Permission::check('ADMIN', 'any', $member);
2846
    }
2847
2848
    /**
2849
     * Debugging used by Debug::show()
2850
     *
2851
     * @return string HTML data representing this object
2852
     */
2853
    public function debug()
2854
    {
2855
        $class = static::class;
2856
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2857
        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...
2858
            foreach ($this->record as $fieldName => $fieldVal) {
2859
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2860
            }
2861
        }
2862
        $val .= "</ul>\n";
2863
        return $val;
2864
    }
2865
2866
    /**
2867
     * Return the DBField object that represents the given field.
2868
     * This works similarly to obj() with 2 key differences:
2869
     *   - it still returns an object even when the field has no value.
2870
     *   - it only matches fields and not methods
2871
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2872
     *
2873
     * @param string $fieldName Name of the field
2874
     * @return DBField The field as a DBField object
2875
     */
2876
    public function dbObject($fieldName)
2877
    {
2878
        // Check for field in DB
2879
        $schema = static::getSchema();
2880
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2881
        if (!$helper) {
2882
            return null;
2883
        }
2884
2885
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2886
            $tableClass = $this->record[$fieldName . '_Lazy'];
2887
            $this->loadLazyFields($tableClass);
2888
        }
2889
2890
        $value = isset($this->record[$fieldName])
2891
            ? $this->record[$fieldName]
2892
            : null;
2893
2894
        // If we have a DBField object in $this->record, then return that
2895
        if ($value instanceof DBField) {
2896
            return $value;
2897
        }
2898
2899
        list($class, $spec) = explode('.', $helper);
2900
        /** @var DBField $obj */
2901
        $table = $schema->tableName($class);
2902
        $obj = Injector::inst()->create($spec, $fieldName);
2903
        $obj->setTable($table);
2904
        $obj->setValue($value, $this, false);
2905
        return $obj;
2906
    }
2907
2908
    /**
2909
     * Traverses to a DBField referenced by relationships between data objects.
2910
     *
2911
     * The path to the related field is specified with dot separated syntax
2912
     * (eg: Parent.Child.Child.FieldName).
2913
     *
2914
     * If a relation is blank, this will return null instead.
2915
     * If a relation name is invalid (e.g. non-relation on a parent) this
2916
     * can throw a LogicException.
2917
     *
2918
     * @param string $fieldPath List of paths on this object. All items in this path
2919
     * must be ViewableData implementors
2920
     *
2921
     * @return mixed DBField of the field on the object or a DataList instance.
2922
     * @throws LogicException If accessing invalid relations
2923
     */
2924
    public function relObject($fieldPath)
2925
    {
2926
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
2927
        $component = $this;
2928
2929
        // Parse all relations
2930
        foreach (explode('.', $fieldPath) as $relation) {
2931
            if (!$component) {
2932
                return null;
2933
            }
2934
2935
            // Inspect relation type
2936
            if (ClassInfo::hasMethod($component, $relation)) {
2937
                $component = $component->$relation();
2938
            } elseif ($component instanceof Relation || $component instanceof DataList) {
2939
                // $relation could either be a field (aggregate), or another relation
2940
                $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

2940
                $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...
2941
                $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

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

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

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

3298
        /** @scrutinizer ignore-call */ 
3299
        $extensions = self::database_extensions(static::class);
Loading history...
3299
3300
        if (empty($table)) {
3301
            throw new LogicException(
3302
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3303
            );
3304
        }
3305
3306
        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...
3307
            $hasAutoIncPK = get_parent_class($this) === self::class;
3308
            DB::require_table(
3309
                $table,
3310
                $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

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

3311
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3312
                $hasAutoIncPK,
3313
                $this->config()->get('create_table_options'),
3314
                $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

3314
                /** @scrutinizer ignore-type */ $extensions
Loading history...
3315
            );
3316
        } else {
3317
            DB::dont_require_table($table);
3318
        }
3319
3320
        // Build any child tables for many_many items
3321
        if ($manyMany = $this->uninherited('many_many')) {
3322
            $extras = $this->uninherited('many_many_extraFields');
3323
            foreach ($manyMany as $component => $spec) {
3324
                // Get many_many spec
3325
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3326
                $parentField = $manyManyComponent['parentField'];
3327
                $childField = $manyManyComponent['childField'];
3328
                $tableOrClass = $manyManyComponent['join'];
3329
3330
                // Skip if backed by actual class
3331
                if (class_exists($tableOrClass)) {
3332
                    continue;
3333
                }
3334
3335
                // Build fields
3336
                $manymanyFields = array(
3337
                    $parentField => "Int",
3338
                    $childField => "Int",
3339
                );
3340
                if (isset($extras[$component])) {
3341
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3342
                }
3343
3344
                // Build index list
3345
                $manymanyIndexes = [
3346
                    $parentField => [
3347
                        'type' => 'index',
3348
                        'name' => $parentField,
3349
                        'columns' => [$parentField],
3350
                    ],
3351
                    $childField => [
3352
                        'type' => 'index',
3353
                        'name' => $childField,
3354
                        'columns' => [$childField],
3355
                    ],
3356
                ];
3357
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Bug introduced by
$manymanyFields of type string[]|array is incompatible with the type string expected by parameter $fieldSchema of SilverStripe\ORM\DB::require_table(). ( Ignorable by Annotation )

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

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

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

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