Completed
Push — master ( f39c4d...b2e354 )
by Sam
03:35 queued 03:17
created

DataObject::validateWrite()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 4
nop 0
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Object;
8
use SilverStripe\Core\Injector\Injector;
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\Queries\SQLSelect;
22
use SilverStripe\ORM\FieldType\DBField;
23
use SilverStripe\ORM\FieldType\DBDatetime;
24
use SilverStripe\ORM\FieldType\DBComposite;
25
use SilverStripe\ORM\FieldType\DBClassName;
26
use SilverStripe\Security\Member;
27
use SilverStripe\Security\Permission;
28
use SilverStripe\View\ViewableData;
29
use LogicException;
30
use InvalidArgumentException;
31
use BadMethodCallException;
32
use Exception;
33
34
/**
35
 * A single database record & abstract class for the data-access-model.
36
 *
37
 * <h2>Extensions</h2>
38
 *
39
 * See {@link Extension} and {@link DataExtension}.
40
 *
41
 * <h2>Permission Control</h2>
42
 *
43
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
44
 * strings which can be selected on a group-by-group basis.
45
 *
46
 * <code>
47
 * class Article extends DataObject implements PermissionProvider {
48
 *  static $api_access = true;
49
 *
50
 *  function canView($member = false) {
51
 *    return Permission::check('ARTICLE_VIEW');
52
 *  }
53
 *  function canEdit($member = false) {
54
 *    return Permission::check('ARTICLE_EDIT');
55
 *  }
56
 *  function canDelete() {
57
 *    return Permission::check('ARTICLE_DELETE');
58
 *  }
59
 *  function canCreate() {
60
 *    return Permission::check('ARTICLE_CREATE');
61
 *  }
62
 *  function providePermissions() {
63
 *    return array(
64
 *      'ARTICLE_VIEW' => 'Read an article object',
65
 *      'ARTICLE_EDIT' => 'Edit an article object',
66
 *      'ARTICLE_DELETE' => 'Delete an article object',
67
 *      'ARTICLE_CREATE' => 'Create an article object',
68
 *    );
69
 *  }
70
 * }
71
 * </code>
72
 *
73
 * Object-level access control by {@link Group} membership:
74
 * <code>
75
 * class Article extends DataObject {
76
 *   static $api_access = true;
77
 *
78
 *   function canView($member = false) {
79
 *     if(!$member) $member = Member::currentUser();
80
 *     return $member->inGroup('Subscribers');
81
 *   }
82
 *   function canEdit($member = false) {
83
 *     if(!$member) $member = Member::currentUser();
84
 *     return $member->inGroup('Editors');
85
 *   }
86
 *
87
 *   // ...
88
 * }
89
 * </code>
90
 *
91
 * If any public method on this class is prefixed with an underscore,
92
 * the results are cached in memory through {@link cachedCall()}.
93
 *
94
 *
95
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
96
 *  and defineMethods()
97
 *
98
 * @property integer ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
99
 * @property string ClassName Class name of the DataObject
100
 * @property string LastEdited Date and time of DataObject's last modification.
101
 * @property string Created Date and time of DataObject creation.
102
 */
103
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider
104
{
105
106
    /**
107
     * Human-readable singular name.
108
     * @var string
109
     * @config
110
     */
111
    private static $singular_name = null;
112
113
    /**
114
     * Human-readable plural name
115
     * @var string
116
     * @config
117
     */
118
    private static $plural_name = null;
119
120
    /**
121
     * Allow API access to this object?
122
     * @todo Define the options that can be set here
123
     * @config
124
     */
125
    private static $api_access = false;
126
127
    /**
128
     * Allows specification of a default value for the ClassName field.
129
     * Configure this value only in subclasses of DataObject.
130
     *
131
     * @config
132
     * @var string
133
     */
134
    private static $default_classname = null;
135
136
    /**
137
     * True if this DataObject has been destroyed.
138
     * @var boolean
139
     */
140
    public $destroyed = false;
141
142
    /**
143
     * The DataModel from this this object comes
144
     */
145
    protected $model;
146
147
    /**
148
     * Data stored in this objects database record. An array indexed by fieldname.
149
     *
150
     * Use {@link toMap()} if you want an array representation
151
     * of this object, as the $record array might contain lazy loaded field aliases.
152
     *
153
     * @var array
154
     */
155
    protected $record;
156
157
    /**
158
     * If selected through a many_many through relation, this is the instance of the through record
159
     *
160
     * @var DataObject
161
     */
162
    protected $joinRecord;
163
164
    /**
165
     * Represents a field that hasn't changed (before === after, thus before == after)
166
     */
167
    const CHANGE_NONE = 0;
168
169
    /**
170
     * Represents a field that has changed type, although not the loosely defined value.
171
     * (before !== after && before == after)
172
     * E.g. change 1 to true or "true" to true, but not true to 0.
173
     * Value changes are by nature also considered strict changes.
174
     */
175
    const CHANGE_STRICT = 1;
176
177
    /**
178
     * Represents a field that has changed the loosely defined value
179
     * (before != after, thus, before !== after))
180
     * E.g. change false to true, but not false to 0
181
     */
182
    const CHANGE_VALUE = 2;
183
184
    /**
185
     * An array indexed by fieldname, true if the field has been changed.
186
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
187
     * the changed state.
188
     *
189
     * @var array
190
     */
191
    private $changed;
192
193
    /**
194
     * The database record (in the same format as $record), before
195
     * any changes.
196
     * @var array
197
     */
198
    protected $original;
199
200
    /**
201
     * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
202
     * @var boolean
203
     */
204
    protected $brokenOnDelete = false;
205
206
    /**
207
     * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
208
     * @var boolean
209
     */
210
    protected $brokenOnWrite = false;
211
212
    /**
213
     * @config
214
     * @var boolean Should dataobjects be validated before they are written?
215
     * Caution: Validation can contain safeguards against invalid/malicious data,
216
     * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
217
     * to only disable validation for very specific use cases.
218
     */
219
    private static $validation_enabled = true;
220
221
    /**
222
     * Static caches used by relevant functions.
223
     *
224
     * @var array
225
     */
226
    protected static $_cache_get_one;
227
228
    /**
229
     * Cache of field labels
230
     *
231
     * @var array
232
     */
233
    protected static $_cache_field_labels = array();
234
235
    /**
236
     * Base fields which are not defined in static $db
237
     *
238
     * @config
239
     * @var array
240
     */
241
    private static $fixed_fields = array(
242
        'ID' => 'PrimaryKey',
243
        'ClassName' => 'DBClassName',
244
        'LastEdited' => 'DBDatetime',
245
        'Created' => 'DBDatetime',
246
    );
247
248
    /**
249
     * Core dataobject extensions
250
     *
251
     * @config
252
     * @var array
253
     */
254
    private static $extensions = array(
255
        'AssetControl' => 'SilverStripe\\Assets\\AssetControlExtension'
256
    );
257
258
    /**
259
     * Override table name for this class. If ignored will default to FQN of class.
260
     * This option is not inheritable, and must be set on each class.
261
     * If left blank naming will default to the legacy (3.x) behaviour.
262
     *
263
     * @var string
264
     */
265
    private static $table_name = null;
266
267
    /**
268
     * Non-static relationship cache, indexed by component name.
269
     *
270
     * @var DataObject[]
271
     */
272
    protected $components;
273
274
    /**
275
     * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
276
     *
277
     * @var UnsavedRelationList[]
278
     */
279
    protected $unsavedRelations;
280
281
    /**
282
     * Get schema object
283
     *
284
     * @return DataObjectSchema
285
     */
286
    public static function getSchema()
287
    {
288
        return Injector::inst()->get(DataObjectSchema::class);
289
    }
290
291
    /**
292
     * Construct a new DataObject.
293
     *
294
     * @param array|null $record This will be null for a new database record.  Alternatively, you can pass an array of
295
     * field values.  Normally this constructor is only used by the internal systems that get objects from the database.
296
     * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
297
     *                             Singletons don't have their defaults set.
298
     * @param DataModel $model
299
     * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
300
     */
301
    public function __construct($record = null, $isSingleton = false, $model = null, $queryParams = array())
302
    {
303
        parent::__construct();
304
305
        // Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
306
        $this->setSourceQueryParams($queryParams);
307
308
        // Set the fields data.
309
        if (!$record) {
310
            $record = array(
311
                'ID' => 0,
312
                'ClassName' => static::class,
313
                'RecordClassName' => static::class
314
            );
315
        }
316
317
        if (!is_array($record) && !is_a($record, "stdClass")) {
318
            if (is_object($record)) {
319
                $passed = "an object of type '".get_class($record)."'";
320
            } else {
321
                $passed = "The value '$record'";
322
            }
323
324
            user_error(
325
                "DataObject::__construct passed $passed.  It's supposed to be passed an array,"
326
                . " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
327
                E_USER_WARNING
328
            );
329
            $record = null;
330
        }
331
332
        if (is_a($record, "stdClass")) {
333
            $record = (array)$record;
334
        }
335
336
        // Set $this->record to $record, but ignore NULLs
337
        $this->record = array();
338
        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...
339
            // Ensure that ID is stored as a number and not a string
340
            // To do: this kind of clean-up should be done on all numeric fields, in some relatively
341
            // performant manner
342
            if ($v !== null) {
343
                if ($k == 'ID' && is_numeric($v)) {
344
                    $this->record[$k] = (int)$v;
345
                } else {
346
                    $this->record[$k] = $v;
347
                }
348
            }
349
        }
350
351
        // Identify fields that should be lazy loaded, but only on existing records
352
        if (!empty($record['ID'])) {
353
            // Get all field specs scoped to class for later lazy loading
354
            $fields = static::getSchema()->fieldSpecs(
355
                static::class,
356
                DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
357
            );
358
            foreach ($fields as $field => $fieldSpec) {
359
                $fieldClass = strtok($fieldSpec, ".");
360
                if (!array_key_exists($field, $record)) {
361
                    $this->record[$field.'_Lazy'] = $fieldClass;
362
                }
363
            }
364
        }
365
366
        $this->original = $this->record;
367
368
        // Keep track of the modification date of all the data sourced to make this page
369
        // From this we create a Last-Modified HTTP header
370
        if (isset($record['LastEdited'])) {
371
            HTTP::register_modification_date($record['LastEdited']);
372
        }
373
374
        // this must be called before populateDefaults(), as field getters on a DataObject
375
        // may call getComponent() and others, which rely on $this->model being set.
376
        $this->model = $model ? $model : DataModel::inst();
377
378
        // Must be called after parent constructor
379
        if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
380
            $this->populateDefaults();
381
        }
382
383
        // prevent populateDefaults() and setField() from marking overwritten defaults as changed
384
        $this->changed = array();
385
    }
386
387
    /**
388
     * Set the DataModel
389
     * @param DataModel $model
390
     * @return DataObject $this
391
     */
392
    public function setDataModel(DataModel $model)
393
    {
394
        $this->model = $model;
395
        return $this;
396
    }
397
398
    /**
399
     * Destroy all of this objects dependant objects and local caches.
400
     * You'll need to call this to get the memory of an object that has components or extensions freed.
401
     */
402
    public function destroy()
403
    {
404
        //$this->destroyed = true;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
405
        gc_collect_cycles();
406
        $this->flushCache(false);
407
    }
408
409
    /**
410
     * Create a duplicate of this node.
411
     * Note: now also duplicates relations.
412
     *
413
     * @param bool $doWrite Perform a write() operation before returning the object.
414
     * If this is true, it will create the duplicate in the database.
415
     * @return DataObject A duplicate of this node. The exact type will be the type of this node.
416
     */
417
    public function duplicate($doWrite = true)
418
    {
419
        /** @var static $clone */
420
        $clone = Injector::inst()->create(static::class, $this->toMap(), false, $this->model);
421
        $clone->ID = 0;
422
423
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
424
        if ($doWrite) {
425
            $clone->write();
426
            $this->duplicateManyManyRelations($this, $clone);
427
        }
428
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
429
430
        return $clone;
431
    }
432
433
    /**
434
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object
435
     * The destinationObject must be written to the database already and have an ID. Writing is performed
436
     * automatically when adding the new relations.
437
     *
438
     * @param DataObject $sourceObject the source object to duplicate from
439
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
440
     * @return DataObject with the new many_many relations copied in
441
     */
442
    protected function duplicateManyManyRelations($sourceObject, $destinationObject)
443
    {
444
        if (!$destinationObject || $destinationObject->ID < 1) {
445
            user_error(
446
                "Can't duplicate relations for an object that has not been written to the database",
447
                E_USER_ERROR
448
            );
449
        }
450
451
        //duplicate complex relations
452
        // DO NOT copy has_many relations, because copying the relation would result in us changing the has_one
453
        // relation on the other side of this relation to point at the copy and no longer the original (being a
454
        // has_one, it can only point at one thing at a time). So, all relations except has_many can and are copied
455
        if ($sourceObject->hasOne()) {
456
            foreach ($sourceObject->hasOne() as $name => $type) {
0 ignored issues
show
Bug introduced by
The expression $sourceObject->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...
457
                $this->duplicateRelations($sourceObject, $destinationObject, $name);
458
            }
459
        }
460
        if ($sourceObject->manyMany()) {
461
            foreach ($sourceObject->manyMany() as $name => $type) {
462
            //many_many include belongs_many_many
463
                $this->duplicateRelations($sourceObject, $destinationObject, $name);
464
            }
465
        }
466
467
        return $destinationObject;
468
    }
469
470
    /**
471
     * Helper function to duplicate relations from one object to another
472
     * @param DataObject $sourceObject the source object to duplicate from
473
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
474
     * @param string $name the name of the relation to duplicate (e.g. members)
475
     */
476
    private function duplicateRelations($sourceObject, $destinationObject, $name)
477
    {
478
        $relations = $sourceObject->$name();
479
        if ($relations) {
480
            if ($relations instanceof RelationList) {   //many-to-something relation
481
                if ($relations->count() > 0) {  //with more than one thing it is related to
482
                    foreach ($relations as $relation) {
483
                        $destinationObject->$name()->add($relation);
484
                    }
485
                }
486
            } else {    //one-to-one relation
487
                $destinationObject->{"{$name}ID"} = $relations->ID;
488
            }
489
        }
490
    }
491
492
    /**
493
     * Return obsolete class name, if this is no longer a valid class
494
     *
495
     * @return string
496
     */
497
    public function getObsoleteClassName()
498
    {
499
        $className = $this->getField("ClassName");
500
        if (!ClassInfo::exists($className)) {
501
            return $className;
502
        }
503
        return null;
504
    }
505
506
    /**
507
     * Gets name of this class
508
     *
509
     * @return string
510
     */
