Passed
Push — 4.1 ( 802261...1362b2 )
by
unknown
15:08 queued 06:46
created

DataObject::writeManipulation()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 33
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

Loading history...
2554
                foreach ($fields as $name => $level) {
2555
                    if ($level < $changeLevel) {
2556
                        unset($fields[$name]);
2557
                    }
2558
                }
2559
            }
2560
        }
2561
2562
        if ($fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2563
            foreach ($fields as $name => $level) {
2564
                $changedFields[$name] = array(
2565
                    'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2566
                    'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2567
                    'level' => $level
2568
                );
2569
            }
2570
        }
2571
2572
        return $changedFields;
2573
    }
2574
2575
    /**
2576
     * Uses {@link getChangedFields()} to determine if fields have been changed
2577
     * since loading them from the database.
2578
     *
2579
     * @param string $fieldName Name of the database field to check, will check for any if not given
2580
     * @param int $changeLevel See {@link getChangedFields()}
2581
     * @return boolean
2582
     */
2583
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2584
    {
2585
        $fields = $fieldName ? array($fieldName) : true;
2586
        $changed = $this->getChangedFields($fields, $changeLevel);
2587
        if (!isset($fieldName)) {
2588
            return !empty($changed);
2589
        } else {
2590
            return array_key_exists($fieldName, $changed);
2591
        }
2592
    }
2593
2594
    /**
2595
     * Set the value of the field
2596
     * Called by {@link __set()} and any setFieldName() methods you might create.
2597
     *
2598
     * @param string $fieldName Name of the field
2599
     * @param mixed $val New field value
2600
     * @return $this
2601
     */
2602
    public function setField($fieldName, $val)
2603
    {
2604
        $this->objCacheClear();
2605
        //if it's a has_one component, destroy the cache
2606
        if (substr($fieldName, -2) == 'ID') {
2607
            unset($this->components[substr($fieldName, 0, -2)]);
2608
        }
2609
2610
        // If we've just lazy-loaded the column, then we need to populate the $original array
2611
        if (isset($this->record[$fieldName . '_Lazy'])) {
2612
            $tableClass = $this->record[$fieldName . '_Lazy'];
2613
            $this->loadLazyFields($tableClass);
2614
        }
2615
2616
        // Support component assignent via field setter
2617
        $schema = static::getSchema();
2618
        if ($schema->unaryComponent(static::class, $fieldName)) {
2619
            unset($this->components[$fieldName]);
2620
            // Assign component directly
2621
            if (is_null($val) || $val instanceof DataObject) {
2622
                return $this->setComponent($fieldName, $val);
2623
            }
2624
            // Assign by ID instead of object
2625
            if (is_numeric($val)) {
2626
                $fieldName .= 'ID';
2627
            }
2628
        }
2629
2630
        // Situation 1: Passing an DBField
2631
        if ($val instanceof DBField) {
2632
            $val->setName($fieldName);
2633
            $val->saveInto($this);
2634
2635
            // Situation 1a: Composite fields should remain bound in case they are
2636
            // later referenced to update the parent dataobject
2637
            if ($val instanceof DBComposite) {
2638
                $val->bindTo($this);
2639
                $this->record[$fieldName] = $val;
2640
            }
2641
        // Situation 2: Passing a literal or non-DBField object
2642
        } else {
2643
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2644
            if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) {
2645
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2646
            }
2647
2648
            if (!empty($val) && !is_scalar($val)) {
2649
                $dbField = $this->dbObject($fieldName);
2650
                if ($dbField && $dbField->scalarValueOnly()) {
2651
                    throw new InvalidArgumentException(
2652
                        sprintf(
2653
                            'DataObject::setField: %s only accepts scalars',
2654
                            $fieldName
2655
                        )
2656
                    );
2657
                }
2658
            }
2659
2660
            // if a field is not existing or has strictly changed
2661
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2662
                // TODO Add check for php-level defaults which are not set in the db
2663
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2664
                // At the very least, the type has changed
2665
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2666
2667
                if ((!isset($this->record[$fieldName]) && $val)
2668
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2669
                ) {
2670
                    // Value has changed as well, not just the type
2671
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2672
                }
2673
2674
                // Value is always saved back when strict check succeeds.
2675
                $this->record[$fieldName] = $val;
2676
            }
