Completed
Pull Request — master (#6930)
by Daniel
09:37
created

DataObject::databaseIndexes()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 13
nc 8
nop 0
dl 0
loc 26
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Core\Resettable;
9
use SilverStripe\Dev\Deprecation;
10
use SilverStripe\Dev\Debug;
11
use SilverStripe\Control\HTTP;
12
use SilverStripe\Forms\FieldList;
13
use SilverStripe\Forms\FormField;
14
use SilverStripe\Forms\FormScaffolder;
15
use SilverStripe\i18n\i18n;
16
use SilverStripe\i18n\i18nEntityProvider;
17
use SilverStripe\ORM\Filters\SearchFilter;
18
use SilverStripe\ORM\Search\SearchContext;
19
use SilverStripe\ORM\Queries\SQLInsert;
20
use SilverStripe\ORM\Queries\SQLDelete;
21
use SilverStripe\ORM\FieldType\DBField;
22
use SilverStripe\ORM\FieldType\DBDatetime;
23
use SilverStripe\ORM\FieldType\DBComposite;
24
use SilverStripe\ORM\FieldType\DBClassName;
25
use SilverStripe\Security\Member;
26
use SilverStripe\Security\Permission;
27
use SilverStripe\View\ViewableData;
28
use LogicException;
29
use InvalidArgumentException;
30
use BadMethodCallException;
31
use Exception;
32
use stdClass;
33
34
/**
35
 * A single database record & abstract class for the data-access-model.
36
 *
37
 * <h2>Extensions</h2>
38
 *
39
 * See {@link Extension} and {@link DataExtension}.
40
 *
41
 * <h2>Permission Control</h2>
42
 *
43
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
44
 * strings which can be selected on a group-by-group basis.
45
 *
46
 * <code>
47
 * class Article extends DataObject implements PermissionProvider {
48
 *  static $api_access = true;
49
 *
50
 *  function canView($member = false) {
51
 *    return Permission::check('ARTICLE_VIEW');
52
 *  }
53
 *  function canEdit($member = false) {
54
 *    return Permission::check('ARTICLE_EDIT');
55
 *  }
56
 *  function canDelete() {
57
 *    return Permission::check('ARTICLE_DELETE');
58
 *  }
59
 *  function canCreate() {
60
 *    return Permission::check('ARTICLE_CREATE');
61
 *  }
62
 *  function providePermissions() {
63
 *    return array(
64
 *      'ARTICLE_VIEW' => 'Read an article object',
65
 *      'ARTICLE_EDIT' => 'Edit an article object',
66
 *      'ARTICLE_DELETE' => 'Delete an article object',
67
 *      'ARTICLE_CREATE' => 'Create an article object',
68
 *    );
69
 *  }
70
 * }
71
 * </code>
72
 *
73
 * Object-level access control by {@link Group} membership:
74
 * <code>
75
 * class Article extends DataObject {
76
 *   static $api_access = true;
77
 *
78
 *   function canView($member = false) {
79
 *     if(!$member) $member = Member::currentUser();
80
 *     return $member->inGroup('Subscribers');
81
 *   }
82
 *   function canEdit($member = false) {
83
 *     if(!$member) $member = Member::currentUser();
84
 *     return $member->inGroup('Editors');
85
 *   }
86
 *
87
 *   // ...
88
 * }
89
 * </code>
90
 *
91
 * If any public method on this class is prefixed with an underscore,
92
 * the results are cached in memory through {@link cachedCall()}.
93
 *
94
 *
95
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
96
 *  and defineMethods()
97
 *
98
 * @property integer ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
99
 * @property string ClassName Class name of the DataObject
100
 * @property string LastEdited Date and time of DataObject's last modification.
101
 * @property string Created Date and time of DataObject creation.
102
 */
103
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable
104
{
105
106
    /**
107
     * Human-readable singular name.
108
     * @var string
109
     * @config
110
     */
111
    private static $singular_name = null;
112
113
    /**
114
     * Human-readable plural name
115
     * @var string
116
     * @config
117
     */
118
    private static $plural_name = null;
119
120
    /**
121
     * Allow API access to this object?
122
     * @todo Define the options that can be set here
123
     * @config
124
     */
125
    private static $api_access = false;
126
127
    /**
128
     * Allows specification of a default value for the ClassName field.
129
     * Configure this value only in subclasses of DataObject.
130
     *
131
     * @config
132
     * @var string
133
     */
134
    private static $default_classname = null;
135
136
    /**
137
     * True if this DataObject has been destroyed.
138
     * @var boolean
139
     */
140
    public $destroyed = false;
141
142
    /**
143
     * The DataModel from this this object comes
144
     */
145
    protected $model;
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
     * Get schema object
273
     *
274
     * @return DataObjectSchema
275
     */
276
    public static function getSchema()
277
    {
278
        return Injector::inst()->get(DataObjectSchema::class);
279
    }
280
281
    /**
282
     * Construct a new DataObject.
283
     *
284
285
     * @param array|null $record Used internally for rehydrating an object from database content.
286
     *                           Bypasses setters on this class, and hence should not be used
287
     *                           for populating data on new records.
288
     * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
289
     *                             Singletons don't have their defaults set.
290
     * @param DataModel $model
291
     * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
292
     */
293
    public function __construct($record = null, $isSingleton = false, $model = null, $queryParams = array())
294
    {
295
        parent::__construct();
296
297
        // Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
298
        $this->setSourceQueryParams($queryParams);
299
300
        // Set the fields data.
301
        if (!$record) {
302
            $record = array(
303
                'ID' => 0,
304
                'ClassName' => static::class,
305
                'RecordClassName' => static::class
306
            );
307
        }
308
309
        if ($record instanceof stdClass) {
310
            $record = (array)$record;
311
        }
312
313
        if (!is_array($record)) {
314
            if (is_object($record)) {
315
                $passed = "an object of type '".get_class($record)."'";
316
            } else {
317
                $passed = "The value '$record'";
318
            }
319
320
            user_error(
321
                "DataObject::__construct passed $passed.  It's supposed to be passed an array,"
322
                . " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
323
                E_USER_WARNING
324
            );
325
            $record = null;
326
        }
327
328
        // Set $this->record to $record, but ignore NULLs
329
        $this->record = array();
330
        foreach ($record as $k => $v) {
0 ignored issues
show
Bug introduced by
The expression $record of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
331
            // Ensure that ID is stored as a number and not a string
332
            // To do: this kind of clean-up should be done on all numeric fields, in some relatively
333
            // performant manner
334
            if ($v !== null) {
335
                if ($k == 'ID' && is_numeric($v)) {
336
                    $this->record[$k] = (int)$v;
337
                } else {
338
                    $this->record[$k] = $v;
339
                }
340
            }
341
        }
342
343
        // Identify fields that should be lazy loaded, but only on existing records
344
        if (!empty($record['ID'])) {
345
            // Get all field specs scoped to class for later lazy loading
346
            $fields = static::getSchema()->fieldSpecs(
347
                static::class,
348
                DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
349
            );
350
            foreach ($fields as $field => $fieldSpec) {
351
                $fieldClass = strtok($fieldSpec, ".");
352
                if (!array_key_exists($field, $record)) {
353
                    $this->record[$field.'_Lazy'] = $fieldClass;
354
                }
355
            }
356
        }
357
358
        $this->original = $this->record;
359
360
        // Keep track of the modification date of all the data sourced to make this page
361
        // From this we create a Last-Modified HTTP header
362
        if (isset($record['LastEdited'])) {
363
            HTTP::register_modification_date($record['LastEdited']);
364
        }
365
366
        // this must be called before populateDefaults(), as field getters on a DataObject
367
        // may call getComponent() and others, which rely on $this->model being set.
368
        $this->model = $model ? $model : DataModel::inst();
369
370
        // Must be called after parent constructor
371
        if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
372
            $this->populateDefaults();
373
        }
374
375
        // prevent populateDefaults() and setField() from marking overwritten defaults as changed
376
        $this->changed = array();
377
    }
378
379
    /**
380
     * Set the DataModel
381
     * @param DataModel $model
382
     * @return DataObject $this
383
     */
384
    public function setDataModel(DataModel $model)
385
    {
386
        $this->model = $model;
387
        return $this;
388
    }
389
390
    /**
391
     * Destroy all of this objects dependant objects and local caches.
392
     * You'll need to call this to get the memory of an object that has components or extensions freed.
393
     */
394
    public function destroy()
395
    {
396
        //$this->destroyed = true;
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...
397
        gc_collect_cycles();
398
        $this->flushCache(false);
399
    }
400
401
    /**
402
     * Create a duplicate of this node. Can duplicate many_many relations
403
     *
404
     * @param bool $doWrite Perform a write() operation before returning the object.
405
     * If this is true, it will create the duplicate in the database.
406
     * @param bool|string $manyMany Which many-many to duplicate. Set to true to duplicate all, false to duplicate none.
407
     * Alternatively set to the string of the relation config to duplicate
408
     * (supports 'many_many', or 'belongs_many_many')
409
     * @return static A duplicate of this node. The exact type will be the type of this node.
410
     */
411
    public function duplicate($doWrite = true, $manyMany = 'many_many')
412
    {
413
        $map = $this->toMap();
414
        unset($map['Created']);
415
        /** @var static $clone */
416
        $clone = Injector::inst()->create(static::class, $map, false, $this->model);
417
        $clone->ID = 0;
418
419
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $manyMany);
420
        if ($manyMany) {
421
            $this->duplicateManyManyRelations($this, $clone, $manyMany);
422
        }
423
        if ($doWrite) {
424
            $clone->write();
425
        }
426
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $manyMany);
427
428
        return $clone;
429
    }
430
431
    /**
432
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
433
     *
434
     * @param DataObject $sourceObject the source object to duplicate from
435
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
436
     * @param bool|string $filter
437
     */
438
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
439
    {
440
        // Get list of relations to duplicate
441
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
442
            $relations = $sourceObject->config()->get($filter);
443
        } elseif ($filter === true) {
444
            $relations = $sourceObject->manyMany();
445
        } else {
446
            throw new InvalidArgumentException("Invalid many_many duplication filter");
447
        }
448
        foreach ($relations as $manyManyName => $type) {
449
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
450
        }
451
    }
452
453
    /**
454
     * Duplicates a single many_many relation from one object to another
455
     *
456
     * @param DataObject $sourceObject
457
     * @param DataObject $destinationObject
458
     * @param string $manyManyName
459
     */
460
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName)
461
    {
462
        // Ensure this component exists on the destination side as well
463
        if (!static::getSchema()->manyManyComponent(get_class($destinationObject), $manyManyName)) {
464
            return;
465
        }
466
467
        // Copy all components from source to destination
468
        $source = $sourceObject->getManyManyComponents($manyManyName);
469
        $dest = $destinationObject->getManyManyComponents($manyManyName);
470
        foreach ($source as $item) {
471
            $dest->add($item);
472
        }
473
    }
474
475
    /**
476
     * Return obsolete class name, if this is no longer a valid class
477
     *
478
     * @return string
479
     */
480
    public function getObsoleteClassName()
481
    {
482
        $className = $this->getField("ClassName");
483
        if (!ClassInfo::exists($className)) {
484
            return $className;
485
        }
486
        return null;
487
    }
488
489
    /**
490
     * Gets name of this class
491
     *
492
     * @return string
493
     */
494
    public function getClassName()
495
    {
496
        $className = $this->getField("ClassName");
497
        if (!ClassInfo::exists($className)) {
498
            return static::class;
499
        }
500
        return $className;
501
    }
502
503
    /**
504
     * Set the ClassName attribute. {@link $class} is also updated.
505
     * Warning: This will produce an inconsistent record, as the object
506
     * instance will not automatically switch to the new subclass.
507
     * Please use {@link newClassInstance()} for this purpose,
508
     * or destroy and reinstanciate the record.
509
     *
510
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
511
     * @return $this
512
     */
513
    public function setClassName($className)
514
    {
515
        $className = trim($className);
516
        if (!$className || !is_subclass_of($className, self::class)) {
517
            return $this;
518
        }
519
520
        $this->setField("ClassName", $className);
521
        $this->setField('RecordClassName', $className);
522
        return $this;
523
    }
524
525
    /**
526
     * Create a new instance of a different class from this object's record.
527
     * This is useful when dynamically changing the type of an instance. Specifically,
528
     * it ensures that the instance of the class is a match for the className of the
529
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
530
     * property manually before calling this method, as it will confuse change detection.
531
     *
532
     * If the new class is different to the original class, defaults are populated again
533
     * because this will only occur automatically on instantiation of a DataObject if
534
     * there is no record, or the record has no ID. In this case, we do have an ID but
535
     * we still need to repopulate the defaults.
536
     *
537
     * @param string $newClassName The name of the new class
538
     *
539
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
540
     */
541
    public function newClassInstance($newClassName)
542
    {
543
        if (!is_subclass_of($newClassName, self::class)) {
544
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
545
        }
546
547
        $originalClass = $this->ClassName;
548
549
        /** @var DataObject $newInstance */
550
        $newInstance = Injector::inst()->create($newClassName, $this->record, false, $this->model);
551
552
        // Modify ClassName
553
        if ($newClassName != $originalClass) {
554
            $newInstance->setClassName($newClassName);
555
            $newInstance->populateDefaults();
556
            $newInstance->forceChange();
557
        }
558
559
        return $newInstance;
560
    }
561
562
    /**
563
     * Adds methods from the extensions.
564
     * Called by Object::__construct() once per class.
565
     */
566
    public function defineMethods()
