Completed
Push — get-one-miss-null ( 3738c0 )
by Sam
09:42
created

DataObject::get_one()   C

Complexity

Conditions 11
Paths 24

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 20
nc 24
nop 4
dl 0
loc 32
rs 5.2653
c 0
b 0
f 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
919
                foreach ($manyMany as $relationship => $class) {
920
                    /** @var DataObject $leftComponents */
921
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
922
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
923
                    if ($rightComponents && $rightComponents->exists()) {
924
                        $leftComponents->addMany($rightComponents->column('ID'));
925
                    }
926
                    $leftComponents->write();
927
                }
928
            }
929
930 View Code Duplication
            if ($hasMany = $this->hasMany()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
931
                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...
932
                    $leftComponents = $leftObj->getComponents($relationship);
933
                    $rightComponents = $rightObj->getComponents($relationship);
934
                    if ($rightComponents && $rightComponents->exists()) {
935
                        $leftComponents->addMany($rightComponents->column('ID'));
936
                    }
937
                    $leftComponents->write();
938
                }
939
            }
940
        }
941
942
        return true;
943
    }
944
945
    /**
946
     * Forces the record to think that all its data has changed.
947
     * Doesn't write to the database. Only sets fields as changed
948
     * if they are not already marked as changed.
949
     *
950
     * @return $this
951
     */
952
    public function forceChange()
953
    {
954
        // Ensure lazy fields loaded
955
        $this->loadLazyFields();
956
        $fields = static::getSchema()->fieldSpecs(static::class);
957
958
        // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
959
        $fieldNames = array_unique(array_merge(
960
            array_keys($this->record),
961
            array_keys($fields)
962
        ));
963
964
        foreach ($fieldNames as $fieldName) {
965
            if (!isset($this->changed[$fieldName])) {
966
                $this->changed[$fieldName] = self::CHANGE_STRICT;
967
            }
968
            // Populate the null values in record so that they actually get written
969
            if (!isset($this->record[$fieldName])) {
970
                $this->record[$fieldName] = null;
971
            }
972
        }
973
974
        // @todo Find better way to allow versioned to write a new version after forceChange
975
        if ($this->isChanged('Version')) {
976
            unset($this->changed['Version']);
977
        }
978
        return $this;
979
    }
980
981
    /**
982
     * Validate the current object.
983
     *
984
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
985
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
986
     *
987
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
988
     * and onAfterWrite() won't get called either.
989
     *
990
     * It is expected that you call validate() in your own application to test that an object is valid before
991
     * attempting a write, and respond appropriately if it isn't.
992
     *
993
     * @see {@link ValidationResult}
994
     * @return ValidationResult
995
     */
996
    public function validate()
997
    {
998
        $result = ValidationResult::create();
999
        $this->extend('validate', $result);
1000
        return $result;
1001
    }
1002
1003
    /**
1004
     * Public accessor for {@see DataObject::validate()}
1005
     *
1006
     * @return ValidationResult
1007
     */
1008
    public function doValidate()
1009
    {
1010
        Deprecation::notice('5.0', 'Use validate');
1011
        return $this->validate();
1012
    }
1013
1014
    /**
1015
     * Event handler called before writing to the database.
1016
     * You can overload this to clean up or otherwise process data before writing it to the
1017
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1018
     *
1019
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1020
     *
1021
     * @uses DataExtension->onBeforeWrite()
1022
     */
1023
    protected function onBeforeWrite()
1024
    {
1025
        $this->brokenOnWrite = false;
1026
1027
        $dummy = null;
1028
        $this->extend('onBeforeWrite', $dummy);
1029
    }
1030
1031
    /**
1032
     * Event handler called after writing to the database.
1033
     * You can overload this to act upon changes made to the data after it is written.
1034
     * $this->changed will have a record
1035
     * database.  Don't forget to call parent::onAfterWrite(), though!
1036
     *
1037
     * @uses DataExtension->onAfterWrite()
1038
     */
1039
    protected function onAfterWrite()
1040
    {
1041
        $dummy = null;
1042
        $this->extend('onAfterWrite', $dummy);
1043
    }
1044
1045
    /**
1046
     * Event handler called before deleting from the database.
1047
     * You can overload this to clean up or otherwise process data before delete this
1048
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1049
     *
1050
     * @uses DataExtension->onBeforeDelete()
1051
     */
1052
    protected function onBeforeDelete()
1053
    {
1054
        $this->brokenOnDelete = false;
1055
1056
        $dummy = null;
1057
        $this->extend('onBeforeDelete', $dummy);
1058
    }
1059
1060
    protected function onAfterDelete()
1061
    {
1062
        $this->extend('onAfterDelete');
1063
    }
1064
1065
    /**
1066
     * Load the default values in from the self::$defaults array.
1067
     * Will traverse the defaults of the current class and all its parent classes.
1068
     * Called by the constructor when creating new records.
1069
     *
1070
     * @uses DataExtension->populateDefaults()
1071
     * @return DataObject $this
1072
     */
1073
    public function populateDefaults()
1074
    {
1075
        $classes = array_reverse(ClassInfo::ancestry($this));
1076
1077
        foreach ($classes as $class) {
1078
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1079
1080
            if ($defaults && !is_array($defaults)) {
1081
                user_error(
1082
                    "Bad '" . static::class . "' defaults given: " . var_export($defaults, true),
1083
                    E_USER_WARNING
1084
                );
1085
                $defaults = null;
1086
            }
1087
1088
            if ($defaults) {
1089
                foreach ($defaults as $fieldName => $fieldValue) {
1090
                // SRM 2007-03-06: Stricter check
1091
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1092
                        $this->$fieldName = $fieldValue;
1093
                    }
1094
                // Set many-many defaults with an array of ids
1095
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1096
                        /** @var ManyManyList $manyManyJoin */
1097
                        $manyManyJoin = $this->$fieldName();
1098
                        $manyManyJoin->setByIDList($fieldValue);
1099
                    }
1100
                }
1101
            }
1102
            if ($class == self::class) {
1103
                break;
1104
            }
1105
        }
1106
1107
        $this->extend('populateDefaults');
1108
        return $this;
1109
    }
1110
1111
    /**
1112
     * Determine validation of this object prior to write
1113
     *
1114
     * @return ValidationException Exception generated by this write, or null if valid
1115
     */
1116
    protected function validateWrite()
1117
    {
1118
        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...
1119
            return new ValidationException(
1120
                "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...
1121
                "you need to change the ClassName before you can write it"
1122
            );
1123
        }
1124
1125
        // Note: Validation can only be disabled at the global level, not per-model
1126
        if (DataObject::config()->uninherited('validation_enabled')) {
1127
            $result = $this->validate();
1128
            if (!$result->isValid()) {
1129
                return new ValidationException($result);
1130
            }
1131
        }
1132
        return null;
1133
    }
1134
1135
    /**
1136
     * Prepare an object prior to write
1137
     *
1138
     * @throws ValidationException
1139
     */
1140
    protected function preWrite()
1141
    {
1142
        // Validate this object
1143
        if ($writeException = $this->validateWrite()) {
1144
            // Used by DODs to clean up after themselves, eg, Versioned
1145
            $this->invokeWithExtensions('onAfterSkippedWrite');
1146
            throw $writeException;
1147
        }
1148
1149
        // Check onBeforeWrite
1150
        $this->brokenOnWrite = true;
1151
        $this->onBeforeWrite();
1152
        if ($this->brokenOnWrite) {
1153
            user_error(static::class . " has a broken onBeforeWrite() function."
1154
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1155
        }
1156
    }
1157
1158
    /**
1159
     * Detects and updates all changes made to this object
1160
     *
1161
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1162
     * @return bool True if any changes are detected
1163
     */
1164
    protected function updateChanges($forceChanges = false)
1165
    {
1166
        if ($forceChanges) {
1167
            // Force changes, but only for loaded fields
1168
            foreach ($this->record as $field => $value) {
1169
                $this->changed[$field] = static::CHANGE_VALUE;
1170
            }
1171
            return true;
1172
        }
1173
        return $this->isChanged();
1174
    }
1175
1176
    /**
1177
     * Writes a subset of changes for a specific table to the given manipulation
1178
     *
1179
     * @param string $baseTable Base table
1180
     * @param string $now Timestamp to use for the current time
1181
     * @param bool $isNewRecord Whether this should be treated as a new record write
1182
     * @param array $manipulation Manipulation to write to
1183
     * @param string $class Class of table to manipulate
1184
     */
1185
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1186
    {
1187
        $schema = $this->getSchema();
1188
        $table = $schema->tableName($class);
1189
        $manipulation[$table] = array();
1190
1191
        // Extract records for this table
1192
        foreach ($this->record as $fieldName => $fieldValue) {
1193
            // we're not attempting to reset the BaseTable->ID
1194
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1195
            if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1196
                continue;
1197
            }
1198
1199
            // Ensure this field pertains to this table
1200
            $specification = $schema->fieldSpec($class, $fieldName, DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED);
1201
            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...
1202
                continue;
1203
            }
1204
1205
            // if database column doesn't correlate to a DBField instance...
1206
            $fieldObj = $this->dbObject($fieldName);
1207
            if (!$fieldObj) {
1208
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1209
            }
1210
1211
            // Write to manipulation
1212
            $fieldObj->writeToManipulation($manipulation[$table]);
1213
        }
1214
1215
        // Ensure update of Created and LastEdited columns
1216
        if ($baseTable === $table) {
1217
            $manipulation[$table]['fields']['LastEdited'] = $now;
1218
            if ($isNewRecord) {
1219
                $manipulation[$table]['fields']['Created']
1220
                    = empty($this->record['Created'])
1221
                        ? $now
1222
                        : $this->record['Created'];
1223
                $manipulation[$table]['fields']['ClassName'] = static::class;
1224
            }
1225
        }
1226
1227
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1228
        // attempt an update, as though it were a normal update.
1229
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1230
        $manipulation[$table]['id'] = $this->record['ID'];
1231
        $manipulation[$table]['class'] = $class;
1232
    }
1233
1234
    /**
1235
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1236
     *
1237
     * Does nothing if an ID is already assigned for this record
1238
     *
1239
     * @param string $baseTable Base table
1240
     * @param string $now Timestamp to use for the current time
1241
     */
1242
    protected function writeBaseRecord($baseTable, $now)
1243
    {
1244
        // Generate new ID if not specified
1245
        if ($this->isInDB()) {
1246
            return;
1247
        }
1248
1249
        // Perform an insert on the base table
1250
        $insert = new SQLInsert('"'.$baseTable.'"');
1251
        $insert
1252
            ->assign('"Created"', $now)
1253
            ->execute();
1254
        $this->changed['ID'] = self::CHANGE_VALUE;
1255
        $this->record['ID'] = DB::get_generated_id($baseTable);
1256
    }