2677
        }
2678
        return $this;
2679
    }
2680
2681
    /**
2682
     * Set the value of the field, using a casting object.
2683
     * This is useful when you aren't sure that a date is in SQL format, for example.
2684
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2685
     * can be saved into the Image table.
2686
     *
2687
     * @param string $fieldName Name of the field
2688
     * @param mixed $value New field value
2689
     * @return $this
2690
     */
2691
    public function setCastedField($fieldName, $value)
2692
    {
2693
        if (!$fieldName) {
2694
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2695
        }
2696
        $fieldObj = $this->dbObject($fieldName);
2697
        if ($fieldObj) {
0 ignored issues
show
introduced by
$fieldObj is of type SilverStripe\ORM\FieldType\DBField, thus it always evaluated to true.
Loading history...
2698
            $fieldObj->setValue($value);
2699
            $fieldObj->saveInto($this);
2700
        } else {
2701
            $this->$fieldName = $value;
2702
        }
2703
        return $this;
2704
    }
2705
2706
    /**
2707
     * {@inheritdoc}
2708
     */
2709
    public function castingHelper($field)
2710
    {
2711
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2712
        if ($fieldSpec) {
2713
            return $fieldSpec;
2714
        }
2715
2716
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2717
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2718
        $queryParams = $this->getSourceQueryParams();
2719
        if (!empty($queryParams['Component.ExtraFields'])) {
2720
            $extraFields = $queryParams['Component.ExtraFields'];
2721
2722
            if (isset($extraFields[$field])) {
2723
                return $extraFields[$field];
2724
            }
2725
        }
2726
2727
        return parent::castingHelper($field);
2728
    }
2729
2730
    /**
2731
     * Returns true if the given field exists in a database column on any of
2732
     * the objects tables and optionally look up a dynamic getter with
2733
     * get<fieldName>().
2734
     *
2735
     * @param string $field Name of the field
2736
     * @return boolean True if the given field exists
2737
     */
2738
    public function hasField($field)
2739
    {
2740
        $schema = static::getSchema();
2741
        return (
2742
            array_key_exists($field, $this->record)
2743
            || array_key_exists($field, $this->components)
2744
            || $schema->fieldSpec(static::class, $field)
2745
            || $schema->unaryComponent(static::class, $field)
2746
            || $this->hasMethod("get{$field}")
2747
        );
2748
    }
2749
2750
    /**
2751
     * Returns true if the given field exists as a database column
2752
     *
2753
     * @param string $field Name of the field
2754
     *
2755
     * @return boolean
2756
     */
2757
    public function hasDatabaseField($field)
2758
    {
2759
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2760
        return !empty($spec);
2761
    }
2762
2763
    /**
2764
     * Returns true if the member is allowed to do the given action.
2765
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2766
     *
2767
     * @param string $perm The permission to be checked, such as 'View'.
2768
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2769
     * in user.
2770
     * @param array $context Additional $context to pass to extendedCan()
2771
     *
2772
     * @return boolean True if the the member is allowed to do the given action
2773
     */
2774
    public function can($perm, $member = null, $context = array())
2775
    {
2776
        if (!$member) {
2777
            $member = Security::getCurrentUser();
2778
        }
2779
2780
        if ($member && Permission::checkMember($member, "ADMIN")) {
2781
            return true;
2782
        }
2783
2784
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2785
            $method = 'can' . ucfirst($perm);
2786
            return $this->$method($member);
2787
        }
2788
2789
        $results = $this->extendedCan('can', $member);
2790
        if (isset($results)) {
2791
            return $results;
2792
        }
2793
2794
        return ($member && Permission::checkMember($member, $perm));
2795
    }