511
    public function getClassName()
512
    {
513
        $className = $this->getField("ClassName");
514
        if (!ClassInfo::exists($className)) {
515
            return static::class;
516
        }
517
        return $className;
518
    }
519
520
    /**
521
     * Set the ClassName attribute. {@link $class} is also updated.
522
     * Warning: This will produce an inconsistent record, as the object
523
     * instance will not automatically switch to the new subclass.
524
     * Please use {@link newClassInstance()} for this purpose,
525
     * or destroy and reinstanciate the record.
526
     *
527
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
528
     * @return $this
529
     */
530
    public function setClassName($className)
531
    {
532
        $className = trim($className);
533
        if (!$className || !is_subclass_of($className, self::class)) {
534
            return $this;
535
        }
536
537
        $this->class = $className;
538
        $this->setField("ClassName", $className);
539
        $this->setField('RecordClassName', $className);
540
        return $this;
541
    }
542
543
    /**
544
     * Create a new instance of a different class from this object's record.
545
     * This is useful when dynamically changing the type of an instance. Specifically,
546
     * it ensures that the instance of the class is a match for the className of the
547
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
548
     * property manually before calling this method, as it will confuse change detection.
549
     *
550
     * If the new class is different to the original class, defaults are populated again
551
     * because this will only occur automatically on instantiation of a DataObject if
552
     * there is no record, or the record has no ID. In this case, we do have an ID but
553
     * we still need to repopulate the defaults.
554
     *
555
     * @param string $newClassName The name of the new class
556
     *
557
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
558
     */
559
    public function newClassInstance($newClassName)
560
    {
561
        if (!is_subclass_of($newClassName, self::class)) {
562
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
563
        }
564
565
        $originalClass = $this->ClassName;
566
567
        /** @var DataObject $newInstance */
568
        $newInstance = Injector::inst()->create($newClassName, $this->record, false, $this->model);
569
570
        // Modify ClassName
571
        if ($newClassName != $originalClass) {
572
            $newInstance->setClassName($newClassName);
573
            $newInstance->populateDefaults();
574
            $newInstance->forceChange();
575
        }
576
577
        return $newInstance;
578
    }
579
580
    /**
581
     * Adds methods from the extensions.
582
     * Called by Object::__construct() once per class.
583
     */
584
    public function defineMethods()
585
    {
586
        parent::defineMethods();
587
588
        // Define the extra db fields - this is only necessary for extensions added in the
589
        // class definition.  Object::add_extension() will call this at definition time for
590
        // those objects, which is a better mechanism.  Perhaps extensions defined inside the
591
        // class def can somehow be applied at definiton time also?
592
        if ($this->extension_instances) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extension_instances of type SilverStripe\Core\Extension[] 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...
593
            foreach ($this->extension_instances as $i => $instance) {
594
                if (!$instance->class) {
595
                    $class = get_class($instance);
596
                    user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls"
597
                    . " parent::__construct()", E_USER_ERROR);
598
                }
599
            }
600
        }
601
602
        if (static::class === self::class) {
603
             return;
604
        }
605
606
        // Set up accessors for joined items
607
        if ($manyMany = $this->manyMany()) {
608
            foreach ($manyMany as $relationship => $class) {
609
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
610
            }
611
        }
612
        if ($hasMany = $this->hasMany()) {
613
            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...
614
                $this->addWrapperMethod($relationship, 'getComponents');
615
            }
616
        }
617
        if ($hasOne = $this->hasOne()) {
618
            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...
619
                $this->addWrapperMethod($relationship, 'getComponent');
620
            }
621
        }
622
        if ($belongsTo = $this->belongsTo()) {
623
            foreach (array_keys($belongsTo) as $relationship) {
624
                $this->addWrapperMethod($relationship, 'getComponent');
625
            }
626
        }
627
    }
628
629
    /**
630
     * Returns true if this object "exists", i.e., has a sensible value.
631
     * The default behaviour for a DataObject is to return true if
632
     * the object exists in the database, you can override this in subclasses.
633
     *
634
     * @return boolean true if this object exists
635
     */
636
    public function exists()
637
    {
638
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
639
    }
640
641
    /**
642
     * Returns TRUE if all values (other than "ID") are
643
     * considered empty (by weak boolean comparison).
644
     *
645
     * @return boolean
646
     */
647
    public function isEmpty()
648
    {
649
        $fixed = $this->config()->fixed_fields;
0 ignored issues
show
Documentation introduced by
The property fixed_fields does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
650
        foreach ($this->toMap() as $field => $value) {
651
            // only look at custom fields
652
            if (isset($fixed[$field])) {
653
                continue;
654
            }
655
656
            $dbObject = $this->dbObject($field);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $dbObject is correct as $this->dbObject($field) (which targets SilverStripe\ORM\DataObject::dbObject()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
657
            if (!$dbObject) {
658
                continue;
659
            }
660
            if ($dbObject->exists()) {
661
                return false;
662
            }
663
        }
664
        return true;
665
    }
666
667
    /**
668
     * Pluralise this item given a specific count.
669
     *
670
     * E.g. "0 Pages", "1 File", "3 Images"
671
     *
672
     * @param string $count
673
     * @param bool $prependNumber Include number in result. Defaults to true.
674
     * @return string
675
     */
676
    public function i18n_pluralise($count, $prependNumber = true)
677
    {
678
        return i18n::pluralise(
679
            $this->i18n_singular_name(),
680
            $this->i18n_plural_name(),
681
            $count,
682
            $prependNumber
683
        );
684
    }
685
686
    /**
687
     * Get the user friendly singular name of this DataObject.
688
     * If the name is not defined (by redefining $singular_name in the subclass),
689
     * this returns the class name.
690
     *
691
     * @return string User friendly singular name of this DataObject
692
     */
693
    public function singular_name()
694
    {
695
        if (!$name = $this->stat('singular_name')) {
696
            $reflection = new \ReflectionClass($this);
697
            $name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $reflection->getShortName()))));
698
        }
699
700
        return $name;
701
    }
702
703
    /**
704
     * Get the translated user friendly singular name of this DataObject
705
     * same as singular_name() but runs it through the translating function
706
     *
707
     * Translating string is in the form:
708
     *     $this->class.SINGULARNAME
709
     * Example:
710
     *     Page.SINGULARNAME
711
     *
712
     * @return string User friendly translated singular name of this DataObject
713
     */
714
    public function i18n_singular_name()
715
    {
716
        // @todo Update localisation to FQN for all classes
717
        $reflection = new \ReflectionClass($this);
718
        return _t($reflection->getShortName().'.SINGULARNAME', $this->singular_name());
719
    }
720
721
    /**
722
     * Get the user friendly plural name of this DataObject
723
     * If the name is not defined (by renaming $plural_name in the subclass),
724
     * this returns a pluralised version of the class name.
725
     *
726
     * @return string User friendly plural name of this DataObject
727
     */
728
    public function plural_name()
729
    {
730
        if ($name = $this->stat('plural_name')) {
731
            return $name;
732
        } else {
733
            $name = $this->singular_name();
734
            //if the penultimate character is not a vowel, replace "y" with "ies"
735
            if (preg_match('/[^aeiou]y$/i', $name)) {
736
                $name = substr($name, 0, -1) . 'ie';
737
            }
738
            return ucfirst($name . 's');
739
        }
740
    }
741
742
    /**
743
     * Get the translated user friendly plural name of this DataObject
744
     * Same as plural_name but runs it through the translation function
745
     * Translation string is in the form:
746
     *      $this->class.PLURALNAME
747
     * Example:
748
     *      Page.PLURALNAME
749
     *
750
     * @return string User friendly translated plural name of this DataObject
751
     */
752
    public function i18n_plural_name()
753
    {
754
        // @todo Update localisation to FQN for all classes
755
        $name = $this->plural_name();
756
        $reflection = new \ReflectionClass($this);
757
        return _t($reflection->getShortName().'.PLURALNAME', $name);
758
    }
759
760
    /**
761
     * Standard implementation of a title/label for a specific
762
     * record. Tries to find properties 'Title' or 'Name',
763
     * and falls back to the 'ID'. Useful to provide
764
     * user-friendly identification of a record, e.g. in errormessages
765
     * or UI-selections.
766
     *
767
     * Overload this method to have a more specialized implementation,
768
     * e.g. for an Address record this could be:
769
     * <code>
770
     * function getTitle() {
771
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
772
     * }
773
     * </code>
774
     *
775
     * @return string
776
     */
777
    public function getTitle()
778
    {
779
        $schema = static::getSchema();
780
        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...
781
            return $this->getField('Title');
782
        }
783
        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...
784
            return $this->getField('Name');
785
        }
786
787
        return "#{$this->ID}";
788
    }
789
790
    /**
791
     * Returns the associated database record - in this case, the object itself.
792
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
793
     *
794
     * @return DataObject Associated database record
795
     */
796
    public function data()
797
    {
798
        return $this;
799
    }
800
801
    /**
802
     * Convert this object to a map.
803
     *
804
     * @return array The data as a map.
805
     */
806
    public function toMap()
807
    {
808
        $this->loadLazyFields();
809
        return $this->record;
810
    }
811
812
    /**
813
     * Return all currently fetched database fields.
814
     *
815
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
816
     * Obviously, this makes it a lot faster.
817
     *
818
     * @return array The data as a map.
819
     */
820
    public function getQueriedDatabaseFields()
821
    {
822
        return $this->record;
823
    }
824
825
    /**
826
     * Update a number of fields on this object, given a map of the desired changes.
827
     *
828
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
829
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
830
     *
831
     * update() doesn't write the main object, but if you use the dot syntax, it will write()
832
     * the related objects that it alters.
833
     *
834
     * @param array $data A map of field name to data values to update.
835
     * @return DataObject $this
836
     */
837
    public function update($data)
838
    {
839
        foreach ($data as $k => $v) {
840
            // Implement dot syntax for updates
841
            if (strpos($k, '.') !== false) {
842
                $relations = explode('.', $k);
843
                $fieldName = array_pop($relations);
844
                $relObj = $this;
845
                $relation = null;
846
                foreach ($relations as $i => $relation) {
847
                    // no support for has_many or many_many relationships,
848
                    // as the updater wouldn't know which object to write to (or create)
849
                    if ($relObj->$relation() instanceof DataObject) {
850
                        $parentObj = $relObj;
851
                        $relObj = $relObj->$relation();
852
                        // If the intermediate relationship objects have been created, then write them
853
                        if ($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
854
                            $relObj->write();
855
                            $relatedFieldName = $relation."ID";
856
                            $parentObj->$relatedFieldName = $relObj->ID;
857
                            $parentObj->write();
858
                        }
859
                    } else {
860
                        user_error(
861
                            "DataObject::update(): Can't traverse relationship '$relation'," .
862
                            "it has to be a has_one relationship or return a single DataObject",
863
                            E_USER_NOTICE
864
                        );
865
                        // unset relation object so we don't write properties to the wrong object
866
                        $relObj = null;
867
                        break;
868
                    }
869
                }
870
871
                if ($relObj) {
872
                    $relObj->$fieldName = $v;
873
                    $relObj->write();
874
                    $relatedFieldName = $relation."ID";
875
                    $this->$relatedFieldName = $relObj->ID;
876
                    $relObj->flushCache();
877
                } else {
878
                    user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
879
                }
880
            } else {
881
                $this->$k = $v;
882
            }
883
        }
884
        return $this;
885
    }
886
887
    /**
888
     * Pass changes as a map, and try to
889
     * get automatic casting for these fields.
890
     * Doesn't write to the database. To write the data,
891
     * use the write() method.
892
     *
893
     * @param array $data A map of field name to data values to update.
894
     * @return DataObject $this
895
     */
896
    public function castedUpdate($data)
897
    {
898
        foreach ($data as $k => $v) {
899
            $this->setCastedField($k, $v);
900
        }
901
        return $this;
902
    }
903
904
    /**
905
     * Merges data and relations from another object of same class,
906
     * without conflict resolution. Allows to specify which
907
     * dataset takes priority in case its not empty.
908
     * has_one-relations are just transferred with priority 'right'.
909
     * has_many and many_many-relations are added regardless of priority.
910
     *
911
     * Caution: has_many/many_many relations are moved rather than duplicated,
912
     * meaning they are not connected to the merged object any longer.
913
     * Caution: Just saves updated has_many/many_many relations to the database,
914
     * doesn't write the updated object itself (just writes the object-properties).
915
     * Caution: Does not delete the merged object.
916
     * Caution: Does now overwrite Created date on the original object.
917
     *
918
     * @param DataObject $rightObj
919
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
920
     * @param bool $includeRelations Merge any existing relations (optional)
921
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
922
     *                            Only applicable with $priority='right'. (optional)
923
     * @return Boolean
924
     */
925
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
926
    {
927
        $leftObj = $this;
928
929
        if ($leftObj->ClassName != $rightObj->ClassName) {
930
            // we can't merge similiar subclasses because they might have additional relations
931
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
932
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
933
            return false;
934
        }
935
936
        if (!$rightObj->ID) {
937
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
938
				to make sure all relations are transferred properly.').", E_USER_WARNING);
939
            return false;
940
        }
941
942
        // makes sure we don't merge data like ID or ClassName
943
        $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1481
        $this->ID = 0;
1482
    }
1483
1484
    /**
1485
     * Delete the record with the given ID.
1486
     *
1487
     * @param string $className The class name of the record to be deleted
1488
     * @param int $id ID of record to be deleted
1489
     */
1490
    public static function delete_by_id($className, $id)
1491
    {
1492
        $obj = DataObject::get_by_id($className, $id);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1493
        if ($obj) {
1494
            $obj->delete();
1495
        } else {
1496
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1497
        }
1498
    }
1499
1500
    /**
1501
     * Get the class ancestry, including the current class name.
1502
     * The ancestry will be returned as an array of class names, where the 0th element
1503
     * will be the class that inherits directly from DataObject, and the last element
1504
     * will be the current class.
1505
     *
1506
     * @return array Class ancestry
1507
     */
1508
    public function getClassAncestry()
1509
    {
1510
        return ClassInfo::ancestry(static::class);
1511
    }
1512
1513
    /**
1514
     * Return a component object from a one to one relationship, as a DataObject.
1515
     * If no component is available, an 'empty component' will be returned for
1516
     * non-polymorphic relations, or for polymorphic relations with a class set.
1517
     *
1518
     * @param string $componentName Name of the component
1519
     * @return DataObject The component object. It's exact type will be that of the component.
1520
     * @throws Exception
1521
     */
1522
    public function getComponent($componentName)