567
    {
568
        parent::defineMethods();
569
570
        if (static::class === self::class) {
571
             return;
572
        }
573
574
        // Set up accessors for joined items
575
        if ($manyMany = $this->manyMany()) {
576
            foreach ($manyMany as $relationship => $class) {
577
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
578
            }
579
        }
580
        if ($hasMany = $this->hasMany()) {
581
            foreach ($hasMany as $relationship => $class) {
0 ignored issues
show
Bug introduced by
The expression $hasMany of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
582
                $this->addWrapperMethod($relationship, 'getComponents');
583
            }
584
        }
585
        if ($hasOne = $this->hasOne()) {
586
            foreach ($hasOne as $relationship => $class) {
0 ignored issues
show
Bug introduced by
The expression $hasOne of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
587
                $this->addWrapperMethod($relationship, 'getComponent');
588
            }
589
        }
590
        if ($belongsTo = $this->belongsTo()) {
591
            foreach (array_keys($belongsTo) as $relationship) {
592
                $this->addWrapperMethod($relationship, 'getComponent');
593
            }
594
        }
595
    }
596
597
    /**
598
     * Returns true if this object "exists", i.e., has a sensible value.
599
     * The default behaviour for a DataObject is to return true if
600
     * the object exists in the database, you can override this in subclasses.
601
     *
602
     * @return boolean true if this object exists
603
     */
604
    public function exists()
605
    {
606
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
607
    }
608
609
    /**
610
     * Returns TRUE if all values (other than "ID") are
611
     * considered empty (by weak boolean comparison).
612
     *
613
     * @return boolean
614
     */
615
    public function isEmpty()
616
    {
617
        $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...
618
        foreach ($this->toMap() as $field => $value) {
619
            // only look at custom fields
620
            if (isset($fixed[$field])) {
621
                continue;
622
            }
623
624
            $dbObject = $this->dbObject($field);
625
            if (!$dbObject) {
626
                continue;
627
            }
628
            if ($dbObject->exists()) {
629
                return false;
630
            }
631
        }
632
        return true;
633
    }
634
635
    /**
636
     * Pluralise this item given a specific count.
637
     *
638
     * E.g. "0 Pages", "1 File", "3 Images"
639
     *
640
     * @param string $count
641
     * @return string
642
     */
643
    public function i18n_pluralise($count)
644
    {
645
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
646
        return i18n::_t(
647
            static::class.'.PLURALS',
648
            $default,
649
            [ 'count' => $count ]
650
        );
651
    }
652
653
    /**
654
     * Get the user friendly singular name of this DataObject.
655
     * If the name is not defined (by redefining $singular_name in the subclass),
656
     * this returns the class name.
657
     *
658
     * @return string User friendly singular name of this DataObject
659
     */
660
    public function singular_name()
661
    {
662
        $name = $this->stat('singular_name');
663
        if ($name) {
664
            return $name;
665
        }
666
        return ucwords(trim(strtolower(preg_replace(
667
            '/_?([A-Z])/',
668
            ' $1',
669
            ClassInfo::shortName($this)
670
        ))));
671
    }
672
673
    /**
674
     * Get the translated user friendly singular name of this DataObject
675
     * same as singular_name() but runs it through the translating function
676
     *
677
     * Translating string is in the form:
678
     *     $this->class.SINGULARNAME
679
     * Example:
680
     *     Page.SINGULARNAME
681
     *
682
     * @return string User friendly translated singular name of this DataObject
683
     */
684
    public function i18n_singular_name()
685
    {
686
        return _t(static::class.'.SINGULARNAME', $this->singular_name());
687
    }
688
689
    /**
690
     * Get the user friendly plural name of this DataObject
691
     * If the name is not defined (by renaming $plural_name in the subclass),
692
     * this returns a pluralised version of the class name.
693
     *
694
     * @return string User friendly plural name of this DataObject
695
     */
696
    public function plural_name()
697
    {
698
        if ($name = $this->stat('plural_name')) {
699
            return $name;
700
        }
701
        $name = $this->singular_name();
702
        //if the penultimate character is not a vowel, replace "y" with "ies"
703
        if (preg_match('/[^aeiou]y$/i', $name)) {
704
            $name = substr($name, 0, -1) . 'ie';
705
        }
706
        return ucfirst($name . 's');
707
    }
708
709
    /**
710
     * Get the translated user friendly plural name of this DataObject
711
     * Same as plural_name but runs it through the translation function
712
     * Translation string is in the form:
713
     *      $this->class.PLURALNAME
714
     * Example:
715
     *      Page.PLURALNAME
716
     *
717
     * @return string User friendly translated plural name of this DataObject
718
     */
719
    public function i18n_plural_name()
720
    {
721
        return _t(static::class.'.PLURALNAME', $this->plural_name());
722
    }
723
724
    /**
725
     * Standard implementation of a title/label for a specific
726
     * record. Tries to find properties 'Title' or 'Name',
727
     * and falls back to the 'ID'. Useful to provide
728
     * user-friendly identification of a record, e.g. in errormessages
729
     * or UI-selections.
730
     *
731
     * Overload this method to have a more specialized implementation,
732
     * e.g. for an Address record this could be:
733
     * <code>
734
     * function getTitle() {
735
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
736
     * }
737
     * </code>
738
     *
739
     * @return string
740
     */
741
    public function getTitle()
742
    {
743
        $schema = static::getSchema();
744
        if ($schema->fieldSpec($this, 'Title')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema->fieldSpec($this, 'Title') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
745
            return $this->getField('Title');
746
        }
747
        if ($schema->fieldSpec($this, 'Name')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema->fieldSpec($this, 'Name') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
748
            return $this->getField('Name');
749
        }
750
751
        return "#{$this->ID}";
752
    }
753
754
    /**
755
     * Returns the associated database record - in this case, the object itself.
756
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
757
     *
758
     * @return DataObject Associated database record
759
     */
760
    public function data()
761
    {
762
        return $this;
763
    }
764
765
    /**
766
     * Convert this object to a map.
767
     *
768
     * @return array The data as a map.
769
     */
770
    public function toMap()
771
    {
772
        $this->loadLazyFields();
773
        return $this->record;
774
    }
775
776
    /**
777
     * Return all currently fetched database fields.
778
     *
779
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
780
     * Obviously, this makes it a lot faster.
781
     *
782
     * @return array The data as a map.
783
     */
784
    public function getQueriedDatabaseFields()
785
    {
786
        return $this->record;
787
    }
788
789
    /**
790
     * Update a number of fields on this object, given a map of the desired changes.
791
     *
792
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
793
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
794
     *
795
     * update() doesn't write the main object, but if you use the dot syntax, it will write()
796
     * the related objects that it alters.
797
     *
798
     * @param array $data A map of field name to data values to update.
799
     * @return DataObject $this
800
     */
801
    public function update($data)
802
    {
803
        foreach ($data as $key => $value) {
804
            // Implement dot syntax for updates
805
            if (strpos($key, '.') !== false) {
806
                $relations = explode('.', $key);
807
                $fieldName = array_pop($relations);
808
                /** @var static $relObj */
809
                $relObj = $this;
810
                $relation = null;
811
                foreach ($relations as $i => $relation) {
812
                    // no support for has_many or many_many relationships,
813
                    // as the updater wouldn't know which object to write to (or create)
814
                    if ($relObj->$relation() instanceof DataObject) {
815
                        $parentObj = $relObj;
816
                        $relObj = $relObj->$relation();
817
                        // If the intermediate relationship objects have been created, then write them
818
                        if ($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj !== $this)) {
819
                            $relObj->write();
820
                            $relatedFieldName = $relation."ID";
821
                            $parentObj->$relatedFieldName = $relObj->ID;
822
                            $parentObj->write();
823
                        }
824
                    } else {
825
                        user_error(
826
                            "DataObject::update(): Can't traverse relationship '$relation'," .
827
                            "it has to be a has_one relationship or return a single DataObject",
828
                            E_USER_NOTICE
829
                        );
830
                        // unset relation object so we don't write properties to the wrong object
831
                        $relObj = null;
832
                        break;
833
                    }
834
                }
835
836
                if ($relObj) {
837
                    $relObj->$fieldName = $value;
838
                    $relObj->write();
839
                    $relatedFieldName = $relation."ID";
840
                    $this->$relatedFieldName = $relObj->ID;
841
                    $relObj->flushCache();
842
                } else {
843
                    $class = static::class;
844
                    user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
845
                }
846
            } else {
847
                $this->$key = $value;
848
            }
849
        }
850
        return $this;
851
    }
852
853
    /**
854
     * Pass changes as a map, and try to
855
     * get automatic casting for these fields.
856
     * Doesn't write to the database. To write the data,
857
     * use the write() method.
858
     *
859
     * @param array $data A map of field name to data values to update.
860
     * @return DataObject $this
861
     */
862
    public function castedUpdate($data)
863
    {
864
        foreach ($data as $k => $v) {
865
            $this->setCastedField($k, $v);
866
        }
867
        return $this;
868
    }
869
870
    /**
871
     * Merges data and relations from another object of same class,
872
     * without conflict resolution. Allows to specify which
873
     * dataset takes priority in case its not empty.
874
     * has_one-relations are just transferred with priority 'right'.
875
     * has_many and many_many-relations are added regardless of priority.
876
     *
877
     * Caution: has_many/many_many relations are moved rather than duplicated,
878
     * meaning they are not connected to the merged object any longer.
879
     * Caution: Just saves updated has_many/many_many relations to the database,
880
     * doesn't write the updated object itself (just writes the object-properties).
881
     * Caution: Does not delete the merged object.
882
     * Caution: Does now overwrite Created date on the original object.
883
     *
884
     * @param DataObject $rightObj
885
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
886
     * @param bool $includeRelations Merge any existing relations (optional)
887
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
888
     *                            Only applicable with $priority='right'. (optional)
889
     * @return Boolean
890
     */
891
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
892
    {
893
        $leftObj = $this;
894
895
        if ($leftObj->ClassName != $rightObj->ClassName) {
896
            // we can't merge similiar subclasses because they might have additional relations
897
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
898
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
899
            return false;
900
        }
901
902
        if (!$rightObj->ID) {
903
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
904
				to make sure all relations are transferred properly.').", E_USER_WARNING);
905
            return false;
906
        }
907
908
        // makes sure we don't merge data like ID or ClassName
909
        $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...
910
        foreach ($rightData as $key => $rightSpec) {
911
            // Don't merge ID
912
            if ($key === 'ID') {
913
                continue;
914
            }
915
916
            // Only merge relations if allowed
917
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
918
                continue;
919
            }
920
921
            // don't merge conflicting values if priority is 'left'
922
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
923
                continue;
924
            }
925
926
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
927
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
928
                continue;
929
            }
930
931
            // TODO remove redundant merge of has_one fields
932
            $leftObj->{$key} = $rightObj->{$key};
933
        }
934
935
        // merge relations
936
        if ($includeRelations) {
937
            if ($manyMany = $this->manyMany()) {
938
                foreach ($manyMany as $relationship => $class) {
939
                    /** @var DataObject $leftComponents */
940
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
941
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
942
                    if ($rightComponents && $rightComponents->exists()) {
943
                        $leftComponents->addMany($rightComponents->column('ID'));
944
                    }
945
                    $leftComponents->write();
946
                }
947
            }
948
949
            if ($hasMany = $this->hasMany()) {
950
                foreach ($hasMany as $relationship => $class) {
0 ignored issues
show
Bug introduced by
The expression $hasMany of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
951
                    $leftComponents = $leftObj->getComponents($relationship);
952
                    $rightComponents = $rightObj->getComponents($relationship);
953
                    if ($rightComponents && $rightComponents->exists()) {
954
                        $leftComponents->addMany($rightComponents->column('ID'));
955
                    }
956
                    $leftComponents->write();
957
                }
958
            }
959
        }
960
961
        return true;
962
    }
963
964
    /**
965
     * Forces the record to think that all its data has changed.
966
     * Doesn't write to the database. Only sets fields as changed
967
     * if they are not already marked as changed.
968
     *
969
     * @return $this
970
     */
971
    public function forceChange()
972
    {
973
        // Ensure lazy fields loaded
974
        $this->loadLazyFields();
975
        $fields = static::getSchema()->fieldSpecs(static::class);
976
977
        // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
978
        $fieldNames = array_unique(array_merge(
979
            array_keys($this->record),
980
            array_keys($fields)
981
        ));
982
983
        foreach ($fieldNames as $fieldName) {
984
            if (!isset($this->changed[$fieldName])) {
985
                $this->changed[$fieldName] = self::CHANGE_STRICT;
986
            }
987
            // Populate the null values in record so that they actually get written
988
            if (!isset($this->record[$fieldName])) {
989
                $this->record[$fieldName] = null;
990
            }
991
        }
992
993
        // @todo Find better way to allow versioned to write a new version after forceChange
994
        if ($this->isChanged('Version')) {
995
            unset($this->changed['Version']);
996
        }
997
        return $this;
998
    }
999
1000
    /**
1001
     * Validate the current object.
1002
     *
1003
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1004
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1005
     *
1006
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1007
     * and onAfterWrite() won't get called either.
1008
     *
1009
     * It is expected that you call validate() in your own application to test that an object is valid before
1010
     * attempting a write, and respond appropriately if it isn't.
1011
     *
1012
     * @see {@link ValidationResult}
1013
     * @return ValidationResult
1014
     */
1015
    public function validate()
1016
    {
1017
        $result = ValidationResult::create();
1018
        $this->extend('validate', $result);
1019
        return $result;
1020
    }
1021
1022
    /**
1023
     * Public accessor for {@see DataObject::validate()}
1024
     *
1025
     * @return ValidationResult
1026
     */
1027
    public function doValidate()
1028
    {
1029
        Deprecation::notice('5.0', 'Use validate');
1030
        return $this->validate();
1031
    }
1032
1033
    /**
1034
     * Event handler called before writing to the database.
1035
     * You can overload this to clean up or otherwise process data before writing it to the
1036
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1037
     *
1038
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1039
     *
1040
     * @uses DataExtension->onBeforeWrite()
1041
     */
1042
    protected function onBeforeWrite()