2796
2797
    /**
2798
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2799
     * expected to return one of three values:
2800
     *
2801
     *  - false: Disallow this permission, regardless of what other extensions say
2802
     *  - true: Allow this permission, as long as no other extensions return false
2803
     *  - NULL: Don't affect the outcome
2804
     *
2805
     * This method itself returns a tri-state value, and is designed to be used like this:
2806
     *
2807
     * <code>
2808
     * $extended = $this->extendedCan('canDoSomething', $member);
2809
     * if($extended !== null) return $extended;
2810
     * else return $normalValue;
2811
     * </code>
2812
     *
2813
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2814
     * @param Member|int $member
2815
     * @param array $context Optional context
2816
     * @return boolean|null
2817
     */
2818
    public function extendedCan($methodName, $member, $context = array())
2819
    {
2820
        $results = $this->extend($methodName, $member, $context);
2821
        if ($results && is_array($results)) {
2822
            // Remove NULLs
2823
            $results = array_filter($results, function ($v) {
2824
                return !is_null($v);
2825
            });
2826
            // If there are any non-NULL responses, then return the lowest one of them.
2827
            // If any explicitly deny the permission, then we don't get access
2828
            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...
2829
                return min($results);
2830
            }
2831
        }
2832
        return null;
2833
    }
2834
2835
    /**
2836
     * @param Member $member
2837
     * @return boolean
2838
     */
2839
    public function canView($member = null)
2840
    {
2841
        $extended = $this->extendedCan(__FUNCTION__, $member);
2842
        if ($extended !== null) {
2843
            return $extended;
2844
        }
2845
        return Permission::check('ADMIN', 'any', $member);
2846
    }
2847
2848
    /**
2849
     * @param Member $member
2850
     * @return boolean
2851
     */
2852
    public function canEdit($member = null)
2853
    {
2854
        $extended = $this->extendedCan(__FUNCTION__, $member);
2855
        if ($extended !== null) {
2856
            return $extended;
2857
        }
2858
        return Permission::check('ADMIN', 'any', $member);
2859
    }
2860
2861
    /**
2862
     * @param Member $member
2863
     * @return boolean
2864
     */
2865
    public function canDelete($member = null)
2866
    {
2867
        $extended = $this->extendedCan(__FUNCTION__, $member);
2868
        if ($extended !== null) {
2869
            return $extended;
2870
        }
2871
        return Permission::check('ADMIN', 'any', $member);
2872
    }
2873
2874
    /**
2875
     * @param Member $member
2876
     * @param array $context Additional context-specific data which might
2877
     * affect whether (or where) this object could be created.
2878
     * @return boolean
2879
     */
2880
    public function canCreate($member = null, $context = array())
2881
    {
2882
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
2883
        if ($extended !== null) {
2884
            return $extended;
2885
        }
2886
        return Permission::check('ADMIN', 'any', $member);
2887
    }
2888
2889
    /**
2890
     * Debugging used by Debug::show()
2891
     *
2892
     * @return string HTML data representing this object
2893
     */
2894
    public function debug()
2895
    {
2896
        $class = static::class;
2897
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2898
        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...
2899
            foreach ($this->record as $fieldName => $fieldVal) {
2900
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2901
            }
2902
        }
2903
        $val .= "</ul>\n";
2904
        return $val;
2905
    }
2906
2907
    /**
2908
     * Return the DBField object that represents the given field.
2909
     * This works similarly to obj() with 2 key differences:
2910
     *   - it still returns an object even when the field has no value.
2911
     *   - it only matches fields and not methods
2912
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2913
     *
2914
     * @param string $fieldName Name of the field
2915
     * @return DBField The field as a DBField object
2916
     */
2917
    public function dbObject($fieldName)
2918
    {
2919
        // Check for field in DB
2920
        $schema = static::getSchema();
2921
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2922
        if (!$helper) {
2923
            return null;
2924
        }
2925
2926
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2927
            $tableClass = $this->record[$fieldName . '_Lazy'];
2928
            $this->loadLazyFields($tableClass);
2929
        }
2930
2931
        $value = isset($this->record[$fieldName])
2932
            ? $this->record[$fieldName]
2933
            : null;
2934
2935
        // If we have a DBField object in $this->record, then return that
2936
        if ($value instanceof DBField) {
2937
            return $value;
2938
        }
2939
2940
        list($class, $spec) = explode('.', $helper);
2941
        /** @var DBField $obj */
2942
        $table = $schema->tableName($class);
2943
        $obj = Injector::inst()->create($spec, $fieldName);