1523
    {
1524
        if (isset($this->components[$componentName])) {
1525
            return $this->components[$componentName];
1526
        }
1527
1528
        $schema = static::getSchema();
1529
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1530
            $joinField = $componentName . 'ID';
1531
            $joinID    = $this->getField($joinField);
1532
1533
            // Extract class name for polymorphic relations
1534
            if ($class === self::class) {
1535
                $class = $this->getField($componentName . 'Class');
1536
                if (empty($class)) {
1537
                    return null;
1538
                }
1539
            }
1540
1541
            if ($joinID) {
1542
                // Ensure that the selected object originates from the same stage, subsite, etc
1543
                $component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1544
                    ->filter('ID', $joinID)
1545
                    ->setDataQueryParam($this->getInheritableQueryParams())
1546
                    ->first();
1547
            }
1548
1549
            if (empty($component)) {
1550
                $component = $this->model->$class->newObject();
1551
            }
1552
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1553
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1554
            $joinID = $this->ID;
1555
1556
            if ($joinID) {
1557
                // Prepare filter for appropriate join type
1558
                if ($polymorphic) {
1559
                    $filter = array(
1560
                        "{$joinField}ID" => $joinID,
1561
                        "{$joinField}Class" => $this->class
1562
                    );
1563
                } else {
1564
                    $filter = array(
1565
                        $joinField => $joinID
1566
                    );
1567
                }
1568
1569
                // Ensure that the selected object originates from the same stage, subsite, etc
1570
                $component = DataObject::get($class)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1571
                    ->filter($filter)
1572
                    ->setDataQueryParam($this->getInheritableQueryParams())
1573
                    ->first();
1574
            }
1575
1576
            if (empty($component)) {
1577
                $component = $this->model->$class->newObject();
1578
                if ($polymorphic) {
1579
                    $component->{$joinField.'ID'} = $this->ID;
1580
                    $component->{$joinField.'Class'} = $this->class;
1581
                } else {
1582
                    $component->$joinField = $this->ID;
1583
                }
1584
            }
1585
        } else {
1586
            throw new InvalidArgumentException(
1587
                "DataObject->getComponent(): Could not find component '$componentName'."
1588
            );
1589
        }
1590
1591
        $this->components[$componentName] = $component;
1592
        return $component;
1593
    }
1594
1595
    /**
1596
     * Returns a one-to-many relation as a HasManyList
1597
     *
1598
     * @param string $componentName Name of the component
1599
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1600
     */
1601
    public function getComponents($componentName)
1602
    {
1603
        $result = null;
1604
1605
        $schema = $this->getSchema();
1606
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1607
        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...
1608
            throw new InvalidArgumentException(sprintf(
1609
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1610
                $componentName,
1611
                $this->class
1612
            ));
1613
        }
1614
1615
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1616
        if (!$this->ID) {
1617
            if (!isset($this->unsavedRelations[$componentName])) {
1618
                $this->unsavedRelations[$componentName] =
1619
                    new UnsavedRelationList($this->class, $componentName, $componentClass);
1620
            }
1621
            return $this->unsavedRelations[$componentName];
1622
        }
1623
1624
        // Determine type and nature of foreign relation
1625
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1626
        /** @var HasManyList $result */
1627
        if ($polymorphic) {
1628
            $result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1629
        } else {
1630
            $result = HasManyList::create($componentClass, $joinField);
1631
        }
1632
1633
        if ($this->model) {
1634
            $result->setDataModel($this->model);
1635
        }
1636
1637
        return $result
1638
            ->setDataQueryParam($this->getInheritableQueryParams())
1639
            ->forForeignID($this->ID);
1640
    }
1641
1642
    /**
1643
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1644
     *
1645
     * @param string $relationName Relation name.
1646
     * @return string Class name, or null if not found.
1647
     */
1648
    public function getRelationClass($relationName)
1649
    {
1650
        // Parse many_many
1651
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1652
        if ($manyManyComponent) {
1653
            list(
1654
                $relationClass, $parentClass, $componentClass,
0 ignored issues
show
Unused Code introduced by
The assignment to $relationClass is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $parentClass is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1655
                $parentField, $childField, $tableOrClass
0 ignored issues
show
Unused Code introduced by
The assignment to $parentField is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $childField is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $tableOrClass is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1656
            ) = $manyManyComponent;
1657
            return $componentClass;
1658
        }
1659
1660
        // Go through all relationship configuration fields.
1661
        $candidates = array_merge(
1662
            ($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1663
            ($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1664
            ($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1665
        );
1666
1667
        if (isset($candidates[$relationName])) {
1668
            $remoteClass = $candidates[$relationName];
1669
1670
            // If dot notation is present, extract just the first part that contains the class.
1671
            if (($fieldPos = strpos($remoteClass, '.'))!==false) {
1672
                return substr($remoteClass, 0, $fieldPos);
1673
            }
1674
1675
            // Otherwise just return the class
1676
            return $remoteClass;
1677
        }
1678
1679
        return null;
1680
    }
1681
1682
    /**
1683
     * Given a relation name, determine the relation type
1684
     *
1685
     * @param string $component Name of component
1686
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1687
     */
1688
    public function getRelationType($component)
1689
    {
1690
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1691
        foreach ($types as $type) {
1692
            $relations = Config::inst()->get($this->class, $type);
1693
            if ($relations && isset($relations[$component])) {
1694
                return $type;
1695
            }
1696
        }
1697
        return null;
1698
    }
1699
1700
    /**
1701
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1702
     * side of the relation.
1703
     *
1704
     * Notes on behaviour:
1705
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1706
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1707
     *  - Cannot be used on polymorphic relationships
1708
     *  - Cannot be used on unsaved objects.
1709
     *
1710
     * @param string $remoteClass
1711
     * @param string $remoteRelation
1712
     * @return DataList|DataObject The component, either as a list or single object
1713
     * @throws BadMethodCallException
1714
     * @throws InvalidArgumentException
1715
     */
1716
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1717
    {
1718
        $remote = DataObject::singleton($remoteClass);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1719
        $class = $remote->getRelationClass($remoteRelation);
1720
        $schema = static::getSchema();
1721
1722
        // Validate arguments
1723
        if (!$this->isInDB()) {
1724
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1725
        }
1726
        if (empty($class)) {
1727
            throw new InvalidArgumentException(sprintf(
1728
                "%s invoked with invalid relation %s.%s",
1729
                __METHOD__,
1730
                $remoteClass,
1731
                $remoteRelation
1732
            ));
1733
        }
1734
        if ($class === self::class) {
1735
            throw new InvalidArgumentException(sprintf(
1736
                "%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1737
                "This method does not support polymorphic relationships",
1738
                __METHOD__,
1739
                $remoteClass,
1740
                $remoteRelation
1741
            ));
1742
        }
1743
        if (!is_a($this, $class, true)) {
1744
            throw new InvalidArgumentException(sprintf(
1745
                "Relation %s on %s does not refer to objects of type %s",
1746
                $remoteRelation,
1747
                $remoteClass,
1748
                static::class
1749
            ));
1750
        }
1751
1752
        // Check the relation type to mock
1753
        $relationType = $remote->getRelationType($remoteRelation);
1754
        switch ($relationType) {
1755
            case 'has_one': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1756
                // Mock has_many
1757
                $joinField = "{$remoteRelation}ID";
1758
                $componentClass = $schema->classForField($remoteClass, $joinField);
1759
                $result = HasManyList::create($componentClass, $joinField);
1760
                if ($this->model) {
1761
                    $result->setDataModel($this->model);
1762
                }
1763
                return $result
1764
                    ->setDataQueryParam($this->getInheritableQueryParams())
1765
                    ->forForeignID($this->ID);
1766
            }
1767
            case 'belongs_to':
1768
            case 'has_many': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1769
                // These relations must have a has_one on the other end, so find it
1770
                $joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic);
1771
                if ($polymorphic) {
1772
                    throw new InvalidArgumentException(sprintf(
1773
                        "%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1774
                        "to be a has_one polymorphic. This method does not support polymorphic relationships",
1775
                        __METHOD__,
1776
                        $remoteClass,
1777
                        $remoteRelation
1778
                    ));
1779
                }
1780
                $joinID = $this->getField($joinField);
1781
                if (empty($joinID)) {
1782
                    return null;
1783
                }
1784
                // Get object by joined ID
1785
                return DataObject::get($remoteClass)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1786
                    ->filter('ID', $joinID)
1787
                    ->setDataQueryParam($this->getInheritableQueryParams())
1788
                    ->first();
1789
            }
1790
            case 'many_many':
1791
            case 'belongs_many_many': {
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1792
                // Get components and extra fields from parent
1793
                list($relationClass, $componentClass, $parentClass, $componentField, $parentField, $table)
0 ignored issues
show
Unused Code introduced by
The assignment to $parentClass is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1794
                    = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1795
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1796
1797
                // Reverse parent and component fields and create an inverse ManyManyList
1798
                /** @var RelationList $result */
1799
                $result = Injector::inst()->create(
1800
                    $relationClass,
1801
                    $componentClass,
1802
                    $table,
1803
                    $componentField,
1804
                    $parentField,
1805
                    $extraFields
1806
                );
1807
                if ($this->model) {
1808
                    $result->setDataModel($this->model);
1809
                }
1810
                $this->extend('updateManyManyComponents', $result);
1811
1812
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1813
                // foreignID set elsewhere.
1814
                return $result
1815
                    ->setDataQueryParam($this->getInheritableQueryParams())
1816
                    ->forForeignID($this->ID);
1817
            }
1818
            default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1819
                return null;
1820
            }
1821
        }
1822
    }
1823
1824
    /**
1825
     * Returns a many-to-many component, as a ManyManyList.
1826
     * @param string $componentName Name of the many-many component
1827
     * @return RelationList|UnsavedRelationList The set of components
1828
     */
1829
    public function getManyManyComponents($componentName)
1830
    {
1831
        $schema = static::getSchema();
1832
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
1833
        if (!$manyManyComponent) {
1834
            throw new InvalidArgumentException(sprintf(
1835
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1836
                $componentName,
1837
                $this->class
1838
            ));
1839
        }
1840
1841
        list($relationClass, $parentClass, $componentClass, $parentField, $componentField, $tableOrClass)
1842
            = $manyManyComponent;
1843
1844
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1845
        if (!$this->ID) {
1846
            if (!isset($this->unsavedRelations[$componentName])) {
1847
                $this->unsavedRelations[$componentName] =
1848
                    new UnsavedRelationList($parentClass, $componentName, $componentClass);
1849
            }
1850
            return $this->unsavedRelations[$componentName];
1851
        }
1852
1853
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
1854
        /** @var RelationList $result */
1855
        $result = Injector::inst()->create(
1856
            $relationClass,
1857
            $componentClass,
1858
            $tableOrClass,
1859
            $componentField,
1860
            $parentField,
1861
            $extraFields
1862
        );
1863
1864
1865
        // Store component data in query meta-data
1866
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
1867
            /** @var DataQuery $query */
1868
            $query->setQueryParam('Component.ExtraFields', $extraFields);
1869
        });
1870
1871
        if ($this->model) {
1872
            $result->setDataModel($this->model);
1873
        }
1874
1875
        $this->extend('updateManyManyComponents', $result);
1876
1877
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1878
        // foreignID set elsewhere.
1879
        return $result
1880
            ->setDataQueryParam($this->getInheritableQueryParams())
1881
            ->forForeignID($this->ID);
1882
    }
1883
1884
    /**
1885
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1886
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1887
     *
1888
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1889
     *                          their classes.
1890
     */
1891
    public function hasOne()
1892
    {
1893
        return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1894
    }
1895
1896
    /**
1897
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1898
     * their class name will be returned.
1899
     *
1900
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1901
     *        the field data stripped off. It defaults to TRUE.
1902
     * @return string|array
1903
     */
1904
    public function belongsTo($classOnly = true)
1905
    {
1906
        $belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1907
        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...
1908
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1909
        } else {
1910
            return $belongsTo ? $belongsTo : array();
1911
        }
1912
    }
1913
1914
    /**
1915
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1916
     * relationships and their classes will be returned.
1917
     *
1918
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1919
     *        the field data stripped off. It defaults to TRUE.
1920
     * @return string|array|false
1921
     */
1922
    public function hasMany($classOnly = true)
1923
    {
1924
        $hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
1925
        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...
1926
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1927
        } else {
1928
            return $hasMany ? $hasMany : array();
1929
        }
1930
    }
1931
1932
    /**
1933
     * Return the many-to-many extra fields specification.
1934
     *
1935
     * If you don't specify a component name, it returns all
1936
     * extra fields for all components available.
1937
     *
1938
     * @return array|null
1939
     */
1940
    public function manyManyExtraFields()
1941
    {
1942
        return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
1943
    }
1944
1945
    /**
1946
     * Return information about a many-to-many component.
1947
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
1948
     * components are returned.
1949
     *
1950
     * @see DataObjectSchema::manyManyComponent()
1951
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
1952
     */
1953
    public function manyMany()
1954
    {
1955
        $manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
1956
        $belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
1957
        $items = array_merge($manyManys, $belongsManyManys);
1958
        return $items;
1959
    }
1960
1961
    /**
1962
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
1963
     *
1964
     * This is experimental, and is currently only a Postgres-specific enhancement.
1965
     *
1966
     * @param string $class
1967
     * @return array|false
1968
     */
1969
    public function database_extensions($class)
1970
    {
1971
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
1972
1973
        if ($extensions) {
1974
            return $extensions;
1975
        } else {
1976
            return false;
1977
        }
1978
    }
1979
1980
    /**
1981
     * Generates a SearchContext to be used for building and processing
1982
     * a generic search form for properties on this object.
1983
     *
1984
     * @return SearchContext
1985
     */
1986
    public function getDefaultSearchContext()
1987
    {
1988
        return new SearchContext(
1989
            $this->class,
1990
            $this->scaffoldSearchFields(),
1991
            $this->defaultSearchFilters()
1992
        );
1993
    }
1994
1995
    /**
1996
     * Determine which properties on the DataObject are
1997
     * searchable, and map them to their default {@link FormField}
1998
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
1999
     *
2000
     * Some additional logic is included for switching field labels, based on
2001
     * how generic or specific the field type is.
2002
     *
2003
     * Used by {@link SearchContext}.
2004
     *
2005
     * @param array $_params
2006
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2007
     *   'restrictFields': Numeric array of a field name whitelist
2008
     * @return FieldList
2009
     */
2010
    public function scaffoldSearchFields($_params = null)