1257
1258
    /**
1259
     * Generate and write the database manipulation for all changed fields
1260
     *
1261
     * @param string $baseTable Base table
1262
     * @param string $now Timestamp to use for the current time
1263
     * @param bool $isNewRecord If this is a new record
1264
     */
1265
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1266
    {
1267
        // Generate database manipulations for each class
1268
        $manipulation = array();
1269
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1270
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1271
        }
1272
1273
        // Allow extensions to extend this manipulation
1274
        $this->extend('augmentWrite', $manipulation);
1275
1276
        // New records have their insert into the base data table done first, so that they can pass the
1277
        // generated ID on to the rest of the manipulation
1278
        if ($isNewRecord) {
1279
            $manipulation[$baseTable]['command'] = 'update';
1280
        }
1281
1282
        // Perform the manipulation
1283
        DB::manipulate($manipulation);
1284
    }
1285
1286
    /**
1287
     * Writes all changes to this object to the database.
1288
     *  - It will insert a record whenever ID isn't set, otherwise update.
1289
     *  - All relevant tables will be updated.
1290
     *  - $this->onBeforeWrite() gets called beforehand.
1291
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1292
     *
1293
     *  @uses DataExtension->augmentWrite()
1294
     *
1295
     * @param boolean $showDebug Show debugging information
1296
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1297
     * @param boolean $forceWrite Write to database even if there are no changes
1298
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1299
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1300
     *                                 {@link getManyManyComponents()} (Default: false)
1301
     * @return int The ID of the record
1302
     * @throws ValidationException Exception that can be caught and handled by the calling function
1303
     */
1304
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1305
    {
1306
        $now = DBDatetime::now()->Rfc2822();
1307
1308
        // Execute pre-write tasks
1309
        $this->preWrite();
1310
1311
        // Check if we are doing an update or an insert
1312
        $isNewRecord = !$this->isInDB() || $forceInsert;
1313
1314
        // Check changes exist, abort if there are none
1315
        $hasChanges = $this->updateChanges($isNewRecord);
1316
        if ($hasChanges || $forceWrite || $isNewRecord) {
1317
            // New records have their insert into the base data table done first, so that they can pass the
1318
            // generated primary key on to the rest of the manipulation
1319
            $baseTable = $this->baseTable();
1320
            $this->writeBaseRecord($baseTable, $now);
1321
1322
            // Write the DB manipulation for all changed fields
1323
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1324
1325
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1326
            $this->writeRelations();
1327
            $this->onAfterWrite();
1328
            $this->changed = array();
1329
        } else {
1330
            if ($showDebug) {
1331
                Debug::message("no changes for DataObject");
1332
            }
1333
1334
            // Used by DODs to clean up after themselves, eg, Versioned
1335
            $this->invokeWithExtensions('onAfterSkippedWrite');
1336
        }
1337
1338
        // Ensure Created and LastEdited are populated
1339
        if (!isset($this->record['Created'])) {
1340
            $this->record['Created'] = $now;
1341
        }
1342
        $this->record['LastEdited'] = $now;
1343
1344
        // Write relations as necessary
1345
        if ($writeComponents) {
1346
            $this->writeComponents(true);
1347
        }
1348
1349
        // Clears the cache for this object so get_one returns the correct object.
1350
        $this->flushCache();
1351
1352
        return $this->record['ID'];
1353
    }
1354
1355
    /**
1356
     * Writes cached relation lists to the database, if possible
1357
     */
1358
    public function writeRelations()
1359
    {
1360
        if (!$this->isInDB()) {
1361
            return;
1362
        }
1363
1364
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1365
        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...
1366
            foreach ($this->unsavedRelations as $name => $list) {
1367
                $list->changeToList($this->$name());
1368
            }
1369
            $this->unsavedRelations = array();
1370
        }
1371
    }
1372
1373
    /**
1374
     * Write the cached components to the database. Cached components could refer to two different instances of the
1375
     * same record.
1376
     *
1377
     * @param bool $recursive Recursively write components
1378
     * @return DataObject $this
1379
     */
1380
    public function writeComponents($recursive = false)
1381
    {
1382
        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...
1383
            foreach ($this->components as $component) {
1384
                $component->write(false, false, false, $recursive);
1385
            }
1386
        }
1387
1388
        if ($join = $this->getJoin()) {
1389
            $join->write(false, false, false, $recursive);
1390
        }
1391
1392
        return $this;
1393
    }
1394
1395
    /**
1396
     * Delete this data object.
1397
     * $this->onBeforeDelete() gets called.
1398
     * Note that in Versioned objects, both Stage and Live will be deleted.
1399
     *  @uses DataExtension->augmentSQL()
1400
     */
1401
    public function delete()
1402
    {
1403
        $this->brokenOnDelete = true;
1404
        $this->onBeforeDelete();
1405
        if ($this->brokenOnDelete) {
1406
            user_error(static::class . " has a broken onBeforeDelete() function."
1407
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1408
        }
1409
1410
        // Deleting a record without an ID shouldn't do anything
1411
        if (!$this->ID) {
1412
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1413
        }
1414
1415
        // TODO: This is quite ugly.  To improve:
1416
        //  - move the details of the delete code in the DataQuery system
1417
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1418
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1419
        $srcQuery = DataList::create(static::class)
1420
            ->filter('ID', $this->ID)
1421
            ->dataQuery()
1422
            ->query();
1423
        foreach ($srcQuery->queriedTables() as $table) {
1424
            $delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1425
            $delete->execute();
1426
        }
1427
        // Remove this item out of any caches
1428
        $this->flushCache();
1429
1430
        $this->onAfterDelete();
1431
1432
        $this->OldID = $this->ID;
1433
        $this->ID = 0;
1434
    }
1435
1436
    /**
1437
     * Delete the record with the given ID.
1438
     *
1439
     * @param string $className The class name of the record to be deleted
1440
     * @param int $id ID of record to be deleted
1441
     */
1442
    public static function delete_by_id($className, $id)
1443
    {
1444
        $obj = DataObject::get_by_id($className, $id);
1445
        if ($obj) {
1446
            $obj->delete();
1447
        } else {
1448
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1449
        }
1450
    }
1451
1452
    /**
1453
     * Get the class ancestry, including the current class name.
1454
     * The ancestry will be returned as an array of class names, where the 0th element
1455
     * will be the class that inherits directly from DataObject, and the last element
1456
     * will be the current class.
1457
     *
1458
     * @return array Class ancestry
1459
     */
1460
    public function getClassAncestry()
1461
    {
1462
        return ClassInfo::ancestry(static::class);
1463
    }
1464
1465
    /**
1466
     * Return a component object from a one to one relationship, as a DataObject.
1467
     * If no component is available, an 'empty component' will be returned for
1468
     * non-polymorphic relations, or for polymorphic relations with a class set.
1469
     *
1470
     * @param string $componentName Name of the component
1471
     * @return DataObject The component object. It's exact type will be that of the component.
1472
     * @throws Exception
1473
     */
1474
    public function getComponent($componentName)
1475
    {
1476
        if (isset($this->components[$componentName])) {
1477
            return $this->components[$componentName];
1478
        }
1479
1480
        $schema = static::getSchema();
1481
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1482
            $joinField = $componentName . 'ID';
1483
            $joinID    = $this->getField($joinField);
1484
1485
            // Extract class name for polymorphic relations
1486
            if ($class === self::class) {
1487
                $class = $this->getField($componentName . 'Class');
1488
                if (empty($class)) {
1489
                    return null;
1490
                }
1491
            }
1492
1493
            if ($joinID) {
1494
                // Ensure that the selected object originates from the same stage, subsite, etc
1495
                $component = DataObject::get($class)
1496
                    ->filter('ID', $joinID)
1497
                    ->setDataQueryParam($this->getInheritableQueryParams())
1498
                    ->first();
1499
            }
1500
1501
            if (empty($component)) {
1502
                $component = Injector::inst()->create($class);
1503
            }
1504
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1505
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1506
            $joinID = $this->ID;
1507
1508
            if ($joinID) {
1509
                // Prepare filter for appropriate join type
1510
                if ($polymorphic) {
1511
                    $filter = array(
1512
                        "{$joinField}ID" => $joinID,
1513
                        "{$joinField}Class" => static::class,
1514
                    );
1515
                } else {
1516
                    $filter = array(
1517
                        $joinField => $joinID
1518
                    );
1519
                }
1520
1521
                // Ensure that the selected object originates from the same stage, subsite, etc
1522
                $component = DataObject::get($class)
1523
                    ->filter($filter)
1524
                    ->setDataQueryParam($this->getInheritableQueryParams())
1525
                    ->first();
1526
            }
1527
1528
            if (empty($component)) {
1529
                $component = Injector::inst()->create($class);
1530
                if ($polymorphic) {
1531
                    $component->{$joinField.'ID'} = $this->ID;
1532
                    $component->{$joinField.'Class'} = static::class;
1533
                } else {
1534
                    $component->$joinField = $this->ID;
1535
                }
1536
            }
1537
        } else {
1538
            throw new InvalidArgumentException(
1539
                "DataObject->getComponent(): Could not find component '$componentName'."
1540
            );
1541
        }
1542
1543
        $this->components[$componentName] = $component;
1544
        return $component;
1545
    }
1546
1547
    /**
1548
     * Returns a one-to-many relation as a HasManyList
1549
     *
1550
     * @param string $componentName Name of the component
1551
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1552
     */
1553
    public function getComponents($componentName)
1554
    {
1555
        $result = null;
0 ignored issues
show
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1556
1557
        $schema = $this->getSchema();
1558
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1559
        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...
1560
            throw new InvalidArgumentException(sprintf(
1561
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1562
                $componentName,
1563
                static::class
1564
            ));
1565
        }
1566
1567
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1568
        if (!$this->ID) {
1569
            if (!isset($this->unsavedRelations[$componentName])) {
1570
                $this->unsavedRelations[$componentName] =
1571
                    new UnsavedRelationList(static::class, $componentName, $componentClass);
1572
            }
1573
            return $this->unsavedRelations[$componentName];
1574
        }
1575
1576
        // Determine type and nature of foreign relation
1577
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1578
        /** @var HasManyList $result */
1579
        if ($polymorphic) {
1580
            $result = PolymorphicHasManyList::create($componentClass, $joinField, static::class);
1581
        } else {
1582
            $result = HasManyList::create($componentClass, $joinField);
1583
        }
1584
1585
        return $result
1586
            ->setDataQueryParam($this->getInheritableQueryParams())
1587
            ->forForeignID($this->ID);
1588
    }