1043
    {
1044
        $this->brokenOnWrite = false;
1045
1046
        $dummy = null;
1047
        $this->extend('onBeforeWrite', $dummy);
1048
    }
1049
1050
    /**
1051
     * Event handler called after writing to the database.
1052
     * You can overload this to act upon changes made to the data after it is written.
1053
     * $this->changed will have a record
1054
     * database.  Don't forget to call parent::onAfterWrite(), though!
1055
     *
1056
     * @uses DataExtension->onAfterWrite()
1057
     */
1058
    protected function onAfterWrite()
1059
    {
1060
        $dummy = null;
1061
        $this->extend('onAfterWrite', $dummy);
1062
    }
1063
1064
    /**
1065
     * Event handler called before deleting from the database.
1066
     * You can overload this to clean up or otherwise process data before delete this
1067
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1068
     *
1069
     * @uses DataExtension->onBeforeDelete()
1070
     */
1071
    protected function onBeforeDelete()
1072
    {
1073
        $this->brokenOnDelete = false;
1074
1075
        $dummy = null;
1076
        $this->extend('onBeforeDelete', $dummy);
1077
    }
1078
1079
    protected function onAfterDelete()
1080
    {
1081
        $this->extend('onAfterDelete');
1082
    }
1083
1084
    /**
1085
     * Load the default values in from the self::$defaults array.
1086
     * Will traverse the defaults of the current class and all its parent classes.
1087
     * Called by the constructor when creating new records.
1088
     *
1089
     * @uses DataExtension->populateDefaults()
1090
     * @return DataObject $this
1091
     */
1092
    public function populateDefaults()
1093
    {
1094
        $classes = array_reverse(ClassInfo::ancestry($this));
1095
1096
        foreach ($classes as $class) {
1097
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1098
1099
            if ($defaults && !is_array($defaults)) {
1100
                user_error(
1101
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1102
                    E_USER_WARNING
1103
                );
1104
                $defaults = null;
1105
            }
1106
1107
            if ($defaults) {
1108
                foreach ($defaults as $fieldName => $fieldValue) {
1109
                // SRM 2007-03-06: Stricter check
1110
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1111
                        $this->$fieldName = $fieldValue;
1112
                    }
1113
                // Set many-many defaults with an array of ids
1114
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1115
                        /** @var ManyManyList $manyManyJoin */
1116
                        $manyManyJoin = $this->$fieldName();
1117
                        $manyManyJoin->setByIDList($fieldValue);
1118
                    }
1119
                }
1120
            }
1121
            if ($class == self::class) {
1122
                break;
1123
            }
1124
        }
1125
1126
        $this->extend('populateDefaults');
1127
        return $this;
1128
    }
1129
1130
    /**
1131
     * Determine validation of this object prior to write
1132
     *
1133
     * @return ValidationException Exception generated by this write, or null if valid
1134
     */
1135
    protected function validateWrite()
1136
    {
1137
        if ($this->ObsoleteClassName) {
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1138
            return new ValidationException(
1139
                "Object is of class '{$this->ObsoleteClassName}' which doesn't exist - ".
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1140
                "you need to change the ClassName before you can write it"
1141
            );
1142
        }
1143
1144
        // Note: Validation can only be disabled at the global level, not per-model
1145
        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...
1146
            $result = $this->validate();
1147
            if (!$result->isValid()) {
1148
                return new ValidationException($result);
1149
            }
1150
        }
1151
        return null;
1152
    }
1153
1154
    /**
1155
     * Prepare an object prior to write
1156
     *
1157
     * @throws ValidationException
1158
     */
1159
    protected function preWrite()
1160
    {
1161
        // Validate this object
1162
        if ($writeException = $this->validateWrite()) {
1163
            // Used by DODs to clean up after themselves, eg, Versioned
1164
            $this->invokeWithExtensions('onAfterSkippedWrite');
1165
            throw $writeException;
1166
        }
1167
1168
        // Check onBeforeWrite
1169
        $this->brokenOnWrite = true;
1170
        $this->onBeforeWrite();
1171
        if ($this->brokenOnWrite) {
1172
            user_error(static::class . " has a broken onBeforeWrite() function."
1173
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1174
        }
1175
    }
1176
1177
    /**
1178
     * Detects and updates all changes made to this object
1179
     *
1180
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1181
     * @return bool True if any changes are detected
1182
     */
1183
    protected function updateChanges($forceChanges = false)
1184
    {
1185
        if ($forceChanges) {
1186
            // Force changes, but only for loaded fields
1187
            foreach ($this->record as $field => $value) {
1188
                $this->changed[$field] = static::CHANGE_VALUE;
1189
            }
1190
            return true;
1191
        }
1192
        return $this->isChanged();
1193
    }
1194
1195
    /**
1196
     * Writes a subset of changes for a specific table to the given manipulation
1197
     *
1198
     * @param string $baseTable Base table
1199
     * @param string $now Timestamp to use for the current time
1200
     * @param bool $isNewRecord Whether this should be treated as a new record write
1201
     * @param array $manipulation Manipulation to write to
1202
     * @param string $class Class of table to manipulate
1203
     */
1204
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1205
    {
1206
        $schema = $this->getSchema();
1207
        $table = $schema->tableName($class);
1208
        $manipulation[$table] = array();
1209
1210
        // Extract records for this table
1211
        foreach ($this->record as $fieldName => $fieldValue) {
1212
            // we're not attempting to reset the BaseTable->ID
1213
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1214
            if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1215
                continue;
1216
            }
1217
1218
            // Ensure this field pertains to this table
1219
            $specification = $schema->fieldSpec($class, $fieldName, DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED);
1220
            if (!$specification) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $specification of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1221
                continue;
1222
            }
1223
1224
            // if database column doesn't correlate to a DBField instance...
1225
            $fieldObj = $this->dbObject($fieldName);
1226
            if (!$fieldObj) {
1227
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1228
            }
1229
1230
            // Write to manipulation
1231
            $fieldObj->writeToManipulation($manipulation[$table]);
1232
        }
1233
1234
        // Ensure update of Created and LastEdited columns
1235
        if ($baseTable === $table) {
1236
            $manipulation[$table]['fields']['LastEdited'] = $now;
1237
            if ($isNewRecord) {
1238
                $manipulation[$table]['fields']['Created']
1239
                    = empty($this->record['Created'])
1240
                        ? $now
1241
                        : $this->record['Created'];
1242
                $manipulation[$table]['fields']['ClassName'] = static::class;
1243
            }
1244
        }
1245
1246
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1247
        // attempt an update, as though it were a normal update.
1248
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1249
        $manipulation[$table]['id'] = $this->record['ID'];
1250
        $manipulation[$table]['class'] = $class;
1251
    }
1252
1253
    /**
1254
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1255
     *
1256
     * Does nothing if an ID is already assigned for this record
1257
     *
1258
     * @param string $baseTable Base table
1259
     * @param string $now Timestamp to use for the current time
1260
     */
1261
    protected function writeBaseRecord($baseTable, $now)
1262
    {
1263
        // Generate new ID if not specified
1264
        if ($this->isInDB()) {
1265
            return;
1266
        }
1267
1268
        // Perform an insert on the base table
1269
        $insert = new SQLInsert('"'.$baseTable.'"');
1270
        $insert
1271
            ->assign('"Created"', $now)
1272
            ->execute();
1273
        $this->changed['ID'] = self::CHANGE_VALUE;
1274
        $this->record['ID'] = DB::get_generated_id($baseTable);
1275
    }
1276
1277
    /**
1278
     * Generate and write the database manipulation for all changed fields
1279
     *
1280
     * @param string $baseTable Base table
1281
     * @param string $now Timestamp to use for the current time
1282
     * @param bool $isNewRecord If this is a new record
1283
     */
1284
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1285
    {
1286
        // Generate database manipulations for each class
1287
        $manipulation = array();
1288
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1289
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1290
        }
1291
1292
        // Allow extensions to extend this manipulation
1293
        $this->extend('augmentWrite', $manipulation);
1294
1295
        // New records have their insert into the base data table done first, so that they can pass the
1296
        // generated ID on to the rest of the manipulation
1297
        if ($isNewRecord) {
1298
            $manipulation[$baseTable]['command'] = 'update';
1299
        }
1300
1301
        // Perform the manipulation
1302
        DB::manipulate($manipulation);
1303
    }
1304
1305
    /**
1306
     * Writes all changes to this object to the database.
1307
     *  - It will insert a record whenever ID isn't set, otherwise update.
1308
     *  - All relevant tables will be updated.
1309
     *  - $this->onBeforeWrite() gets called beforehand.
1310
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1311
     *
1312
     *  @uses DataExtension->augmentWrite()
1313
     *
1314
     * @param boolean $showDebug Show debugging information
1315
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1316
     * @param boolean $forceWrite Write to database even if there are no changes
1317
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1318
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1319
     *                                 {@link getManyManyComponents()} (Default: false)
1320
     * @return int The ID of the record
1321
     * @throws ValidationException Exception that can be caught and handled by the calling function
1322
     */
1323
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1324
    {
1325
        $now = DBDatetime::now()->Rfc2822();
1326
1327
        // Execute pre-write tasks
1328
        $this->preWrite();
1329
1330
        // Check if we are doing an update or an insert
1331
        $isNewRecord = !$this->isInDB() || $forceInsert;
1332
1333
        // Check changes exist, abort if there are none
1334
        $hasChanges = $this->updateChanges($isNewRecord);
1335
        if ($hasChanges || $forceWrite || $isNewRecord) {
1336
            // New records have their insert into the base data table done first, so that they can pass the
1337
            // generated primary key on to the rest of the manipulation
1338
            $baseTable = $this->baseTable();
1339
            $this->writeBaseRecord($baseTable, $now);
1340
1341
            // Write the DB manipulation for all changed fields
1342
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1343
1344
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1345
            $this->writeRelations();
1346
            $this->onAfterWrite();
1347
            $this->changed = array();
1348
        } else {
1349
            if ($showDebug) {
1350
                Debug::message("no changes for DataObject");
1351
            }
1352
1353
            // Used by DODs to clean up after themselves, eg, Versioned
1354
            $this->invokeWithExtensions('onAfterSkippedWrite');
1355
        }
1356
1357
        // Ensure Created and LastEdited are populated
1358
        if (!isset($this->record['Created'])) {
1359
            $this->record['Created'] = $now;
1360
        }
1361
        $this->record['LastEdited'] = $now;
1362
1363
        // Write relations as necessary
1364
        if ($writeComponents) {
1365
            $this->writeComponents(true);
1366
        }
1367
1368
        // Clears the cache for this object so get_one returns the correct object.
1369
        $this->flushCache();
1370
1371
        return $this->record['ID'];
1372
    }
1373
1374
    /**
1375
     * Writes cached relation lists to the database, if possible
1376
     */
1377
    public function writeRelations()
1378
    {
1379
        if (!$this->isInDB()) {
1380
            return;
1381
        }
1382
1383
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1384
        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...
1385
            foreach ($this->unsavedRelations as $name => $list) {
1386
                $list->changeToList($this->$name());
1387
            }
1388
            $this->unsavedRelations = array();
1389
        }
1390
    }
1391
1392
    /**
1393
     * Write the cached components to the database. Cached components could refer to two different instances of the
1394
     * same record.
1395
     *
1396
     * @param bool $recursive Recursively write components
1397
     * @return DataObject $this
1398
     */
1399
    public function writeComponents($recursive = false)
1400
    {
1401
        if ($this->components) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->components of type SilverStripe\ORM\DataObject[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1402
            foreach ($this->components as $component) {
1403
                $component->write(false, false, false, $recursive);
1404
            }
1405
        }
1406
1407
        if ($join = $this->getJoin()) {
1408
            $join->write(false, false, false, $recursive);
1409
        }
1410
1411
        return $this;
1412
    }
1413
1414
    /**
1415
     * Delete this data object.
1416
     * $this->onBeforeDelete() gets called.
1417
     * Note that in Versioned objects, both Stage and Live will be deleted.
1418
     *  @uses DataExtension->augmentSQL()
1419
     */
1420
    public function delete()