2011
    {
2012
        $params = array_merge(
2013
            array(
2014
                'fieldClasses' => false,
2015
                'restrictFields' => false
2016
            ),
2017
            (array)$_params
2018
        );
2019
        $fields = new FieldList();
2020
        foreach ($this->searchableFields() as $fieldName => $spec) {
2021
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
2022
                continue;
2023
            }
2024
2025
            // If a custom fieldclass is provided as a string, use it
2026
            $field = null;
2027
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2028
                $fieldClass = $params['fieldClasses'][$fieldName];
2029
                $field = new $fieldClass($fieldName);
2030
            // If we explicitly set a field, then construct that
2031
            } elseif (isset($spec['field'])) {
2032
                // If it's a string, use it as a class name and construct
2033
                if (is_string($spec['field'])) {
2034
                    $fieldClass = $spec['field'];
2035
                    $field = new $fieldClass($fieldName);
2036
2037
                // If it's a FormField object, then just use that object directly.
2038
                } elseif ($spec['field'] instanceof FormField) {
2039
                    $field = $spec['field'];
2040
2041
                // Otherwise we have a bug
2042
                } else {
2043
                    user_error("Bad value for searchable_fields, 'field' value: "
2044
                        . var_export($spec['field'], true), E_USER_WARNING);
2045
                }
2046
2047
            // Otherwise, use the database field's scaffolder
2048
            } else {
2049
                $field = $this->relObject($fieldName)->scaffoldSearchField();
2050
            }
2051
2052
            // Allow fields to opt out of search
2053
            if (!$field) {
2054
                continue;
2055
            }
2056
2057
            if (strstr($fieldName, '.')) {
2058
                $field->setName(str_replace('.', '__', $fieldName));
2059
            }
2060
            $field->setTitle($spec['title']);
2061
2062
            $fields->push($field);
2063
        }
2064
        return $fields;
2065
    }
2066
2067
    /**
2068
     * Scaffold a simple edit form for all properties on this dataobject,
2069
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2070
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2071
     *
2072
     * @uses FormScaffolder
2073
     *
2074
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2075
     * @return FieldList
2076
     */
2077
    public function scaffoldFormFields($_params = null)
2078
    {
2079
        $params = array_merge(
2080
            array(
2081
                'tabbed' => false,
2082
                'includeRelations' => false,
2083
                'restrictFields' => false,
2084
                'fieldClasses' => false,
2085
                'ajaxSafe' => false
2086
            ),
2087
            (array)$_params
2088
        );
2089
2090
        $fs = new FormScaffolder($this);
2091
        $fs->tabbed = $params['tabbed'];
2092
        $fs->includeRelations = $params['includeRelations'];
2093
        $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...
2094
        $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...
2095
        $fs->ajaxSafe = $params['ajaxSafe'];
2096
2097
        return $fs->getFieldList();
2098
    }
2099
2100
    /**
2101
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2102
     * being called on extensions
2103
     *
2104
     * @param callable $callback The callback to execute
2105
     */
2106
    protected function beforeUpdateCMSFields($callback)
2107
    {
2108
        $this->beforeExtending('updateCMSFields', $callback);
2109
    }
2110
2111
    /**
2112
     * Centerpiece of every data administration interface in Silverstripe,
2113
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2114
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2115
     * generate this set. To customize, overload this method in a subclass
2116
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2117
     *
2118
     * <code>
2119
     * class MyCustomClass extends DataObject {
2120
     *  static $db = array('CustomProperty'=>'Boolean');
2121
     *
2122
     *  function getCMSFields() {
2123
     *    $fields = parent::getCMSFields();
2124
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2125
     *    return $fields;
2126
     *  }
2127
     * }
2128
     * </code>
2129
     *
2130
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2131
     *
2132
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2133
     */
2134
    public function getCMSFields()
2135
    {
2136
        $tabbedFields = $this->scaffoldFormFields(array(
2137
            // Don't allow has_many/many_many relationship editing before the record is first saved
2138
            'includeRelations' => ($this->ID > 0),
2139
            'tabbed' => true,
2140
            'ajaxSafe' => true
2141
        ));
2142
2143
        $this->extend('updateCMSFields', $tabbedFields);
2144
2145
        return $tabbedFields;
2146
    }
2147
2148
    /**
2149
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2150
     * including that dataobject's extensions customised actions could be added to the EditForm.
2151
     *
2152
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2153
     */
2154
    public function getCMSActions()
2155
    {
2156
        $actions = new FieldList();
2157
        $this->extend('updateCMSActions', $actions);
2158
        return $actions;
2159
    }
2160
2161
2162
    /**
2163
     * Used for simple frontend forms without relation editing
2164
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2165
     * by default. To customize, either overload this method in your
2166
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2167
     *
2168
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2169
     *
2170
     * @param array $params See {@link scaffoldFormFields()}
2171
     * @return FieldList Always returns a simple field collection without TabSet.
2172
     */
2173
    public function getFrontEndFields($params = null)
2174
    {
2175
        $untabbedFields = $this->scaffoldFormFields($params);
2176
        $this->extend('updateFrontEndFields', $untabbedFields);
2177
2178
        return $untabbedFields;
2179
    }
2180
2181
    /**
2182
     * Gets the value of a field.
2183
     * Called by {@link __get()} and any getFieldName() methods you might create.
2184
     *
2185
     * @param string $field The name of the field
2186
     * @return mixed The field value
2187
     */
2188
    public function getField($field)
2189
    {
2190
        // If we already have an object in $this->record, then we should just return that
2191
        if (isset($this->record[$field]) && is_object($this->record[$field])) {
2192
            return $this->record[$field];
2193
        }
2194
2195
        // Do we have a field that needs to be lazy loaded?
2196
        if (isset($this->record[$field.'_Lazy'])) {
2197
            $tableClass = $this->record[$field.'_Lazy'];
2198
            $this->loadLazyFields($tableClass);
2199
        }
2200
2201
        // In case of complex fields, return the DBField object
2202
        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...
2203
            $this->record[$field] = $this->dbObject($field);
2204
        }
2205
2206
        return isset($this->record[$field]) ? $this->record[$field] : null;
2207
    }
2208
2209
    /**
2210
     * Loads all the stub fields that an initial lazy load didn't load fully.
2211
     *
2212
     * @param string $class Class to load the values from. Others are joined as required.
2213
     * Not specifying a tableClass will load all lazy fields from all tables.
2214
     * @return bool Flag if lazy loading succeeded
2215
     */
2216
    protected function loadLazyFields($class = null)
2217
    {
2218
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2219
            return false;
2220
        }
2221
2222
        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...
2223
            $loaded = array();
2224
2225
            foreach ($this->record as $key => $value) {
2226
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2227
                    $this->loadLazyFields($value);
2228
                    $loaded[$value] = $value;
2229
                }
2230
            }
2231
2232
            return false;
2233
        }
2234
2235
        $dataQuery = new DataQuery($class);
2236
2237
        // Reset query parameter context to that of this DataObject
2238
        if ($params = $this->getSourceQueryParams()) {
2239
            foreach ($params as $key => $value) {
2240
                $dataQuery->setQueryParam($key, $value);
2241
            }
2242
        }
2243
2244
        // Limit query to the current record, unless it has the Versioned extension,
2245
        // in which case it requires special handling through augmentLoadLazyFields()
2246
        $schema = static::getSchema();
2247
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2248
        $dataQuery->where([
2249
            $baseIDColumn => $this->record['ID']
2250
        ])->limit(1);
2251
2252
        $columns = array();
2253
2254
        // Add SQL for fields, both simple & multi-value
2255
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2256
        $databaseFields = $schema->databaseFields($class, false);
2257
        foreach ($databaseFields as $k => $v) {
2258
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2259
                $columns[] = $k;
2260
            }
2261
        }
2262
2263
        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...
2264
            $query = $dataQuery->query();
2265
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2266
            $this->extend('augmentSQL', $query, $dataQuery);
2267
2268
            $dataQuery->setQueriedColumns($columns);
2269
            $newData = $dataQuery->execute()->record();
2270
2271
            // Load the data into record
2272
            if ($newData) {
2273
                foreach ($newData as $k => $v) {
2274
                    if (in_array($k, $columns)) {
2275
                        $this->record[$k] = $v;
2276
                        $this->original[$k] = $v;
2277
                        unset($this->record[$k . '_Lazy']);
2278
                    }
2279
                }
2280
2281
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2282
            } else {
2283
                foreach ($columns as $k) {
2284
                    $this->record[$k] = null;
2285
                    $this->original[$k] = null;
2286
                    unset($this->record[$k . '_Lazy']);
2287
                }
2288
            }
2289
        }
2290
        return true;
2291
    }
2292
2293
    /**
2294
     * Return the fields that have changed.
2295
     *
2296
     * The change level affects what the functions defines as "changed":
2297
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2298
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2299
     *   for example a change from 0 to null would not be included.
2300
     *
2301
     * Example return:
2302
     * <code>
2303
     * array(
2304
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2305
     * )
2306
     * </code>
2307
     *
2308
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2309
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2310
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2311
     * @return array
2312
     */
2313
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2314
    {
2315
        $changedFields = array();
2316
2317
        // Update the changed array with references to changed obj-fields
2318
        foreach ($this->record as $k => $v) {
2319
            // Prevents DBComposite infinite looping on isChanged
2320
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2321
                continue;
2322
            }
2323
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2324
                $this->changed[$k] = self::CHANGE_VALUE;
2325
            }
2326
        }
2327
2328
        if (is_array($databaseFieldsOnly)) {
2329
            $fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2330
        } elseif ($databaseFieldsOnly) {
2331
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2332
            $fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
2333
        } else {
2334
            $fields = $this->changed;
2335
        }
2336
2337
        // Filter the list to those of a certain change level
2338
        if ($changeLevel > self::CHANGE_STRICT) {
2339
            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...
2340
                foreach ($fields as $name => $level) {
2341
                    if ($level < $changeLevel) {
2342
                        unset($fields[$name]);
2343
                    }
2344
                }
2345
            }
2346
        }
2347
2348
        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...
2349
            foreach ($fields as $name => $level) {
2350
                $changedFields[$name] = array(
2351
                'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2352
                'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2353
                'level' => $level
2354
                );
2355
            }
2356
        }
2357
2358
        return $changedFields;
2359
    }
2360
2361
    /**
2362
     * Uses {@link getChangedFields()} to determine if fields have been changed
2363
     * since loading them from the database.
2364
     *
2365
     * @param string $fieldName Name of the database field to check, will check for any if not given
2366
     * @param int $changeLevel See {@link getChangedFields()}
2367
     * @return boolean
2368
     */
2369
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2370
    {
2371
        $fields = $fieldName ? array($fieldName) : true;
2372
        $changed = $this->getChangedFields($fields, $changeLevel);
2373
        if (!isset($fieldName)) {
2374
            return !empty($changed);
2375
        } else {
2376
            return array_key_exists($fieldName, $changed);
2377
        }
2378
    }
2379
2380
    /**
2381
     * Set the value of the field
2382
     * Called by {@link __set()} and any setFieldName() methods you might create.
2383
     *
2384
     * @param string $fieldName Name of the field
2385
     * @param mixed $val New field value
2386
     * @return $this
2387
     */
2388
    public function setField($fieldName, $val)
2389
    {
2390
        $this->objCacheClear();
2391
        //if it's a has_one component, destroy the cache
2392
        if (substr($fieldName, -2) == 'ID') {
2393
            unset($this->components[substr($fieldName, 0, -2)]);
2394
        }
2395
2396
        // If we've just lazy-loaded the column, then we need to populate the $original array
2397
        if (isset($this->record[$fieldName.'_Lazy'])) {
2398
            $tableClass = $this->record[$fieldName.'_Lazy'];
2399
            $this->loadLazyFields($tableClass);
2400
        }
2401
2402
        // Situation 1: Passing an DBField
2403
        if ($val instanceof DBField) {
2404
            $val->setName($fieldName);
2405
            $val->saveInto($this);
2406
2407
            // Situation 1a: Composite fields should remain bound in case they are
2408
            // later referenced to update the parent dataobject
2409
            if ($val instanceof DBComposite) {
2410
                $val->bindTo($this);
2411
                $this->record[$fieldName] = $val;
2412
            }
2413
        // Situation 2: Passing a literal or non-DBField object
2414
        } else {
2415
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2416
            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...
2417
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2418
            }
2419
2420
            // if a field is not existing or has strictly changed
2421
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2422
                // TODO Add check for php-level defaults which are not set in the db
2423
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2424
                // At the very least, the type has changed
2425
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2426
2427
                if ((!isset($this->record[$fieldName]) && $val)
2428
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2429
                ) {
2430
                    // Value has changed as well, not just the type
2431
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2432
                }
2433
2434
                // Value is always saved back when strict check succeeds.
2435
                $this->record[$fieldName] = $val;
2436
            }
2437
        }
2438
        return $this;
2439
    }
2440
2441
    /**
2442
     * Set the value of the field, using a casting object.
2443
     * This is useful when you aren't sure that a date is in SQL format, for example.
2444
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2445
     * can be saved into the Image table.
2446
     *
2447
     * @param string $fieldName Name of the field
2448
     * @param mixed $value New field value
2449
     * @return $this
2450
     */
2451
    public function setCastedField($fieldName, $value)
2452
    {
2453
        if (!$fieldName) {
2454
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2455
        }
2456
        $fieldObj = $this->dbObject($fieldName);
2457
        if ($fieldObj) {
2458
            $fieldObj->setValue($value);
2459
            $fieldObj->saveInto($this);
2460
        } else {
2461
            $this->$fieldName = $value;
2462
        }
2463
        return $this;
2464
    }
2465
2466
    /**
2467
     * {@inheritdoc}
2468
     */
2469
    public function castingHelper($field)
2470
    {
2471
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2472
        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...
2473
            return $fieldSpec;
2474
        }
2475
2476
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2477
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2478
        $queryParams = $this->getSourceQueryParams();
2479
        if (!empty($queryParams['Component.ExtraFields'])) {
2480
            $extraFields = $queryParams['Component.ExtraFields'];
2481
2482
            if (isset($extraFields[$field])) {
2483
                return $extraFields[$field];
2484
            }
2485
        }
2486
2487
        return parent::castingHelper($field);
2488
    }
2489
2490
    /**
2491
     * Returns true if the given field exists in a database column on any of
2492
     * the objects tables and optionally look up a dynamic getter with
2493
     * get<fieldName>().
2494
     *
2495
     * @param string $field Name of the field
2496
     * @return boolean True if the given field exists
2497
     */
2498
    public function hasField($field)