1589
1590
    /**
1591
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1592
     *
1593
     * @param string $relationName Relation name.
1594
     * @return string Class name, or null if not found.
1595
     */
1596
    public function getRelationClass($relationName)
1597
    {
1598
        // Parse many_many
1599
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1600
        if ($manyManyComponent) {
1601
            return $manyManyComponent['childClass'];
1602
        }
1603
1604
        // Go through all relationship configuration fields.
1605
        $config = $this->config();
1606
        $candidates = array_merge(
1607
            ($relations = $config->get('has_one')) ? $relations : array(),
1608
            ($relations = $config->get('has_many')) ? $relations : array(),
1609
            ($relations = $config->get('belongs_to')) ? $relations : array()
1610
        );
1611
1612
        if (isset($candidates[$relationName])) {
1613
            $remoteClass = $candidates[$relationName];
1614
1615
            // If dot notation is present, extract just the first part that contains the class.
1616 View Code Duplication
            if (($fieldPos = strpos($remoteClass, '.'))!==false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1617
                return substr($remoteClass, 0, $fieldPos);
1618
            }
1619
1620
            // Otherwise just return the class
1621
            return $remoteClass;
1622
        }
1623
1624
        return null;
1625
    }
1626
1627
    /**
1628
     * Given a relation name, determine the relation type
1629
     *
1630
     * @param string $component Name of component
1631
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1632
     */
1633
    public function getRelationType($component)
1634
    {
1635
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1636
        $config = $this->config();
1637
        foreach ($types as $type) {
1638
            $relations = $config->get($type);
1639
            if ($relations && isset($relations[$component])) {
1640
                return $type;
1641
            }
1642
        }
1643
        return null;
1644
    }
1645
1646
    /**
1647
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1648
     * side of the relation.
1649
     *
1650
     * Notes on behaviour:
1651
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1652
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1653
     *  - Cannot be used on polymorphic relationships
1654
     *  - Cannot be used on unsaved objects.
1655
     *
1656
     * @param string $remoteClass
1657
     * @param string $remoteRelation
1658
     * @return DataList|DataObject The component, either as a list or single object
1659
     * @throws BadMethodCallException
1660
     * @throws InvalidArgumentException
1661
     */
1662
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1663
    {
1664
        $remote = DataObject::singleton($remoteClass);
1665
        $class = $remote->getRelationClass($remoteRelation);
1666
        $schema = static::getSchema();
1667
1668
        // Validate arguments
1669
        if (!$this->isInDB()) {
1670
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1671
        }
1672
        if (empty($class)) {
1673
            throw new InvalidArgumentException(sprintf(
1674
                "%s invoked with invalid relation %s.%s",
1675
                __METHOD__,
1676
                $remoteClass,
1677
                $remoteRelation
1678
            ));
1679
        }
1680
        if ($class === self::class) {
1681
            throw new InvalidArgumentException(sprintf(
1682
                "%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1683
                "This method does not support polymorphic relationships",
1684
                __METHOD__,
1685
                $remoteClass,
1686
                $remoteRelation
1687
            ));
1688
        }
1689
        if (!is_a($this, $class, true)) {
1690
            throw new InvalidArgumentException(sprintf(
1691
                "Relation %s on %s does not refer to objects of type %s",
1692
                $remoteRelation,
1693
                $remoteClass,
1694
                static::class
1695
            ));
1696
        }
1697
1698
        // Check the relation type to mock
1699
        $relationType = $remote->getRelationType($remoteRelation);
1700
        switch ($relationType) {
1701
            case 'has_one': {
1702
                // Mock has_many
1703
                $joinField = "{$remoteRelation}ID";
1704
                $componentClass = $schema->classForField($remoteClass, $joinField);
1705
                $result = HasManyList::create($componentClass, $joinField);
1706
                return $result
1707
                    ->setDataQueryParam($this->getInheritableQueryParams())
1708
                    ->forForeignID($this->ID);
1709
            }
1710
            case 'belongs_to':
1711
            case 'has_many': {
1712
                // These relations must have a has_one on the other end, so find it
1713
                $joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic);
1714
                if ($polymorphic) {
1715
                    throw new InvalidArgumentException(sprintf(
1716
                        "%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1717
                        "to be a has_one polymorphic. This method does not support polymorphic relationships",
1718
                        __METHOD__,
1719
                        $remoteClass,
1720
                        $remoteRelation
1721
                    ));
1722
                }
1723
                $joinID = $this->getField($joinField);
1724
                if (empty($joinID)) {
1725
                    return null;
1726
                }
1727
                // Get object by joined ID
1728
                return DataObject::get($remoteClass)
1729
                    ->filter('ID', $joinID)
1730
                    ->setDataQueryParam($this->getInheritableQueryParams())
1731
                    ->first();
1732
            }
1733
            case 'many_many':
1734
            case 'belongs_many_many': {
1735
                // Get components and extra fields from parent
1736
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1737
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1738
1739
                // Reverse parent and component fields and create an inverse ManyManyList
1740
                /** @var RelationList $result */
1741
                $result = Injector::inst()->create(
1742
                    $manyMany['relationClass'],
1743
                    $manyMany['parentClass'], // Substitute parent class for dataClass
1744
                    $manyMany['join'],
1745
                    $manyMany['parentField'], // Reversed parent / child field
1746
                    $manyMany['childField'], // Reversed parent / child field
1747
                    $extraFields
1748
                );
1749
                $this->extend('updateManyManyComponents', $result);
1750
1751
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1752
                // foreignID set elsewhere.
1753
                return $result
1754
                    ->setDataQueryParam($this->getInheritableQueryParams())
1755
                    ->forForeignID($this->ID);
1756
            }
1757
            default: {
1758
                return null;
1759
            }
1760
        }
1761
    }
1762
1763
    /**
1764
     * Returns a many-to-many component, as a ManyManyList.
1765
     * @param string $componentName Name of the many-many component
1766
     * @return RelationList|UnsavedRelationList The set of components
1767
     */
1768
    public function getManyManyComponents($componentName)
1769
    {
1770
        $schema = static::getSchema();
1771
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
1772
        if (!$manyManyComponent) {
1773
            throw new InvalidArgumentException(sprintf(
1774
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1775
                $componentName,
1776
                static::class
1777
            ));
1778
        }
1779
1780
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1781
        if (!$this->ID) {
1782
            if (!isset($this->unsavedRelations[$componentName])) {
1783
                $this->unsavedRelations[$componentName] =
1784
                    new UnsavedRelationList($manyManyComponent['parentClass'], $componentName, $manyManyComponent['childClass']);
1785
            }
1786
            return $this->unsavedRelations[$componentName];
1787
        }
1788
1789
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
1790
        /** @var RelationList $result */
1791
        $result = Injector::inst()->create(
1792
            $manyManyComponent['relationClass'],
1793
            $manyManyComponent['childClass'],
1794
            $manyManyComponent['join'],
1795
            $manyManyComponent['childField'],
1796
            $manyManyComponent['parentField'],
1797
            $extraFields
1798
        );
1799
1800
1801
        // Store component data in query meta-data
1802
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
1803
            /** @var DataQuery $query */
1804
            $query->setQueryParam('Component.ExtraFields', $extraFields);
1805
        });
1806
1807
        $this->extend('updateManyManyComponents', $result);
1808
1809
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1810
        // foreignID set elsewhere.
1811
        return $result
1812
            ->setDataQueryParam($this->getInheritableQueryParams())
1813
            ->forForeignID($this->ID);
1814
    }
1815
1816
    /**
1817
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1818
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1819
     *
1820
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1821
     *                          their classes.
1822
     */
1823
    public function hasOne()
1824
    {
1825
        return (array)$this->config()->get('has_one');
1826
    }
1827
1828
    /**
1829
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1830
     * their class name will be returned.
1831
     *
1832
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1833
     *        the field data stripped off. It defaults to TRUE.
1834
     * @return string|array
1835
     */
1836 View Code Duplication
    public function belongsTo($classOnly = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1837
    {
1838
        $belongsTo = (array)$this->config()->get('belongs_to');
1839
        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...
1840
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1841
        } else {
1842
            return $belongsTo ? $belongsTo : array();
1843
        }
1844
    }
1845
1846
    /**
1847
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1848
     * relationships and their classes will be returned.
1849
     *
1850
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1851
     *        the field data stripped off. It defaults to TRUE.
1852
     * @return string|array|false
1853
     */
1854 View Code Duplication
    public function hasMany($classOnly = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1855
    {
1856
        $hasMany = (array)$this->config()->get('has_many');
1857
        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...
1858
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1859
        } else {
1860
            return $hasMany ? $hasMany : array();
1861
        }
1862
    }
1863
1864
    /**
1865
     * Return the many-to-many extra fields specification.
1866
     *
1867
     * If you don't specify a component name, it returns all
1868
     * extra fields for all components available.
1869
     *
1870
     * @return array|null
1871
     */
1872
    public function manyManyExtraFields()
1873
    {
1874
        return $this->config()->get('many_many_extraFields');
1875
    }
1876
1877
    /**
1878
     * Return information about a many-to-many component.
1879
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
1880
     * components are returned.
1881
     *
1882
     * @see DataObjectSchema::manyManyComponent()
1883
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
1884
     */
1885
    public function manyMany()
1886
    {
1887
        $config = $this->config();
1888
        $manyManys = (array)$config->get('many_many');
1889
        $belongsManyManys = (array)$config->get('belongs_many_many');
1890
        $items = array_merge($manyManys, $belongsManyManys);
1891
        return $items;
1892
    }
1893
1894
    /**
1895
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
1896
     *
1897
     * This is experimental, and is currently only a Postgres-specific enhancement.
1898
     *
1899
     * @param string $class
1900
     * @return array|false
1901
     */
1902
    public function database_extensions($class)
1903
    {
1904
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
1905
        if ($extensions) {
1906
            return $extensions;
1907
        } else {
1908
            return false;
1909
        }
1910
    }
1911
1912
    /**
1913
     * Generates a SearchContext to be used for building and processing
1914
     * a generic search form for properties on this object.
1915
     *
1916
     * @return SearchContext
1917
     */
1918
    public function getDefaultSearchContext()
1919
    {
1920
        return new SearchContext(
1921
            static::class,
1922
            $this->scaffoldSearchFields(),
1923
            $this->defaultSearchFilters()
1924
        );
1925
    }
1926
1927
    /**
1928
     * Determine which properties on the DataObject are
1929
     * searchable, and map them to their default {@link FormField}
1930
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
1931
     *
1932
     * Some additional logic is included for switching field labels, based on
1933
     * how generic or specific the field type is.
1934
     *
1935
     * Used by {@link SearchContext}.
1936
     *
1937
     * @param array $_params
1938
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
1939
     *   'restrictFields': Numeric array of a field name whitelist
1940
     * @return FieldList
1941
     */