1421
    {
1422
        $this->brokenOnDelete = true;
1423
        $this->onBeforeDelete();
1424
        if ($this->brokenOnDelete) {
1425
            user_error(static::class . " has a broken onBeforeDelete() function."
1426
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1427
        }
1428
1429
        // Deleting a record without an ID shouldn't do anything
1430
        if (!$this->ID) {
1431
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1432
        }
1433
1434
        // TODO: This is quite ugly.  To improve:
1435
        //  - move the details of the delete code in the DataQuery system
1436
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1437
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1438
        $srcQuery = DataList::create(static::class, $this->model)
1439
            ->filter('ID', $this->ID)
1440
            ->dataQuery()
1441
            ->query();
1442
        foreach ($srcQuery->queriedTables() as $table) {
1443
            $delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1444
            $delete->execute();
1445
        }
1446
        // Remove this item out of any caches
1447
        $this->flushCache();
1448
1449
        $this->onAfterDelete();
1450
1451
        $this->OldID = $this->ID;
0 ignored issues
show
Documentation introduced by
The property OldID does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1452
        $this->ID = 0;
1453
    }
1454
1455
    /**
1456
     * Delete the record with the given ID.
1457
     *
1458
     * @param string $className The class name of the record to be deleted
1459
     * @param int $id ID of record to be deleted
1460
     */
1461
    public static function delete_by_id($className, $id)
1462
    {
1463
        $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...
1464
        if ($obj) {
1465
            $obj->delete();
1466
        } else {
1467
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1468
        }
1469
    }
1470
1471
    /**
1472
     * Get the class ancestry, including the current class name.
1473
     * The ancestry will be returned as an array of class names, where the 0th element
1474
     * will be the class that inherits directly from DataObject, and the last element
1475
     * will be the current class.
1476
     *
1477
     * @return array Class ancestry
1478
     */
1479
    public function getClassAncestry()
1480
    {
1481
        return ClassInfo::ancestry(static::class);
1482
    }
1483
1484
    /**
1485
     * Return a component object from a one to one relationship, as a DataObject.
1486
     * If no component is available, an 'empty component' will be returned for
1487
     * non-polymorphic relations, or for polymorphic relations with a class set.
1488
     *
1489
     * @param string $componentName Name of the component
1490
     * @return DataObject The component object. It's exact type will be that of the component.
1491
     * @throws Exception
1492
     */
1493
    public function getComponent($componentName)
1494
    {
1495
        if (isset($this->components[$componentName])) {
1496
            return $this->components[$componentName];
1497
        }
1498
1499
        $schema = static::getSchema();
1500
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1501
            $joinField = $componentName . 'ID';
1502
            $joinID    = $this->getField($joinField);
1503
1504
            // Extract class name for polymorphic relations
1505
            if ($class === self::class) {
1506
                $class = $this->getField($componentName . 'Class');
1507
                if (empty($class)) {
1508
                    return null;
1509
                }
1510
            }
1511
1512
            if ($joinID) {
1513
                // Ensure that the selected object originates from the same stage, subsite, etc
1514
                $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...
1515
                    ->filter('ID', $joinID)
1516
                    ->setDataQueryParam($this->getInheritableQueryParams())
1517
                    ->first();
1518
            }
1519
1520
            if (empty($component)) {
1521
                $component = $this->model->$class->newObject();
1522
            }
1523
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1524
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1525
            $joinID = $this->ID;
1526
1527
            if ($joinID) {
1528
                // Prepare filter for appropriate join type
1529
                if ($polymorphic) {
1530
                    $filter = array(
1531
                        "{$joinField}ID" => $joinID,
1532
                        "{$joinField}Class" => static::class,
1533
                    );
1534
                } else {
1535
                    $filter = array(
1536
                        $joinField => $joinID
1537
                    );
1538
                }
1539
1540
                // Ensure that the selected object originates from the same stage, subsite, etc
1541
                $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...
1542
                    ->filter($filter)
1543
                    ->setDataQueryParam($this->getInheritableQueryParams())
1544
                    ->first();
1545
            }
1546
1547
            if (empty($component)) {
1548
                $component = $this->model->$class->newObject();
1549
                if ($polymorphic) {
1550
                    $component->{$joinField.'ID'} = $this->ID;
1551
                    $component->{$joinField.'Class'} = static::class;
1552
                } else {
1553
                    $component->$joinField = $this->ID;
1554
                }
1555
            }
1556
        } else {
1557
            throw new InvalidArgumentException(
1558
                "DataObject->getComponent(): Could not find component '$componentName'."
1559
            );
1560
        }
1561
1562
        $this->components[$componentName] = $component;
1563
        return $component;
1564
    }
1565
1566
    /**
1567
     * Returns a one-to-many relation as a HasManyList
1568
     *
1569
     * @param string $componentName Name of the component
1570
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1571
     */
1572
    public function getComponents($componentName)
1573
    {
1574
        $result = null;
1575
1576
        $schema = $this->getSchema();
1577
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1578
        if (!$componentClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $componentClass of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1579
            throw new InvalidArgumentException(sprintf(
1580
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1581
                $componentName,
1582
                static::class
1583
            ));
1584
        }
1585
1586
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1587
        if (!$this->ID) {
1588
            if (!isset($this->unsavedRelations[$componentName])) {
1589
                $this->unsavedRelations[$componentName] =
1590
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1591
            }
1592
            return $this->unsavedRelations[$componentName];
1593
        }
1594
1595
        // Determine type and nature of foreign relation
1596
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1597
        /** @var HasManyList $result */
1598
        if ($polymorphic) {
1599
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
1600
        } else {
1601
            $result = HasManyList::create($componentClass, $joinField);
1602
        }
1603
1604
        if ($this->model) {
1605
            $result->setDataModel($this->model);
1606
        }
1607
1608
        return $result
1609
            ->setDataQueryParam($this->getInheritableQueryParams())
1610
            ->forForeignID($this->ID);
1611
    }
1612
1613
    /**
1614
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1615
     *
1616
     * @param string $relationName Relation name.
1617
     * @return string Class name, or null if not found.
1618
     */
1619
    public function getRelationClass($relationName)
1620
    {
1621
        // Parse many_many
1622
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1623
        if ($manyManyComponent) {
1624
            return $manyManyComponent['childClass'];
1625
        }
1626
1627
        // Go through all relationship configuration fields.
1628
        $config = $this->config();
1629
        $candidates = array_merge(
1630
            ($relations = $config->get('has_one')) ? $relations : array(),
1631
            ($relations = $config->get('has_many')) ? $relations : array(),
1632
            ($relations = $config->get('belongs_to')) ? $relations : array()
1633
        );
1634
1635
        if (isset($candidates[$relationName])) {
1636
            $remoteClass = $candidates[$relationName];
1637
1638
            // If dot notation is present, extract just the first part that contains the class.
1639
            if (($fieldPos = strpos($remoteClass, '.'))!==false) {
1640
                return substr($remoteClass, 0, $fieldPos);
1641
            }
1642
1643
            // Otherwise just return the class
1644
            return $remoteClass;
1645
        }
1646
1647
        return null;
1648
    }
1649
1650
    /**
1651
     * Given a relation name, determine the relation type
1652
     *
1653
     * @param string $component Name of component
1654
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1655
     */
1656
    public function getRelationType($component)
1657
    {
1658
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1659
        $config = $this->config();
1660
        foreach ($types as $type) {
1661
            $relations = $config->get($type);
1662
            if ($relations && isset($relations[$component])) {
1663
                return $type;
1664
            }
1665
        }
1666
        return null;
1667
    }
1668
1669
    /**
1670
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1671
     * side of the relation.
1672
     *
1673
     * Notes on behaviour:
1674
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1675
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1676
     *  - Cannot be used on polymorphic relationships
1677
     *  - Cannot be used on unsaved objects.
1678
     *
1679
     * @param string $remoteClass
1680
     * @param string $remoteRelation
1681
     * @return DataList|DataObject The component, either as a list or single object
1682
     * @throws BadMethodCallException
1683
     * @throws InvalidArgumentException
1684
     */
1685
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1686
    {
1687
        $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...
1688
        $class = $remote->getRelationClass($remoteRelation);
1689
        $schema = static::getSchema();
1690
1691
        // Validate arguments
1692
        if (!$this->isInDB()) {
1693
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1694
        }
1695
        if (empty($class)) {
1696
            throw new InvalidArgumentException(sprintf(
1697
                "%s invoked with invalid relation %s.%s",
1698
                __METHOD__,
1699
                $remoteClass,
1700
                $remoteRelation
1701
            ));
1702
        }
1703
        if ($class === self::class) {
1704
            throw new InvalidArgumentException(sprintf(
1705
                "%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1706
                "This method does not support polymorphic relationships",
1707
                __METHOD__,
1708
                $remoteClass,
1709
                $remoteRelation
1710
            ));
1711
        }
1712
        if (!is_a($this, $class, true)) {
1713
            throw new InvalidArgumentException(sprintf(
1714
                "Relation %s on %s does not refer to objects of type %s",
1715
                $remoteRelation,
1716
                $remoteClass,
1717
                static::class
1718
            ));
1719
        }
1720
1721
        // Check the relation type to mock
1722
        $relationType = $remote->getRelationType($remoteRelation);
1723
        switch ($relationType) {
1724
            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...
1725
                // Mock has_many
1726
                $joinField = "{$remoteRelation}ID";
1727
                $componentClass = $schema->classForField($remoteClass, $joinField);
1728
                $result = HasManyList::create($componentClass, $joinField);
1729
                if ($this->model) {
1730
                    $result->setDataModel($this->model);
1731
                }
1732
                return $result
1733
                    ->setDataQueryParam($this->getInheritableQueryParams())
1734
                    ->forForeignID($this->ID);
1735
            }
1736
            case 'belongs_to':
1737
            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...
1738
                // These relations must have a has_one on the other end, so find it
1739
                $joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic);
1740
                if ($polymorphic) {
1741
                    throw new InvalidArgumentException(sprintf(
1742
                        "%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1743
                        "to be a has_one polymorphic. This method does not support polymorphic relationships",
1744
                        __METHOD__,
1745
                        $remoteClass,
1746
                        $remoteRelation
1747
                    ));
1748
                }
1749
                $joinID = $this->getField($joinField);
1750
                if (empty($joinID)) {
1751
                    return null;
1752
                }
1753
                // Get object by joined ID
1754
                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...
1755
                    ->filter('ID', $joinID)
1756
                    ->setDataQueryParam($this->getInheritableQueryParams())
1757
                    ->first();
1758
            }
1759
            case 'many_many':
1760
            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...
1761
                // Get components and extra fields from parent
1762
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1763
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1764
1765
                // Reverse parent and component fields and create an inverse ManyManyList
1766
                /** @var RelationList $result */
1767
                $result = Injector::inst()->create(
1768
                    $manyMany['relationClass'],
1769
                    $manyMany['parentClass'], // Substitute parent class for dataClass
1770
                    $manyMany['join'],
1771
                    $manyMany['parentField'], // Reversed parent / child field
1772
                    $manyMany['childField'], // Reversed parent / child field
1773
                    $extraFields
1774
                );
1775
                if ($this->model) {
1776
                    $result->setDataModel($this->model);
1777
                }
1778
                $this->extend('updateManyManyComponents', $result);
1779
1780
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1781
                // foreignID set elsewhere.
1782
                return $result
1783
                    ->setDataQueryParam($this->getInheritableQueryParams())
1784
                    ->forForeignID($this->ID);
1785
            }
1786
            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...
1787
                return null;
1788
            }
1789
        }
1790
    }
1791
1792
    /**
1793
     * Returns a many-to-many component, as a ManyManyList.
1794
     * @param string $componentName Name of the many-many component
1795
     * @return RelationList|UnsavedRelationList The set of components
1796
     */
1797
    public function getManyManyComponents($componentName)
1798
    {
1799
        $schema = static::getSchema();
1800
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
1801
        if (!$manyManyComponent) {
1802
            throw new InvalidArgumentException(sprintf(
1803
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1804
                $componentName,
1805
                static::class
1806
            ));
1807
        }
1808
1809
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1810
        if (!$this->ID) {
1811
            if (!isset($this->unsavedRelations[$componentName])) {
1812
                $this->unsavedRelations[$componentName] =
1813
                    new UnsavedRelationList($manyManyComponent['parentClass'], $componentName, $manyManyComponent['childClass']);
1814
            }
1815
            return $this->unsavedRelations[$componentName];
1816
        }
1817
1818
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
1819
        /** @var RelationList $result */
1820
        $result = Injector::inst()->create(
1821
            $manyManyComponent['relationClass'],
1822
            $manyManyComponent['childClass'],
1823
            $manyManyComponent['join'],
1824
            $manyManyComponent['childField'],
1825
            $manyManyComponent['parentField'],
1826
            $extraFields
1827
        );
1828
1829
1830
        // Store component data in query meta-data
1831
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
1832
            /** @var DataQuery $query */
1833
            $query->setQueryParam('Component.ExtraFields', $extraFields);
1834
        });
1835
1836
        if ($this->model) {
1837
            $result->setDataModel($this->model);
1838
        }
1839
1840
        $this->extend('updateManyManyComponents', $result);
1841
1842
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1843
        // foreignID set elsewhere.
1844
        return $result
1845
            ->setDataQueryParam($this->getInheritableQueryParams())
1846
            ->forForeignID($this->ID);
1847
    }
1848
1849
    /**
1850
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1851
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1852
     *
1853
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1854
     *                          their classes.
1855
     */
1856
    public function hasOne()
1857
    {
1858
        return (array)$this->config()->get('has_one');
1859
    }
1860
1861
    /**
1862
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1863
     * their class name will be returned.
1864
     *
1865
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1866
     *        the field data stripped off. It defaults to TRUE.
1867
     * @return string|array
1868
     */
1869
    public function belongsTo($classOnly = true)
1870
    {
1871
        $belongsTo = (array)$this->config()->get('belongs_to');
1872
        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...
1873
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1874
        } else {
1875
            return $belongsTo ? $belongsTo : array();
1876
        }
1877
    }
1878
1879
    /**
1880
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1881
     * relationships and their classes will be returned.
1882
     *
1883
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1884
     *        the field data stripped off. It defaults to TRUE.
1885
     * @return string|array|false
1886
     */
1887
    public function hasMany($classOnly = true)
1888
    {
1889
        $hasMany = (array)$this->config()->get('has_many');
1890
        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...
1891
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1892
        } else {
1893
            return $hasMany ? $hasMany : array();
1894
        }
1895
    }
1896
1897
    /**
1898
     * Return the many-to-many extra fields specification.
1899
     *
1900
     * If you don't specify a component name, it returns all
1901
     * extra fields for all components available.
1902
     *
1903
     * @return array|null
1904
     */
1905
    public function manyManyExtraFields()
1906
    {
1907
        return $this->config()->get('many_many_extraFields');
1908
    }
1909
1910
    /**
1911
     * Return information about a many-to-many component.
1912
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
1913
     * components are returned.
1914
     *
1915
     * @see DataObjectSchema::manyManyComponent()
1916
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
1917
     */
1918
    public function manyMany()
1919
    {
1920
        $config = $this->config();
1921
        $manyManys = (array)$config->get('many_many');
1922
        $belongsManyManys = (array)$config->get('belongs_many_many');
1923
        $items = array_merge($manyManys, $belongsManyManys);
1924
        return $items;
1925
    }
1926
1927
    /**
1928
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
1929
     *
1930
     * This is experimental, and is currently only a Postgres-specific enhancement.
1931
     *
1932
     * @param string $class
1933
     * @return array|false
1934
     */