2499
    {
2500
        $schema = static::getSchema();
2501
        return (
2502
            array_key_exists($field, $this->record)
2503
            || $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...
2504
            || (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...
2505
            || $this->hasMethod("get{$field}")
2506
        );
2507
    }
2508
2509
    /**
2510
     * Returns true if the given field exists as a database column
2511
     *
2512
     * @param string $field Name of the field
2513
     *
2514
     * @return boolean
2515
     */
2516
    public function hasDatabaseField($field)
2517
    {
2518
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2519
        return !empty($spec);
2520
    }
2521
2522
    /**
2523
     * Returns true if the member is allowed to do the given action.
2524
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2525
     *
2526
     * @param string $perm The permission to be checked, such as 'View'.
2527
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2528
     * in user.
2529
     * @param array $context Additional $context to pass to extendedCan()
2530
     *
2531
     * @return boolean True if the the member is allowed to do the given action
2532
     */
2533
    public function can($perm, $member = null, $context = array())
2534
    {
2535
        if (!isset($member)) {
2536
            $member = Member::currentUser();
2537
        }
2538
        if (Permission::checkMember($member, "ADMIN")) {
2539
            return true;
2540
        }
2541
2542
        if ($this->getSchema()->manyManyComponent(static::class, 'Can' . $perm)) {
2543
            if ($this->ParentID && $this->SecurityType == 'Inherit') {
2544
                if (!($p = $this->Parent)) {
0 ignored issues
show
Documentation introduced by
The property Parent does not exist on object<SilverStripe\ORM\DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2545
                    return false;
2546
                }
2547
                return $this->Parent->can($perm, $member);
2548
            } else {
2549
                $permissionCache = $this->uninherited('permissionCache');
2550
                $memberID = $member ? $member->ID : 'none';
2551
2552
                if (!isset($permissionCache[$memberID][$perm])) {
2553
                    if ($member->ID) {
2554
                        $groups = $member->Groups();
2555
                    }
2556
2557
                    $groupList = implode(', ', $groups->column("ID"));
0 ignored issues
show
Bug introduced by
The variable $groups does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2558
2559
                    // TODO Fix relation table hardcoding
2560
                    $query = new SQLSelect(
2561
                        "\"Page_Can$perm\".PageID",
2562
                        array("\"Page_Can$perm\""),
2563
                        "GroupID IN ($groupList)"
2564
                    );
2565
2566
                    $permissionCache[$memberID][$perm] = $query->execute()->column();
2567
2568
                    if ($perm == "View") {
2569
                        // TODO Fix relation table hardcoding
2570
                        $query = new SQLSelect("\"SiteTree\".\"ID\"", array(
2571
                            "\"SiteTree\"",
2572
                            "LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2573
                            ), "\"Page_CanView\".\"PageID\" IS NULL");
2574
2575
                            $unsecuredPages = $query->execute()->column();
2576
                        if ($permissionCache[$memberID][$perm]) {
2577
                            $permissionCache[$memberID][$perm]
2578
                            = array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2579
                        } else {
2580
                            $permissionCache[$memberID][$perm] = $unsecuredPages;
2581
                        }
2582
                    }
2583
2584
                    Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2585
                }
2586
2587
                if ($permissionCache[$memberID][$perm]) {
2588
                    return in_array($this->ID, $permissionCache[$memberID][$perm]);
2589
                }
2590
            }
2591
        } else {
2592
            return parent::can($perm, $member);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SilverStripe\View\ViewableData as the method can() does only exist in the following sub-classes of SilverStripe\View\ViewableData: FormFactoryTest_TestController, FormFactoryTest_TestObject, SilverStripe\Admin\AdminRootController, SilverStripe\Admin\CMSProfileController, SilverStripe\Admin\CampaignAdmin, SilverStripe\Admin\LeftAndMain, SilverStripe\Admin\ModelAdmin, SilverStripe\Admin\SecurityAdmin, SilverStripe\Admin\Tests\CMSMenuTest\CustomTitle, SilverStripe\Admin\Tests...t\LeftAndMainController, SilverStripe\Admin\Tests...inTest\InvalidChangeSet, SilverStripe\Admin\Tests...MainTest\TestController, SilverStripe\Admin\Tests...tAndMainTest\TestObject, SilverStripe\Admin\Tests\ModelAdminTest\Contact, SilverStripe\Admin\Tests...lAdminTest\ContactAdmin, SilverStripe\Admin\Tests\ModelAdminTest\Player, SilverStripe\Admin\Tests...elAdminTest\PlayerAdmin, SilverStripe\Assets\File, SilverStripe\Assets\Folder, SilverStripe\Assets\Image, SilverStripe\Assets\Stor...ProtectedFileController, SilverStripe\Assets\Test...sionTest\ArchivedObject, SilverStripe\Assets\Test...xtensionTest\TestObject, SilverStripe\Assets\Test...ionTest\VersionedObject, SilverStripe\Assets\Tests\FileTest\MyCustomFile, SilverStripe\Assets\Upload, SilverStripe\Control\CliController, SilverStripe\Control\Controller, SilverStripe\Control\Tes...st\AccessBaseController, SilverStripe\Control\Tes...AccessSecuredController, SilverStripe\Control\Tes...ldcardSecuredController, SilverStripe\Control\Tes...est\ContainerController, SilverStripe\Control\Tes...ontrollerTest\HasAction, SilverStripe\Control\Tes...est\HasAction_Unsecured, SilverStripe\Control\Tes...\IndexSecuredController, SilverStripe\Control\Tes...ollerTest\SubController, SilverStripe\Control\Tes...llerTest\TestController, SilverStripe\Control\Tes...est\UnsecuredController, SilverStripe\Control\Tes...ctorTest\TestController, SilverStripe\Control\Tests\FakeController, SilverStripe\Control\Tes...gTest\AllowedController, SilverStripe\Control\Tes...rFormWithAllowedActions, SilverStripe\Control\Tes...ingTest\FieldController, SilverStripe\Control\Tes...st\FormActionController, SilverStripe\Control\Tes...lingTest\TestController, SilverStripe\Core\Tests\ClassInfoTest\BaseClass, SilverStripe\Core\Tests\...sInfoTest\BaseDataClass, SilverStripe\Core\Tests\ClassInfoTest\ChildClass, SilverStripe\Core\Tests\...nfoTest\GrandChildClass, SilverStripe\Core\Tests\ClassInfoTest\HasFields, SilverStripe\Core\Tests\ClassInfoTest\NoFields, SilverStripe\Core\Tests\...nfoTest\WithCustomTable, SilverStripe\Core\Tests\ClassInfoTest\WithRelation, SilverStripe\Dev\DevBuildController, SilverStripe\Dev\DevelopmentAdmin, SilverStripe\Dev\InstallerTest, SilverStripe\Dev\SapphireInfo, SilverStripe\Dev\SapphireREPL, SilverStripe\Dev\TaskRunner, SilverStripe\Dev\Tests\BulkLoaderResultTest\Player, SilverStripe\Dev\Tests\CsvBulkLoaderTest\Player, SilverStripe\Dev\Tests\C...aderTest\PlayerContract, SilverStripe\Dev\Tests\CsvBulkLoaderTest\Team, SilverStripe\Dev\Tests\D...trollerTest\Controller1, SilverStripe\Dev\Tests\F...reBlueprintTest\Article, SilverStripe\Dev\Tests\F...eBlueprintTest\TestPage, SilverStripe\Dev\Tests\F...eprintTest\TestSiteTree, SilverStripe\Dev\Tests\F...Test\DataObjectRelation, SilverStripe\Dev\Tests\F...toryTest\TestDataObject, SilverStripe\Dev\Tests\Y...Test\DataObjectRelation, SilverStripe\Dev\Tests\Y...tureTest\TestDataObject, SilverStripe\Forms\Tests...ieldTest\TestController, SilverStripe\Forms\Tests\AssetFieldTest\TestObject, SilverStripe\Forms\Tests\CheckboxFieldtest\Article, SilverStripe\Forms\Tests...boxSetFieldTest\Article, SilverStripe\Forms\Tests\CheckboxSetFieldTest\Tag, SilverStripe\Forms\Tests\DBFileTest\ImageOnly, SilverStripe\Forms\Tests\DBFileTest\Subclass, SilverStripe\Forms\Tests\DBFileTest\TestObject, SilverStripe\Forms\Tests\DatetimeFieldTest\Model, SilverStripe\Forms\Tests...ieldTest\TestController, SilverStripe\Forms\Tests...mScaffolderTest\Article, SilverStripe\Forms\Tests\FormScaffolderTest\Author, SilverStripe\Forms\Tests\FormScaffolderTest\Tag, SilverStripe\Forms\Tests...rollerWithSecurityToken, SilverStripe\Forms\Tests...llerWithStrictPostCheck, SilverStripe\Forms\Tests\FormTest\Player, SilverStripe\Forms\Tests\FormTest\Team, SilverStripe\Forms\Tests\FormTest\TestController, SilverStripe\Forms\Tests...eterTest\TestController, SilverStripe\Forms\Tests...DetailFormTest\Category, SilverStripe\Forms\Tests...Test\CategoryController, SilverStripe\Forms\Tests...ormTest\GroupController, SilverStripe\Forms\Tests...ailFormTest\PeopleGroup, SilverStripe\Forms\Tests...ldDetailFormTest\Person, SilverStripe\Forms\Tests...FormTest\TestController, SilverStripe\Forms\Tests...ExportButtonTest\NoView, SilverStripe\Forms\Tests...ldExportButtonTest\Team, SilverStripe\Forms\Tests...ntButtonTest\TestObject, SilverStripe\Forms\Tests...eHeaderTest\Cheerleader, SilverStripe\Forms\Tests...aderTest\CheerleaderHat, SilverStripe\Forms\Tests...dSortableHeaderTest\Mom, SilverStripe\Forms\Tests...SortableHeaderTest\Team, SilverStripe\Forms\Tests...bleHeaderTest\TeamGroup, SilverStripe\Forms\Tests...idFieldTest\Cheerleader, SilverStripe\Forms\Tests...idFieldTest\Permissions, SilverStripe\Forms\Tests...ld\GridFieldTest\Player, SilverStripe\Forms\Tests...ield\GridFieldTest\Team, SilverStripe\Forms\Tests...dlerTest\TestController, SilverStripe\Forms\Tests...torFieldTest\TestObject, SilverStripe\Forms\Tests\ListboxFieldTest\Article, SilverStripe\Forms\Tests\ListboxFieldTest\Tag, SilverStripe\Forms\Tests...boxFieldTest\TestObject, SilverStripe\Forms\Tests...est\CustomSetter_Object, SilverStripe\Forms\Tests\MoneyFieldTest\TestObject, SilverStripe\Forms\Tests...ricFieldTest\TestObject, SilverStripe\Forms\Tests...dFieldTest\ExtendedFile, SilverStripe\Forms\Tests...ieldTest\TestController, SilverStripe\Forms\Tests...oadFieldTest\TestRecord, SilverStripe\Framework\Tests\ClassI, SilverStripe\ORM\DataObject, SilverStripe\ORM\DatabaseAdmin, SilverStripe\ORM\Tests\C...temTest\VersionedObject, SilverStripe\ORM\Tests\ChangeSetTest\BaseObject, SilverStripe\ORM\Tests\ChangeSetTest\EndObject, SilverStripe\ORM\Tests\C...eSetTest\EndObjectChild, SilverStripe\ORM\Tests\ChangeSetTest\MidObject, SilverStripe\ORM\Tests\ComponentSetTest\Player, SilverStripe\ORM\Tests\ComponentSetTest\Team, SilverStripe\ORM\Tests\D...sNameTest\CustomDefault, SilverStripe\ORM\Tests\D...t\CustomDefaultSubclass, SilverStripe\ORM\Tests\D...NameTest\ObjectSubClass, SilverStripe\ORM\Tests\D...eTest\ObjectSubSubClass, SilverStripe\ORM\Tests\DBClassNameTest\OtherClass, SilverStripe\ORM\Tests\DBClassNameTest\TestObject, SilverStripe\ORM\Tests\D...SubclassedDBFieldObject, SilverStripe\ORM\Tests\DBCompositeTest\TestObject, SilverStripe\ORM\Tests\DBMoneyTest\TestObject, SilverStripe\ORM\Tests\D...Test\TestObjectSubclass, SilverStripe\ORM\Tests\D...st\HasOneRelationObject, SilverStripe\ORM\Tests\D...ferencerTest\TestObject, SilverStripe\ORM\Tests\D...nsionTest\CMSFieldsBase, SilverStripe\ORM\Tests\D...sionTest\CMSFieldsChild, SilverStripe\ORM\Tests\D...est\CMSFieldsGrandChild, SilverStripe\ORM\Tests\DataExtensionTest\MyObject, SilverStripe\ORM\Tests\DataExtensionTest\Player, SilverStripe\ORM\Tests\D...nsionTest\RelatedObject, SilverStripe\ORM\Tests\D...xtensionTest\TestMember, SilverStripe\ORM\Tests\D...tDuplicationTest\Class1, SilverStripe\ORM\Tests\D...tDuplicationTest\Class2, SilverStripe\ORM\Tests\D...tDuplicationTest\Class3, SilverStripe\ORM\Tests\D...ingTest\VersionedObject, SilverStripe\ORM\Tests\D...Test\VersionedSubObject, SilverStripe\ORM\Tests\D...ionTest\TestIndexObject, SilverStripe\ORM\Tests\D...nerationTest\TestObject, SilverStripe\ORM\Tests\D...ectSchemaTest\BaseClass, SilverStripe\ORM\Tests\D...chemaTest\BaseDataClass, SilverStripe\ORM\Tests\D...ctSchemaTest\ChildClass, SilverStripe\ORM\Tests\D...emaTest\GrandChildClass, SilverStripe\ORM\Tests\D...ectSchemaTest\HasFields, SilverStripe\ORM\Tests\D...jectSchemaTest\NoFields, SilverStripe\ORM\Tests\D...emaTest\WithCustomTable, SilverStripe\ORM\Tests\D...SchemaTest\WithRelation, SilverStripe\ORM\Tests\DataObjectTest\Bogey, SilverStripe\ORM\Tests\DataObjectTest\CEO, SilverStripe\ORM\Tests\DataObjectTest\Company, SilverStripe\ORM\Tests\D...ctTest\EquipmentCompany, SilverStripe\ORM\Tests\D...est\ExtendedTeamComment, SilverStripe\ORM\Tests\DataObjectTest\Fan, SilverStripe\ORM\Tests\D...tTest\FieldlessSubTable, SilverStripe\ORM\Tests\D...jectTest\FieldlessTable, SilverStripe\ORM\Tests\DataObjectTest\Fixture, SilverStripe\ORM\Tests\D...erSubclassWithSameField, SilverStripe\ORM\Tests\DataObjectTest\Play, SilverStripe\ORM\Tests\DataObjectTest\Player, SilverStripe\ORM\Tests\DataObjectTest\Ploy, SilverStripe\ORM\Tests\DataObjectTest\Sortable, SilverStripe\ORM\Tests\DataObjectTest\Staff, SilverStripe\ORM\Tests\D...est\SubEquipmentCompany, SilverStripe\ORM\Tests\DataObjectTest\SubTeam, SilverStripe\ORM\Tests\DataObjectTest\Team, SilverStripe\ORM\Tests\DataObjectTest\TeamComment, SilverStripe\ORM\Tests\D...ectTest\ValidatedObject, SilverStripe\ORM\Tests\DataQueryTest\ObjectA, SilverStripe\ORM\Tests\DataQueryTest\ObjectB, SilverStripe\ORM\Tests\DataQueryTest\ObjectC, SilverStripe\ORM\Tests\DataQueryTest\ObjectD, SilverStripe\ORM\Tests\DataQueryTest\ObjectE, SilverStripe\ORM\Tests\DataQueryTest\ObjectF, SilverStripe\ORM\Tests\DataQueryTest\ObjectG, SilverStripe\ORM\Tests\DatabaseTest\MyObject, SilverStripe\ORM\Tests\DecimalTest\TestObject, SilverStripe\ORM\Tests\F...xtFilterTest\TestObject, SilverStripe\ORM\Tests\F...lationTest\HasManyChild, SilverStripe\ORM\Tests\F...nTest\HasManyGrandChild, SilverStripe\ORM\Tests\F...ationTest\HasManyParent, SilverStripe\ORM\Tests\F...elationTest\HasOneChild, SilverStripe\ORM\Tests\F...onTest\HasOneGrandChild, SilverStripe\ORM\Tests\F...lationTest\HasOneParent, SilverStripe\ORM\Tests\F...ationTest\ManyManyChild, SilverStripe\ORM\Tests\F...Test\ManyManyGrandChild, SilverStripe\ORM\Tests\F...tionTest\ManyManyParent, SilverStripe\ORM\Tests\F...RelationTest\TestObject, SilverStripe\ORM\Tests\H...rchyTest\HideTestObject, SilverStripe\ORM\Tests\H...yTest\HideTestSubObject, SilverStripe\ORM\Tests\HierarchyTest\TestObject, SilverStripe\ORM\Tests\ManyManyListTest\Category, SilverStripe\ORM\Tests\M...tTest\ExtraFieldsObject, SilverStripe\ORM\Tests\M...istTest\IndirectPrimary, SilverStripe\ORM\Tests\ManyManyListTest\Product, SilverStripe\ORM\Tests\ManyManyListTest\Secondary, SilverStripe\ORM\Tests\M...nyListTest\SecondarySub, SilverStripe\ORM\Tests\M...anyThroughListTest\Item, SilverStripe\ORM\Tests\M...oughListTest\JoinObject, SilverStripe\ORM\Tests\M...oughListTest\TestObject, SilverStripe\ORM\Tests\M...hListTest\VersionedItem, SilverStripe\ORM\Tests\M...est\VersionedJoinObject, SilverStripe\ORM\Tests\M...istTest\VersionedObject, SilverStripe\ORM\Tests\MySQLDatabaseTest\Data, SilverStripe\ORM\Tests\S...tTest\SQLInsertTestBase, SilverStripe\ORM\Tests\SQLSelectTest\TestBase, SilverStripe\ORM\Tests\SQLSelectTest\TestChild, SilverStripe\ORM\Tests\SQLSelectTest\TestObject, SilverStripe\ORM\Tests\SQLUpdateTest\TestBase, SilverStripe\ORM\Tests\SQLUpdateTest\TestChild, SilverStripe\ORM\Tests\S...earchContextTest\Action, SilverStripe\ORM\Tests\S...textTest\AllFilterTypes, SilverStripe\ORM\Tests\S...\SearchContextTest\Book, SilverStripe\ORM\Tests\S...archContextTest\Company, SilverStripe\ORM\Tests\S...rchContextTest\Deadline, SilverStripe\ORM\Tests\S...earchContextTest\Person, SilverStripe\ORM\Tests\S...archContextTest\Project, SilverStripe\ORM\Tests\TransactionTest\TestObject, SilverStripe\ORM\Tests\U...tionListTest\TestObject, SilverStripe\ORM\Tests\V...tensionsTest\TestObject, SilverStripe\ORM\Tests\V...wnershipTest\Attachment, SilverStripe\ORM\Tests\V...nedOwnershipTest\Banner, SilverStripe\ORM\Tests\V...shipTest\CustomRelation, SilverStripe\ORM\Tests\V...onedOwnershipTest\Image, SilverStripe\ORM\Tests\V...edOwnershipTest\Related, SilverStripe\ORM\Tests\V...nershipTest\RelatedMany, SilverStripe\ORM\Tests\V...dOwnershipTest\Subclass, SilverStripe\ORM\Tests\V...wnershipTest\TestObject, SilverStripe\ORM\Tests\V...dOwnershipTest\TestPage, SilverStripe\ORM\Tests\V...nedTest\AnotherSubclass, SilverStripe\ORM\Tests\VersionedTest\CustomTable, SilverStripe\ORM\Tests\VersionedTest\PublicStage, SilverStripe\ORM\Tests\V...Test\PublicViaExtension, SilverStripe\ORM\Tests\V...t\RelatedWithoutversion, SilverStripe\ORM\Tests\VersionedTest\SingleStage, SilverStripe\ORM\Tests\VersionedTest\Subclass, SilverStripe\ORM\Tests\VersionedTest\TestObject, SilverStripe\ORM\Tests\V...st\UnversionedWithField, SilverStripe\ORM\Tests\VersionedTest\WithIndexes, SilverStripe\ORM\Versioning\ChangeSet, SilverStripe\ORM\Versioning\ChangeSetItem, SilverStripe\Security\CMSSecurity, SilverStripe\Security\Group, SilverStripe\Security\LoginAttempt, SilverStripe\Security\Member, SilverStripe\Security\MemberPassword, SilverStripe\Security\Permission, SilverStripe\Security\PermissionRole, SilverStripe\Security\PermissionRoleCode, SilverStripe\Security\RememberLoginHash, SilverStripe\Security\Security, SilverStripe\Security\Te...erSecuredWithPermission, SilverStripe\Security\Te...ecuredWithoutPermission, SilverStripe\Security\Tests\GroupTest\TestMember, SilverStripe\Security\Te...rityTest\NullController, SilverStripe\Security\Te...yTest\SecuredController, SilverStripe\View\Tests\...acheBlockTest\TestModel, SilverStripe\View\Tests\...lockTest\VersionedModel, SilverStripe\View\Tests\...rTest\SSViewerTestModel, SilverStripe\View\Tests\...werTestModel_Controller, SilverStripe\View\Tests\SSViewerTest\TestObject, SilverStripe\i18n\Tests\i18nTest\MyObject, SilverStripe\i18n\Tests\i18nTest\MySubObject, SilverStripe\i18n\Tests\i18nTest\TestDataObject, i18nTestModule. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
2593
        }
2594
    }
2595
2596
    /**
2597
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2598
     * expected to return one of three values:
2599
     *
2600
     *  - false: Disallow this permission, regardless of what other extensions say
2601
     *  - true: Allow this permission, as long as no other extensions return false
2602
     *  - NULL: Don't affect the outcome
2603
     *
2604
     * This method itself returns a tri-state value, and is designed to be used like this:
2605
     *
2606
     * <code>
2607
     * $extended = $this->extendedCan('canDoSomething', $member);
2608
     * if($extended !== null) return $extended;
2609
     * else return $normalValue;
2610
     * </code>
2611
     *
2612
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2613
     * @param Member|int $member
2614
     * @param array $context Optional context
2615
     * @return boolean|null
2616
     */
2617
    public function extendedCan($methodName, $member, $context = array())
2618
    {
2619
        $results = $this->extend($methodName, $member, $context);
2620
        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...
2621
            // Remove NULLs
2622
            $results = array_filter($results, function ($v) {
2623
                return !is_null($v);
2624
            });
2625
            // If there are any non-NULL responses, then return the lowest one of them.
2626
            // If any explicitly deny the permission, then we don't get access
2627
            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...
2628
                return min($results);
2629
            }
2630
        }