1942
    public function scaffoldSearchFields($_params = null)
1943
    {
1944
        $params = array_merge(
1945
            array(
1946
                'fieldClasses' => false,
1947
                'restrictFields' => false
1948
            ),
1949
            (array)$_params
1950
        );
1951
        $fields = new FieldList();
1952
        foreach ($this->searchableFields() as $fieldName => $spec) {
1953
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
1954
                continue;
1955
            }
1956
1957
            // If a custom fieldclass is provided as a string, use it
1958
            $field = null;
1959
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
1960
                $fieldClass = $params['fieldClasses'][$fieldName];
1961
                $field = new $fieldClass($fieldName);
1962
            // If we explicitly set a field, then construct that
1963
            } elseif (isset($spec['field'])) {
1964
                // If it's a string, use it as a class name and construct
1965
                if (is_string($spec['field'])) {
1966
                    $fieldClass = $spec['field'];
1967
                    $field = new $fieldClass($fieldName);
1968
1969
                // If it's a FormField object, then just use that object directly.
1970
                } elseif ($spec['field'] instanceof FormField) {
1971
                    $field = $spec['field'];
1972
1973
                // Otherwise we have a bug
1974
                } else {
1975
                    user_error("Bad value for searchable_fields, 'field' value: "
1976
                        . var_export($spec['field'], true), E_USER_WARNING);
1977
                }
1978
1979
            // Otherwise, use the database field's scaffolder
1980
            } else {
1981
                $field = $this->relObject($fieldName)->scaffoldSearchField();
1982
            }
1983
1984
            // Allow fields to opt out of search
1985
            if (!$field) {
1986
                continue;
1987
            }
1988
1989
            if (strstr($fieldName, '.')) {
1990
                $field->setName(str_replace('.', '__', $fieldName));
1991
            }
1992
            $field->setTitle($spec['title']);
1993
1994
            $fields->push($field);
1995
        }
1996
        return $fields;
1997
    }
1998
1999
    /**
2000
     * Scaffold a simple edit form for all properties on this dataobject,
2001
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2002
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2003
     *
2004
     * @uses FormScaffolder
2005
     *
2006
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2007
     * @return FieldList
2008
     */
2009
    public function scaffoldFormFields($_params = null)
2010
    {
2011
        $params = array_merge(
2012
            array(
2013
                'tabbed' => false,
2014
                'includeRelations' => false,
2015
                'restrictFields' => false,
2016
                'fieldClasses' => false,
2017
                'ajaxSafe' => false
2018
            ),
2019
            (array)$_params
2020
        );
2021
2022
        $fs = FormScaffolder::create($this);
2023
        $fs->tabbed = $params['tabbed'];
2024
        $fs->includeRelations = $params['includeRelations'];
2025
        $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...
2026
        $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...
2027
        $fs->ajaxSafe = $params['ajaxSafe'];
2028
2029
        return $fs->getFieldList();
2030
    }
2031
2032
    /**
2033
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2034
     * being called on extensions
2035
     *
2036
     * @param callable $callback The callback to execute
2037
     */
2038
    protected function beforeUpdateCMSFields($callback)
2039
    {
2040
        $this->beforeExtending('updateCMSFields', $callback);
2041
    }
2042
2043
    /**
2044
     * Centerpiece of every data administration interface in Silverstripe,
2045
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2046
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2047
     * generate this set. To customize, overload this method in a subclass
2048
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2049
     *
2050
     * <code>
2051
     * class MyCustomClass extends DataObject {
2052
     *  static $db = array('CustomProperty'=>'Boolean');
2053
     *
2054
     *  function getCMSFields() {
2055
     *    $fields = parent::getCMSFields();
2056
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2057
     *    return $fields;
2058
     *  }
2059
     * }
2060
     * </code>
2061
     *
2062
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2063
     *
2064
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2065
     */
2066
    public function getCMSFields()
2067
    {
2068
        $tabbedFields = $this->scaffoldFormFields(array(
2069
            // Don't allow has_many/many_many relationship editing before the record is first saved
2070
            'includeRelations' => ($this->ID > 0),
2071
            'tabbed' => true,
2072
            'ajaxSafe' => true
2073
        ));
2074
2075
        $this->extend('updateCMSFields', $tabbedFields);
2076
2077
        return $tabbedFields;
2078
    }
2079
2080
    /**
2081
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2082
     * including that dataobject's extensions customised actions could be added to the EditForm.
2083
     *
2084
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2085
     */
2086
    public function getCMSActions()
2087
    {
2088
        $actions = new FieldList();
2089
        $this->extend('updateCMSActions', $actions);
2090
        return $actions;
2091
    }
2092
2093
2094
    /**
2095
     * Used for simple frontend forms without relation editing
2096
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2097
     * by default. To customize, either overload this method in your
2098
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2099
     *
2100
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2101
     *
2102
     * @param array $params See {@link scaffoldFormFields()}
2103
     * @return FieldList Always returns a simple field collection without TabSet.
2104
     */
2105
    public function getFrontEndFields($params = null)
2106
    {
2107
        $untabbedFields = $this->scaffoldFormFields($params);
2108
        $this->extend('updateFrontEndFields', $untabbedFields);
2109
2110
        return $untabbedFields;
2111
    }
2112
2113
    /**
2114
     * Gets the value of a field.
2115
     * Called by {@link __get()} and any getFieldName() methods you might create.
2116
     *
2117
     * @param string $field The name of the field
2118
     * @return mixed The field value
2119
     */
2120
    public function getField($field)
2121
    {
2122
        // If we already have an object in $this->record, then we should just return that
2123
        if (isset($this->record[$field]) && is_object($this->record[$field])) {
2124
            return $this->record[$field];
2125
        }
2126
2127
        // Do we have a field that needs to be lazy loaded?
2128 View Code Duplication
        if (isset($this->record[$field.'_Lazy'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2129
            $tableClass = $this->record[$field.'_Lazy'];
2130
            $this->loadLazyFields($tableClass);
2131
        }
2132
2133
        // In case of complex fields, return the DBField object
2134
        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...
2135
            $this->record[$field] = $this->dbObject($field);
2136
        }
2137
2138
        return isset($this->record[$field]) ? $this->record[$field] : null;
2139
    }
2140
2141
    /**
2142
     * Loads all the stub fields that an initial lazy load didn't load fully.
2143
     *
2144
     * @param string $class Class to load the values from. Others are joined as required.
2145
     * Not specifying a tableClass will load all lazy fields from all tables.
2146
     * @return bool Flag if lazy loading succeeded
2147
     */
2148
    protected function loadLazyFields($class = null)
2149
    {
2150
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2151
            return false;
2152
        }
2153
2154
        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...
2155
            $loaded = array();
2156
2157
            foreach ($this->record as $key => $value) {
2158
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2159
                    $this->loadLazyFields($value);
2160
                    $loaded[$value] = $value;
2161
                }
2162
            }
2163
2164
            return false;
2165
        }
2166
2167
        $dataQuery = new DataQuery($class);
2168
2169
        // Reset query parameter context to that of this DataObject
2170
        if ($params = $this->getSourceQueryParams()) {
2171
            foreach ($params as $key => $value) {
2172
                $dataQuery->setQueryParam($key, $value);
2173
            }
2174
        }
2175
2176
        // Limit query to the current record, unless it has the Versioned extension,
2177
        // in which case it requires special handling through augmentLoadLazyFields()
2178
        $schema = static::getSchema();
2179
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2180
        $dataQuery->where([
2181
            $baseIDColumn => $this->record['ID']
2182
        ])->limit(1);
2183
2184
        $columns = array();
2185
2186
        // Add SQL for fields, both simple & multi-value
2187
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2188
        $databaseFields = $schema->databaseFields($class, false);
2189
        foreach ($databaseFields as $k => $v) {
2190
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2191
                $columns[] = $k;
2192
            }
2193
        }
2194
2195
        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...
2196
            $query = $dataQuery->query();
2197
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2198
            $this->extend('augmentSQL', $query, $dataQuery);
2199
2200
            $dataQuery->setQueriedColumns($columns);
2201
            $newData = $dataQuery->execute()->record();
2202
2203
            // Load the data into record
2204
            if ($newData) {
2205
                foreach ($newData as $k => $v) {
2206
                    if (in_array($k, $columns)) {
2207
                        $this->record[$k] = $v;
2208
                        $this->original[$k] = $v;
2209
                        unset($this->record[$k . '_Lazy']);
2210
                    }
2211
                }
2212
2213
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2214
            } else {
2215
                foreach ($columns as $k) {
2216
                    $this->record[$k] = null;
2217
                    $this->original[$k] = null;
2218
                    unset($this->record[$k . '_Lazy']);
2219
                }
2220
            }
2221
        }
2222
        return true;
2223
    }
2224
2225
    /**
2226
     * Return the fields that have changed.
2227
     *
2228
     * The change level affects what the functions defines as "changed":
2229
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2230
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2231
     *   for example a change from 0 to null would not be included.
2232
     *
2233
     * Example return:
2234
     * <code>
2235
     * array(
2236
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2237
     * )
2238
     * </code>
2239
     *
2240
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2241
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2242
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2243
     * @return array
2244
     */
2245
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2246
    {
2247
        $changedFields = array();
2248
2249
        // Update the changed array with references to changed obj-fields
2250
        foreach ($this->record as $k => $v) {
2251
            // Prevents DBComposite infinite looping on isChanged
2252
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2253
                continue;
2254
            }
2255
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2256
                $this->changed[$k] = self::CHANGE_VALUE;
2257
            }
2258
        }
2259
2260
        if (is_array($databaseFieldsOnly)) {
2261
            $fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2262
        } elseif ($databaseFieldsOnly) {
2263
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2264
            $fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
2265
        } else {
2266
            $fields = $this->changed;
2267
        }
2268
2269
        // Filter the list to those of a certain change level
2270
        if ($changeLevel > self::CHANGE_STRICT) {
2271
            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...
2272
                foreach ($fields as $name => $level) {
2273
                    if ($level < $changeLevel) {
2274
                        unset($fields[$name]);
2275
                    }
2276
                }
2277
            }
2278
        }
2279
2280
        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...
2281
            foreach ($fields as $name => $level) {
2282
                $changedFields[$name] = array(
2283
                'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2284
                'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2285
                'level' => $level
2286
                );
2287
            }
2288
        }
2289
2290
        return $changedFields;
2291
    }