1935
    public function database_extensions($class)
1936
    {
1937
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
1938
        if ($extensions) {
1939
            return $extensions;
1940
        } else {
1941
            return false;
1942
        }
1943
    }
1944
1945
    /**
1946
     * Generates a SearchContext to be used for building and processing
1947
     * a generic search form for properties on this object.
1948
     *
1949
     * @return SearchContext
1950
     */
1951
    public function getDefaultSearchContext()
1952
    {
1953
        return new SearchContext(
1954
            static::class,
1955
            $this->scaffoldSearchFields(),
1956
            $this->defaultSearchFilters()
1957
        );
1958
    }
1959
1960
    /**
1961
     * Determine which properties on the DataObject are
1962
     * searchable, and map them to their default {@link FormField}
1963
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
1964
     *
1965
     * Some additional logic is included for switching field labels, based on
1966
     * how generic or specific the field type is.
1967
     *
1968
     * Used by {@link SearchContext}.
1969
     *
1970
     * @param array $_params
1971
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
1972
     *   'restrictFields': Numeric array of a field name whitelist
1973
     * @return FieldList
1974
     */
1975
    public function scaffoldSearchFields($_params = null)
1976
    {
1977
        $params = array_merge(
1978
            array(
1979
                'fieldClasses' => false,
1980
                'restrictFields' => false
1981
            ),
1982
            (array)$_params
1983
        );
1984
        $fields = new FieldList();
1985
        foreach ($this->searchableFields() as $fieldName => $spec) {
1986
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
1987
                continue;
1988
            }
1989
1990
            // If a custom fieldclass is provided as a string, use it
1991
            $field = null;
1992
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
1993
                $fieldClass = $params['fieldClasses'][$fieldName];
1994
                $field = new $fieldClass($fieldName);
1995
            // If we explicitly set a field, then construct that
1996
            } elseif (isset($spec['field'])) {
1997
                // If it's a string, use it as a class name and construct
1998
                if (is_string($spec['field'])) {
1999
                    $fieldClass = $spec['field'];
2000
                    $field = new $fieldClass($fieldName);
2001
2002
                // If it's a FormField object, then just use that object directly.
2003
                } elseif ($spec['field'] instanceof FormField) {
2004
                    $field = $spec['field'];
2005
2006
                // Otherwise we have a bug
2007
                } else {
2008
                    user_error("Bad value for searchable_fields, 'field' value: "
2009
                        . var_export($spec['field'], true), E_USER_WARNING);
2010
                }
2011
2012
            // Otherwise, use the database field's scaffolder
2013
            } else {
2014
                $field = $this->relObject($fieldName)->scaffoldSearchField();
2015
            }
2016
2017
            // Allow fields to opt out of search
2018
            if (!$field) {
2019
                continue;
2020
            }
2021
2022
            if (strstr($fieldName, '.')) {
2023
                $field->setName(str_replace('.', '__', $fieldName));
2024
            }
2025
            $field->setTitle($spec['title']);
2026
2027
            $fields->push($field);
2028
        }
2029
        return $fields;
2030
    }
2031
2032
    /**
2033
     * Scaffold a simple edit form for all properties on this dataobject,
2034
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2035
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2036
     *
2037
     * @uses FormScaffolder
2038
     *
2039
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2040
     * @return FieldList
2041
     */
2042
    public function scaffoldFormFields($_params = null)
2043
    {
2044
        $params = array_merge(
2045
            array(
2046
                'tabbed' => false,
2047
                'includeRelations' => false,
2048
                'restrictFields' => false,
2049
                'fieldClasses' => false,
2050
                'ajaxSafe' => false
2051
            ),
2052
            (array)$_params
2053
        );
2054
2055
        $fs = FormScaffolder::create($this);
2056
        $fs->tabbed = $params['tabbed'];
2057
        $fs->includeRelations = $params['includeRelations'];
2058
        $fs->restrictFields = $params['restrictFields'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $params['restrictFields'] of type false is incompatible with the declared type array of property $restrictFields.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2059
        $fs->fieldClasses = $params['fieldClasses'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $params['fieldClasses'] of type false is incompatible with the declared type array of property $fieldClasses.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2060
        $fs->ajaxSafe = $params['ajaxSafe'];
2061
2062
        return $fs->getFieldList();
2063
    }
2064
2065
    /**
2066
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2067
     * being called on extensions
2068
     *
2069
     * @param callable $callback The callback to execute
2070
     */
2071
    protected function beforeUpdateCMSFields($callback)
2072
    {
2073
        $this->beforeExtending('updateCMSFields', $callback);
2074
    }
2075
2076
    /**
2077
     * Centerpiece of every data administration interface in Silverstripe,
2078
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2079
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2080
     * generate this set. To customize, overload this method in a subclass
2081
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2082
     *
2083
     * <code>
2084
     * class MyCustomClass extends DataObject {
2085
     *  static $db = array('CustomProperty'=>'Boolean');
2086
     *
2087
     *  function getCMSFields() {
2088
     *    $fields = parent::getCMSFields();
2089
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2090
     *    return $fields;
2091
     *  }
2092
     * }
2093
     * </code>
2094
     *
2095
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2096
     *
2097
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2098
     */
2099
    public function getCMSFields()
2100
    {
2101
        $tabbedFields = $this->scaffoldFormFields(array(
2102
            // Don't allow has_many/many_many relationship editing before the record is first saved
2103
            'includeRelations' => ($this->ID > 0),
2104
            'tabbed' => true,
2105
            'ajaxSafe' => true
2106
        ));
2107
2108
        $this->extend('updateCMSFields', $tabbedFields);
2109
2110
        return $tabbedFields;
2111
    }
2112
2113
    /**
2114
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2115
     * including that dataobject's extensions customised actions could be added to the EditForm.
2116
     *
2117
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2118
     */
2119
    public function getCMSActions()
2120
    {
2121
        $actions = new FieldList();
2122
        $this->extend('updateCMSActions', $actions);
2123
        return $actions;
2124
    }
2125
2126
2127
    /**
2128
     * Used for simple frontend forms without relation editing
2129
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2130
     * by default. To customize, either overload this method in your
2131
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2132
     *
2133
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2134
     *
2135
     * @param array $params See {@link scaffoldFormFields()}
2136
     * @return FieldList Always returns a simple field collection without TabSet.
2137
     */
2138
    public function getFrontEndFields($params = null)
2139
    {
2140
        $untabbedFields = $this->scaffoldFormFields($params);
2141
        $this->extend('updateFrontEndFields', $untabbedFields);
2142
2143
        return $untabbedFields;
2144
    }
2145
2146
    /**
2147
     * Gets the value of a field.
2148
     * Called by {@link __get()} and any getFieldName() methods you might create.
2149
     *
2150
     * @param string $field The name of the field
2151
     * @return mixed The field value
2152
     */
2153
    public function getField($field)
2154
    {
2155
        // If we already have an object in $this->record, then we should just return that
2156
        if (isset($this->record[$field]) && is_object($this->record[$field])) {
2157
            return $this->record[$field];
2158
        }
2159
2160
        // Do we have a field that needs to be lazy loaded?
2161
        if (isset($this->record[$field.'_Lazy'])) {
2162
            $tableClass = $this->record[$field.'_Lazy'];
2163
            $this->loadLazyFields($tableClass);
2164
        }
2165
2166
        // In case of complex fields, return the DBField object
2167
        if (static::getSchema()->compositeField(static::class, $field)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::getSchema()->com...(static::class, $field) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2168
            $this->record[$field] = $this->dbObject($field);
2169
        }
2170
2171
        return isset($this->record[$field]) ? $this->record[$field] : null;
2172
    }
2173
2174
    /**
2175
     * Loads all the stub fields that an initial lazy load didn't load fully.
2176
     *
2177
     * @param string $class Class to load the values from. Others are joined as required.
2178
     * Not specifying a tableClass will load all lazy fields from all tables.
2179
     * @return bool Flag if lazy loading succeeded
2180
     */
2181
    protected function loadLazyFields($class = null)
2182
    {
2183
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2184
            return false;
2185
        }
2186
2187
        if (!$class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2188
            $loaded = array();
2189
2190
            foreach ($this->record as $key => $value) {
2191
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2192
                    $this->loadLazyFields($value);
2193
                    $loaded[$value] = $value;
2194
                }
2195
            }
2196
2197
            return false;
2198
        }
2199
2200
        $dataQuery = new DataQuery($class);
2201
2202
        // Reset query parameter context to that of this DataObject
2203
        if ($params = $this->getSourceQueryParams()) {
2204
            foreach ($params as $key => $value) {
2205
                $dataQuery->setQueryParam($key, $value);
2206
            }
2207
        }
2208
2209
        // Limit query to the current record, unless it has the Versioned extension,
2210
        // in which case it requires special handling through augmentLoadLazyFields()
2211
        $schema = static::getSchema();
2212
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2213
        $dataQuery->where([
2214
            $baseIDColumn => $this->record['ID']
2215
        ])->limit(1);
2216
2217
        $columns = array();
2218
2219
        // Add SQL for fields, both simple & multi-value
2220
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2221
        $databaseFields = $schema->databaseFields($class, false);
2222
        foreach ($databaseFields as $k => $v) {
2223
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2224
                $columns[] = $k;
2225
            }
2226
        }
2227
2228
        if ($columns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $columns 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...
2229
            $query = $dataQuery->query();
2230
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2231
            $this->extend('augmentSQL', $query, $dataQuery);
2232
2233
            $dataQuery->setQueriedColumns($columns);
2234
            $newData = $dataQuery->execute()->record();
2235
2236
            // Load the data into record
2237
            if ($newData) {
2238
                foreach ($newData as $k => $v) {
2239
                    if (in_array($k, $columns)) {
2240
                        $this->record[$k] = $v;
2241
                        $this->original[$k] = $v;
2242
                        unset($this->record[$k . '_Lazy']);
2243
                    }
2244
                }
2245
2246
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2247
            } else {
2248
                foreach ($columns as $k) {
2249
                    $this->record[$k] = null;
2250
                    $this->original[$k] = null;
2251
                    unset($this->record[$k . '_Lazy']);
2252
                }
2253
            }
2254
        }
2255
        return true;
2256
    }
2257
2258
    /**
2259
     * Return the fields that have changed.
2260
     *
2261
     * The change level affects what the functions defines as "changed":
2262
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2263
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2264
     *   for example a change from 0 to null would not be included.
2265
     *
2266
     * Example return:
2267
     * <code>
2268
     * array(
2269
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2270
     * )
2271
     * </code>
2272
     *
2273
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2274
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2275
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2276
     * @return array
2277
     */
2278
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2279
    {
2280
        $changedFields = array();
2281
2282
        // Update the changed array with references to changed obj-fields
2283
        foreach ($this->record as $k => $v) {
2284
            // Prevents DBComposite infinite looping on isChanged
2285
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2286
                continue;
2287
            }
2288
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2289
                $this->changed[$k] = self::CHANGE_VALUE;
2290
            }
2291
        }
2292
2293
        if (is_array($databaseFieldsOnly)) {
2294
            $fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2295
        } elseif ($databaseFieldsOnly) {
2296
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2297
            $fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
2298
        } else {
2299
            $fields = $this->changed;
2300
        }
2301
2302
        // Filter the list to those of a certain change level
2303
        if ($changeLevel > self::CHANGE_STRICT) {
2304
            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...
2305
                foreach ($fields as $name => $level) {
2306
                    if ($level < $changeLevel) {
2307
                        unset($fields[$name]);
2308
                    }
2309
                }
2310
            }
2311
        }
2312
2313
        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...
2314
            foreach ($fields as $name => $level) {
2315
                $changedFields[$name] = array(
2316
                'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2317
                'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2318
                'level' => $level
2319
                );
2320
            }
2321
        }
2322
2323
        return $changedFields;
2324
    }
2325
2326
    /**
2327
     * Uses {@link getChangedFields()} to determine if fields have been changed
2328
     * since loading them from the database.
2329
     *
2330
     * @param string $fieldName Name of the database field to check, will check for any if not given
2331
     * @param int $changeLevel See {@link getChangedFields()}
2332
     * @return boolean
2333
     */
2334
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2335
    {
2336
        $fields = $fieldName ? array($fieldName) : true;
2337
        $changed = $this->getChangedFields($fields, $changeLevel);
2338
        if (!isset($fieldName)) {
2339
            return !empty($changed);
2340
        } else {
2341
            return array_key_exists($fieldName, $changed);
2342
        }
2343
    }
2344
2345
    /**
2346
     * Set the value of the field
2347
     * Called by {@link __set()} and any setFieldName() methods you might create.
2348
     *
2349
     * @param string $fieldName Name of the field
2350
     * @param mixed $val New field value
2351
     * @return $this
2352
     */
2353
    public function setField($fieldName, $val)
2354
    {
2355
        $this->objCacheClear();
2356
        //if it's a has_one component, destroy the cache
2357
        if (substr($fieldName, -2) == 'ID') {
2358
            unset($this->components[substr($fieldName, 0, -2)]);
2359
        }
2360
2361
        // If we've just lazy-loaded the column, then we need to populate the $original array
2362
        if (isset($this->record[$fieldName.'_Lazy'])) {
2363
            $tableClass = $this->record[$fieldName.'_Lazy'];
2364
            $this->loadLazyFields($tableClass);
2365
        }
2366
2367
        // Situation 1: Passing an DBField
2368
        if ($val instanceof DBField) {
2369
            $val->setName($fieldName);
2370
            $val->saveInto($this);
2371
2372
            // Situation 1a: Composite fields should remain bound in case they are
2373
            // later referenced to update the parent dataobject
2374
            if ($val instanceof DBComposite) {
2375
                $val->bindTo($this);
2376
                $this->record[$fieldName] = $val;
2377
            }
2378
        // Situation 2: Passing a literal or non-DBField object
2379
        } else {
2380
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2381
            if (is_object($val) && static::getSchema()->fieldSpec(static::class, $fieldName)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::getSchema()->fie...tic::class, $fieldName) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2382
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2383
            }
2384
2385
            // if a field is not existing or has strictly changed
2386
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2387
                // TODO Add check for php-level defaults which are not set in the db
2388
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2389
                // At the very least, the type has changed
2390
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2391
2392
                if ((!isset($this->record[$fieldName]) && $val)
2393
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2394
                ) {
2395
                    // Value has changed as well, not just the type
2396
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2397
                }
2398
2399
                // Value is always saved back when strict check succeeds.
2400
                $this->record[$fieldName] = $val;
2401
            }