2631
        return null;
2632
    }
2633
2634
    /**
2635
     * @param Member $member
2636
     * @return boolean
2637
     */
2638
    public function canView($member = null)
2639
    {
2640
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2638 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...
2641
        if ($extended !== null) {
2642
            return $extended;
2643
        }
2644
        return Permission::check('ADMIN', 'any', $member);
2645
    }
2646
2647
    /**
2648
     * @param Member $member
2649
     * @return boolean
2650
     */
2651
    public function canEdit($member = null)
2652
    {
2653
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2651 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...
2654
        if ($extended !== null) {
2655
            return $extended;
2656
        }
2657
        return Permission::check('ADMIN', 'any', $member);
2658
    }
2659
2660
    /**
2661
     * @param Member $member
2662
     * @return boolean
2663
     */
2664
    public function canDelete($member = null)
2665
    {
2666
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2664 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...
2667
        if ($extended !== null) {
2668
            return $extended;
2669
        }
2670
        return Permission::check('ADMIN', 'any', $member);
2671
    }
2672
2673
    /**
2674
     * @param Member $member
2675
     * @param array $context Additional context-specific data which might
2676
     * affect whether (or where) this object could be created.
2677
     * @return boolean
2678
     */
2679
    public function canCreate($member = null, $context = array())
2680
    {
2681
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2679 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...
2682
        if ($extended !== null) {
2683
            return $extended;
2684
        }
2685
        return Permission::check('ADMIN', 'any', $member);
2686
    }
2687
2688
    /**
2689
     * Debugging used by Debug::show()
2690
     *
2691
     * @return string HTML data representing this object
2692
     */
2693
    public function debug()
2694
    {
2695
        $val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2696
        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...
2697
            foreach ($this->record as $fieldName => $fieldVal) {
2698
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2699
            }
2700
        }
2701
        $val .= "</ul>\n";
2702
        return $val;
2703
    }
2704
2705
    /**
2706
     * Return the DBField object that represents the given field.
2707
     * This works similarly to obj() with 2 key differences:
2708
     *   - it still returns an object even when the field has no value.
2709
     *   - it only matches fields and not methods
2710
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2711
     *
2712
     * @param string $fieldName Name of the field
2713
     * @return DBField The field as a DBField object
2714
     */
2715
    public function dbObject($fieldName)
2716
    {
2717
        // Check for field in DB
2718
        $schema = static::getSchema();
2719
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2720
        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...
2721
            return null;
2722
        }
2723
2724
        $value = isset($this->record[$fieldName])
2725
            ? $this->record[$fieldName]
2726
            : null;
2727
2728
        // If we have a DBField object in $this->record, then return that
2729
        if ($value instanceof DBField) {
2730
            return $value;
2731
        }
2732
2733
        list($class, $spec) = explode('.', $helper);
2734
        /** @var DBField $obj */
2735
        $table = $schema->tableName($class);
2736
        $obj = Object::create_from_string($spec, $fieldName);
2737
        $obj->setTable($table);
2738
        $obj->setValue($value, $this, false);
2739
        return $obj;
2740
    }
2741
2742
    /**
2743
     * Traverses to a DBField referenced by relationships between data objects.
2744
     *
2745
     * The path to the related field is specified with dot separated syntax
2746
     * (eg: Parent.Child.Child.FieldName).
2747
     *
2748
     * @param string $fieldPath
2749
     *
2750
     * @return mixed DBField of the field on the object or a DataList instance.
2751
     */
2752
    public function relObject($fieldPath)
2753
    {
2754
        $object = null;
2755
2756
        if (strpos($fieldPath, '.') !== false) {
2757
            $parts = explode('.', $fieldPath);
2758
            $fieldName = array_pop($parts);
2759
2760
            // Traverse dot syntax
2761
            $component = $this;
2762
2763
            foreach ($parts as $relation) {
2764
                if ($component instanceof SS_List) {
2765
                    if (method_exists($component, $relation)) {
2766
                        $component = $component->$relation();
2767
                    } else {
2768
                        $component = $component->relation($relation);
2769
                    }
2770
                } else {
2771
                    $component = $component->$relation();
2772
                }
2773
            }
2774
2775
            $object = $component->dbObject($fieldName);
2776
        } else {
2777
            $object = $this->dbObject($fieldPath);
2778
        }
2779
2780
        return $object;
2781
    }
2782
2783
    /**
2784
     * Traverses to a field referenced by relationships between data objects, returning the value
2785
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2786
     *
2787
     * @param $fieldName string
2788
     * @return string | null - will return null on a missing value
2789
     */
2790
    public function relField($fieldName)
2791
    {
2792
        $component = $this;
2793
2794
        // We're dealing with relations here so we traverse the dot syntax
2795
        if (strpos($fieldName, '.') !== false) {
2796
            $relations = explode('.', $fieldName);
2797
            $fieldName = array_pop($relations);
2798
            foreach ($relations as $relation) {
2799
                // Inspect $component for element $relation
2800
                if ($component->hasMethod($relation)) {
2801
                    // Check nested method
2802
                    $component = $component->$relation();
2803
                } elseif ($component instanceof SS_List) {
2804
                    // Select adjacent relation from DataList
2805
                    $component = $component->relation($relation);
2806
                } elseif ($component instanceof DataObject
2807
                    && ($dbObject = $component->dbObject($relation))
2808
                ) {
2809
                    // Select db object
2810
                    $component = $dbObject;
2811
                } else {
2812
                    user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
2813
                }
2814
            }
2815
        }
2816
2817
        // Bail if the component is null
2818
        if (!$component) {
2819
            return null;
2820
        }
2821
        if ($component->hasMethod($fieldName)) {
2822
            return $component->$fieldName();
2823
        }
2824
        return $component->$fieldName;
2825
    }
2826
2827
    /**
2828
     * Temporary hack to return an association name, based on class, to get around the mangle
2829
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2830
     *
2831
     * @param string $className
2832
     * @return string
2833
     */
2834
    public function getReverseAssociation($className)
2835
    {
2836
        if (is_array($this->manyMany())) {
2837
            $many_many = array_flip($this->manyMany());
2838
            if (array_key_exists($className, $many_many)) {
2839
                return $many_many[$className];
2840
            }
2841
        }
2842
        if (is_array($this->hasMany())) {
2843
            $has_many = array_flip($this->hasMany());
2844
            if (array_key_exists($className, $has_many)) {
2845
                return $has_many[$className];
2846
            }
2847
        }
2848
        if (is_array($this->hasOne())) {
2849
            $has_one = array_flip($this->hasOne());
2850
            if (array_key_exists($className, $has_one)) {
2851
                return $has_one[$className];
2852
            }
2853
        }
2854
2855
        return false;
2856
    }