2292
2293
    /**
2294
     * Uses {@link getChangedFields()} to determine if fields have been changed
2295
     * since loading them from the database.
2296
     *
2297
     * @param string $fieldName Name of the database field to check, will check for any if not given
2298
     * @param int $changeLevel See {@link getChangedFields()}
2299
     * @return boolean
2300
     */
2301
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2302
    {
2303
        $fields = $fieldName ? array($fieldName) : true;
2304
        $changed = $this->getChangedFields($fields, $changeLevel);
2305
        if (!isset($fieldName)) {
2306
            return !empty($changed);
2307
        } else {
2308
            return array_key_exists($fieldName, $changed);
2309
        }
2310
    }
2311
2312
    /**
2313
     * Set the value of the field
2314
     * Called by {@link __set()} and any setFieldName() methods you might create.
2315
     *
2316
     * @param string $fieldName Name of the field
2317
     * @param mixed $val New field value
2318
     * @return $this
2319
     */
2320
    public function setField($fieldName, $val)
2321
    {
2322
        $this->objCacheClear();
2323
        //if it's a has_one component, destroy the cache
2324
        if (substr($fieldName, -2) == 'ID') {
2325
            unset($this->components[substr($fieldName, 0, -2)]);
2326
        }
2327
2328
        // If we've just lazy-loaded the column, then we need to populate the $original array
2329 View Code Duplication
        if (isset($this->record[$fieldName.'_Lazy'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2330
            $tableClass = $this->record[$fieldName.'_Lazy'];
2331
            $this->loadLazyFields($tableClass);
2332
        }
2333
2334
        // Situation 1: Passing an DBField
2335
        if ($val instanceof DBField) {
2336
            $val->setName($fieldName);
2337
            $val->saveInto($this);
2338
2339
            // Situation 1a: Composite fields should remain bound in case they are
2340
            // later referenced to update the parent dataobject
2341
            if ($val instanceof DBComposite) {
2342
                $val->bindTo($this);
2343
                $this->record[$fieldName] = $val;
2344
            }
2345
        // Situation 2: Passing a literal or non-DBField object
2346
        } else {
2347
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2348
            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...
2349
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2350
            }
2351
2352
            // if a field is not existing or has strictly changed
2353
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2354
                // TODO Add check for php-level defaults which are not set in the db
2355
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2356
                // At the very least, the type has changed
2357
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2358
2359
                if ((!isset($this->record[$fieldName]) && $val)
2360
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2361
                ) {
2362
                    // Value has changed as well, not just the type
2363
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2364
                }
2365
2366
                // Value is always saved back when strict check succeeds.
2367
                $this->record[$fieldName] = $val;
2368
            }
2369
        }
2370
        return $this;
2371
    }
2372
2373
    /**
2374
     * Set the value of the field, using a casting object.
2375
     * This is useful when you aren't sure that a date is in SQL format, for example.
2376
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2377
     * can be saved into the Image table.
2378
     *
2379
     * @param string $fieldName Name of the field
2380
     * @param mixed $value New field value
2381
     * @return $this
2382
     */
2383
    public function setCastedField($fieldName, $value)
2384
    {
2385
        if (!$fieldName) {
2386
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2387
        }
2388
        $fieldObj = $this->dbObject($fieldName);
2389
        if ($fieldObj) {
2390
            $fieldObj->setValue($value);
2391
            $fieldObj->saveInto($this);
2392
        } else {
2393
            $this->$fieldName = $value;
2394
        }
2395
        return $this;
2396
    }
2397
2398
    /**
2399
     * {@inheritdoc}
2400
     */
2401
    public function castingHelper($field)
2402
    {
2403
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2404
        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...
2405
            return $fieldSpec;
2406
        }
2407
2408
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2409
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2410
        $queryParams = $this->getSourceQueryParams();
2411
        if (!empty($queryParams['Component.ExtraFields'])) {
2412
            $extraFields = $queryParams['Component.ExtraFields'];
2413
2414
            if (isset($extraFields[$field])) {
2415
                return $extraFields[$field];
2416
            }
2417
        }
2418
2419
        return parent::castingHelper($field);
2420
    }
2421
2422
    /**
2423
     * Returns true if the given field exists in a database column on any of
2424
     * the objects tables and optionally look up a dynamic getter with
2425
     * get<fieldName>().
2426
     *
2427
     * @param string $field Name of the field
2428
     * @return boolean True if the given field exists
2429
     */
2430
    public function hasField($field)
2431
    {
2432
        $schema = static::getSchema();
2433
        return (
2434
            array_key_exists($field, $this->record)
2435
            || $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...
2436
            || (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...
2437
            || $this->hasMethod("get{$field}")
2438
        );
2439
    }
2440
2441
    /**
2442
     * Returns true if the given field exists as a database column
2443
     *
2444
     * @param string $field Name of the field
2445
     *
2446
     * @return boolean
2447
     */
2448
    public function hasDatabaseField($field)
2449
    {
2450
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2451
        return !empty($spec);
2452
    }
2453
2454
    /**
2455
     * Returns true if the member is allowed to do the given action.
2456
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2457
     *
2458
     * @param string $perm The permission to be checked, such as 'View'.
2459
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2460
     * in user.
2461
     * @param array $context Additional $context to pass to extendedCan()
2462
     *
2463
     * @return boolean True if the the member is allowed to do the given action
2464
     */
2465
    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...
2466
    {
2467
        if (!$member) {
2468
            $member = Security::getCurrentUser();
2469
        }
2470
2471
        if ($member && Permission::checkMember($member, "ADMIN")) {
2472
            return true;
2473
        }
2474
2475
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2476
            $method = 'can' . ucfirst($perm);
2477
            return $this->$method($member);
2478
        }
2479
2480
        $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...
2481
        if (isset($results)) {
2482
            return $results;
2483
        }
2484
2485
        return ($member && Permission::checkMember($member, $perm));
2486
    }
2487
2488
    /**
2489
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2490
     * expected to return one of three values:
2491
     *
2492
     *  - false: Disallow this permission, regardless of what other extensions say
2493
     *  - true: Allow this permission, as long as no other extensions return false
2494
     *  - NULL: Don't affect the outcome
2495
     *
2496
     * This method itself returns a tri-state value, and is designed to be used like this:
2497
     *
2498
     * <code>
2499
     * $extended = $this->extendedCan('canDoSomething', $member);
2500
     * if($extended !== null) return $extended;
2501
     * else return $normalValue;
2502
     * </code>
2503
     *
2504
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2505
     * @param Member|int $member
2506
     * @param array $context Optional context
2507
     * @return boolean|null
2508
     */
2509
    public function extendedCan($methodName, $member, $context = array())
2510
    {
2511
        $results = $this->extend($methodName, $member, $context);
2512
        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...
2513
            // Remove NULLs
2514
            $results = array_filter($results, function ($v) {
2515
                return !is_null($v);
2516
            });
2517
            // If there are any non-NULL responses, then return the lowest one of them.
2518
            // If any explicitly deny the permission, then we don't get access
2519
            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...
2520
                return min($results);
2521
            }
2522
        }
2523
        return null;
2524
    }
2525
2526
    /**
2527
     * @param Member $member
2528
     * @return boolean
2529
     */