2402
        }
2403
        return $this;
2404
    }
2405
2406
    /**
2407
     * Set the value of the field, using a casting object.
2408
     * This is useful when you aren't sure that a date is in SQL format, for example.
2409
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2410
     * can be saved into the Image table.
2411
     *
2412
     * @param string $fieldName Name of the field
2413
     * @param mixed $value New field value
2414
     * @return $this
2415
     */
2416
    public function setCastedField($fieldName, $value)
2417
    {
2418
        if (!$fieldName) {
2419
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2420
        }
2421
        $fieldObj = $this->dbObject($fieldName);
2422
        if ($fieldObj) {
2423
            $fieldObj->setValue($value);
2424
            $fieldObj->saveInto($this);
2425
        } else {
2426
            $this->$fieldName = $value;
2427
        }
2428
        return $this;
2429
    }
2430
2431
    /**
2432
     * {@inheritdoc}
2433
     */
2434
    public function castingHelper($field)
2435
    {
2436
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2437
        if ($fieldSpec) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldSpec of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2438
            return $fieldSpec;
2439
        }
2440
2441
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2442
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2443
        $queryParams = $this->getSourceQueryParams();
2444
        if (!empty($queryParams['Component.ExtraFields'])) {
2445
            $extraFields = $queryParams['Component.ExtraFields'];
2446
2447
            if (isset($extraFields[$field])) {
2448
                return $extraFields[$field];
2449
            }
2450
        }
2451
2452
        return parent::castingHelper($field);
2453
    }
2454
2455
    /**
2456
     * Returns true if the given field exists in a database column on any of
2457
     * the objects tables and optionally look up a dynamic getter with
2458
     * get<fieldName>().
2459
     *
2460
     * @param string $field Name of the field
2461
     * @return boolean True if the given field exists
2462
     */
2463
    public function hasField($field)
2464
    {
2465
        $schema = static::getSchema();
2466
        return (
2467
            array_key_exists($field, $this->record)
2468
            || $schema->fieldSpec(static::class, $field)
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema->fieldSpec(static::class, $field) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2469
            || (substr($field, -2) == 'ID') && $schema->hasOneComponent(static::class, substr($field, 0, -2))
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema->hasOneComponent... substr($field, 0, -2)) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2470
            || $this->hasMethod("get{$field}")
2471
        );
2472
    }
2473
2474
    /**
2475
     * Returns true if the given field exists as a database column
2476
     *
2477
     * @param string $field Name of the field
2478
     *
2479
     * @return boolean
2480
     */
2481
    public function hasDatabaseField($field)
2482
    {
2483
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2484
        return !empty($spec);
2485
    }
2486
2487
    /**
2488
     * Returns true if the member is allowed to do the given action.
2489
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2490
     *
2491
     * @param string $perm The permission to be checked, such as 'View'.
2492
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2493
     * in user.
2494
     * @param array $context Additional $context to pass to extendedCan()
2495
     *
2496
     * @return boolean True if the the member is allowed to do the given action
2497
     */
2498
    public function can($perm, $member = null, $context = array())
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2499
    {
2500
        if (!$member) {
2501
            $member = Member::currentUser();
2502
        }
2503
2504
        if ($member && Permission::checkMember($member, "ADMIN")) {
2505
            return true;
2506
        }
2507
2508
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2509
            $method = 'can' . ucfirst($perm);
2510
            return $this->$method($member);
2511
        }
2512
2513
        $results = $this->extendedCan('can', $member);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2514
        if (isset($results)) {
2515
            return $results;
2516
        }
2517
2518
        return ($member && Permission::checkMember($member, $perm));
2519
    }
2520
2521
    /**
2522
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2523
     * expected to return one of three values:
2524
     *
2525
     *  - false: Disallow this permission, regardless of what other extensions say
2526
     *  - true: Allow this permission, as long as no other extensions return false
2527
     *  - NULL: Don't affect the outcome
2528
     *
2529
     * This method itself returns a tri-state value, and is designed to be used like this:
2530
     *
2531
     * <code>
2532
     * $extended = $this->extendedCan('canDoSomething', $member);
2533
     * if($extended !== null) return $extended;
2534
     * else return $normalValue;
2535
     * </code>
2536
     *
2537
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2538
     * @param Member|int $member
2539
     * @param array $context Optional context
2540
     * @return boolean|null
2541
     */
2542
    public function extendedCan($methodName, $member, $context = array())
2543
    {
2544
        $results = $this->extend($methodName, $member, $context);
2545
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2546
            // Remove NULLs
2547
            $results = array_filter($results, function ($v) {
2548
                return !is_null($v);
2549
            });
2550
            // If there are any non-NULL responses, then return the lowest one of them.
2551
            // If any explicitly deny the permission, then we don't get access
2552
            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...
2553
                return min($results);
2554
            }
2555
        }
2556
        return null;
2557
    }
2558
2559
    /**
2560
     * @param Member $member
2561
     * @return boolean
2562
     */
2563
    public function canView($member = null)
2564
    {
2565
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2563 can be null; however, SilverStripe\ORM\DataObject::extendedCan() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2566
        if ($extended !== null) {
2567
            return $extended;
2568
        }
2569
        return Permission::check('ADMIN', 'any', $member);
2570
    }
2571
2572
    /**
2573
     * @param Member $member
2574
     * @return boolean
2575
     */
2576
    public function canEdit($member = null)
2577
    {
2578
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2576 can be null; however, SilverStripe\ORM\DataObject::extendedCan() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2579
        if ($extended !== null) {
2580
            return $extended;
2581
        }
2582
        return Permission::check('ADMIN', 'any', $member);
2583
    }
2584
2585
    /**
2586
     * @param Member $member
2587
     * @return boolean
2588
     */
2589
    public function canDelete($member = null)
2590
    {
2591
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2589 can be null; however, SilverStripe\ORM\DataObject::extendedCan() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2592
        if ($extended !== null) {
2593
            return $extended;
2594
        }
2595
        return Permission::check('ADMIN', 'any', $member);
2596
    }
2597
2598
    /**
2599
     * @param Member $member
2600
     * @param array $context Additional context-specific data which might
2601
     * affect whether (or where) this object could be created.
2602
     * @return boolean
2603
     */
2604
    public function canCreate($member = null, $context = array())
2605
    {
2606
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2604 can be null; however, SilverStripe\ORM\DataObject::extendedCan() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
2607
        if ($extended !== null) {
2608
            return $extended;
2609
        }
2610
        return Permission::check('ADMIN', 'any', $member);
2611
    }
2612
2613
    /**
2614
     * Debugging used by Debug::show()
2615
     *
2616
     * @return string HTML data representing this object
2617
     */
2618
    public function debug()
2619
    {
2620
        $class = static::class;
2621
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2622
        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...
2623
            foreach ($this->record as $fieldName => $fieldVal) {
2624
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2625
            }
2626
        }
2627
        $val .= "</ul>\n";
2628
        return $val;
2629
    }
2630
2631
    /**
2632
     * Return the DBField object that represents the given field.
2633
     * This works similarly to obj() with 2 key differences:
2634
     *   - it still returns an object even when the field has no value.
2635
     *   - it only matches fields and not methods
2636
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2637
     *
2638
     * @param string $fieldName Name of the field
2639
     * @return DBField The field as a DBField object
2640
     */
2641
    public function dbObject($fieldName)
2642
    {
2643
        // Check for field in DB
2644
        $schema = static::getSchema();
2645
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2646
        if (!$helper) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $helper of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2647
            return null;
2648
        }
2649
2650
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
2651
            $tableClass = $this->record[$fieldName . '_Lazy'];
2652
            $this->loadLazyFields($tableClass);
2653
        }
2654
2655
        $value = isset($this->record[$fieldName])
2656
            ? $this->record[$fieldName]
2657
            : null;
2658
2659
        // If we have a DBField object in $this->record, then return that
2660
        if ($value instanceof DBField) {
2661
            return $value;
2662
        }
2663
2664
        list($class, $spec) = explode('.', $helper);
2665
        /** @var DBField $obj */
2666
        $table = $schema->tableName($class);
2667
        $obj = Injector::inst()->create($spec, $fieldName);
2668
        $obj->setTable($table);
2669
        $obj->setValue($value, $this, false);
2670
        return $obj;
2671
    }
2672
2673
    /**
2674
     * Traverses to a DBField referenced by relationships between data objects.
2675
     *
2676
     * The path to the related field is specified with dot separated syntax
2677
     * (eg: Parent.Child.Child.FieldName).
2678
     *
2679
     * @param string $fieldPath
2680
     *
2681
     * @return mixed DBField of the field on the object or a DataList instance.
2682
     */
2683
    public function relObject($fieldPath)
2684
    {
2685
        $object = null;
2686
2687
        if (strpos($fieldPath, '.') !== false) {
2688
            $parts = explode('.', $fieldPath);
2689
            $fieldName = array_pop($parts);
2690
2691
            // Traverse dot syntax
2692
            $component = $this;
2693
2694
            foreach ($parts as $relation) {
2695
                if ($component instanceof SS_List) {
2696
                    if (method_exists($component, $relation)) {
2697
                        $component = $component->$relation();
2698
                    } else {
2699
                        /** @var DataList $component */
2700
                        $component = $component->relation($relation);
2701
                    }
2702
                } else {
2703
                    $component = $component->$relation();
2704
                }
2705
            }
2706
2707
            $object = $component->dbObject($fieldName);
2708
        } else {
2709
            $object = $this->dbObject($fieldPath);
2710
        }
2711
2712
        return $object;
2713
    }
2714
2715
    /**
2716
     * Traverses to a field referenced by relationships between data objects, returning the value
2717
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2718
     *
2719
     * @param $fieldName string
2720
     * @return string | null - will return null on a missing value
2721
     */
2722
    public function relField($fieldName)
2723
    {
2724
        $component = $this;
2725
2726
        // We're dealing with relations here so we traverse the dot syntax
2727
        if (strpos($fieldName, '.') !== false) {
2728
            $relations = explode('.', $fieldName);
2729
            $fieldName = array_pop($relations);
2730
            foreach ($relations as $relation) {
2731
                // Inspect $component for element $relation
2732
                if ($component->hasMethod($relation)) {
2733
                    // Check nested method
2734
                    $component = $component->$relation();
2735
                } elseif ($component instanceof SS_List) {
2736
                    // Select adjacent relation from DataList
2737
                    /** @var DataList $component */
2738
                    $component = $component->relation($relation);
2739
                } elseif ($component instanceof DataObject
2740
                    && ($dbObject = $component->dbObject($relation))
2741
                ) {
2742
                    // Select db object
2743
                    $component = $dbObject;
2744
                } else {
2745
                    user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
2746
                }
2747
            }
2748
        }
2749
2750
        // Bail if the component is null
2751
        if (!$component) {
2752
            return null;
2753
        }
2754
        if ($component->hasMethod($fieldName)) {
2755
            return $component->$fieldName();
2756
        }
2757
        return $component->$fieldName;
2758
    }
2759
2760
    /**
2761
     * Temporary hack to return an association name, based on class, to get around the mangle
2762
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2763
     *
2764
     * @param string $className
2765
     * @return string
2766
     */
2767
    public function getReverseAssociation($className)
2768
    {
2769
        if (is_array($this->manyMany())) {
2770
            $many_many = array_flip($this->manyMany());
2771
            if (array_key_exists($className, $many_many)) {
2772
                return $many_many[$className];
2773
            }
2774
        }
2775
        if (is_array($this->hasMany())) {
2776
            $has_many = array_flip($this->hasMany());
2777
            if (array_key_exists($className, $has_many)) {
2778
                return $has_many[$className];
2779
            }
2780
        }
2781
        if (is_array($this->hasOne())) {
2782
            $has_one = array_flip($this->hasOne());
2783
            if (array_key_exists($className, $has_one)) {
2784
                return $has_one[$className];
2785
            }
2786
        }
2787
2788
        return false;
2789
    }
2790
2791
    /**
2792
     * Return all objects matching the filter
2793
     * sub-classes are automatically selected and included
2794
     *
2795
     * @param string $callerClass The class of objects to be returned
2796
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2797
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2798
     * @param string|array $sort A sort expression to be inserted into the ORDER
2799
     * BY clause.  If omitted, self::$default_sort will be used.
2800
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
2801
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2802
     * @param string $containerClass The container class to return the results in.
2803
     *
2804
     * @todo $containerClass is Ignored, why?
2805
     *
2806
     * @return DataList The objects matching the filter, in the class specified by $containerClass
2807
     */
2808
    public static function get(
2809
        $callerClass = null,
2810
        $filter = "",
2811
        $sort = "",
2812
        $join = "",
2813
        $limit = null,
2814
        $containerClass = DataList::class
2815
    ) {
2816
2817
        if ($callerClass == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $callerClass of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2818
            $callerClass = get_called_class();
2819
            if ($callerClass == self::class) {
2820
                throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
2821
            }
2822
2823
            if ($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
2824
                throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
2825
                    . ' arguments');
2826
            }
2827
2828
            $result = DataList::create(get_called_class());
2829
            $result->setDataModel(DataModel::inst());
2830
            return $result;
2831
        }