2857
2858
    /**
2859
     * Return all objects matching the filter
2860
     * sub-classes are automatically selected and included
2861
     *
2862
     * @param string $callerClass The class of objects to be returned
2863
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2864
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2865
     * @param string|array $sort A sort expression to be inserted into the ORDER
2866
     * BY clause.  If omitted, self::$default_sort will be used.
2867
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
2868
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2869
     * @param string $containerClass The container class to return the results in.
2870
     *
2871
     * @todo $containerClass is Ignored, why?
2872
     *
2873
     * @return DataList The objects matching the filter, in the class specified by $containerClass
2874
     */
2875
    public static function get(
2876
        $callerClass = null,
2877
        $filter = "",
2878
        $sort = "",
2879
        $join = "",
2880
        $limit = null,
2881
        $containerClass = DataList::class
2882
    ) {
2883
2884
        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...
2885
            $callerClass = get_called_class();
2886
            if ($callerClass == self::class) {
2887
                throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
2888
            }
2889
2890
            if ($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
2891
                throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
2892
                    . ' arguments');
2893
            }
2894
2895
            $result = DataList::create(get_called_class());
2896
            $result->setDataModel(DataModel::inst());
2897
            return $result;
2898
        }
2899
2900
        if ($join) {
2901
            throw new \InvalidArgumentException(
2902
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
2903
            );
2904
        }
2905
2906
        $result = DataList::create($callerClass)->where($filter)->sort($sort);
2907
2908
        if ($limit && strpos($limit, ',') !== false) {
2909
            $limitArguments = explode(',', $limit);
2910
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
2911
        } elseif ($limit) {
2912
            $result = $result->limit($limit);
2913
        }
2914
2915
        $result->setDataModel(DataModel::inst());
2916
        return $result;
2917
    }
2918
2919
2920
    /**
2921
     * Return the first item matching the given query.
2922
     * All calls to get_one() are cached.
2923
     *
2924
     * @param string $callerClass The class of objects to be returned
2925
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2926
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2927
     * @param boolean $cache Use caching
2928
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
2929
     *
2930
     * @return DataObject The first item matching the query
2931
     */
2932
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
2933
    {
2934
        $SNG = singleton($callerClass);
2935
2936
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
2937
        $cacheKey = md5(var_export($cacheComponents, true));
2938
2939
        // Flush destroyed items out of the cache
2940
        if ($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
2941
                && self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
2942
                && self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
2943
            self::$_cache_get_one[$callerClass][$cacheKey] = false;
2944
        }
2945
        $item = null;
2946
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
2947
            $dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
2948
            $item = $dl->first();
2949
2950
            if ($cache) {
2951
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
2952
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
2953
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
2954
                }
2955
            }
2956
        }
2957
        return $cache ? self::$_cache_get_one[$callerClass][$cacheKey] : $item;
2958
    }
2959
2960
    /**
2961
     * Flush the cached results for all relations (has_one, has_many, many_many)
2962
     * Also clears any cached aggregate data.
2963
     *
2964
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
2965
     *                            When false will just clear session-local cached data
2966
     * @return DataObject $this
2967
     */
2968
    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...
2969
    {
2970
        if ($this->class == self::class) {
2971
            self::$_cache_get_one = array();
2972
            return $this;
2973
        }
2974
2975
        $classes = ClassInfo::ancestry($this->class);
2976
        foreach ($classes as $class) {
2977
            if (isset(self::$_cache_get_one[$class])) {
2978
                unset(self::$_cache_get_one[$class]);
2979
            }
2980
        }
2981
2982
        $this->extend('flushCache');
2983
2984
        $this->components = array();
2985
        return $this;
2986
    }
2987
2988
    /**
2989
     * Flush the get_one global cache and destroy associated objects.
2990
     */
2991
    public static function flush_and_destroy_cache()
2992
    {
2993
        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...
2994
            foreach (self::$_cache_get_one as $class => $items) {
2995
                if (is_array($items)) {
2996
                    foreach ($items as $item) {
2997
                        if ($item) {
2998
                            $item->destroy();
2999
                        }
3000
                    }
3001
                }
3002
            }
3003
        }
3004
        self::$_cache_get_one = array();
3005
    }
3006
3007
    /**
3008
     * Reset all global caches associated with DataObject.
3009
     */
3010
    public static function reset()
3011
    {
3012
        // @todo Decouple these
3013
        DBClassName::clear_classname_cache();
3014
        ClassInfo::reset_db_cache();
3015
        static::getSchema()->reset();
3016
        self::$_cache_get_one = array();
3017
        self::$_cache_field_labels = array();
3018
    }
3019
3020
    /**
3021
     * Return the given element, searching by ID
3022
     *
3023
     * @param string $callerClass The class of the object to be returned
3024
     * @param int $id The id of the element
3025
     * @param boolean $cache See {@link get_one()}
3026
     *
3027
     * @return DataObject The element
3028
     */
3029
    public static function get_by_id($callerClass, $id, $cache = true)
3030
    {
3031
        if (!is_numeric($id)) {
3032
            user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3033
        }
3034
3035
        // Pass to get_one
3036
        $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
3037
        return DataObject::get_one($callerClass, array($column => $id), $cache);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3038
    }
3039
3040
    /**
3041
     * Get the name of the base table for this object
3042
     *
3043
     * @return string
3044
     */
3045
    public function baseTable()
3046
    {
3047
        return static::getSchema()->baseDataTable($this);
3048
    }
3049
3050
    /**
3051
     * Get the base class for this object
3052
     *
3053
     * @return string
3054
     */
3055
    public function baseClass()
3056
    {
3057
        return static::getSchema()->baseDataClass($this);
3058
    }
3059
3060
    /**
3061
     * @var array Parameters used in the query that built this object.
3062
     * This can be used by decorators (e.g. lazy loading) to
3063
     * run additional queries using the same context.
3064
     */
3065
    protected $sourceQueryParams;
3066
3067
    /**
3068
     * @see $sourceQueryParams
3069
     * @return array
3070
     */
3071
    public function getSourceQueryParams()
3072
    {
3073
        return $this->sourceQueryParams;
3074
    }
3075
3076
    /**
3077
     * Get list of parameters that should be inherited to relations on this object
3078
     *
3079
     * @return array
3080
     */
3081
    public function getInheritableQueryParams()
3082
    {
3083
        $params = $this->getSourceQueryParams();
3084
        $this->extend('updateInheritableQueryParams', $params);
3085
        return $params;
3086
    }
3087
3088
    /**
3089
     * @see $sourceQueryParams
3090
     * @param array
3091
     */
3092
    public function setSourceQueryParams($array)
3093
    {
3094
        $this->sourceQueryParams = $array;
3095
    }
3096
3097
    /**
3098
     * @see $sourceQueryParams
3099
     * @param string $key
3100
     * @param string $value
3101
     */
3102
    public function setSourceQueryParam($key, $value)
3103
    {
3104
        $this->sourceQueryParams[$key] = $value;
3105
    }
3106
3107
    /**
3108
     * @see $sourceQueryParams
3109
     * @param string $key
3110
     * @return string
3111
     */
3112
    public function getSourceQueryParam($key)
3113
    {
3114
        if (isset($this->sourceQueryParams[$key])) {
3115
            return $this->sourceQueryParams[$key];
3116
        }
3117
        return null;
3118
    }
3119
3120
    //-------------------------------------------------------------------------------------------//
3121
3122
    /**
3123
     * Return the database indexes on this table.
3124
     * This array is indexed by the name of the field with the index, and
3125
     * the value is the type of index.
3126
     */
3127
    public function databaseIndexes()
3128
    {
3129
        $has_one = $this->uninherited('has_one');
3130
        $classIndexes = $this->uninherited('indexes');
3131
        //$fileIndexes = $this->uninherited('fileIndexes', true);
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3132
3133
        $indexes = array();
3134
3135
        if ($has_one) {
3136
            foreach ($has_one as $relationshipName => $fieldType) {
3137
                $indexes[$relationshipName . 'ID'] = true;
3138
            }
3139
        }
3140
3141
        if ($classIndexes) {
3142
            foreach ($classIndexes as $indexName => $indexType) {
3143
                $indexes[$indexName] = $indexType;
3144
            }
3145
        }
3146
3147
        if (get_parent_class($this) == self::class) {
3148
            $indexes['ClassName'] = true;
3149
        }
3150
3151
        return $indexes;
3152
    }
3153
3154
    /**
3155
     * Check the database schema and update it as necessary.
3156
     *
3157
     * @uses DataExtension->augmentDatabase()
3158
     */
3159
    public function requireTable()
3160
    {
3161
        // Only build the table if we've actually got fields
3162
        $schema = static::getSchema();
3163
        $fields = $schema->databaseFields(static::class, false);
3164
        $table = $schema->tableName(static::class);
3165
        $extensions = self::database_extensions(static::class);
3166
3167
        $indexes = $this->databaseIndexes();
3168
3169
        if (empty($table)) {
3170
            throw new LogicException(
3171
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3172
            );
3173
        }
3174
3175
        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...
3176
            $hasAutoIncPK = get_parent_class($this) === self::class;
3177
            DB::require_table(
3178
                $table,
3179
                $fields,
3180
                $indexes,
3181
                $hasAutoIncPK,
3182
                $this->stat('create_table_options'),
3183
                $extensions
0 ignored issues
show
Security Bug introduced by
It seems like $extensions defined by self::database_extensions(static::class) on line 3165 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...
3184
            );
3185
        } else {
3186
            DB::dont_require_table($table);
3187
        }
3188
3189
        // Build any child tables for many_many items
3190
        if ($manyMany = $this->uninherited('many_many')) {
3191
            $extras = $this->uninherited('many_many_extraFields');
3192
            foreach ($manyMany as $component => $spec) {
3193
                // Get many_many spec
3194
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3195
                list(
3196
                    $relationClass, $parentClass, $componentClass,
0 ignored issues
show
Unused Code introduced by
The assignment to $componentClass is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $relationClass is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $parentClass is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
3197
                    $parentField, $childField, $tableOrClass
3198
                ) = $manyManyComponent;
3199
3200
                // Skip if backed by actual class
3201
                if (class_exists($tableOrClass)) {
3202
                    continue;
3203
                }
3204
3205
                // Build fields
3206
                $manymanyFields = array(
3207
                    $parentField => "Int",
3208
                    $childField => "Int",
3209
                );
3210
                if (isset($extras[$component])) {
3211
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3212
                }
3213
3214
                // Build index list
3215
                $manymanyIndexes = array(
3216
                    $parentField => true,
3217
                    $childField => true,
3218
                );
3219
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Security Bug introduced by
It seems like $extensions defined by self::database_extensions(static::class) on line 3165 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...
3220
            }
3221
        }
3222
3223
        // Let any extentions make their own database fields
3224
        $this->extend('augmentDatabase', $dummy);
3225
    }
3226
3227
    /**
3228
     * Add default records to database. This function is called whenever the
3229
     * database is built, after the database tables have all been created. Overload
3230
     * this to add default records when the database is built, but make sure you
3231
     * call parent::requireDefaultRecords().
3232
     *
3233
     * @uses DataExtension->requireDefaultRecords()
3234
     */
3235
    public function requireDefaultRecords()
3236
    {
3237
        $defaultRecords = $this->config()->get('default_records', Config::UNINHERITED);
3238
3239
        if (!empty($defaultRecords)) {
3240
            $hasData = DataObject::get_one($this->class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
3241
            if (!$hasData) {
3242
                $className = $this->class;
3243
                foreach ($defaultRecords as $record) {
3244
                    $obj = $this->model->$className->newObject($record);
3245
                    $obj->write();
3246
                }
3247
                DB::alteration_message("Added default records to $className table", "created");
3248
            }
3249
        }
3250
3251
        // Let any extentions make their own database default data
3252
        $this->extend('requireDefaultRecords', $dummy);
3253
    }
3254
3255
    /**
3256
     * Get the default searchable fields for this object, as defined in the
3257
     * $searchable_fields list. If searchable fields are not defined on the
3258
     * data object, uses a default selection of summary fields.
3259
     *
3260
     * @return array
3261
     */
3262
    public function searchableFields()
3263
    {
3264
        // can have mixed format, need to make consistent in most verbose form
3265
        $fields = $this->stat('searchable_fields');
3266
        $labels = $this->fieldLabels();
3267
3268
        // fallback to summary fields (unless empty array is explicitly specified)
3269
        if (! $fields && ! is_array($fields)) {
3270
            $summaryFields = array_keys($this->summaryFields());
3271
            $fields = array();
3272
3273
            // remove the custom getters as the search should not include them
3274
            $schema = static::getSchema();
3275
            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...
3276
                foreach ($summaryFields as $key => $name) {
3277
                    $spec = $name;
3278
3279
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3280
                    if (($fieldPos = strpos($name, '.')) !== false) {
3281
                        $name = substr($name, 0, $fieldPos);
3282
                    }
3283
3284
                    if ($schema->fieldSpec($this, $name)) {
3285
                        $fields[] = $name;
3286
                    } elseif ($this->relObject($spec)) {
3287
                        $fields[] = $spec;
3288
                    }
3289
                }
3290
            }
3291
        }
3292
3293
        // we need to make sure the format is unified before
3294
        // augmenting fields, so extensions can apply consistent checks
3295
        // but also after augmenting fields, because the extension
3296
        // might use the shorthand notation as well
3297
3298
        // rewrite array, if it is using shorthand syntax
3299
        $rewrite = array();
3300
        foreach ($fields as $name => $specOrName) {
3301
            $identifer = (is_int($name)) ? $specOrName : $name;
3302
3303
            if (is_int($name)) {
3304
                // Format: array('MyFieldName')
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3305
                $rewrite[$identifer] = array();
3306
            } elseif (is_array($specOrName)) {
3307
                // Format: array('MyFieldName' => array(
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3308
                //   'filter => 'ExactMatchFilter',
3309
                //   'field' => 'NumericField', // optional
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3310
                //   'title' => 'My Title', // optional
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3311
                // ))
3312
                $rewrite[$identifer] = array_merge(
3313
                    array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3314
                    (array)$specOrName
3315
                );
3316
            } else {
3317
                // Format: array('MyFieldName' => 'ExactMatchFilter')
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3318
                $rewrite[$identifer] = array(
3319
                    'filter' => $specOrName,
3320
                );
3321
            }
3322
            if (!isset($rewrite[$identifer]['title'])) {
3323
                $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3324
                    ? $labels[$identifer] : FormField::name_to_label($identifer);
3325
            }
3326
            if (!isset($rewrite[$identifer]['filter'])) {
3327
                /** @skipUpgrade */
3328
                $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3329
            }
3330
        }
3331
3332
        $fields = $rewrite;
3333
3334
        // apply DataExtensions if present
3335
        $this->extend('updateSearchableFields', $fields);
3336
3337
        return $fields;
3338
    }