2944
        $obj->setTable($table);
2945
        $obj->setValue($value, $this, false);
2946
        return $obj;
2947
    }
2948
2949
    /**
2950
     * Traverses to a DBField referenced by relationships between data objects.
2951
     *
2952
     * The path to the related field is specified with dot separated syntax
2953
     * (eg: Parent.Child.Child.FieldName).
2954
     *
2955
     * If a relation is blank, this will return null instead.
2956
     * If a relation name is invalid (e.g. non-relation on a parent) this
2957
     * can throw a LogicException.
2958
     *
2959
     * @param string $fieldPath List of paths on this object. All items in this path
2960
     * must be ViewableData implementors
2961
     *
2962
     * @return mixed DBField of the field on the object or a DataList instance.
2963
     * @throws LogicException If accessing invalid relations
2964
     */
2965
    public function relObject($fieldPath)
2966
    {
2967
        $object = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $object is dead and can be removed.
Loading history...
2968
        $component = $this;
2969
2970
        // Parse all relations
2971
        foreach (explode('.', $fieldPath) as $relation) {
2972
            if (!$component) {
2973
                return null;
2974
            }
2975
2976
            // Inspect relation type
2977
            if (ClassInfo::hasMethod($component, $relation)) {
2978
                $component = $component->$relation();
2979
            } elseif ($component instanceof Relation || $component instanceof DataList) {
2980
                // $relation could either be a field (aggregate), or another relation
2981
                $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

2981
                $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...
2982
                $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

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

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

3103
            $limitArguments = explode(',', /** @scrutinizer ignore-type */ $limit);
Loading history...
3104
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
3105
        } elseif ($limit) {
3106
            $result = $result->limit($limit);
3107
        }
3108
3109
        return $result;
3110
    }
3111
3112
3113
    /**
3114
     * Return the first item matching the given query.
3115
     * All calls to get_one() are cached.
3116
     *
3117
     * @param string $callerClass The class of objects to be returned
3118
     * @param string|array $filter A filter to be inserted into the WHERE clause.
3119
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
3120
     * @param boolean $cache Use caching
3121
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
3122
     *
3123
     * @return DataObject|null The first item matching the query
3124
     */
3125
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
3126
    {
3127
        $SNG = singleton($callerClass);
3128
3129
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
3130
        $cacheKey = md5(serialize($cacheComponents));
3131
3132
        $item = null;
3133
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
3134
            $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...
3135
            $item = $dl->first();
3136
3137
            if ($cache) {
3138
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
3139
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
3140
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
3141
                }
3142
            }
3143
        }
3144
3145
        if ($cache) {
3146
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
3147
        } else {
3148
            return $item;
3149
        }
3150
    }
3151
3152
    /**
3153
     * Flush the cached results for all relations (has_one, has_many, many_many)
3154
     * Also clears any cached aggregate data.
3155
     *
3156
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
3157
     *                            When false will just clear session-local cached data
3158
     * @return DataObject $this
3159
     */
3160
    public function flushCache($persistent = true)
3161
    {
3162
        if (static::class == self::class) {
0 ignored issues
show
introduced by
The condition static::class == self::class is always true.
Loading history...
3163
            self::$_cache_get_one = array();
3164
            return $this;
3165
        }
3166
3167
        $classes = ClassInfo::ancestry(static::class);
3168
        foreach ($classes as $class) {
3169
            if (isset(self::$_cache_get_one[$class])) {
3170
                unset(self::$_cache_get_one[$class]);
3171
            }
3172
        }
3173
3174
        $this->extend('flushCache');
3175
3176
        $this->components = array();
3177
        return $this;
3178
    }
3179
3180
    /**
3181
     * Flush the get_one global cache and destroy associated objects.
3182
     */
3183
    public static function flush_and_destroy_cache()
3184
    {
3185
        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...
3186
            foreach (self::$_cache_get_one as $class => $items) {
3187
                if (is_array($items)) {
3188
                    foreach ($items as $item) {
3189
                        if ($item) {
3190
                            $item->destroy();
3191
                        }
3192
                    }
3193
                }
3194
            }
3195
        }