2530 View Code Duplication
    public function canView($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2531
    {
2532
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2530 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...
2533
        if ($extended !== null) {
2534
            return $extended;
2535
        }
2536
        return Permission::check('ADMIN', 'any', $member);
2537
    }
2538
2539
    /**
2540
     * @param Member $member
2541
     * @return boolean
2542
     */
2543 View Code Duplication
    public function canEdit($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2544
    {
2545
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2543 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...
2546
        if ($extended !== null) {
2547
            return $extended;
2548
        }
2549
        return Permission::check('ADMIN', 'any', $member);
2550
    }
2551
2552
    /**
2553
     * @param Member $member
2554
     * @return boolean
2555
     */
2556 View Code Duplication
    public function canDelete($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2557
    {
2558
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2556 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...
2559
        if ($extended !== null) {
2560
            return $extended;
2561
        }
2562
        return Permission::check('ADMIN', 'any', $member);
2563
    }
2564
2565
    /**
2566
     * @param Member $member
2567
     * @param array $context Additional context-specific data which might
2568
     * affect whether (or where) this object could be created.
2569
     * @return boolean
2570
     */
2571 View Code Duplication
    public function canCreate($member = null, $context = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2572
    {
2573
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2571 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...
2574
        if ($extended !== null) {
2575
            return $extended;
2576
        }
2577
        return Permission::check('ADMIN', 'any', $member);
2578
    }
2579
2580
    /**
2581
     * Debugging used by Debug::show()
2582
     *
2583
     * @return string HTML data representing this object
2584
     */
2585
    public function debug()
2586
    {
2587
        $class = static::class;
2588
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2589
        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...
2590
            foreach ($this->record as $fieldName => $fieldVal) {
2591
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2592
            }
2593
        }
2594
        $val .= "</ul>\n";
2595
        return $val;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $val; (string) is incompatible with the return type of the parent method SilverStripe\View\ViewableData::Debug of type SilverStripe\View\ViewableData_Debugger.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2596
    }
2597
2598
    /**
2599
     * Return the DBField object that represents the given field.
2600
     * This works similarly to obj() with 2 key differences:
2601
     *   - it still returns an object even when the field has no value.
2602
     *   - it only matches fields and not methods
2603
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2604
     *
2605
     * @param string $fieldName Name of the field
2606
     * @return DBField The field as a DBField object
2607
     */
2608
    public function dbObject($fieldName)
2609
    {
2610
        // Check for field in DB
2611
        $schema = static::getSchema();
2612
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2613
        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...
2614
            return null;
2615
        }
2616
2617 View Code Duplication
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2618
            $tableClass = $this->record[$fieldName . '_Lazy'];
2619
            $this->loadLazyFields($tableClass);
2620
        }
2621
2622
        $value = isset($this->record[$fieldName])
2623
            ? $this->record[$fieldName]
2624
            : null;
2625
2626
        // If we have a DBField object in $this->record, then return that
2627
        if ($value instanceof DBField) {
2628
            return $value;
2629
        }
2630
2631
        list($class, $spec) = explode('.', $helper);
2632
        /** @var DBField $obj */
2633
        $table = $schema->tableName($class);
2634
        $obj = Injector::inst()->create($spec, $fieldName);
2635
        $obj->setTable($table);
2636
        $obj->setValue($value, $this, false);
2637
        return $obj;
2638
    }
2639
2640
    /**
2641
     * Traverses to a DBField referenced by relationships between data objects.
2642
     *
2643
     * The path to the related field is specified with dot separated syntax
2644
     * (eg: Parent.Child.Child.FieldName).
2645
     *
2646
     * @param string $fieldPath
2647
     *
2648
     * @return mixed DBField of the field on the object or a DataList instance.
2649
     */
2650
    public function relObject($fieldPath)
2651
    {
2652
        $object = null;
0 ignored issues
show
Unused Code introduced by
$object is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
2653
2654
        if (strpos($fieldPath, '.') !== false) {
2655
            $parts = explode('.', $fieldPath);
2656
            $fieldName = array_pop($parts);
2657
2658
            // Traverse dot syntax
2659
            $component = $this;
2660
2661
            foreach ($parts as $relation) {
2662
                if ($component instanceof SS_List) {
2663
                    if (method_exists($component, $relation)) {
2664
                        $component = $component->$relation();
2665
                    } else {
2666
                        /** @var DataList $component */
2667
                        $component = $component->relation($relation);
2668
                    }
2669
                } else {
2670
                    $component = $component->$relation();
2671
                }
2672
            }
2673
2674
            $object = $component->dbObject($fieldName);
2675
        } else {
2676
            $object = $this->dbObject($fieldPath);
2677
        }
2678
2679
        return $object;
2680
    }
2681
2682
    /**
2683
     * Traverses to a field referenced by relationships between data objects, returning the value
2684
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2685
     *
2686
     * @param $fieldName string
2687
     * @return string | null - will return null on a missing value
2688
     */
2689
    public function relField($fieldName)
2690
    {
2691
        $component = $this;
2692
2693
        // We're dealing with relations here so we traverse the dot syntax
2694
        if (strpos($fieldName, '.') !== false) {
2695
            $relations = explode('.', $fieldName);
2696
            $fieldName = array_pop($relations);
2697
            foreach ($relations as $relation) {
2698
                // Inspect $component for element $relation
2699
                if ($component->hasMethod($relation)) {
2700
                    // Check nested method
2701
                    $component = $component->$relation();
2702
                } elseif ($component instanceof SS_List) {
2703
                    // Select adjacent relation from DataList
2704
                    /** @var DataList $component */
2705
                    $component = $component->relation($relation);
2706
                } elseif ($component instanceof DataObject
2707
                    && ($dbObject = $component->dbObject($relation))
2708
                ) {
2709
                    // Select db object
2710
                    $component = $dbObject;
2711
                } else {
2712
                    user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
2713
                }
2714
            }
2715
        }
2716
2717
        // Bail if the component is null
2718
        if (!$component) {
2719
            return null;
2720
        }
2721
        if ($component->hasMethod($fieldName)) {
2722
            return $component->$fieldName();
2723
        }
2724
        return $component->$fieldName;
2725
    }
2726
2727
    /**
2728
     * Temporary hack to return an association name, based on class, to get around the mangle
2729
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2730
     *
2731
     * @param string $className
2732
     * @return string
2733
     */
2734
    public function getReverseAssociation($className)
2735
    {
2736
        if (is_array($this->manyMany())) {
2737
            $many_many = array_flip($this->manyMany());
2738
            if (array_key_exists($className, $many_many)) {
2739
                return $many_many[$className];
2740
            }
2741
        }
2742
        if (is_array($this->hasMany())) {
2743
            $has_many = array_flip($this->hasMany());
2744
            if (array_key_exists($className, $has_many)) {
2745
                return $has_many[$className];
2746
            }
2747
        }
2748
        if (is_array($this->hasOne())) {
2749
            $has_one = array_flip($this->hasOne());
2750
            if (array_key_exists($className, $has_one)) {
2751
                return $has_one[$className];
2752
            }
2753
        }
2754
2755
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\ORM\DataObject::getReverseAssociation of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2756
    }
2757
2758
    /**
2759
     * Return all objects matching the filter
2760
     * sub-classes are automatically selected and included
2761
     *
2762
     * @param string $callerClass The class of objects to be returned
2763
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2764
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2765
     * @param string|array $sort A sort expression to be inserted into the ORDER
2766
     * BY clause.  If omitted, self::$default_sort will be used.
2767
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
2768
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2769
     * @param string $containerClass The container class to return the results in.
2770
     *
2771
     * @todo $containerClass is Ignored, why?
2772
     *
2773
     * @return DataList The objects matching the filter, in the class specified by $containerClass
2774
     */
2775
    public static function get(
2776
        $callerClass = null,
2777
        $filter = "",
2778
        $sort = "",
2779
        $join = "",
2780
        $limit = null,
2781
        $containerClass = DataList::class
2782
    ) {
2783
2784
        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...
2785
            $callerClass = get_called_class();
2786
            if ($callerClass == self::class) {
2787
                throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
2788
            }
2789
2790
            if ($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
2791
                throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
2792
                    . ' arguments');
2793
            }
2794
2795
            return DataList::create(get_called_class());
2796
        }
2797
2798
        if ($join) {
2799
            throw new \InvalidArgumentException(
2800
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
2801
            );
2802
        }
2803
2804
        $result = DataList::create($callerClass)->where($filter)->sort($sort);
2805
2806
        if ($limit && strpos($limit, ',') !== false) {
2807
            $limitArguments = explode(',', $limit);
2808
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
2809
        } elseif ($limit) {
2810
            $result = $result->limit($limit);
0 ignored issues
show
Documentation introduced by
$limit is of type string|array, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2811
        }
2812
2813
        return $result;
2814
    }
2815
2816
2817
    /**
2818
     * Return the first item matching the given query.
2819
     * All calls to get_one() are cached.
2820
     *
2821
     * @param string $callerClass The class of objects to be returned
2822
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2823
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2824
     * @param boolean $cache Use caching
2825
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
2826
     *
2827
     * @return DataObject|null The first item matching the query
2828
     */
2829
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
2830
    {
2831
        $SNG = singleton($callerClass);
2832
2833
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
2834
        $cacheKey = md5(var_export($cacheComponents, true));
2835
2836
        // Flush destroyed items out of the cache
2837
        if ($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
2838
                && self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
2839
                && self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
2840
            self::$_cache_get_one[$callerClass][$cacheKey] = false;
2841
        }
2842
        $item = null;
2843
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
2844
            $dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
2845
            $item = $dl->first();
2846
2847
            if ($cache) {
2848
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
2849
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
2850
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
2851
                }
2852
            }
2853
        }
2854
2855
        if ($cache) {
2856
            return self::$_cache_get_one[$callerClass][$cacheKey] ?: null;
2857
        } else {
2858
            return $item;
2859
        }
2860
    }
2861
2862
    /**
2863
     * Flush the cached results for all relations (has_one, has_many, many_many)
2864
     * Also clears any cached aggregate data.
2865
     *
2866
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
2867
     *                            When false will just clear session-local cached data
2868
     * @return DataObject $this
2869
     */
2870
    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...
2871
    {
2872
        if (static::class == self::class) {
2873
            self::$_cache_get_one = array();
2874
            return $this;
2875
        }
2876
2877
        $classes = ClassInfo::ancestry(static::class);
2878
        foreach ($classes as $class) {
2879
            if (isset(self::$_cache_get_one[$class])) {
2880
                unset(self::$_cache_get_one[$class]);
2881
            }
2882
        }
2883
2884
        $this->extend('flushCache');
2885
2886
        $this->components = array();
2887
        return $this;
2888
    }
2889
2890
    /**
2891
     * Flush the get_one global cache and destroy associated objects.
2892
     */
2893
    public static function flush_and_destroy_cache()
2894
    {
2895
        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...
2896
            foreach (self::$_cache_get_one as $class => $items) {
2897
                if (is_array($items)) {
2898
                    foreach ($items as $item) {
2899
                        if ($item) {
2900
                            $item->destroy();
2901
                        }
2902
                    }
2903
                }
2904
            }
2905
        }
2906
        self::$_cache_get_one = array();
2907
    }
2908
2909
    /**
2910
     * Reset all global caches associated with DataObject.
2911
     */
2912
    public static function reset()
2913
    {
2914
        // @todo Decouple these
2915
        DBClassName::clear_classname_cache();
2916
        ClassInfo::reset_db_cache();
2917
        static::getSchema()->reset();
2918
        self::$_cache_get_one = array();
2919
        self::$_cache_field_labels = array();
2920
    }
2921
2922
    /**
2923
     * Return the given element, searching by ID
2924
     *
2925
     * @param string $callerClass The class of the object to be returned
2926
     * @param int $id The id of the element
2927
     * @param boolean $cache See {@link get_one()}
2928
     *
2929
     * @return DataObject The element
2930
     */
2931
    public static function get_by_id($callerClass, $id, $cache = true)
2932
    {
2933
        if (!is_numeric($id)) {
2934
            user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
2935
        }
2936
2937
        // Pass to get_one
2938
        $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
2939
        return DataObject::get_one($callerClass, array($column => $id), $cache);
2940
    }
2941
2942
    /**
2943
     * Get the name of the base table for this object
2944
     *
2945
     * @return string
2946
     */
2947
    public function baseTable()
2948
    {
2949
        return static::getSchema()->baseDataTable($this);
2950
    }
2951
2952
    /**
2953
     * Get the base class for this object
2954
     *
2955
     * @return string
2956
     */
2957
    public function baseClass()
2958
    {
2959
        return static::getSchema()->baseDataClass($this);
2960
    }
2961
2962
    /**
2963
     * @var array Parameters used in the query that built this object.
2964
     * This can be used by decorators (e.g. lazy loading) to
2965
     * run additional queries using the same context.
2966
     */
2967
    protected $sourceQueryParams;
2968
2969
    /**
2970
     * @see $sourceQueryParams
2971
     * @return array
2972
     */
2973
    public function getSourceQueryParams()
2974
    {
2975
        return $this->sourceQueryParams;
2976
    }
2977
2978
    /**
2979
     * Get list of parameters that should be inherited to relations on this object
2980
     *
2981
     * @return array
2982
     */
2983
    public function getInheritableQueryParams()
2984
    {
2985
        $params = $this->getSourceQueryParams();
2986
        $this->extend('updateInheritableQueryParams', $params);
2987
        return $params;
2988
    }
2989
2990
    /**
2991
     * @see $sourceQueryParams
2992
     * @param array
2993
     */
2994
    public function setSourceQueryParams($array)
2995
    {
2996
        $this->sourceQueryParams = $array;
2997
    }
2998
2999
    /**
3000
     * @see $sourceQueryParams
3001
     * @param string $key
3002
     * @param string $value
3003
     */
3004
    public function setSourceQueryParam($key, $value)
3005
    {
3006
        $this->sourceQueryParams[$key] = $value;
3007
    }
3008
3009
    /**
3010
     * @see $sourceQueryParams
3011
     * @param string $key
3012
     * @return string
3013
     */
3014
    public function getSourceQueryParam($key)