3339
3340
    /**
3341
     * Get any user defined searchable fields labels that
3342
     * exist. Allows overriding of default field names in the form
3343
     * interface actually presented to the user.
3344
     *
3345
     * The reason for keeping this separate from searchable_fields,
3346
     * which would be a logical place for this functionality, is to
3347
     * avoid bloating and complicating the configuration array. Currently
3348
     * much of this system is based on sensible defaults, and this property
3349
     * would generally only be set in the case of more complex relationships
3350
     * between data object being required in the search interface.
3351
     *
3352
     * Generates labels based on name of the field itself, if no static property
3353
     * {@link self::field_labels} exists.
3354
     *
3355
     * @uses $field_labels
3356
     * @uses FormField::name_to_label()
3357
     *
3358
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3359
     *
3360
     * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3361
     */
3362
    public function fieldLabels($includerelations = true)
3363
    {
3364
        $cacheKey = $this->class . '_' . $includerelations;
3365
3366
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
3367
            $customLabels = $this->stat('field_labels');
3368
            $autoLabels = array();
3369
3370
            // get all translated static properties as defined in i18nCollectStatics()
3371
            $ancestry = ClassInfo::ancestry($this->class);
3372
            $ancestry = array_reverse($ancestry);
3373
            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...
3374
                foreach ($ancestry as $ancestorClass) {
3375
                    if ($ancestorClass === ViewableData::class) {
3376
                        break;
3377
                    }
3378
                    $types = array(
3379
                    'db'        => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3380
                    );
3381
                    if ($includerelations) {
3382
                        $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3383
                        $types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3384
                        $types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3385
                        $types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3386
                    }
3387
                    foreach ($types as $type => $attrs) {
3388
                        foreach ($attrs as $name => $spec) {
3389
                            $autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}", FormField::name_to_label($name));
3390
                        }
3391
                    }
3392
                }
3393
            }
3394
3395
            $labels = array_merge((array)$autoLabels, (array)$customLabels);
3396
            $this->extend('updateFieldLabels', $labels);
3397
            self::$_cache_field_labels[$cacheKey] = $labels;
3398
        }
3399
3400
        return self::$_cache_field_labels[$cacheKey];
3401
    }
3402
3403
    /**
3404
     * Get a human-readable label for a single field,
3405
     * see {@link fieldLabels()} for more details.
3406
     *
3407
     * @uses fieldLabels()
3408
     * @uses FormField::name_to_label()
3409
     *
3410
     * @param string $name Name of the field
3411
     * @return string Label of the field
3412
     */
3413
    public function fieldLabel($name)
3414
    {
3415
        $labels = $this->fieldLabels();
3416
        return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3417
    }
3418
3419
    /**
3420
     * Get the default summary fields for this object.
3421
     *
3422
     * @todo use the translation apparatus to return a default field selection for the language
3423
     *
3424
     * @return array
3425
     */
3426
    public function summaryFields()
3427
    {
3428
        $fields = $this->stat('summary_fields');
3429
3430
        // if fields were passed in numeric array,
3431
        // convert to an associative array
3432
        if ($fields && array_key_exists(0, $fields)) {
3433
            $fields = array_combine(array_values($fields), array_values($fields));
3434
        }
3435
3436
        if (!$fields) {
3437
            $fields = array();
3438
            // try to scaffold a couple of usual suspects
3439
            if ($this->hasField('Name')) {
3440
                $fields['Name'] = 'Name';
3441
            }
3442
            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...
3443
                $fields['Title'] = 'Title';
3444
            }
3445
            if ($this->hasField('Description')) {
3446
                $fields['Description'] = 'Description';
3447
            }
3448
            if ($this->hasField('FirstName')) {
3449
                $fields['FirstName'] = 'First Name';
3450
            }
3451
        }
3452
        $this->extend("updateSummaryFields", $fields);
3453
3454
        // Final fail-over, just list ID field
3455
        if (!$fields) {
3456
            $fields['ID'] = 'ID';
3457
        }
3458
3459
        // Localize fields (if possible)
3460
        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...
3461
            // only attempt to localize if the label definition is the same as the field name.
3462
            // this will preserve any custom labels set in the summary_fields configuration
3463
            if (isset($fields[$name]) && $name === $fields[$name]) {
3464
                $fields[$name] = $label;
3465
            }
3466
        }
3467
3468
        return $fields;
3469
    }
3470
3471
    /**
3472
     * Defines a default list of filters for the search context.
3473
     *
3474
     * If a filter class mapping is defined on the data object,
3475
     * it is constructed here. Otherwise, the default filter specified in
3476
     * {@link DBField} is used.
3477
     *
3478
     * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3479
     *
3480
     * @return array
3481
     */
3482
    public function defaultSearchFilters()
3483
    {
3484
        $filters = array();
3485
3486
        foreach ($this->searchableFields() as $name => $spec) {
3487
            if (empty($spec['filter'])) {
3488
                /** @skipUpgrade */
3489
                $filters[$name] = 'PartialMatchFilter';
3490
            } elseif ($spec['filter'] instanceof SearchFilter) {
3491
                $filters[$name] = $spec['filter'];
3492
            } else {
3493
                $filters[$name] = Injector::inst()->create($spec['filter'], $name);
3494
            }
3495
        }
3496
3497
        return $filters;
3498
    }
3499
3500
    /**
3501
     * @return boolean True if the object is in the database
3502
     */
3503
    public function isInDB()
3504
    {
3505
        return is_numeric($this->ID) && $this->ID > 0;
3506
    }
3507
3508
    /*
3509
	 * @ignore
3510
	 */
3511
    private static $subclass_access = true;
3512
3513
    /**
3514
     * Temporarily disable subclass access in data object qeur
3515
     */
3516
    public static function disable_subclass_access()
3517
    {
3518
        self::$subclass_access = false;
3519
    }
3520
    public static function enable_subclass_access()
3521
    {
3522
        self::$subclass_access = true;
3523
    }
3524
3525
    //-------------------------------------------------------------------------------------------//
3526
3527
    /**
3528
     * Database field definitions.
3529
     * This is a map from field names to field type. The field
3530
     * type should be a class that extends .
3531
     * @var array
3532
     * @config
3533
     */
3534
    private static $db = null;
3535
3536
    /**
3537
     * Use a casting object for a field. This is a map from
3538
     * field name to class name of the casting object.
3539
     *
3540
     * @var array
3541
     */
3542
    private static $casting = array(
3543
        "Title" => 'Text',
3544
    );
3545
3546
    /**
3547
     * Specify custom options for a CREATE TABLE call.
3548
     * Can be used to specify a custom storage engine for specific database table.
3549
     * All options have to be keyed for a specific database implementation,
3550
     * identified by their class name (extending from {@link SS_Database}).
3551
     *
3552
     * <code>
3553
     * array(
3554
     *  'MySQLDatabase' => 'ENGINE=MyISAM'
3555
     * )
3556
     * </code>
3557
     *
3558
     * Caution: This API is experimental, and might not be
3559
     * included in the next major release. Please use with care.
3560
     *
3561
     * @var array
3562
     * @config
3563
     */
3564
    private static $create_table_options = array(
3565
        'SilverStripe\ORM\Connect\MySQLDatabase' => 'ENGINE=InnoDB'
3566
    );
3567
3568
    /**
3569
     * If a field is in this array, then create a database index
3570
     * on that field. This is a map from fieldname to index type.
3571
     * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3572
     *
3573
     * @var array
3574
     * @config
3575
     */
3576
    private static $indexes = null;
3577
3578
    /**
3579
     * Inserts standard column-values when a DataObject
3580
     * is instanciated. Does not insert default records {@see $default_records}.
3581
     * This is a map from fieldname to default value.
3582
     *
3583
     *  - If you would like to change a default value in a sub-class, just specify it.
3584
     *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3585
     *    or false in your subclass.  Setting it to null won't work.
3586
     *
3587
     * @var array
3588
     * @config
3589
     */
3590
    private static $defaults = null;
3591
3592
    /**
3593
     * Multidimensional array which inserts default data into the database
3594
     * on a db/build-call as long as the database-table is empty. Please use this only
3595
     * for simple constructs, not for SiteTree-Objects etc. which need special
3596
     * behaviour such as publishing and ParentNodes.
3597
     *
3598
     * Example:
3599
     * array(
3600
     *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3601
     *  array('Title' => "DefaultPage2")
3602
     * ).
3603
     *
3604
     * @var array
3605
     * @config
3606
     */
3607
    private static $default_records = null;
3608
3609
    /**
3610
     * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3611
     * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3612
     *
3613
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3614
     *
3615
     *  @var array
3616
     * @config
3617
     */
3618
    private static $has_one = null;
3619
3620
    /**
3621
     * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3622
     *
3623
     * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3624
     * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3625
     * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3626
     *
3627
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3628
     *
3629
     * @var array
3630
     * @config
3631
     */
3632
    private static $belongs_to;
3633
3634
    /**
3635
     * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3636
     *
3637
     * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3638
     * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3639
     * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3640
     * which foreign key to use.
3641
     *
3642
     * @var array
3643
     * @config
3644
     */
3645
    private static $has_many = null;
3646
3647
    /**
3648
     * many-many relationship definitions.
3649
     * This is a map from component name to data type.
3650
     * @var array
3651
     * @config
3652
     */
3653
    private static $many_many = null;
3654
3655
    /**
3656
     * Extra fields to include on the connecting many-many table.
3657
     * This is a map from field name to field type.
3658
     *
3659
     * Example code:
3660
     * <code>
3661
     * public static $many_many_extraFields = array(
3662
     *  'Members' => array(
3663
     *          'Role' => 'Varchar(100)'
3664
     *      )
3665
     * );
3666
     * </code>
3667
     *
3668
     * @var array
3669
     * @config
3670
     */
3671
    private static $many_many_extraFields = null;
3672
3673
    /**
3674
     * The inverse side of a many-many relationship.
3675
     * This is a map from component name to data type.
3676
     * @var array
3677
     * @config
3678
     */
3679
    private static $belongs_many_many = null;
3680
3681
    /**
3682
     * The default sort expression. This will be inserted in the ORDER BY
3683
     * clause of a SQL query if no other sort expression is provided.
3684
     * @var string
3685
     * @config
3686
     */
3687
    private static $default_sort = null;
3688
3689
    /**
3690
     * Default list of fields that can be scaffolded by the ModelAdmin
3691
     * search interface.
3692
     *
3693
     * Overriding the default filter, with a custom defined filter:
3694
     * <code>
3695
     *  static $searchable_fields = array(
3696
     *     "Name" => "PartialMatchFilter"
3697
     *  );
3698
     * </code>
3699
     *
3700
     * Overriding the default form fields, with a custom defined field.
3701
     * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3702
     * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3703
     * <code>
3704
     *  static $searchable_fields = array(
3705
     *    "Name" => array(
3706
     *      "field" => "TextField"
3707
     *    )
3708
     *  );
3709
     * </code>
3710
     *
3711
     * Overriding the default form field, filter and title:
3712
     * <code>
3713
     *  static $searchable_fields = array(
3714
     *    "Organisation.ZipCode" => array(
3715
     *      "field" => "TextField",
3716
     *      "filter" => "PartialMatchFilter",
3717
     *      "title" => 'Organisation ZIP'
3718
     *    )
3719
     *  );
3720
     * </code>
3721
     * @config
3722
     */
3723
    private static $searchable_fields = null;
3724
3725
    /**
3726
     * User defined labels for searchable_fields, used to override
3727
     * default display in the search form.
3728
     * @config
3729
     */
3730
    private static $field_labels = null;
3731
3732
    /**
3733
     * Provides a default list of fields to be used by a 'summary'
3734
     * view of this object.
3735
     * @config
3736
     */
3737
    private static $summary_fields = null;
3738
3739
    /**
3740
     * Collect all static properties on the object
3741
     * which contain natural language, and need to be translated.
3742
     * The full entity name is composed from the class name and a custom identifier.
3743
     *
3744
     * @return array A numerical array which contains one or more entities in array-form.
3745
     * Each numeric entity array contains the "arguments" for a _t() call as array values:
3746
     * $entity, $string, $priority, $context.
3747
     */
3748
    public function provideI18nEntities()
3749
    {
3750
        $entities = array();
3751
3752
        $entities["{$this->class}.SINGULARNAME"] = array(
3753
            $this->singular_name(),
3754
3755
            'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
3756
        );
3757
3758
        $entities["{$this->class}.PLURALNAME"] = array(
3759
            $this->plural_name(),
3760
3761
            'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the'
3762
            . ' interface'
3763
        );
3764
3765
        return $entities;
3766
    }
3767
3768
    /**
3769
     * Returns true if the given method/parameter has a value
3770
     * (Uses the DBField::hasValue if the parameter is a database field)
3771
     *
3772
     * @param string $field The field name
3773
     * @param array $arguments
3774
     * @param bool $cache
3775
     * @return boolean
3776
     */
3777
    public function hasValue($field, $arguments = null, $cache = true)
3778
    {
3779
        // has_one fields should not use dbObject to check if a value is given
3780
        $hasOne = static::getSchema()->hasOneComponent(static::class, $field);
3781
        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...
3782
            return $obj->exists();
3783
        } else {
3784
            return parent::hasValue($field, $arguments, $cache);
0 ignored issues
show
Bug introduced by
It seems like $arguments defined by parameter $arguments on line 3777 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...
3785
        }
3786
    }
3787
3788
    /**
3789
     * If selected through a many_many through relation, this is the instance of the joined record
3790
     *
3791
     * @return DataObject
3792
     */
3793
    public function getJoin()
3794
    {
3795
        return $this->joinRecord;
3796
    }
3797
3798
    /**
3799
     * Set joining object
3800
     *
3801
     * @param DataObject $object
3802
     * @param string $alias Alias
3803
     * @return $this
3804
     */
3805
    public function setJoin(DataObject $object, $alias = null)
3806
    {
3807
        $this->joinRecord = $object;
3808
        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...
3809
            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...
3810
                throw new InvalidArgumentException(
3811
                    "Joined record $alias cannot also be a db field"
3812
                );
3813
            }
3814
            $this->record[$alias] = $object;
3815
        }
3816
        return $this;
3817
    }
3818
}
3819