3196
        self::$_cache_get_one = array();
3197
    }
3198
3199
    /**
3200
     * Reset all global caches associated with DataObject.
3201
     */
3202
    public static function reset()
3203
    {
3204
        // @todo Decouple these
3205
        DBClassName::clear_classname_cache();
3206
        ClassInfo::reset_db_cache();
3207
        static::getSchema()->reset();
3208
        self::$_cache_get_one = array();
3209
        self::$_cache_field_labels = array();
3210
    }
3211
3212
    /**
3213
     * Return the given element, searching by ID
3214
     *
3215
     * @param string $callerClass The class of the object to be returned
3216
     * @param int $id The id of the element
3217
     * @param boolean $cache See {@link get_one()}
3218
     *
3219
     * @return DataObject The element
3220
     */
3221
    public static function get_by_id($callerClass, $id, $cache = true)
3222
    {
3223
        if (!is_numeric($id)) {
0 ignored issues
show
introduced by
The condition is_numeric($id) is always true.
Loading history...
3224
            user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3225
        }
3226
3227
        // Pass to get_one
3228
        $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
3229
        return DataObject::get_one($callerClass, array($column => $id), $cache);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3230
    }
3231
3232
    /**
3233
     * Get the name of the base table for this object
3234
     *
3235
     * @return string
3236
     */
3237
    public function baseTable()
3238
    {
3239
        return static::getSchema()->baseDataTable($this);
3240
    }
3241
3242
    /**
3243
     * Get the base class for this object
3244
     *
3245
     * @return string
3246
     */
3247
    public function baseClass()
3248
    {
3249
        return static::getSchema()->baseDataClass($this);
3250
    }
3251
3252
    /**
3253
     * @var array Parameters used in the query that built this object.
3254
     * This can be used by decorators (e.g. lazy loading) to
3255
     * run additional queries using the same context.
3256
     */
3257
    protected $sourceQueryParams;
3258
3259
    /**
3260
     * @see $sourceQueryParams
3261
     * @return array
3262
     */
3263
    public function getSourceQueryParams()
3264
    {
3265
        return $this->sourceQueryParams;
3266
    }
3267
3268
    /**
3269
     * Get list of parameters that should be inherited to relations on this object
3270
     *
3271
     * @return array
3272
     */
3273
    public function getInheritableQueryParams()
3274
    {
3275
        $params = $this->getSourceQueryParams();
3276
        $this->extend('updateInheritableQueryParams', $params);
3277
        return $params;
3278
    }
3279
3280
    /**
3281
     * @see $sourceQueryParams
3282
     * @param array
3283
     */
3284
    public function setSourceQueryParams($array)
3285
    {
3286
        $this->sourceQueryParams = $array;
3287
    }
3288
3289
    /**
3290
     * @see $sourceQueryParams
3291
     * @param string $key
3292
     * @param string $value
3293
     */
3294
    public function setSourceQueryParam($key, $value)
3295
    {
3296
        $this->sourceQueryParams[$key] = $value;
3297
    }
3298
3299
    /**
3300
     * @see $sourceQueryParams
3301
     * @param string $key
3302
     * @return string
3303
     */
3304
    public function getSourceQueryParam($key)
3305
    {
3306
        if (isset($this->sourceQueryParams[$key])) {
3307
            return $this->sourceQueryParams[$key];
3308
        }
3309
        return null;
3310
    }
3311
3312
    //-------------------------------------------------------------------------------------------//
3313
3314
    /**
3315
     * Check the database schema and update it as necessary.
3316
     *
3317
     * @uses DataExtension->augmentDatabase()
3318
     */
3319
    public function requireTable()
3320
    {
3321
        // Only build the table if we've actually got fields
3322
        $schema = static::getSchema();
3323
        $table = $schema->tableName(static::class);
3324
        $fields = $schema->databaseFields(static::class, false);
3325
        $indexes = $schema->databaseIndexes(static::class, false);
3326
        $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

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

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

3339
                /** @scrutinizer ignore-type */ $indexes,
Loading history...
3340
                $hasAutoIncPK,
3341
                $this->config()->get('create_table_options'),
3342
                $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

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

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

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

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

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