2832
2833
        if ($join) {
2834
            throw new \InvalidArgumentException(
2835
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
2836
            );
2837
        }
2838
2839
        $result = DataList::create($callerClass)->where($filter)->sort($sort);
2840
2841
        if ($limit && strpos($limit, ',') !== false) {
2842
            $limitArguments = explode(',', $limit);
2843
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
2844
        } elseif ($limit) {
2845
            $result = $result->limit($limit);
2846
        }
2847
2848
        $result->setDataModel(DataModel::inst());
2849
        return $result;
2850
    }
2851
2852
2853
    /**
2854
     * Return the first item matching the given query.
2855
     * All calls to get_one() are cached.
2856
     *
2857
     * @param string $callerClass The class of objects to be returned
2858
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2859
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2860
     * @param boolean $cache Use caching
2861
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
2862
     *
2863
     * @return DataObject The first item matching the query
2864
     */
2865
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
2866
    {
2867
        $SNG = singleton($callerClass);
2868
2869
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
2870
        $cacheKey = md5(var_export($cacheComponents, true));
2871
2872
        // Flush destroyed items out of the cache
2873
        if ($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
2874
                && self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
2875
                && self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
2876
            self::$_cache_get_one[$callerClass][$cacheKey] = false;
2877
        }
2878
        $item = null;
2879
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
2880
            $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...
2881
            $item = $dl->first();
2882
2883
            if ($cache) {
2884
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
2885
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
2886
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
2887
                }
2888
            }
2889
        }
2890
        return $cache ? self::$_cache_get_one[$callerClass][$cacheKey] : $item;
2891
    }
2892
2893
    /**
2894
     * Flush the cached results for all relations (has_one, has_many, many_many)
2895
     * Also clears any cached aggregate data.
2896
     *
2897
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
2898
     *                            When false will just clear session-local cached data
2899
     * @return DataObject $this
2900
     */
2901
    public function flushCache($persistent = true)
0 ignored issues
show
Unused Code introduced by
The parameter $persistent is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2902
    {
2903
        if (static::class == self::class) {
2904
            self::$_cache_get_one = array();
2905
            return $this;
2906
        }
2907
2908
        $classes = ClassInfo::ancestry(static::class);
2909
        foreach ($classes as $class) {
2910
            if (isset(self::$_cache_get_one[$class])) {
2911
                unset(self::$_cache_get_one[$class]);
2912
            }
2913
        }
2914
2915
        $this->extend('flushCache');
2916
2917
        $this->components = array();
2918
        return $this;
2919
    }
2920
2921
    /**
2922
     * Flush the get_one global cache and destroy associated objects.
2923
     */
2924
    public static function flush_and_destroy_cache()
2925
    {
2926
        if (self::$_cache_get_one) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$_cache_get_one of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2927
            foreach (self::$_cache_get_one as $class => $items) {
2928
                if (is_array($items)) {
2929
                    foreach ($items as $item) {
2930
                        if ($item) {
2931
                            $item->destroy();
2932
                        }
2933
                    }
2934
                }
2935
            }
2936
        }
2937
        self::$_cache_get_one = array();
2938
    }
2939
2940
    /**
2941
     * Reset all global caches associated with DataObject.
2942
     */
2943
    public static function reset()
2944
    {
2945
        // @todo Decouple these
2946
        DBClassName::clear_classname_cache();
2947
        ClassInfo::reset_db_cache();
2948
        static::getSchema()->reset();
2949
        self::$_cache_get_one = array();
2950
        self::$_cache_field_labels = array();
2951
    }
2952
2953
    /**
2954
     * Return the given element, searching by ID
2955
     *
2956
     * @param string $callerClass The class of the object to be returned
2957
     * @param int $id The id of the element
2958
     * @param boolean $cache See {@link get_one()}
2959
     *
2960
     * @return DataObject The element
2961
     */
2962
    public static function get_by_id($callerClass, $id, $cache = true)
2963
    {
2964
        if (!is_numeric($id)) {
2965
            user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
2966
        }
2967
2968
        // Pass to get_one
2969
        $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
2970
        return DataObject::get_one($callerClass, array($column => $id), $cache);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
2971
    }
2972
2973
    /**
2974
     * Get the name of the base table for this object
2975
     *
2976
     * @return string
2977
     */
2978
    public function baseTable()
2979
    {
2980
        return static::getSchema()->baseDataTable($this);
2981
    }
2982
2983
    /**
2984
     * Get the base class for this object
2985
     *
2986
     * @return string
2987
     */
2988
    public function baseClass()
2989
    {
2990
        return static::getSchema()->baseDataClass($this);
2991
    }
2992
2993
    /**
2994
     * @var array Parameters used in the query that built this object.
2995
     * This can be used by decorators (e.g. lazy loading) to
2996
     * run additional queries using the same context.
2997
     */
2998
    protected $sourceQueryParams;
2999
3000
    /**
3001
     * @see $sourceQueryParams
3002
     * @return array
3003
     */
3004
    public function getSourceQueryParams()
3005
    {
3006
        return $this->sourceQueryParams;
3007
    }
3008
3009
    /**
3010
     * Get list of parameters that should be inherited to relations on this object
3011
     *
3012
     * @return array
3013
     */
3014
    public function getInheritableQueryParams()
3015
    {
3016
        $params = $this->getSourceQueryParams();
3017
        $this->extend('updateInheritableQueryParams', $params);
3018
        return $params;
3019
    }
3020
3021
    /**
3022
     * @see $sourceQueryParams
3023
     * @param array
3024
     */
3025
    public function setSourceQueryParams($array)
3026
    {
3027
        $this->sourceQueryParams = $array;
3028
    }
3029
3030
    /**
3031
     * @see $sourceQueryParams
3032
     * @param string $key
3033
     * @param string $value
3034
     */
3035
    public function setSourceQueryParam($key, $value)
3036
    {
3037
        $this->sourceQueryParams[$key] = $value;
3038
    }
3039
3040
    /**
3041
     * @see $sourceQueryParams
3042
     * @param string $key
3043
     * @return string
3044
     */
3045
    public function getSourceQueryParam($key)
3046
    {
3047
        if (isset($this->sourceQueryParams[$key])) {
3048
            return $this->sourceQueryParams[$key];
3049
        }
3050
        return null;
3051
    }
3052
3053
    //-------------------------------------------------------------------------------------------//
3054
3055
    /**
3056
     * Check the database schema and update it as necessary.
3057
     *
3058
     * @uses DataExtension->augmentDatabase()
3059
     */
3060
    public function requireTable()
3061
    {
3062
        // Only build the table if we've actually got fields
3063
        $schema = static::getSchema();
3064
        $table = $schema->tableName(static::class);
3065
        $fields = $schema->databaseFields(static::class, false);
3066
        $indexes = $schema->databaseIndexes(static::class, false);
3067
        $extensions = self::database_extensions(static::class);
3068
3069
        if (empty($table)) {
3070
            throw new LogicException(
3071
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3072
            );
3073
        }
3074
3075
        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...
3076
            $hasAutoIncPK = get_parent_class($this) === self::class;
3077
            DB::require_table(
3078
                $table,
3079
                $fields,
3080
                $indexes,
3081
                $hasAutoIncPK,
3082
                $this->stat('create_table_options'),
3083
                $extensions
0 ignored issues
show
Security Bug introduced by
It seems like $extensions defined by self::database_extensions(static::class) on line 3067 can also be of type false; however, SilverStripe\ORM\DB::require_table() does only seem to accept array|null, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
3084
            );
3085
        } else {
3086
            DB::dont_require_table($table);
3087
        }
3088
3089
        // Build any child tables for many_many items
3090
        if ($manyMany = $this->uninherited('many_many')) {
3091
            $extras = $this->uninherited('many_many_extraFields');
3092
            foreach ($manyMany as $component => $spec) {
3093
                // Get many_many spec
3094
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3095
                $parentField = $manyManyComponent['parentField'];
3096
                $childField = $manyManyComponent['childField'];
3097
                $tableOrClass = $manyManyComponent['join'];
3098
3099
                // Skip if backed by actual class
3100
                if (class_exists($tableOrClass)) {
3101
                    continue;
3102
                }
3103
3104
                // Build fields
3105
                $manymanyFields = array(
3106
                    $parentField => "Int",
3107
                    $childField => "Int",
3108
                );
3109
                if (isset($extras[$component])) {
3110
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3111
                }
3112
3113
                // Build index list
3114
                $manymanyIndexes = [
3115
                    $parentField => [
3116
                        'type' => 'index',
3117
                        'name' => $parentField,
3118
                        'columns' => [$parentField],
3119
                    ],
3120
                    $childField => [
3121
                        'type' => 'index',
3122
                        'name' =>$childField,
3123
                        'columns' => [$childField],
3124
                    ],
3125
                ];
3126
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Security Bug introduced by
It seems like $extensions defined by self::database_extensions(static::class) on line 3067 can also be of type false; however, SilverStripe\ORM\DB::require_table() does only seem to accept array|null, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
3127
            }
3128
        }
3129
3130
        // Let any extentions make their own database fields
3131
        $this->extend('augmentDatabase', $dummy);
3132
    }
3133
3134
    /**
3135
     * Add default records to database. This function is called whenever the
3136
     * database is built, after the database tables have all been created. Overload
3137
     * this to add default records when the database is built, but make sure you
3138
     * call parent::requireDefaultRecords().
3139
     *
3140
     * @uses DataExtension->requireDefaultRecords()
3141
     */
3142
    public function requireDefaultRecords()
3143
    {
3144
        $defaultRecords = $this->config()->uninherited('default_records');
3145
3146
        if (!empty($defaultRecords)) {
3147
            $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...
3148
            if (!$hasData) {
3149
                $className = static::class;
3150
                foreach ($defaultRecords as $record) {
3151
                    $obj = $this->model->$className->newObject($record);
3152
                    $obj->write();
3153
                }
3154
                DB::alteration_message("Added default records to $className table", "created");
3155
            }
3156
        }
3157
3158
        // Let any extentions make their own database default data
3159
        $this->extend('requireDefaultRecords', $dummy);
3160
    }
3161
3162
    /**
3163
     * Get the default searchable fields for this object, as defined in the
3164
     * $searchable_fields list. If searchable fields are not defined on the
3165
     * data object, uses a default selection of summary fields.
3166
     *
3167
     * @return array
3168
     */
3169
    public function searchableFields()
3170
    {
3171
        // can have mixed format, need to make consistent in most verbose form
3172
        $fields = $this->stat('searchable_fields');
3173
        $labels = $this->fieldLabels();
3174
3175
        // fallback to summary fields (unless empty array is explicitly specified)
3176
        if (! $fields && ! is_array($fields)) {
3177
            $summaryFields = array_keys($this->summaryFields());
3178
            $fields = array();
3179
3180
            // remove the custom getters as the search should not include them
3181
            $schema = static::getSchema();
3182
            if ($summaryFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $summaryFields of type array<integer|string> 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...
3183
                foreach ($summaryFields as $key => $name) {
3184
                    $spec = $name;
3185
3186
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3187
                    if (($fieldPos = strpos($name, '.')) !== false) {
3188
                        $name = substr($name, 0, $fieldPos);
3189
                    }
3190
3191
                    if ($schema->fieldSpec($this, $name)) {
3192
                        $fields[] = $name;
3193
                    } elseif ($this->relObject($spec)) {
3194
                        $fields[] = $spec;
3195
                    }
3196
                }
3197
            }
3198
        }
3199
3200
        // we need to make sure the format is unified before
3201
        // augmenting fields, so extensions can apply consistent checks
3202
        // but also after augmenting fields, because the extension
3203
        // might use the shorthand notation as well
3204
3205
        // rewrite array, if it is using shorthand syntax
3206
        $rewrite = array();
3207
        foreach ($fields as $name => $specOrName) {
3208
            $identifer = (is_int($name)) ? $specOrName : $name;
3209
3210
            if (is_int($name)) {
3211
                // 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...
3212
                $rewrite[$identifer] = array();
3213
            } elseif (is_array($specOrName)) {
3214
                // 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...
3215
                //   'filter => 'ExactMatchFilter',
3216
                //   '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...
3217
                //   '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...
3218
                // ))
3219
                $rewrite[$identifer] = array_merge(
3220
                    array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3221
                    (array)$specOrName
3222
                );
3223
            } else {
3224
                // 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...
3225
                $rewrite[$identifer] = array(
3226
                    'filter' => $specOrName,
3227
                );
3228
            }
3229
            if (!isset($rewrite[$identifer]['title'])) {
3230
                $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3231
                    ? $labels[$identifer] : FormField::name_to_label($identifer);
3232
            }
3233
            if (!isset($rewrite[$identifer]['filter'])) {
3234
                /** @skipUpgrade */
3235
                $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3236
            }
3237
        }
3238
3239
        $fields = $rewrite;
3240
3241
        // apply DataExtensions if present
3242
        $this->extend('updateSearchableFields', $fields);
3243
3244
        return $fields;
3245
    }
3246
3247
    /**
3248
     * Get any user defined searchable fields labels that
3249
     * exist. Allows overriding of default field names in the form
3250
     * interface actually presented to the user.
3251
     *
3252
     * The reason for keeping this separate from searchable_fields,
3253
     * which would be a logical place for this functionality, is to
3254
     * avoid bloating and complicating the configuration array. Currently
3255
     * much of this system is based on sensible defaults, and this property
3256
     * would generally only be set in the case of more complex relationships
3257
     * between data object being required in the search interface.
3258
     *
3259
     * Generates labels based on name of the field itself, if no static property
3260
     * {@link self::field_labels} exists.
3261
     *
3262
     * @uses $field_labels
3263
     * @uses FormField::name_to_label()
3264
     *
3265
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3266
     *
3267
     * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3268
     */