3015
    {
3016
        if (isset($this->sourceQueryParams[$key])) {
3017
            return $this->sourceQueryParams[$key];
3018
        }
3019
        return null;
3020
    }
3021
3022
    //-------------------------------------------------------------------------------------------//
3023
3024
    /**
3025
     * Check the database schema and update it as necessary.
3026
     *
3027
     * @uses DataExtension->augmentDatabase()
3028
     */
3029
    public function requireTable()
3030
    {
3031
        // Only build the table if we've actually got fields
3032
        $schema = static::getSchema();
3033
        $table = $schema->tableName(static::class);
3034
        $fields = $schema->databaseFields(static::class, false);
3035
        $indexes = $schema->databaseIndexes(static::class, false);
3036
        $extensions = self::database_extensions(static::class);
3037
3038
        if (empty($table)) {
3039
            throw new LogicException(
3040
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3041
            );
3042
        }
3043
3044
        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...
3045
            $hasAutoIncPK = get_parent_class($this) === self::class;
3046
            DB::require_table(
3047
                $table,
3048
                $fields,
0 ignored issues
show
Documentation introduced by
$fields is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3049
                $indexes,
0 ignored issues
show
Documentation introduced by
$indexes is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3050
                $hasAutoIncPK,
3051
                $this->stat('create_table_options'),
3052
                $extensions
0 ignored issues
show
Security Bug introduced by
It seems like $extensions defined by self::database_extensions(static::class) on line 3036 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...
3053
            );
3054
        } else {
3055
            DB::dont_require_table($table);
3056
        }
3057
3058
        // Build any child tables for many_many items
3059
        if ($manyMany = $this->uninherited('many_many')) {
3060
            $extras = $this->uninherited('many_many_extraFields');
3061
            foreach ($manyMany as $component => $spec) {
3062
                // Get many_many spec
3063
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3064
                $parentField = $manyManyComponent['parentField'];
3065
                $childField = $manyManyComponent['childField'];
3066
                $tableOrClass = $manyManyComponent['join'];
3067
3068
                // Skip if backed by actual class
3069
                if (class_exists($tableOrClass)) {
3070
                    continue;
3071
                }
3072
3073
                // Build fields
3074
                $manymanyFields = array(
3075
                    $parentField => "Int",
3076
                    $childField => "Int",
3077
                );
3078
                if (isset($extras[$component])) {
3079
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3080
                }
3081
3082
                // Build index list
3083
                $manymanyIndexes = [
3084
                    $parentField => [
3085
                        'type' => 'index',
3086
                        'name' => $parentField,
3087
                        'columns' => [$parentField],
3088
                    ],
3089
                    $childField => [
3090
                        'type' => 'index',
3091
                        'name' =>$childField,
3092
                        'columns' => [$childField],
3093
                    ],
3094
                ];
3095
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Documentation introduced by
$manymanyFields is of type array<?,string>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$manymanyIndexes is of type array<?,array<string,?,{...er,?,{\"0\":\"?\"}>"}>>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Security Bug introduced by
It seems like $extensions defined by self::database_extensions(static::class) on line 3036 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...
3096
            }
3097
        }
3098
3099
        // Let any extentions make their own database fields
3100
        $this->extend('augmentDatabase', $dummy);
3101
    }
3102
3103
    /**
3104
     * Add default records to database. This function is called whenever the
3105
     * database is built, after the database tables have all been created. Overload
3106
     * this to add default records when the database is built, but make sure you
3107
     * call parent::requireDefaultRecords().
3108
     *
3109
     * @uses DataExtension->requireDefaultRecords()
3110
     */
3111
    public function requireDefaultRecords()
3112
    {
3113
        $defaultRecords = $this->config()->uninherited('default_records');
3114
3115
        if (!empty($defaultRecords)) {
3116
            $hasData = DataObject::get_one(static::class);
3117
            if (!$hasData) {
3118
                $className = static::class;
3119
                foreach ($defaultRecords as $record) {
3120
                    $obj = Injector::inst()->create($className, $record);
3121
                    $obj->write();
3122
                }
3123
                DB::alteration_message("Added default records to $className table", "created");
3124
            }
3125
        }
3126
3127
        // Let any extentions make their own database default data
3128
        $this->extend('requireDefaultRecords', $dummy);
3129
    }
3130
3131
    /**
3132
     * Get the default searchable fields for this object, as defined in the
3133
     * $searchable_fields list. If searchable fields are not defined on the
3134
     * data object, uses a default selection of summary fields.
3135
     *
3136
     * @return array
3137
     */
3138
    public function searchableFields()
3139
    {
3140
        // can have mixed format, need to make consistent in most verbose form
3141
        $fields = $this->stat('searchable_fields');
3142
        $labels = $this->fieldLabels();
3143
3144
        // fallback to summary fields (unless empty array is explicitly specified)
3145
        if (! $fields && ! is_array($fields)) {
3146
            $summaryFields = array_keys($this->summaryFields());
3147
            $fields = array();
3148
3149
            // remove the custom getters as the search should not include them
3150
            $schema = static::getSchema();
3151
            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...
3152
                foreach ($summaryFields as $key => $name) {
3153
                    $spec = $name;
3154
3155
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3156 View Code Duplication
                    if (($fieldPos = strpos($name, '.')) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3157
                        $name = substr($name, 0, $fieldPos);
3158
                    }
3159
3160
                    if ($schema->fieldSpec($this, $name)) {
3161
                        $fields[] = $name;
3162
                    } elseif ($this->relObject($spec)) {
3163
                        $fields[] = $spec;
3164
                    }
3165
                }
3166
            }
3167
        }
3168
3169
        // we need to make sure the format is unified before
3170
        // augmenting fields, so extensions can apply consistent checks
3171
        // but also after augmenting fields, because the extension
3172
        // might use the shorthand notation as well
3173
3174
        // rewrite array, if it is using shorthand syntax
3175
        $rewrite = array();
3176
        foreach ($fields as $name => $specOrName) {
3177
            $identifer = (is_int($name)) ? $specOrName : $name;
3178
3179
            if (is_int($name)) {
3180
                // Format: array('MyFieldName')
3181
                $rewrite[$identifer] = array();
3182
            } elseif (is_array($specOrName)) {
3183
                // Format: array('MyFieldName' => array(
3184
                //   'filter => 'ExactMatchFilter',
3185
                //   'field' => 'NumericField', // optional
3186
                //   'title' => 'My Title', // optional
3187
                // ))
3188
                $rewrite[$identifer] = array_merge(
3189
                    array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3190
                    (array)$specOrName
3191
                );
3192
            } else {
3193
                // Format: array('MyFieldName' => 'ExactMatchFilter')
3194
                $rewrite[$identifer] = array(
3195
                    'filter' => $specOrName,
3196
                );
3197
            }
3198
            if (!isset($rewrite[$identifer]['title'])) {
3199
                $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3200
                    ? $labels[$identifer] : FormField::name_to_label($identifer);
3201
            }
3202
            if (!isset($rewrite[$identifer]['filter'])) {
3203
                /** @skipUpgrade */
3204
                $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3205
            }
3206
        }
3207
3208
        $fields = $rewrite;
3209
3210
        // apply DataExtensions if present
3211
        $this->extend('updateSearchableFields', $fields);
3212
3213
        return $fields;
3214
    }
3215
3216
    /**
3217
     * Get any user defined searchable fields labels that
3218
     * exist. Allows overriding of default field names in the form
3219
     * interface actually presented to the user.
3220
     *
3221
     * The reason for keeping this separate from searchable_fields,
3222
     * which would be a logical place for this functionality, is to
3223
     * avoid bloating and complicating the configuration array. Currently
3224
     * much of this system is based on sensible defaults, and this property
3225
     * would generally only be set in the case of more complex relationships
3226
     * between data object being required in the search interface.
3227
     *
3228
     * Generates labels based on name of the field itself, if no static property
3229
     * {@link self::field_labels} exists.
3230
     *
3231
     * @uses $field_labels
3232
     * @uses FormField::name_to_label()
3233
     *
3234
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3235
     *
3236
     * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3237
     */
3238
    public function fieldLabels($includerelations = true)
3239
    {
3240
        $cacheKey = static::class . '_' . $includerelations;
3241
3242
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
3243
            $customLabels = $this->stat('field_labels');
3244
            $autoLabels = array();
3245
3246
            // get all translated static properties as defined in i18nCollectStatics()
3247
            $ancestry = ClassInfo::ancestry(static::class);
3248
            $ancestry = array_reverse($ancestry);
3249
            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...
3250
                foreach ($ancestry as $ancestorClass) {
3251
                    if ($ancestorClass === ViewableData::class) {
3252
                        break;
3253
                    }
3254
                    $types = [
3255
                        'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3256
                    ];
3257
                    if ($includerelations) {
3258
                        $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3259
                        $types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3260
                        $types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3261
                        $types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3262
                    }
3263
                    foreach ($types as $type => $attrs) {
3264
                        foreach ($attrs as $name => $spec) {
3265
                            $autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}", FormField::name_to_label($name));
3266
                        }
3267
                    }
3268
                }
3269
            }
3270
3271
            $labels = array_merge((array)$autoLabels, (array)$customLabels);
3272
            $this->extend('updateFieldLabels', $labels);
3273
            self::$_cache_field_labels[$cacheKey] = $labels;
3274
        }
3275
3276
        return self::$_cache_field_labels[$cacheKey];
3277
    }
3278
3279
    /**
3280
     * Get a human-readable label for a single field,
3281
     * see {@link fieldLabels()} for more details.
3282
     *
3283
     * @uses fieldLabels()
3284
     * @uses FormField::name_to_label()
3285
     *
3286
     * @param string $name Name of the field
3287
     * @return string Label of the field
3288
     */
3289
    public function fieldLabel($name)
3290
    {
3291
        $labels = $this->fieldLabels();
3292
        return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3293
    }
3294
3295
    /**
3296
     * Get the default summary fields for this object.
3297
     *
3298
     * @todo use the translation apparatus to return a default field selection for the language
3299
     *
3300
     * @return array
3301
     */
3302
    public function summaryFields()
3303
    {
3304
        $fields = $this->stat('summary_fields');
3305
3306
        // if fields were passed in numeric array,
3307
        // convert to an associative array
3308
        if ($fields && array_key_exists(0, $fields)) {
3309
            $fields = array_combine(array_values($fields), array_values($fields));
3310
        }
3311
3312
        if (!$fields) {
3313
            $fields = array();
3314
            // try to scaffold a couple of usual suspects
3315
            if ($this->hasField('Name')) {
3316
                $fields['Name'] = 'Name';
3317
            }
3318
            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...
3319
                $fields['Title'] = 'Title';
3320
            }
3321
            if ($this->hasField('Description')) {
3322
                $fields['Description'] = 'Description';
3323
            }
3324
            if ($this->hasField('FirstName')) {
3325
                $fields['FirstName'] = 'First Name';
3326
            }
3327
        }