3269
    public function fieldLabels($includerelations = true)
3270
    {
3271
        $cacheKey = static::class . '_' . $includerelations;
3272
3273
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
3274
            $customLabels = $this->stat('field_labels');
3275
            $autoLabels = array();
3276
3277
            // get all translated static properties as defined in i18nCollectStatics()
3278
            $ancestry = ClassInfo::ancestry(static::class);
3279
            $ancestry = array_reverse($ancestry);
3280
            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...
3281
                foreach ($ancestry as $ancestorClass) {
3282
                    if ($ancestorClass === ViewableData::class) {
3283
                        break;
3284
                    }
3285
                    $types = [
3286
                        'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3287
                    ];
3288
                    if ($includerelations) {
3289
                        $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3290
                        $types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3291
                        $types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3292
                        $types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3293
                    }
3294
                    foreach ($types as $type => $attrs) {
3295
                        foreach ($attrs as $name => $spec) {
3296
                            $autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}", FormField::name_to_label($name));
3297
                        }
3298
                    }
3299
                }
3300
            }
3301
3302
            $labels = array_merge((array)$autoLabels, (array)$customLabels);
3303
            $this->extend('updateFieldLabels', $labels);
3304
            self::$_cache_field_labels[$cacheKey] = $labels;
3305
        }
3306
3307
        return self::$_cache_field_labels[$cacheKey];
3308
    }
3309
3310
    /**
3311
     * Get a human-readable label for a single field,
3312
     * see {@link fieldLabels()} for more details.
3313
     *
3314
     * @uses fieldLabels()
3315
     * @uses FormField::name_to_label()
3316
     *
3317
     * @param string $name Name of the field
3318
     * @return string Label of the field
3319
     */
3320
    public function fieldLabel($name)
3321
    {
3322
        $labels = $this->fieldLabels();
3323
        return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3324
    }
3325
3326
    /**
3327
     * Get the default summary fields for this object.
3328
     *
3329
     * @todo use the translation apparatus to return a default field selection for the language
3330
     *
3331
     * @return array
3332
     */
3333
    public function summaryFields()
3334
    {
3335
        $fields = $this->stat('summary_fields');
3336
3337
        // if fields were passed in numeric array,
3338
        // convert to an associative array
3339
        if ($fields && array_key_exists(0, $fields)) {
3340
            $fields = array_combine(array_values($fields), array_values($fields));
3341
        }
3342
3343
        if (!$fields) {
3344
            $fields = array();
3345
            // try to scaffold a couple of usual suspects
3346
            if ($this->hasField('Name')) {
3347
                $fields['Name'] = 'Name';
3348
            }
3349
            if (static::getSchema()->fieldSpec($this, 'Title')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::getSchema()->fieldSpec($this, 'Title') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3350
                $fields['Title'] = 'Title';
3351
            }
3352
            if ($this->hasField('Description')) {
3353
                $fields['Description'] = 'Description';
3354
            }
3355
            if ($this->hasField('FirstName')) {
3356
                $fields['FirstName'] = 'First Name';
3357
            }
3358
        }
3359
        $this->extend("updateSummaryFields", $fields);
3360
3361
        // Final fail-over, just list ID field
3362
        if (!$fields) {
3363
            $fields['ID'] = 'ID';
3364
        }
3365
3366
        // Localize fields (if possible)
3367
        foreach ($this->fieldLabels(false) as $name => $label) {
0 ignored issues
show
Bug introduced by
The expression $this->fieldLabels(false) of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
3368
            // only attempt to localize if the label definition is the same as the field name.
3369
            // this will preserve any custom labels set in the summary_fields configuration
3370
            if (isset($fields[$name]) && $name === $fields[$name]) {
3371
                $fields[$name] = $label;
3372
            }
3373
        }
3374
3375
        return $fields;
3376
    }
3377
3378
    /**
3379
     * Defines a default list of filters for the search context.
3380
     *
3381
     * If a filter class mapping is defined on the data object,
3382
     * it is constructed here. Otherwise, the default filter specified in
3383
     * {@link DBField} is used.
3384
     *
3385
     * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3386
     *
3387
     * @return array
3388
     */
3389
    public function defaultSearchFilters()
3390
    {
3391
        $filters = array();
3392
3393
        foreach ($this->searchableFields() as $name => $spec) {
3394
            if (empty($spec['filter'])) {
3395
                /** @skipUpgrade */
3396
                $filters[$name] = 'PartialMatchFilter';
3397
            } elseif ($spec['filter'] instanceof SearchFilter) {
3398
                $filters[$name] = $spec['filter'];
3399
            } else {
3400
                $filters[$name] = Injector::inst()->create($spec['filter'], $name);
3401
            }
3402
        }
3403
3404
        return $filters;
3405
    }
3406
3407
    /**
3408
     * @return boolean True if the object is in the database
3409
     */
3410
    public function isInDB()
3411
    {
3412
        return is_numeric($this->ID) && $this->ID > 0;
3413
    }
3414
3415
    /*
3416
	 * @ignore
3417
	 */
3418
    private static $subclass_access = true;
3419
3420
    /**
3421
     * Temporarily disable subclass access in data object qeur
3422
     */
3423
    public static function disable_subclass_access()
3424
    {
3425
        self::$subclass_access = false;
3426
    }
3427
    public static function enable_subclass_access()
3428
    {
3429
        self::$subclass_access = true;
3430
    }
3431
3432
    //-------------------------------------------------------------------------------------------//
3433
3434
    /**
3435
     * Database field definitions.
3436
     * This is a map from field names to field type. The field
3437
     * type should be a class that extends .
3438
     * @var array
3439
     * @config
3440
     */
3441
    private static $db = [];
3442
3443
    /**
3444
     * Use a casting object for a field. This is a map from
3445
     * field name to class name of the casting object.
3446
     *
3447
     * @var array
3448
     */
3449
    private static $casting = array(
3450
        "Title" => 'Text',
3451
    );
3452
3453
    /**
3454
     * Specify custom options for a CREATE TABLE call.
3455
     * Can be used to specify a custom storage engine for specific database table.
3456
     * All options have to be keyed for a specific database implementation,
3457
     * identified by their class name (extending from {@link SS_Database}).
3458
     *
3459
     * <code>
3460
     * array(
3461
     *  'MySQLDatabase' => 'ENGINE=MyISAM'
3462
     * )
3463
     * </code>
3464
     *
3465
     * Caution: This API is experimental, and might not be
3466
     * included in the next major release. Please use with care.
3467
     *
3468
     * @var array
3469
     * @config
3470
     */
3471
    private static $create_table_options = array(
3472
        'SilverStripe\ORM\Connect\MySQLDatabase' => 'ENGINE=InnoDB'
3473
    );
3474
3475
    /**
3476
     * If a field is in this array, then create a database index
3477
     * on that field. This is a map from fieldname to index type.
3478
     * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3479
     *
3480
     * @var array
3481
     * @config
3482
     */
3483
    private static $indexes = null;
3484
3485
    /**
3486
     * Inserts standard column-values when a DataObject
3487
     * is instanciated. Does not insert default records {@see $default_records}.
3488
     * This is a map from fieldname to default value.
3489
     *
3490
     *  - If you would like to change a default value in a sub-class, just specify it.
3491
     *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3492
     *    or false in your subclass.  Setting it to null won't work.
3493
     *
3494
     * @var array
3495
     * @config
3496
     */
3497
    private static $defaults = [];
3498
3499
    /**
3500
     * Multidimensional array which inserts default data into the database
3501
     * on a db/build-call as long as the database-table is empty. Please use this only
3502
     * for simple constructs, not for SiteTree-Objects etc. which need special
3503
     * behaviour such as publishing and ParentNodes.
3504
     *
3505
     * Example:
3506
     * array(
3507
     *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3508
     *  array('Title' => "DefaultPage2")
3509
     * ).
3510
     *
3511
     * @var array
3512
     * @config
3513
     */
3514
    private static $default_records = null;
3515
3516
    /**
3517
     * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3518
     * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3519
     *
3520
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3521
     *
3522
     *  @var array
3523
     * @config
3524
     */
3525
    private static $has_one = [];
3526
3527
    /**
3528
     * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3529
     *
3530
     * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3531
     * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3532
     * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3533
     *
3534
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3535
     *
3536
     * @var array
3537
     * @config
3538
     */
3539
    private static $belongs_to = [];
3540
3541
    /**
3542
     * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3543
     *
3544
     * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3545
     * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3546
     * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3547
     * which foreign key to use.
3548
     *
3549
     * @var array
3550
     * @config
3551
     */
3552
    private static $has_many = [];
3553
3554
    /**
3555
     * many-many relationship definitions.
3556
     * This is a map from component name to data type.
3557
     * @var array
3558
     * @config
3559
     */
3560
    private static $many_many = [];
3561
3562
    /**
3563
     * Extra fields to include on the connecting many-many table.
3564
     * This is a map from field name to field type.
3565
     *
3566
     * Example code:
3567
     * <code>
3568
     * public static $many_many_extraFields = array(
3569
     *  'Members' => array(
3570
     *          'Role' => 'Varchar(100)'
3571
     *      )
3572
     * );
3573
     * </code>
3574
     *
3575
     * @var array
3576
     * @config
3577
     */
3578
    private static $many_many_extraFields = [];
3579
3580
    /**
3581
     * The inverse side of a many-many relationship.
3582
     * This is a map from component name to data type.
3583
     * @var array
3584
     * @config
3585
     */
3586
    private static $belongs_many_many = [];
3587
3588
    /**
3589
     * The default sort expression. This will be inserted in the ORDER BY
3590
     * clause of a SQL query if no other sort expression is provided.
3591
     * @var string
3592
     * @config
3593
     */
3594
    private static $default_sort = null;
3595
3596
    /**
3597
     * Default list of fields that can be scaffolded by the ModelAdmin
3598
     * search interface.
3599
     *
3600
     * Overriding the default filter, with a custom defined filter:
3601
     * <code>
3602
     *  static $searchable_fields = array(
3603
     *     "Name" => "PartialMatchFilter"
3604
     *  );
3605
     * </code>
3606
     *
3607
     * Overriding the default form fields, with a custom defined field.
3608
     * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3609
     * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3610
     * <code>
3611
     *  static $searchable_fields = array(
3612
     *    "Name" => array(
3613
     *      "field" => "TextField"
3614
     *    )
3615
     *  );
3616
     * </code>
3617
     *
3618
     * Overriding the default form field, filter and title:
3619
     * <code>
3620
     *  static $searchable_fields = array(
3621
     *    "Organisation.ZipCode" => array(
3622
     *      "field" => "TextField",
3623
     *      "filter" => "PartialMatchFilter",
3624
     *      "title" => 'Organisation ZIP'
3625
     *    )
3626
     *  );
3627
     * </code>
3628
     * @config
3629
     */
3630
    private static $searchable_fields = null;
3631
3632
    /**
3633
     * User defined labels for searchable_fields, used to override
3634
     * default display in the search form.
3635
     * @config
3636
     */
3637
    private static $field_labels = [];
3638
3639
    /**
3640
     * Provides a default list of fields to be used by a 'summary'
3641
     * view of this object.
3642
     * @config
3643
     */
3644
    private static $summary_fields = [];
3645
3646
    public function provideI18nEntities()
3647
    {
3648
        // Note: see http://guides.rubyonrails.org/i18n.html#pluralization for rules
3649
        // Best guess for a/an rule. Better guesses require overriding in subclasses
3650
        $pluralName = $this->plural_name();
3651
        $singularName = $this->singular_name();
3652
        $conjunction = preg_match('/^[aeiou]/i', $singularName) ? 'An ' : 'A ';
3653
        return [
3654
            static::class.'.SINGULARNAME' => $this->singular_name(),
3655
            static::class.'.PLURALNAME' => $pluralName,
3656
            static::class.'.PLURALS' => [
3657
                'one' => $conjunction . $singularName,
3658
                'other' => '{count} ' . $pluralName
3659
            ]
3660
        ];
3661
    }
3662
3663
    /**
3664
     * Returns true if the given method/parameter has a value
3665
     * (Uses the DBField::hasValue if the parameter is a database field)
3666
     *
3667
     * @param string $field The field name
3668
     * @param array $arguments
3669
     * @param bool $cache
3670
     * @return boolean
3671
     */
3672
    public function hasValue($field, $arguments = null, $cache = true)
3673
    {
3674
        // has_one fields should not use dbObject to check if a value is given
3675
        $hasOne = static::getSchema()->hasOneComponent(static::class, $field);
3676
        if (!$hasOne && ($obj = $this->dbObject($field))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasOne of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3677
            return $obj->exists();
3678
        } else {
3679
            return parent::hasValue($field, $arguments, $cache);
0 ignored issues
show
Bug introduced by
It seems like $arguments defined by parameter $arguments on line 3672 can also be of type null; however, SilverStripe\View\ViewableData::hasValue() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
3680
        }
3681
    }
3682
3683
    /**
3684
     * If selected through a many_many through relation, this is the instance of the joined record
3685
     *
3686
     * @return DataObject
3687
     */
3688
    public function getJoin()
3689
    {
3690
        return $this->joinRecord;
3691
    }
3692
3693
    /**
3694
     * Set joining object
3695
     *
3696
     * @param DataObject $object
3697
     * @param string $alias Alias
3698
     * @return $this
3699
     */
3700
    public function setJoin(DataObject $object, $alias = null)
3701
    {
3702
        $this->joinRecord = $object;
3703
        if ($alias) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $alias of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3704
            if (static::getSchema()->fieldSpec(static::class, $alias)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::getSchema()->fie...(static::class, $alias) of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3705
                throw new InvalidArgumentException(
3706
                    "Joined record $alias cannot also be a db field"
3707
                );
3708
            }
3709
            $this->record[$alias] = $object;
3710
        }
3711
        return $this;
3712
    }
3713
}
3714