3328
        $this->extend("updateSummaryFields", $fields);
3329
3330
        // Final fail-over, just list ID field
3331
        if (!$fields) {
3332
            $fields['ID'] = 'ID';
3333
        }
3334
3335
        // Localize fields (if possible)
3336
        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...
3337
            // only attempt to localize if the label definition is the same as the field name.
3338
            // this will preserve any custom labels set in the summary_fields configuration
3339
            if (isset($fields[$name]) && $name === $fields[$name]) {
3340
                $fields[$name] = $label;
3341
            }
3342
        }
3343
3344
        return $fields;
3345
    }
3346
3347
    /**
3348
     * Defines a default list of filters for the search context.
3349
     *
3350
     * If a filter class mapping is defined on the data object,
3351
     * it is constructed here. Otherwise, the default filter specified in
3352
     * {@link DBField} is used.
3353
     *
3354
     * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3355
     *
3356
     * @return array
3357
     */
3358
    public function defaultSearchFilters()
3359
    {
3360
        $filters = array();
3361
3362
        foreach ($this->searchableFields() as $name => $spec) {
3363
            if (empty($spec['filter'])) {
3364
                /** @skipUpgrade */
3365
                $filters[$name] = 'PartialMatchFilter';
3366
            } elseif ($spec['filter'] instanceof SearchFilter) {
3367
                $filters[$name] = $spec['filter'];
3368
            } else {
3369
                $filters[$name] = Injector::inst()->create($spec['filter'], $name);
3370
            }
3371
        }
3372
3373
        return $filters;
3374
    }
3375
3376
    /**
3377
     * @return boolean True if the object is in the database
3378
     */
3379
    public function isInDB()
3380
    {
3381
        return is_numeric($this->ID) && $this->ID > 0;
3382
    }
3383
3384
    /*
3385
	 * @ignore
3386
	 */
3387
    private static $subclass_access = true;
3388
3389
    /**
3390
     * Temporarily disable subclass access in data object qeur
3391
     */
3392
    public static function disable_subclass_access()
3393
    {
3394
        self::$subclass_access = false;
3395
    }
3396
    public static function enable_subclass_access()
3397
    {
3398
        self::$subclass_access = true;
3399
    }
3400
3401
    //-------------------------------------------------------------------------------------------//
3402
3403
    /**
3404
     * Database field definitions.
3405
     * This is a map from field names to field type. The field
3406
     * type should be a class that extends .
3407
     * @var array
3408
     * @config
3409
     */
3410
    private static $db = [];
3411
3412
    /**
3413
     * Use a casting object for a field. This is a map from
3414
     * field name to class name of the casting object.
3415
     *
3416
     * @var array
3417
     */
3418
    private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
3419
        "Title" => 'Text',
3420
    );
3421
3422
    /**
3423
     * Specify custom options for a CREATE TABLE call.
3424
     * Can be used to specify a custom storage engine for specific database table.
3425
     * All options have to be keyed for a specific database implementation,
3426
     * identified by their class name (extending from {@link SS_Database}).
3427
     *
3428
     * <code>
3429
     * array(
3430
     *  'MySQLDatabase' => 'ENGINE=MyISAM'
3431
     * )
3432
     * </code>
3433
     *
3434
     * Caution: This API is experimental, and might not be
3435
     * included in the next major release. Please use with care.
3436
     *
3437
     * @var array
3438
     * @config
3439
     */
3440
    private static $create_table_options = array(
3441
        'SilverStripe\ORM\Connect\MySQLDatabase' => 'ENGINE=InnoDB'
3442
    );
3443
3444
    /**
3445
     * If a field is in this array, then create a database index
3446
     * on that field. This is a map from fieldname to index type.
3447
     * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3448
     *
3449
     * @var array
3450
     * @config
3451
     */
3452
    private static $indexes = null;
3453
3454
    /**
3455
     * Inserts standard column-values when a DataObject
3456
     * is instanciated. Does not insert default records {@see $default_records}.
3457
     * This is a map from fieldname to default value.
3458
     *
3459
     *  - If you would like to change a default value in a sub-class, just specify it.
3460
     *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3461
     *    or false in your subclass.  Setting it to null won't work.
3462
     *
3463
     * @var array
3464
     * @config
3465
     */
3466
    private static $defaults = [];
3467
3468
    /**
3469
     * Multidimensional array which inserts default data into the database
3470
     * on a db/build-call as long as the database-table is empty. Please use this only
3471
     * for simple constructs, not for SiteTree-Objects etc. which need special
3472
     * behaviour such as publishing and ParentNodes.
3473
     *
3474
     * Example:
3475
     * array(
3476
     *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3477
     *  array('Title' => "DefaultPage2")
3478
     * ).
3479
     *
3480
     * @var array
3481
     * @config
3482
     */
3483
    private static $default_records = null;
3484
3485
    /**
3486
     * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3487
     * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3488
     *
3489
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3490
     *
3491
     *  @var array
3492
     * @config
3493
     */
3494
    private static $has_one = [];
3495
3496
    /**
3497
     * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3498
     *
3499
     * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3500
     * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3501
     * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3502
     *
3503
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3504
     *
3505
     * @var array
3506
     * @config
3507
     */
3508
    private static $belongs_to = [];
3509
3510
    /**
3511
     * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3512
     *
3513
     * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3514
     * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3515
     * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3516
     * which foreign key to use.
3517
     *
3518
     * @var array
3519
     * @config
3520
     */
3521
    private static $has_many = [];
3522
3523
    /**
3524
     * many-many relationship definitions.
3525
     * This is a map from component name to data type.
3526
     * @var array
3527
     * @config
3528
     */
3529
    private static $many_many = [];
3530
3531
    /**
3532
     * Extra fields to include on the connecting many-many table.
3533
     * This is a map from field name to field type.
3534
     *
3535
     * Example code:
3536
     * <code>
3537
     * public static $many_many_extraFields = array(
3538
     *  'Members' => array(
3539
     *          'Role' => 'Varchar(100)'
3540
     *      )
3541
     * );
3542
     * </code>
3543
     *
3544
     * @var array
3545
     * @config
3546
     */
3547
    private static $many_many_extraFields = [];
3548
3549
    /**
3550
     * The inverse side of a many-many relationship.
3551
     * This is a map from component name to data type.
3552
     * @var array
3553
     * @config
3554
     */
3555
    private static $belongs_many_many = [];
3556
3557
    /**
3558
     * The default sort expression. This will be inserted in the ORDER BY
3559
     * clause of a SQL query if no other sort expression is provided.
3560
     * @var string
3561
     * @config
3562
     */
3563
    private static $default_sort = null;
3564
3565
    /**
3566
     * Default list of fields that can be scaffolded by the ModelAdmin
3567
     * search interface.
3568
     *
3569
     * Overriding the default filter, with a custom defined filter:
3570
     * <code>
3571
     *  static $searchable_fields = array(
3572
     *     "Name" => "PartialMatchFilter"
3573
     *  );
3574
     * </code>
3575
     *
3576
     * Overriding the default form fields, with a custom defined field.
3577
     * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3578
     * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3579
     * <code>
3580
     *  static $searchable_fields = array(
3581
     *    "Name" => array(
3582
     *      "field" => "TextField"
3583
     *    )
3584
     *  );
3585
     * </code>
3586
     *
3587
     * Overriding the default form field, filter and title:
3588
     * <code>
3589
     *  static $searchable_fields = array(
3590
     *    "Organisation.ZipCode" => array(
3591
     *      "field" => "TextField",
3592
     *      "filter" => "PartialMatchFilter",
3593
     *      "title" => 'Organisation ZIP'
3594
     *    )
3595
     *  );
3596
     * </code>
3597
     * @config
3598
     */
3599
    private static $searchable_fields = null;
3600
3601
    /**
3602
     * User defined labels for searchable_fields, used to override
3603
     * default display in the search form.
3604
     * @config
3605
     */
3606
    private static $field_labels = [];
3607
3608
    /**
3609
     * Provides a default list of fields to be used by a 'summary'
3610
     * view of this object.
3611
     * @config
3612
     */
3613
    private static $summary_fields = [];
3614
3615
    public function provideI18nEntities()
3616
    {
3617
        // Note: see http://guides.rubyonrails.org/i18n.html#pluralization for rules
3618
        // Best guess for a/an rule. Better guesses require overriding in subclasses
3619
        $pluralName = $this->plural_name();
3620
        $singularName = $this->singular_name();
3621
        $conjunction = preg_match('/^[aeiou]/i', $singularName) ? 'An ' : 'A ';
3622
        return [
3623
            static::class.'.SINGULARNAME' => $this->singular_name(),
3624
            static::class.'.PLURALNAME' => $pluralName,
3625
            static::class.'.PLURALS' => [
3626
                'one' => $conjunction . $singularName,
3627
                'other' => '{count} ' . $pluralName
3628
            ]
3629
        ];
3630
    }
3631
3632
    /**
3633
     * Returns true if the given method/parameter has a value
3634
     * (Uses the DBField::hasValue if the parameter is a database field)
3635
     *
3636
     * @param string $field The field name
3637
     * @param array $arguments
3638
     * @param bool $cache
3639
     * @return boolean
3640
     */
3641
    public function hasValue($field, $arguments = null, $cache = true)
3642
    {
3643
        // has_one fields should not use dbObject to check if a value is given
3644
        $hasOne = static::getSchema()->hasOneComponent(static::class, $field);
3645
        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...
3646
            return $obj->exists();
3647
        } else {
3648
            return parent::hasValue($field, $arguments, $cache);
0 ignored issues
show
Bug introduced by
It seems like $arguments defined by parameter $arguments on line 3641 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...
3649
        }
3650
    }
3651
3652
    /**
3653
     * If selected through a many_many through relation, this is the instance of the joined record
3654
     *
3655
     * @return DataObject
3656
     */
3657
    public function getJoin()
3658
    {
3659
        return $this->joinRecord;
3660
    }
3661
3662
    /**
3663
     * Set joining object
3664
     *
3665
     * @param DataObject $object
3666
     * @param string $alias Alias
3667
     * @return $this
3668
     */
3669
    public function setJoin(DataObject $object, $alias = null)
3670
    {
3671
        $this->joinRecord = $object;
3672
        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...
3673
            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...
3674
                throw new InvalidArgumentException(
3675
                    "Joined record $alias cannot also be a db field"
3676
                );
3677
            }
3678
            $this->record[$alias] = $object;
3679
        }
3680
        return $this;
3681
    }
3682
}
3683