Completed
Push — master ( 1be2e7...d38097 )
by Sam
23s
created

DataObject::manyMany()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
rs 9.4285
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
                E_USER_WARNING
1175
            );
1176
        }
1177
1178
        if ($this->config()->get('validation_enabled')) {
1179
            $result = $this->validate();
1180
            if (!$result->valid()) {
1181
                return new ValidationException(
1182
                    $result,
1183
                    $result->message(),
1184
                    E_USER_WARNING
1185
                );
1186
            }
1187
        }
1188
        return null;
1189
    }
1190
1191
    /**
1192
     * Prepare an object prior to write
1193
     *
1194
     * @throws ValidationException
1195
     */
1196
    protected function preWrite()
1197
    {
1198
        // Validate this object
1199
        if ($writeException = $this->validateWrite()) {
1200
            // Used by DODs to clean up after themselves, eg, Versioned
1201
            $this->invokeWithExtensions('onAfterSkippedWrite');
1202
            throw $writeException;
1203
        }
1204
1205
        // Check onBeforeWrite
1206
        $this->brokenOnWrite = true;
1207
        $this->onBeforeWrite();
1208
        if ($this->brokenOnWrite) {
1209
            user_error("$this->class has a broken onBeforeWrite() function."
1210
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1211
        }
1212
    }
1213
1214
    /**
1215
     * Detects and updates all changes made to this object
1216
     *
1217
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1218
     * @return bool True if any changes are detected
1219
     */
1220
    protected function updateChanges($forceChanges = false)
1221
    {
1222
        if ($forceChanges) {
1223
            // Force changes, but only for loaded fields
1224
            foreach ($this->record as $field => $value) {
1225
                $this->changed[$field] = static::CHANGE_VALUE;
1226
            }
1227
            return true;
1228
        }
1229
        return $this->isChanged();
1230
    }
1231
1232
    /**
1233
     * Writes a subset of changes for a specific table to the given manipulation
1234
     *
1235
     * @param string $baseTable Base table
1236
     * @param string $now Timestamp to use for the current time
1237
     * @param bool $isNewRecord Whether this should be treated as a new record write
1238
     * @param array $manipulation Manipulation to write to
1239
     * @param string $class Class of table to manipulate
1240
     */
1241
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1242
    {
1243
        $schema = $this->getSchema();
1244
        $table = $schema->tableName($class);
1245
        $manipulation[$table] = array();
1246
1247
        // Extract records for this table
1248
        foreach ($this->record as $fieldName => $fieldValue) {
1249
            // we're not attempting to reset the BaseTable->ID
1250
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1251
            if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1252
                continue;
1253
            }
1254
1255
            // Ensure this field pertains to this table
1256
            $specification = $schema->fieldSpec($class, $fieldName, DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED);
1257
            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...
1258
                continue;
1259
            }
1260
1261
            // if database column doesn't correlate to a DBField instance...
1262
            $fieldObj = $this->dbObject($fieldName);
1263
            if (!$fieldObj) {
1264
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1265
            }
1266
1267
            // Write to manipulation
1268
            $fieldObj->writeToManipulation($manipulation[$table]);
1269
        }
1270
1271
        // Ensure update of Created and LastEdited columns
1272
        if ($baseTable === $table) {
1273
            $manipulation[$table]['fields']['LastEdited'] = $now;
1274
            if ($isNewRecord) {
1275
                $manipulation[$table]['fields']['Created']
1276
                    = empty($this->record['Created'])
1277
                        ? $now
1278
                        : $this->record['Created'];
1279
                $manipulation[$table]['fields']['ClassName'] = $this->class;
1280
            }
1281
        }
1282
1283
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1284
        // attempt an update, as though it were a normal update.
1285
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1286
        $manipulation[$table]['id'] = $this->record['ID'];
1287
        $manipulation[$table]['class'] = $class;
1288
    }
1289
1290
    /**
1291
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1292
     *
1293
     * Does nothing if an ID is already assigned for this record
1294
     *
1295
     * @param string $baseTable Base table
1296
     * @param string $now Timestamp to use for the current time
1297
     */
1298
    protected function writeBaseRecord($baseTable, $now)
1299
    {
1300
        // Generate new ID if not specified
1301
        if ($this->isInDB()) {
1302
            return;
1303
        }
1304
1305
        // Perform an insert on the base table
1306
        $insert = new SQLInsert('"'.$baseTable.'"');
1307
        $insert
1308
            ->assign('"Created"', $now)
1309
            ->execute();
1310
        $this->changed['ID'] = self::CHANGE_VALUE;
1311
        $this->record['ID'] = DB::get_generated_id($baseTable);
1312
    }
1313
1314
    /**
1315
     * Generate and write the database manipulation for all changed fields
1316
     *
1317
     * @param string $baseTable Base table
1318
     * @param string $now Timestamp to use for the current time
1319
     * @param bool $isNewRecord If this is a new record
1320
     */
1321
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1322
    {
1323
        // Generate database manipulations for each class
1324
        $manipulation = array();
1325
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1326
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1327
        }
1328
1329
        // Allow extensions to extend this manipulation
1330
        $this->extend('augmentWrite', $manipulation);
1331
1332
        // New records have their insert into the base data table done first, so that they can pass the
1333
        // generated ID on to the rest of the manipulation
1334
        if ($isNewRecord) {
1335
            $manipulation[$baseTable]['command'] = 'update';
1336
        }
1337
1338
        // Perform the manipulation
1339
        DB::manipulate($manipulation);
1340
    }
1341
1342
    /**
1343
     * Writes all changes to this object to the database.
1344
     *  - It will insert a record whenever ID isn't set, otherwise update.
1345
     *  - All relevant tables will be updated.
1346
     *  - $this->onBeforeWrite() gets called beforehand.
1347
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1348
     *
1349
     *  @uses DataExtension->augmentWrite()
1350
     *
1351
     * @param boolean $showDebug Show debugging information
1352
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1353
     * @param boolean $forceWrite Write to database even if there are no changes
1354
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1355
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1356
     *                                 {@link getManyManyComponents()} (Default: false)
1357
     * @return int The ID of the record
1358
     * @throws ValidationException Exception that can be caught and handled by the calling function
1359
     */
1360
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1361
    {
1362
        $now = DBDatetime::now()->Rfc2822();
1363
1364
        // Execute pre-write tasks
1365
        $this->preWrite();
1366
1367
        // Check if we are doing an update or an insert
1368
        $isNewRecord = !$this->isInDB() || $forceInsert;
1369
1370
        // Check changes exist, abort if there are none
1371
        $hasChanges = $this->updateChanges($isNewRecord);
1372
        if ($hasChanges || $forceWrite || $isNewRecord) {
1373
            // New records have their insert into the base data table done first, so that they can pass the
1374
            // generated primary key on to the rest of the manipulation
1375
            $baseTable = $this->baseTable();
1376
            $this->writeBaseRecord($baseTable, $now);
1377
1378
            // Write the DB manipulation for all changed fields
1379
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1380
1381
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1382
            $this->writeRelations();
1383
            $this->onAfterWrite();
1384
            $this->changed = array();
1385
        } else {
1386
            if ($showDebug) {
1387
                Debug::message("no changes for DataObject");
1388
            }
1389
1390
            // Used by DODs to clean up after themselves, eg, Versioned
1391
            $this->invokeWithExtensions('onAfterSkippedWrite');
1392
        }
1393
1394
        // Ensure Created and LastEdited are populated
1395
        if (!isset($this->record['Created'])) {
1396
            $this->record['Created'] = $now;
1397
        }
1398
        $this->record['LastEdited'] = $now;
1399
1400
        // Write relations as necessary
1401
        if ($writeComponents) {
1402
            $this->writeComponents(true);
1403
        }
1404
1405
        // Clears the cache for this object so get_one returns the correct object.
1406
        $this->flushCache();
1407
1408
        return $this->record['ID'];
1409
    }
1410
1411
    /**
1412
     * Writes cached relation lists to the database, if possible
1413
     */
1414
    public function writeRelations()
1415
    {
1416
        if (!$this->isInDB()) {
1417
            return;
1418
        }
1419
1420
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1421
        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...
1422
            foreach ($this->unsavedRelations as $name => $list) {
1423
                $list->changeToList($this->$name());
1424
            }
1425
            $this->unsavedRelations = array();
1426
        }
1427
    }
1428
1429
    /**
1430
     * Write the cached components to the database. Cached components could refer to two different instances of the
1431
     * same record.
1432
     *
1433
     * @param bool $recursive Recursively write components
1434
     * @return DataObject $this
1435
     */
1436
    public function writeComponents($recursive = false)
1437
    {
1438
        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...
1439
            foreach ($this->components as $component) {
1440
                $component->write(false, false, false, $recursive);
1441
            }
1442
        }
1443
1444
        if ($join = $this->getJoin()) {
1445
            $join->write(false, false, false, $recursive);
1446
        }
1447
1448
        return $this;
1449
    }
1450
1451
    /**
1452
     * Delete this data object.
1453
     * $this->onBeforeDelete() gets called.
1454
     * Note that in Versioned objects, both Stage and Live will be deleted.
1455
     *  @uses DataExtension->augmentSQL()
1456
     */
1457
    public function delete()
1458
    {
1459
        $this->brokenOnDelete = true;
1460
        $this->onBeforeDelete();
1461
        if ($this->brokenOnDelete) {
1462
            user_error("$this->class has a broken onBeforeDelete() function."
1463
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1464
        }
1465
1466
        // Deleting a record without an ID shouldn't do anything
1467
        if (!$this->ID) {
1468
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1469
        }
1470
1471
        // TODO: This is quite ugly.  To improve:
1472
        //  - move the details of the delete code in the DataQuery system
1473
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1474
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1475
        $srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1476
        foreach ($srcQuery->queriedTables() as $table) {
1477
            $delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1478
            $delete->execute();
1479
        }
1480
        // Remove this item out of any caches
1481
        $this->flushCache();
1482
1483
        $this->onAfterDelete();
1484
1485
        $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...
1486
        $this->ID = 0;
1487
    }
1488
1489
    /**
1490
     * Delete the record with the given ID.
1491
     *
1492
     * @param string $className The class name of the record to be deleted
1493
     * @param int $id ID of record to be deleted
1494
     */
1495
    public static function delete_by_id($className, $id)
1496
    {
1497
        $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...
1498
        if ($obj) {
1499
            $obj->delete();
1500
        } else {
1501
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1502
        }
1503
    }
1504
1505
    /**
1506
     * Get the class ancestry, including the current class name.
1507
     * The ancestry will be returned as an array of class names, where the 0th element
1508
     * will be the class that inherits directly from DataObject, and the last element
1509
     * will be the current class.
1510
     *
1511
     * @return array Class ancestry
1512
     */
1513
    public function getClassAncestry()
1514
    {
1515
        return ClassInfo::ancestry(static::class);
1516
    }
1517
1518
    /**
1519
     * Return a component object from a one to one relationship, as a DataObject.
1520
     * If no component is available, an 'empty component' will be returned for
1521
     * non-polymorphic relations, or for polymorphic relations with a class set.
1522
     *
1523
     * @param string $componentName Name of the component
1524
     * @return DataObject The component object. It's exact type will be that of the component.
1525
     * @throws Exception
1526
     */
1527
    public function getComponent($componentName)
1528
    {
1529
        if (isset($this->components[$componentName])) {
1530
            return $this->components[$componentName];
1531
        }
1532
1533
        $schema = static::getSchema();
1534
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1535
            $joinField = $componentName . 'ID';
1536
            $joinID    = $this->getField($joinField);
1537
1538
            // Extract class name for polymorphic relations
1539
            if ($class === self::class) {
1540
                $class = $this->getField($componentName . 'Class');
1541
                if (empty($class)) {
1542
                    return null;
1543
                }
1544
            }
1545
1546
            if ($joinID) {
1547
                // Ensure that the selected object originates from the same stage, subsite, etc
1548
                $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...
1549
                    ->filter('ID', $joinID)
1550
                    ->setDataQueryParam($this->getInheritableQueryParams())
1551
                    ->first();
1552
            }
1553
1554
            if (empty($component)) {
1555
                $component = $this->model->$class->newObject();
1556
            }
1557
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1558
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1559
            $joinID = $this->ID;
1560
1561
            if ($joinID) {
1562
                // Prepare filter for appropriate join type
1563
                if ($polymorphic) {
1564
                    $filter = array(
1565
                        "{$joinField}ID" => $joinID,
1566
                        "{$joinField}Class" => $this->class
1567
                    );
1568
                } else {
1569
                    $filter = array(
1570
                        $joinField => $joinID
1571
                    );
1572
                }
1573
1574
                // Ensure that the selected object originates from the same stage, subsite, etc
1575
                $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...
1576
                    ->filter($filter)
1577
                    ->setDataQueryParam($this->getInheritableQueryParams())
1578
                    ->first();
1579
            }
1580
1581
            if (empty($component)) {
1582
                $component = $this->model->$class->newObject();
1583
                if ($polymorphic) {
1584
                    $component->{$joinField.'ID'} = $this->ID;
1585
                    $component->{$joinField.'Class'} = $this->class;
1586
                } else {
1587
                    $component->$joinField = $this->ID;
1588
                }
1589
            }
1590
        } else {
1591
            throw new InvalidArgumentException(
1592
                "DataObject->getComponent(): Could not find component '$componentName'."
1593
            );
1594
        }
1595
1596
        $this->components[$componentName] = $component;
1597
        return $component;
1598
    }
1599
1600
    /**
1601
     * Returns a one-to-many relation as a HasManyList
1602
     *
1603
     * @param string $componentName Name of the component
1604
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1605
     */
1606
    public function getComponents($componentName)
1607
    {
1608
        $result = null;
1609
1610
        $schema = $this->getSchema();
1611
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1612
        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...
1613
            throw new InvalidArgumentException(sprintf(
1614
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1615
                $componentName,
1616
                $this->class
1617
            ));
1618
        }
1619
1620
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1621
        if (!$this->ID) {
1622
            if (!isset($this->unsavedRelations[$componentName])) {
1623
                $this->unsavedRelations[$componentName] =
1624
                    new UnsavedRelationList($this->class, $componentName, $componentClass);
1625
            }
1626
            return $this->unsavedRelations[$componentName];
1627
        }
1628
1629
        // Determine type and nature of foreign relation
1630
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1631
        /** @var HasManyList $result */
1632
        if ($polymorphic) {
1633
            $result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1634
        } else {
1635
            $result = HasManyList::create($componentClass, $joinField);
1636
        }
1637
1638
        if ($this->model) {
1639
            $result->setDataModel($this->model);
1640
        }
1641
1642
        return $result
1643
            ->setDataQueryParam($this->getInheritableQueryParams())
1644
            ->forForeignID($this->ID);
1645
    }
1646
1647
    /**
1648
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1649
     *
1650
     * @param string $relationName Relation name.
1651
     * @return string Class name, or null if not found.
1652
     */
1653
    public function getRelationClass($relationName)
1654
    {
1655
        // Parse many_many
1656
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1657
        if ($manyManyComponent) {
1658
            list(
1659
                $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...
1660
                $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...
1661
            ) = $manyManyComponent;
1662
            return $componentClass;
1663
        }
1664
1665
        // Go through all relationship configuration fields.
1666
        $candidates = array_merge(
1667
            ($relations = Config::inst()->get($this->class, 'has_one')) ? $relations : array(),
1668
            ($relations = Config::inst()->get($this->class, 'has_many')) ? $relations : array(),
1669
            ($relations = Config::inst()->get($this->class, 'belongs_to')) ? $relations : array()
1670
        );
1671
1672
        if (isset($candidates[$relationName])) {
1673
            $remoteClass = $candidates[$relationName];
1674
1675
            // If dot notation is present, extract just the first part that contains the class.
1676
            if (($fieldPos = strpos($remoteClass, '.'))!==false) {
1677
                return substr($remoteClass, 0, $fieldPos);
1678
            }
1679
1680
            // Otherwise just return the class
1681
            return $remoteClass;
1682
        }
1683
1684
        return null;
1685
    }
1686
1687
    /**
1688
     * Given a relation name, determine the relation type
1689
     *
1690
     * @param string $component Name of component
1691
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1692
     */
1693
    public function getRelationType($component)
1694
    {
1695
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1696
        foreach ($types as $type) {
1697
            $relations = Config::inst()->get($this->class, $type);
1698
            if ($relations && isset($relations[$component])) {
1699
                return $type;
1700
            }
1701
        }
1702
        return null;
1703
    }
1704
1705
    /**
1706
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1707
     * side of the relation.
1708
     *
1709
     * Notes on behaviour:
1710
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1711
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1712
     *  - Cannot be used on polymorphic relationships
1713
     *  - Cannot be used on unsaved objects.
1714
     *
1715
     * @param string $remoteClass
1716
     * @param string $remoteRelation
1717
     * @return DataList|DataObject The component, either as a list or single object
1718
     * @throws BadMethodCallException
1719
     * @throws InvalidArgumentException
1720
     */
1721
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1722
    {
1723
        $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...
1724
        $class = $remote->getRelationClass($remoteRelation);
1725
        $schema = static::getSchema();
1726
1727
        // Validate arguments
1728
        if (!$this->isInDB()) {
1729
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1730
        }
1731
        if (empty($class)) {
1732
            throw new InvalidArgumentException(sprintf(
1733
                "%s invoked with invalid relation %s.%s",
1734
                __METHOD__,
1735
                $remoteClass,
1736
                $remoteRelation
1737
            ));
1738
        }
1739
        if ($class === self::class) {
1740
            throw new InvalidArgumentException(sprintf(
1741
                "%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1742
                "This method does not support polymorphic relationships",
1743
                __METHOD__,
1744
                $remoteClass,
1745
                $remoteRelation
1746
            ));
1747
        }
1748
        if (!is_a($this, $class, true)) {
1749
            throw new InvalidArgumentException(sprintf(
1750
                "Relation %s on %s does not refer to objects of type %s",
1751
                $remoteRelation,
1752
                $remoteClass,
1753
                static::class
1754
            ));
1755
        }
1756
1757
        // Check the relation type to mock
1758
        $relationType = $remote->getRelationType($remoteRelation);
1759
        switch ($relationType) {
1760
            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...
1761
                // Mock has_many
1762
                $joinField = "{$remoteRelation}ID";
1763
                $componentClass = $schema->classForField($remoteClass, $joinField);
1764
                $result = HasManyList::create($componentClass, $joinField);
1765
                if ($this->model) {
1766
                    $result->setDataModel($this->model);
1767
                }
1768
                return $result
1769
                    ->setDataQueryParam($this->getInheritableQueryParams())
1770
                    ->forForeignID($this->ID);
1771
            }
1772
            case 'belongs_to':
1773
            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...
1774
                // These relations must have a has_one on the other end, so find it
1775
                $joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic);
1776
                if ($polymorphic) {
1777
                    throw new InvalidArgumentException(sprintf(
1778
                        "%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1779
                        "to be a has_one polymorphic. This method does not support polymorphic relationships",
1780
                        __METHOD__,
1781
                        $remoteClass,
1782
                        $remoteRelation
1783
                    ));
1784
                }
1785
                $joinID = $this->getField($joinField);
1786
                if (empty($joinID)) {
1787
                    return null;
1788
                }
1789
                // Get object by joined ID
1790
                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...
1791
                    ->filter('ID', $joinID)
1792
                    ->setDataQueryParam($this->getInheritableQueryParams())
1793
                    ->first();
1794
            }
1795
            case 'many_many':
1796
            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...
1797
                // Get components and extra fields from parent
1798
                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...
1799
                    = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1800
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1801
1802
                // Reverse parent and component fields and create an inverse ManyManyList
1803
                /** @var RelationList $result */
1804
                $result = Injector::inst()->create(
1805
                    $relationClass,
1806
                    $componentClass,
1807
                    $table,
1808
                    $componentField,
1809
                    $parentField,
1810
                    $extraFields
1811
                );
1812
                if ($this->model) {
1813
                    $result->setDataModel($this->model);
1814
                }
1815
                $this->extend('updateManyManyComponents', $result);
1816
1817
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1818
                // foreignID set elsewhere.
1819
                return $result
1820
                    ->setDataQueryParam($this->getInheritableQueryParams())
1821
                    ->forForeignID($this->ID);
1822
            }
1823
            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...
1824
                return null;
1825
            }
1826
        }
1827
    }
1828
1829
    /**
1830
     * Returns a many-to-many component, as a ManyManyList.
1831
     * @param string $componentName Name of the many-many component
1832
     * @return RelationList|UnsavedRelationList The set of components
1833
     */
1834
    public function getManyManyComponents($componentName)
1835
    {
1836
        $schema = static::getSchema();
1837
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
1838
        if (!$manyManyComponent) {
1839
            throw new InvalidArgumentException(sprintf(
1840
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1841
                $componentName,
1842
                $this->class
1843
            ));
1844
        }
1845
1846
        list($relationClass, $parentClass, $componentClass, $parentField, $componentField, $tableOrClass)
1847
            = $manyManyComponent;
1848
1849
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1850
        if (!$this->ID) {
1851
            if (!isset($this->unsavedRelations[$componentName])) {
1852
                $this->unsavedRelations[$componentName] =
1853
                    new UnsavedRelationList($parentClass, $componentName, $componentClass);
1854
            }
1855
            return $this->unsavedRelations[$componentName];
1856
        }
1857
1858
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
1859
        /** @var RelationList $result */
1860
        $result = Injector::inst()->create(
1861
            $relationClass,
1862
            $componentClass,
1863
            $tableOrClass,
1864
            $componentField,
1865
            $parentField,
1866
            $extraFields
1867
        );
1868
1869
1870
        // Store component data in query meta-data
1871
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
1872
            /** @var DataQuery $query */
1873
            $query->setQueryParam('Component.ExtraFields', $extraFields);
1874
        });
1875
1876
        if ($this->model) {
1877
            $result->setDataModel($this->model);
1878
        }
1879
1880
        $this->extend('updateManyManyComponents', $result);
1881
1882
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1883
        // foreignID set elsewhere.
1884
        return $result
1885
            ->setDataQueryParam($this->getInheritableQueryParams())
1886
            ->forForeignID($this->ID);
1887
    }
1888
1889
    /**
1890
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1891
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1892
     *
1893
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1894
     *                          their classes.
1895
     */
1896
    public function hasOne()
1897
    {
1898
        return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED);
1899
    }
1900
1901
    /**
1902
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1903
     * their class name will be returned.
1904
     *
1905
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1906
     *        the field data stripped off. It defaults to TRUE.
1907
     * @return string|array
1908
     */
1909
    public function belongsTo($classOnly = true)
1910
    {
1911
        $belongsTo = (array)Config::inst()->get($this->class, 'belongs_to', Config::INHERITED);
1912
        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...
1913
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1914
        } else {
1915
            return $belongsTo ? $belongsTo : array();
1916
        }
1917
    }
1918
1919
    /**
1920
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1921
     * relationships and their classes will be returned.
1922
     *
1923
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1924
     *        the field data stripped off. It defaults to TRUE.
1925
     * @return string|array|false
1926
     */
1927
    public function hasMany($classOnly = true)
1928
    {
1929
        $hasMany = (array)Config::inst()->get($this->class, 'has_many', Config::INHERITED);
1930
        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...
1931
            return preg_replace('/(.+)?\..+/', '$1', $hasMany);
1932
        } else {
1933
            return $hasMany ? $hasMany : array();
1934
        }
1935
    }
1936
1937
    /**
1938
     * Return the many-to-many extra fields specification.
1939
     *
1940
     * If you don't specify a component name, it returns all
1941
     * extra fields for all components available.
1942
     *
1943
     * @return array|null
1944
     */
1945
    public function manyManyExtraFields()
1946
    {
1947
        return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED);
1948
    }
1949
1950
    /**
1951
     * Return information about a many-to-many component.
1952
     * The return value is an array of (parentclass, childclass).  If $component is null, then all many-many
1953
     * components are returned.
1954
     *
1955
     * @see DataObjectSchema::manyManyComponent()
1956
     * @return array|null An array of (parentclass, childclass), or an array of all many-many components
1957
     */
1958
    public function manyMany()
1959
    {
1960
        $manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED);
1961
        $belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED);
1962
        $items = array_merge($manyManys, $belongsManyManys);
1963
        return $items;
1964
    }
1965
1966
    /**
1967
     * This returns an array (if it exists) describing the database extensions that are required, or false if none
1968
     *
1969
     * This is experimental, and is currently only a Postgres-specific enhancement.
1970
     *
1971
     * @param string $class
1972
     * @return array|false
1973
     */
1974
    public function database_extensions($class)
1975
    {
1976
        $extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
1977
1978
        if ($extensions) {
1979
            return $extensions;
1980
        } else {
1981
            return false;
1982
        }
1983
    }
1984
1985
    /**
1986
     * Generates a SearchContext to be used for building and processing
1987
     * a generic search form for properties on this object.
1988
     *
1989
     * @return SearchContext
1990
     */
1991
    public function getDefaultSearchContext()
1992
    {
1993
        return new SearchContext(
1994
            $this->class,
1995
            $this->scaffoldSearchFields(),
1996
            $this->defaultSearchFilters()
1997
        );
1998
    }
1999
2000
    /**
2001
     * Determine which properties on the DataObject are
2002
     * searchable, and map them to their default {@link FormField}
2003
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
2004
     *
2005
     * Some additional logic is included for switching field labels, based on
2006
     * how generic or specific the field type is.
2007
     *
2008
     * Used by {@link SearchContext}.
2009
     *
2010
     * @param array $_params
2011
     *   'fieldClasses': Associative array of field names as keys and FormField classes as values
2012
     *   'restrictFields': Numeric array of a field name whitelist
2013
     * @return FieldList
2014
     */
2015
    public function scaffoldSearchFields($_params = null)
2016
    {
2017
        $params = array_merge(
2018
            array(
2019
                'fieldClasses' => false,
2020
                'restrictFields' => false
2021
            ),
2022
            (array)$_params
2023
        );
2024
        $fields = new FieldList();
2025
        foreach ($this->searchableFields() as $fieldName => $spec) {
2026
            if ($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) {
2027
                continue;
2028
            }
2029
2030
            // If a custom fieldclass is provided as a string, use it
2031
            $field = null;
2032
            if ($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
2033
                $fieldClass = $params['fieldClasses'][$fieldName];
2034
                $field = new $fieldClass($fieldName);
2035
            // If we explicitly set a field, then construct that
2036
            } elseif (isset($spec['field'])) {
2037
                // If it's a string, use it as a class name and construct
2038
                if (is_string($spec['field'])) {
2039
                    $fieldClass = $spec['field'];
2040
                    $field = new $fieldClass($fieldName);
2041
2042
                // If it's a FormField object, then just use that object directly.
2043
                } elseif ($spec['field'] instanceof FormField) {
2044
                    $field = $spec['field'];
2045
2046
                // Otherwise we have a bug
2047
                } else {
2048
                    user_error("Bad value for searchable_fields, 'field' value: "
2049
                        . var_export($spec['field'], true), E_USER_WARNING);
2050
                }
2051
2052
            // Otherwise, use the database field's scaffolder
2053
            } else {
2054
                $field = $this->relObject($fieldName)->scaffoldSearchField();
2055
            }
2056
2057
            // Allow fields to opt out of search
2058
            if (!$field) {
2059
                continue;
2060
            }
2061
2062
            if (strstr($fieldName, '.')) {
2063
                $field->setName(str_replace('.', '__', $fieldName));
2064
            }
2065
            $field->setTitle($spec['title']);
2066
2067
            $fields->push($field);
2068
        }
2069
        return $fields;
2070
    }
2071
2072
    /**
2073
     * Scaffold a simple edit form for all properties on this dataobject,
2074
     * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
2075
     * Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
2076
     *
2077
     * @uses FormScaffolder
2078
     *
2079
     * @param array $_params Associative array passing through properties to {@link FormScaffolder}.
2080
     * @return FieldList
2081
     */
2082
    public function scaffoldFormFields($_params = null)
2083
    {
2084
        $params = array_merge(
2085
            array(
2086
                'tabbed' => false,
2087
                'includeRelations' => false,
2088
                'restrictFields' => false,
2089
                'fieldClasses' => false,
2090
                'ajaxSafe' => false
2091
            ),
2092
            (array)$_params
2093
        );
2094
2095
        $fs = new FormScaffolder($this);
2096
        $fs->tabbed = $params['tabbed'];
2097
        $fs->includeRelations = $params['includeRelations'];
2098
        $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...
2099
        $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...
2100
        $fs->ajaxSafe = $params['ajaxSafe'];
2101
2102
        return $fs->getFieldList();
2103
    }
2104
2105
    /**
2106
     * Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
2107
     * being called on extensions
2108
     *
2109
     * @param callable $callback The callback to execute
2110
     */
2111
    protected function beforeUpdateCMSFields($callback)
2112
    {
2113
        $this->beforeExtending('updateCMSFields', $callback);
2114
    }
2115
2116
    /**
2117
     * Centerpiece of every data administration interface in Silverstripe,
2118
     * which returns a {@link FieldList} suitable for a {@link Form} object.
2119
     * If not overloaded, we're using {@link scaffoldFormFields()} to automatically
2120
     * generate this set. To customize, overload this method in a subclass
2121
     * or extended onto it by using {@link DataExtension->updateCMSFields()}.
2122
     *
2123
     * <code>
2124
     * class MyCustomClass extends DataObject {
2125
     *  static $db = array('CustomProperty'=>'Boolean');
2126
     *
2127
     *  function getCMSFields() {
2128
     *    $fields = parent::getCMSFields();
2129
     *    $fields->addFieldToTab('Root.Content',new CheckboxField('CustomProperty'));
2130
     *    return $fields;
2131
     *  }
2132
     * }
2133
     * </code>
2134
     *
2135
     * @see Good example of complex FormField building: SiteTree::getCMSFields()
2136
     *
2137
     * @return FieldList Returns a TabSet for usage within the CMS - don't use for frontend forms.
2138
     */
2139
    public function getCMSFields()
2140
    {
2141
        $tabbedFields = $this->scaffoldFormFields(array(
2142
            // Don't allow has_many/many_many relationship editing before the record is first saved
2143
            'includeRelations' => ($this->ID > 0),
2144
            'tabbed' => true,
2145
            'ajaxSafe' => true
2146
        ));
2147
2148
        $this->extend('updateCMSFields', $tabbedFields);
2149
2150
        return $tabbedFields;
2151
    }
2152
2153
    /**
2154
     * need to be overload by solid dataobject, so that the customised actions of that dataobject,
2155
     * including that dataobject's extensions customised actions could be added to the EditForm.
2156
     *
2157
     * @return FieldList an Empty FieldList(); need to be overload by solid subclass
2158
     */
2159
    public function getCMSActions()
2160
    {
2161
        $actions = new FieldList();
2162
        $this->extend('updateCMSActions', $actions);
2163
        return $actions;
2164
    }
2165
2166
2167
    /**
2168
     * Used for simple frontend forms without relation editing
2169
     * or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
2170
     * by default. To customize, either overload this method in your
2171
     * subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
2172
     *
2173
     * @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
2174
     *
2175
     * @param array $params See {@link scaffoldFormFields()}
2176
     * @return FieldList Always returns a simple field collection without TabSet.
2177
     */
2178
    public function getFrontEndFields($params = null)
2179
    {
2180
        $untabbedFields = $this->scaffoldFormFields($params);
2181
        $this->extend('updateFrontEndFields', $untabbedFields);
2182
2183
        return $untabbedFields;
2184
    }
2185
2186
    /**
2187
     * Gets the value of a field.
2188
     * Called by {@link __get()} and any getFieldName() methods you might create.
2189
     *
2190
     * @param string $field The name of the field
2191
     * @return mixed The field value
2192
     */
2193
    public function getField($field)
2194
    {
2195
        // If we already have an object in $this->record, then we should just return that
2196
        if (isset($this->record[$field]) && is_object($this->record[$field])) {
2197
            return $this->record[$field];
2198
        }
2199
2200
        // Do we have a field that needs to be lazy loaded?
2201
        if (isset($this->record[$field.'_Lazy'])) {
2202
            $tableClass = $this->record[$field.'_Lazy'];
2203
            $this->loadLazyFields($tableClass);
2204
        }
2205
2206
        // In case of complex fields, return the DBField object
2207
        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...
2208
            $this->record[$field] = $this->dbObject($field);
2209
        }
2210
2211
        return isset($this->record[$field]) ? $this->record[$field] : null;
2212
    }
2213
2214
    /**
2215
     * Loads all the stub fields that an initial lazy load didn't load fully.
2216
     *
2217
     * @param string $class Class to load the values from. Others are joined as required.
2218
     * Not specifying a tableClass will load all lazy fields from all tables.
2219
     * @return bool Flag if lazy loading succeeded
2220
     */
2221
    protected function loadLazyFields($class = null)
2222
    {
2223
        if (!$this->isInDB() || !is_numeric($this->ID)) {
2224
            return false;
2225
        }
2226
2227
        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...
2228
            $loaded = array();
2229
2230
            foreach ($this->record as $key => $value) {
2231
                if (strlen($key) > 5 && substr($key, -5) == '_Lazy' && !array_key_exists($value, $loaded)) {
2232
                    $this->loadLazyFields($value);
2233
                    $loaded[$value] = $value;
2234
                }
2235
            }
2236
2237
            return false;
2238
        }
2239
2240
        $dataQuery = new DataQuery($class);
2241
2242
        // Reset query parameter context to that of this DataObject
2243
        if ($params = $this->getSourceQueryParams()) {
2244
            foreach ($params as $key => $value) {
2245
                $dataQuery->setQueryParam($key, $value);
2246
            }
2247
        }
2248
2249
        // Limit query to the current record, unless it has the Versioned extension,
2250
        // in which case it requires special handling through augmentLoadLazyFields()
2251
        $schema = static::getSchema();
2252
        $baseIDColumn = $schema->sqlColumnForField($this, 'ID');
2253
        $dataQuery->where([
2254
            $baseIDColumn => $this->record['ID']
2255
        ])->limit(1);
2256
2257
        $columns = array();
2258
2259
        // Add SQL for fields, both simple & multi-value
2260
        // TODO: This is copy & pasted from buildSQL(), it could be moved into a method
2261
        $databaseFields = $schema->databaseFields($class, false);
2262
        foreach ($databaseFields as $k => $v) {
2263
            if (!isset($this->record[$k]) || $this->record[$k] === null) {
2264
                $columns[] = $k;
2265
            }
2266
        }
2267
2268
        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...
2269
            $query = $dataQuery->query();
2270
            $this->extend('augmentLoadLazyFields', $query, $dataQuery, $this);
2271
            $this->extend('augmentSQL', $query, $dataQuery);
2272
2273
            $dataQuery->setQueriedColumns($columns);
2274
            $newData = $dataQuery->execute()->record();
2275
2276
            // Load the data into record
2277
            if ($newData) {
2278
                foreach ($newData as $k => $v) {
2279
                    if (in_array($k, $columns)) {
2280
                        $this->record[$k] = $v;
2281
                        $this->original[$k] = $v;
2282
                        unset($this->record[$k . '_Lazy']);
2283
                    }
2284
                }
2285
2286
            // No data means that the query returned nothing; assign 'null' to all the requested fields
2287
            } else {
2288
                foreach ($columns as $k) {
2289
                    $this->record[$k] = null;
2290
                    $this->original[$k] = null;
2291
                    unset($this->record[$k . '_Lazy']);
2292
                }
2293
            }
2294
        }
2295
        return true;
2296
    }
2297
2298
    /**
2299
     * Return the fields that have changed.
2300
     *
2301
     * The change level affects what the functions defines as "changed":
2302
     * - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
2303
     * - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
2304
     *   for example a change from 0 to null would not be included.
2305
     *
2306
     * Example return:
2307
     * <code>
2308
     * array(
2309
     *   'Title' = array('before' => 'Home', 'after' => 'Home-Changed', 'level' => DataObject::CHANGE_VALUE)
2310
     * )
2311
     * </code>
2312
     *
2313
     * @param boolean|array $databaseFieldsOnly Filter to determine which fields to return. Set to true
2314
     * to return all database fields, or an array for an explicit filter. false returns all fields.
2315
     * @param int $changeLevel The strictness of what is defined as change. Defaults to strict
2316
     * @return array
2317
     */
2318
    public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT)
2319
    {
2320
        $changedFields = array();
2321
2322
        // Update the changed array with references to changed obj-fields
2323
        foreach ($this->record as $k => $v) {
2324
            // Prevents DBComposite infinite looping on isChanged
2325
            if (is_array($databaseFieldsOnly) && !in_array($k, $databaseFieldsOnly)) {
2326
                continue;
2327
            }
2328
            if (is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
2329
                $this->changed[$k] = self::CHANGE_VALUE;
2330
            }
2331
        }
2332
2333
        if (is_array($databaseFieldsOnly)) {
2334
            $fields = array_intersect_key((array)$this->changed, array_flip($databaseFieldsOnly));
2335
        } elseif ($databaseFieldsOnly) {
2336
            $fieldsSpecs = static::getSchema()->fieldSpecs(static::class);
2337
            $fields = array_intersect_key((array)$this->changed, $fieldsSpecs);
2338
        } else {
2339
            $fields = $this->changed;
2340
        }
2341
2342
        // Filter the list to those of a certain change level
2343
        if ($changeLevel > self::CHANGE_STRICT) {
2344
            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...
2345
                foreach ($fields as $name => $level) {
2346
                    if ($level < $changeLevel) {
2347
                        unset($fields[$name]);
2348
                    }
2349
                }
2350
            }
2351
        }
2352
2353
        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...
2354
            foreach ($fields as $name => $level) {
2355
                $changedFields[$name] = array(
2356
                'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
2357
                'after' => array_key_exists($name, $this->record) ? $this->record[$name] : null,
2358
                'level' => $level
2359
                            );
2360
            }
2361
        }
2362
2363
        return $changedFields;
2364
    }
2365
2366
    /**
2367
     * Uses {@link getChangedFields()} to determine if fields have been changed
2368
     * since loading them from the database.
2369
     *
2370
     * @param string $fieldName Name of the database field to check, will check for any if not given
2371
     * @param int $changeLevel See {@link getChangedFields()}
2372
     * @return boolean
2373
     */
2374
    public function isChanged($fieldName = null, $changeLevel = self::CHANGE_STRICT)
2375
    {
2376
        $fields = $fieldName ? array($fieldName) : true;
2377
        $changed = $this->getChangedFields($fields, $changeLevel);
2378
        if (!isset($fieldName)) {
2379
            return !empty($changed);
2380
        } else {
2381
            return array_key_exists($fieldName, $changed);
2382
        }
2383
    }
2384
2385
    /**
2386
     * Set the value of the field
2387
     * Called by {@link __set()} and any setFieldName() methods you might create.
2388
     *
2389
     * @param string $fieldName Name of the field
2390
     * @param mixed $val New field value
2391
     * @return $this
2392
     */
2393
    public function setField($fieldName, $val)
2394
    {
2395
        $this->objCacheClear();
2396
        //if it's a has_one component, destroy the cache
2397
        if (substr($fieldName, -2) == 'ID') {
2398
            unset($this->components[substr($fieldName, 0, -2)]);
2399
        }
2400
2401
        // If we've just lazy-loaded the column, then we need to populate the $original array
2402
        if (isset($this->record[$fieldName.'_Lazy'])) {
2403
            $tableClass = $this->record[$fieldName.'_Lazy'];
2404
            $this->loadLazyFields($tableClass);
2405
        }
2406
2407
        // Situation 1: Passing an DBField
2408
        if ($val instanceof DBField) {
2409
            $val->setName($fieldName);
2410
            $val->saveInto($this);
2411
2412
            // Situation 1a: Composite fields should remain bound in case they are
2413
            // later referenced to update the parent dataobject
2414
            if ($val instanceof DBComposite) {
2415
                $val->bindTo($this);
2416
                $this->record[$fieldName] = $val;
2417
            }
2418
        // Situation 2: Passing a literal or non-DBField object
2419
        } else {
2420
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2421
            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...
2422
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2423
            }
2424
2425
            // if a field is not existing or has strictly changed
2426
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2427
                // TODO Add check for php-level defaults which are not set in the db
2428
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2429
                // At the very least, the type has changed
2430
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2431
2432
                if ((!isset($this->record[$fieldName]) && $val)
2433
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2434
                ) {
2435
                    // Value has changed as well, not just the type
2436
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2437
                }
2438
2439
                // Value is always saved back when strict check succeeds.
2440
                $this->record[$fieldName] = $val;
2441
            }
2442
        }
2443
        return $this;
2444
    }
2445
2446
    /**
2447
     * Set the value of the field, using a casting object.
2448
     * This is useful when you aren't sure that a date is in SQL format, for example.
2449
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2450
     * can be saved into the Image table.
2451
     *
2452
     * @param string $fieldName Name of the field
2453
     * @param mixed $value New field value
2454
     * @return $this
2455
     */
2456
    public function setCastedField($fieldName, $value)
2457
    {
2458
        if (!$fieldName) {
2459
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2460
        }
2461
        $fieldObj = $this->dbObject($fieldName);
2462
        if ($fieldObj) {
2463
            $fieldObj->setValue($value);
2464
            $fieldObj->saveInto($this);
2465
        } else {
2466
            $this->$fieldName = $value;
2467
        }
2468
        return $this;
2469
    }
2470
2471
    /**
2472
     * {@inheritdoc}
2473
     */
2474
    public function castingHelper($field)
2475
    {
2476
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2477
        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...
2478
            return $fieldSpec;
2479
        }
2480
2481
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2482
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2483
        $queryParams = $this->getSourceQueryParams();
2484
        if (!empty($queryParams['Component.ExtraFields'])) {
2485
            $extraFields = $queryParams['Component.ExtraFields'];
2486
2487
            if (isset($extraFields[$field])) {
2488
                return $extraFields[$field];
2489
            }
2490
        }
2491
2492
        return parent::castingHelper($field);
2493
    }
2494
2495
    /**
2496
     * Returns true if the given field exists in a database column on any of
2497
     * the objects tables and optionally look up a dynamic getter with
2498
     * get<fieldName>().
2499
     *
2500
     * @param string $field Name of the field
2501
     * @return boolean True if the given field exists
2502
     */
2503
    public function hasField($field)
2504
    {
2505
        $schema = static::getSchema();
2506
        return (
2507
            array_key_exists($field, $this->record)
2508
            || $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...
2509
            || (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...
2510
            || $this->hasMethod("get{$field}")
2511
        );
2512
    }
2513
2514
    /**
2515
     * Returns true if the given field exists as a database column
2516
     *
2517
     * @param string $field Name of the field
2518
     *
2519
     * @return boolean
2520
     */
2521
    public function hasDatabaseField($field)
2522
    {
2523
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2524
        return !empty($spec);
2525
    }
2526
2527
    /**
2528
     * Returns true if the member is allowed to do the given action.
2529
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2530
     *
2531
     * @param string $perm The permission to be checked, such as 'View'.
2532
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2533
     * in user.
2534
     * @param array $context Additional $context to pass to extendedCan()
2535
     *
2536
     * @return boolean True if the the member is allowed to do the given action
2537
     */
2538
    public function can($perm, $member = null, $context = array())
2539
    {
2540
        if (!isset($member)) {
2541
            $member = Member::currentUser();
2542
        }
2543
        if (Permission::checkMember($member, "ADMIN")) {
2544
            return true;
2545
        }
2546
2547
        if ($this->getSchema()->manyManyComponent(static::class, 'Can' . $perm)) {
2548
            if ($this->ParentID && $this->SecurityType == 'Inherit') {
2549
                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...
2550
                    return false;
2551
                }
2552
                return $this->Parent->can($perm, $member);
2553
            } else {
2554
                $permissionCache = $this->uninherited('permissionCache');
2555
                $memberID = $member ? $member->ID : 'none';
2556
2557
                if (!isset($permissionCache[$memberID][$perm])) {
2558
                    if ($member->ID) {
2559
                        $groups = $member->Groups();
2560
                    }
2561
2562
                    $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...
2563
2564
                    // TODO Fix relation table hardcoding
2565
                    $query = new SQLSelect(
2566
                        "\"Page_Can$perm\".PageID",
2567
                        array("\"Page_Can$perm\""),
2568
                        "GroupID IN ($groupList)"
2569
                    );
2570
2571
                    $permissionCache[$memberID][$perm] = $query->execute()->column();
2572
2573
                    if ($perm == "View") {
2574
                        // TODO Fix relation table hardcoding
2575
                        $query = new SQLSelect("\"SiteTree\".\"ID\"", array(
2576
                            "\"SiteTree\"",
2577
                            "LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
2578
                            ), "\"Page_CanView\".\"PageID\" IS NULL");
2579
2580
                            $unsecuredPages = $query->execute()->column();
2581
                        if ($permissionCache[$memberID][$perm]) {
2582
                            $permissionCache[$memberID][$perm]
2583
                            = array_merge($permissionCache[$memberID][$perm], $unsecuredPages);
2584
                        } else {
2585
                            $permissionCache[$memberID][$perm] = $unsecuredPages;
2586
                        }
2587
                    }
2588
2589
                    Config::inst()->update($this->class, 'permissionCache', $permissionCache);
2590
                }
2591
2592
                if ($permissionCache[$memberID][$perm]) {
2593
                    return in_array($this->ID, $permissionCache[$memberID][$perm]);
2594
                }
2595
            }
2596
        } else {
2597
            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...
2598
        }
2599
    }
2600
2601
    /**
2602
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2603
     * expected to return one of three values:
2604
     *
2605
     *  - false: Disallow this permission, regardless of what other extensions say
2606
     *  - true: Allow this permission, as long as no other extensions return false
2607
     *  - NULL: Don't affect the outcome
2608
     *
2609
     * This method itself returns a tri-state value, and is designed to be used like this:
2610
     *
2611
     * <code>
2612
     * $extended = $this->extendedCan('canDoSomething', $member);
2613
     * if($extended !== null) return $extended;
2614
     * else return $normalValue;
2615
     * </code>
2616
     *
2617
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2618
     * @param Member|int $member
2619
     * @param array $context Optional context
2620
     * @return boolean|null
2621
     */
2622
    public function extendedCan($methodName, $member, $context = array())
2623
    {
2624
        $results = $this->extend($methodName, $member, $context);
2625
        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...
2626
            // Remove NULLs
2627
            $results = array_filter($results, function ($v) {
2628
                return !is_null($v);
2629
            });
2630
            // If there are any non-NULL responses, then return the lowest one of them.
2631
            // If any explicitly deny the permission, then we don't get access
2632
            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...
2633
                return min($results);
2634
            }
2635
        }
2636
        return null;
2637
    }
2638
2639
    /**
2640
     * @param Member $member
2641
     * @return boolean
2642
     */
2643
    public function canView($member = null)
2644
    {
2645
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2643 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...
2646
        if ($extended !== null) {
2647
            return $extended;
2648
        }
2649
        return Permission::check('ADMIN', 'any', $member);
2650
    }
2651
2652
    /**
2653
     * @param Member $member
2654
     * @return boolean
2655
     */
2656
    public function canEdit($member = null)
2657
    {
2658
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2656 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...
2659
        if ($extended !== null) {
2660
            return $extended;
2661
        }
2662
        return Permission::check('ADMIN', 'any', $member);
2663
    }
2664
2665
    /**
2666
     * @param Member $member
2667
     * @return boolean
2668
     */
2669
    public function canDelete($member = null)
2670
    {
2671
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2669 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...
2672
        if ($extended !== null) {
2673
            return $extended;
2674
        }
2675
        return Permission::check('ADMIN', 'any', $member);
2676
    }
2677
2678
    /**
2679
     * @param Member $member
2680
     * @param array $context Additional context-specific data which might
2681
     * affect whether (or where) this object could be created.
2682
     * @return boolean
2683
     */
2684
    public function canCreate($member = null, $context = array())
2685
    {
2686
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2684 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...
2687
        if ($extended !== null) {
2688
            return $extended;
2689
        }
2690
        return Permission::check('ADMIN', 'any', $member);
2691
    }
2692
2693
    /**
2694
     * Debugging used by Debug::show()
2695
     *
2696
     * @return string HTML data representing this object
2697
     */
2698
    public function debug()
2699
    {
2700
        $val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2701
        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...
2702
            foreach ($this->record as $fieldName => $fieldVal) {
2703
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2704
            }
2705
        }
2706
        $val .= "</ul>\n";
2707
        return $val;
2708
    }
2709
2710
    /**
2711
     * Return the DBField object that represents the given field.
2712
     * This works similarly to obj() with 2 key differences:
2713
     *   - it still returns an object even when the field has no value.
2714
     *   - it only matches fields and not methods
2715
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2716
     *
2717
     * @param string $fieldName Name of the field
2718
     * @return DBField The field as a DBField object
2719
     */
2720
    public function dbObject($fieldName)
2721
    {
2722
        // Check for field in DB
2723
        $schema = static::getSchema();
2724
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2725
        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...
2726
            return null;
2727
        }
2728
2729
        $value = isset($this->record[$fieldName])
2730
            ? $this->record[$fieldName]
2731
            : null;
2732
2733
        // If we have a DBField object in $this->record, then return that
2734
        if ($value instanceof DBField) {
2735
            return $value;
2736
        }
2737
2738
        list($class, $spec) = explode('.', $helper);
2739
        /** @var DBField $obj */
2740
        $table = $schema->tableName($class);
2741
        $obj = Object::create_from_string($spec, $fieldName);
2742
        $obj->setTable($table);
2743
        $obj->setValue($value, $this, false);
2744
        return $obj;
2745
    }
2746
2747
    /**
2748
     * Traverses to a DBField referenced by relationships between data objects.
2749
     *
2750
     * The path to the related field is specified with dot separated syntax
2751
     * (eg: Parent.Child.Child.FieldName).
2752
     *
2753
     * @param string $fieldPath
2754
     *
2755
     * @return mixed DBField of the field on the object or a DataList instance.
2756
     */
2757
    public function relObject($fieldPath)
2758
    {
2759
        $object = null;
2760
2761
        if (strpos($fieldPath, '.') !== false) {
2762
            $parts = explode('.', $fieldPath);
2763
            $fieldName = array_pop($parts);
2764
2765
            // Traverse dot syntax
2766
            $component = $this;
2767
2768
            foreach ($parts as $relation) {
2769
                if ($component instanceof SS_List) {
2770
                    if (method_exists($component, $relation)) {
2771
                        $component = $component->$relation();
2772
                    } else {
2773
                        $component = $component->relation($relation);
2774
                    }
2775
                } else {
2776
                    $component = $component->$relation();
2777
                }
2778
            }
2779
2780
            $object = $component->dbObject($fieldName);
2781
        } else {
2782
            $object = $this->dbObject($fieldPath);
2783
        }
2784
2785
        return $object;
2786
    }
2787
2788
    /**
2789
     * Traverses to a field referenced by relationships between data objects, returning the value
2790
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2791
     *
2792
     * @param $fieldName string
2793
     * @return string | null - will return null on a missing value
2794
     */
2795
    public function relField($fieldName)
2796
    {
2797
        $component = $this;
2798
2799
        // We're dealing with relations here so we traverse the dot syntax
2800
        if (strpos($fieldName, '.') !== false) {
2801
            $relations = explode('.', $fieldName);
2802
            $fieldName = array_pop($relations);
2803
            foreach ($relations as $relation) {
2804
                // Inspect $component for element $relation
2805
                if ($component->hasMethod($relation)) {
2806
                    // Check nested method
2807
                    $component = $component->$relation();
2808
                } elseif ($component instanceof SS_List) {
2809
                    // Select adjacent relation from DataList
2810
                    $component = $component->relation($relation);
2811
                } elseif ($component instanceof DataObject
2812
                    && ($dbObject = $component->dbObject($relation))
2813
                ) {
2814
                    // Select db object
2815
                    $component = $dbObject;
2816
                } else {
2817
                    user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
2818
                }
2819
            }
2820
        }
2821
2822
        // Bail if the component is null
2823
        if (!$component) {
2824
            return null;
2825
        }
2826
        if ($component->hasMethod($fieldName)) {
2827
            return $component->$fieldName();
2828
        }
2829
        return $component->$fieldName;
2830
    }
2831
2832
    /**
2833
     * Temporary hack to return an association name, based on class, to get around the mangle
2834
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2835
     *
2836
     * @param string $className
2837
     * @return string
2838
     */
2839
    public function getReverseAssociation($className)
2840
    {
2841
        if (is_array($this->manyMany())) {
2842
            $many_many = array_flip($this->manyMany());
2843
            if (array_key_exists($className, $many_many)) {
2844
                return $many_many[$className];
2845
            }
2846
        }
2847
        if (is_array($this->hasMany())) {
2848
            $has_many = array_flip($this->hasMany());
2849
            if (array_key_exists($className, $has_many)) {
2850
                return $has_many[$className];
2851
            }
2852
        }
2853
        if (is_array($this->hasOne())) {
2854
            $has_one = array_flip($this->hasOne());
2855
            if (array_key_exists($className, $has_one)) {
2856
                return $has_one[$className];
2857
            }
2858
        }
2859
2860
        return false;
2861
    }
2862
2863
    /**
2864
     * Return all objects matching the filter
2865
     * sub-classes are automatically selected and included
2866
     *
2867
     * @param string $callerClass The class of objects to be returned
2868
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2869
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2870
     * @param string|array $sort A sort expression to be inserted into the ORDER
2871
     * BY clause.  If omitted, self::$default_sort will be used.
2872
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
2873
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2874
     * @param string $containerClass The container class to return the results in.
2875
     *
2876
     * @todo $containerClass is Ignored, why?
2877
     *
2878
     * @return DataList The objects matching the filter, in the class specified by $containerClass
2879
     */
2880
    public static function get(
2881
        $callerClass = null,
2882
        $filter = "",
2883
        $sort = "",
2884
        $join = "",
2885
        $limit = null,
2886
        $containerClass = DataList::class
2887
    ) {
2888
2889
        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...
2890
            $callerClass = get_called_class();
2891
            if ($callerClass == self::class) {
2892
                throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
2893
            }
2894
2895
            if ($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
2896
                throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
2897
                    . ' arguments');
2898
            }
2899
2900
            $result = DataList::create(get_called_class());
2901
            $result->setDataModel(DataModel::inst());
2902
            return $result;
2903
        }
2904
2905
        if ($join) {
2906
            throw new \InvalidArgumentException(
2907
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
2908
            );
2909
        }
2910
2911
        $result = DataList::create($callerClass)->where($filter)->sort($sort);
2912
2913
        if ($limit && strpos($limit, ',') !== false) {
2914
            $limitArguments = explode(',', $limit);
2915
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
2916
        } elseif ($limit) {
2917
            $result = $result->limit($limit);
2918
        }
2919
2920
        $result->setDataModel(DataModel::inst());
2921
        return $result;
2922
    }
2923
2924
2925
    /**
2926
     * Return the first item matching the given query.
2927
     * All calls to get_one() are cached.
2928
     *
2929
     * @param string $callerClass The class of objects to be returned
2930
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2931
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2932
     * @param boolean $cache Use caching
2933
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
2934
     *
2935
     * @return DataObject The first item matching the query
2936
     */
2937
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
2938
    {
2939
        $SNG = singleton($callerClass);
2940
2941
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
2942
        $cacheKey = md5(var_export($cacheComponents, true));
2943
2944
        // Flush destroyed items out of the cache
2945
        if ($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
2946
                && self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
2947
                && self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
2948
            self::$_cache_get_one[$callerClass][$cacheKey] = false;
2949
        }
2950
        $item = null;
2951
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
2952
            $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...
2953
            $item = $dl->first();
2954
2955
            if ($cache) {
2956
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
2957
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
2958
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
2959
                }
2960
            }
2961
        }
2962
        return $cache ? self::$_cache_get_one[$callerClass][$cacheKey] : $item;
2963
    }
2964
2965
    /**
2966
     * Flush the cached results for all relations (has_one, has_many, many_many)
2967
     * Also clears any cached aggregate data.
2968
     *
2969
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
2970
     *                            When false will just clear session-local cached data
2971
     * @return DataObject $this
2972
     */
2973
    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...
2974
    {
2975
        if ($this->class == self::class) {
2976
            self::$_cache_get_one = array();
2977
            return $this;
2978
        }
2979
2980
        $classes = ClassInfo::ancestry($this->class);
2981
        foreach ($classes as $class) {
2982
            if (isset(self::$_cache_get_one[$class])) {
2983
                unset(self::$_cache_get_one[$class]);
2984
            }
2985
        }
2986
2987
        $this->extend('flushCache');
2988
2989
        $this->components = array();
2990
        return $this;
2991
    }
2992
2993
    /**
2994
     * Flush the get_one global cache and destroy associated objects.
2995
     */
2996
    public static function flush_and_destroy_cache()
2997
    {
2998
        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...
2999
            foreach (self::$_cache_get_one as $class => $items) {
3000
                if (is_array($items)) {
3001
                    foreach ($items as $item) {
3002
                        if ($item) {
3003
                            $item->destroy();
3004
                        }
3005
                    }
3006
                }
3007
            }
3008
        }
3009
        self::$_cache_get_one = array();
3010
    }
3011
3012
    /**
3013
     * Reset all global caches associated with DataObject.
3014
     */
3015
    public static function reset()
3016
    {
3017
        // @todo Decouple these
3018
        DBClassName::clear_classname_cache();
3019
        ClassInfo::reset_db_cache();
3020
        static::getSchema()->reset();
3021
        self::$_cache_get_one = array();
3022
        self::$_cache_field_labels = array();
3023
    }
3024
3025
    /**
3026
     * Return the given element, searching by ID
3027
     *
3028
     * @param string $callerClass The class of the object to be returned
3029
     * @param int $id The id of the element
3030
     * @param boolean $cache See {@link get_one()}
3031
     *
3032
     * @return DataObject The element
3033
     */
3034
    public static function get_by_id($callerClass, $id, $cache = true)
3035
    {
3036
        if (!is_numeric($id)) {
3037
            user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
3038
        }
3039
3040
        // Pass to get_one
3041
        $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
3042
        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...
3043
    }
3044
3045
    /**
3046
     * Get the name of the base table for this object
3047
     *
3048
     * @return string
3049
     */
3050
    public function baseTable()
3051
    {
3052
        return static::getSchema()->baseDataTable($this);
3053
    }
3054
3055
    /**
3056
     * Get the base class for this object
3057
     *
3058
     * @return string
3059
     */
3060
    public function baseClass()
3061
    {
3062
        return static::getSchema()->baseDataClass($this);
3063
    }
3064
3065
    /**
3066
     * @var array Parameters used in the query that built this object.
3067
     * This can be used by decorators (e.g. lazy loading) to
3068
     * run additional queries using the same context.
3069
     */
3070
    protected $sourceQueryParams;
3071
3072
    /**
3073
     * @see $sourceQueryParams
3074
     * @return array
3075
     */
3076
    public function getSourceQueryParams()
3077
    {
3078
        return $this->sourceQueryParams;
3079
    }
3080
3081
    /**
3082
     * Get list of parameters that should be inherited to relations on this object
3083
     *
3084
     * @return array
3085
     */
3086
    public function getInheritableQueryParams()
3087
    {
3088
        $params = $this->getSourceQueryParams();
3089
        $this->extend('updateInheritableQueryParams', $params);
3090
        return $params;
3091
    }
3092
3093
    /**
3094
     * @see $sourceQueryParams
3095
     * @param array
3096
     */
3097
    public function setSourceQueryParams($array)
3098
    {
3099
        $this->sourceQueryParams = $array;
3100
    }
3101
3102
    /**
3103
     * @see $sourceQueryParams
3104
     * @param string $key
3105
     * @param string $value
3106
     */
3107
    public function setSourceQueryParam($key, $value)
3108
    {
3109
        $this->sourceQueryParams[$key] = $value;
3110
    }
3111
3112
    /**
3113
     * @see $sourceQueryParams
3114
     * @param string $key
3115
     * @return string
3116
     */
3117
    public function getSourceQueryParam($key)
3118
    {
3119
        if (isset($this->sourceQueryParams[$key])) {
3120
            return $this->sourceQueryParams[$key];
3121
        }
3122
        return null;
3123
    }
3124
3125
    //-------------------------------------------------------------------------------------------//
3126
3127
    /**
3128
     * Return the database indexes on this table.
3129
     * This array is indexed by the name of the field with the index, and
3130
     * the value is the type of index.
3131
     */
3132
    public function databaseIndexes()
3133
    {
3134
        $has_one = $this->uninherited('has_one');
3135
        $classIndexes = $this->uninherited('indexes');
3136
        //$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...
3137
3138
        $indexes = array();
3139
3140
        if ($has_one) {
3141
            foreach ($has_one as $relationshipName => $fieldType) {
3142
                $indexes[$relationshipName . 'ID'] = true;
3143
            }
3144
        }
3145
3146
        if ($classIndexes) {
3147
            foreach ($classIndexes as $indexName => $indexType) {
3148
                $indexes[$indexName] = $indexType;
3149
            }
3150
        }
3151
3152
        if (get_parent_class($this) == self::class) {
3153
            $indexes['ClassName'] = true;
3154
        }
3155
3156
        return $indexes;
3157
    }
3158
3159
    /**
3160
     * Check the database schema and update it as necessary.
3161
     *
3162
     * @uses DataExtension->augmentDatabase()
3163
     */
3164
    public function requireTable()
3165
    {
3166
        // Only build the table if we've actually got fields
3167
        $schema = static::getSchema();
3168
        $fields = $schema->databaseFields(static::class, false);
3169
        $table = $schema->tableName(static::class);
3170
        $extensions = self::database_extensions(static::class);
3171
3172
        $indexes = $this->databaseIndexes();
3173
3174
        if (empty($table)) {
3175
            throw new LogicException(
3176
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3177
            );
3178
        }
3179
3180
        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...
3181
            $hasAutoIncPK = get_parent_class($this) === self::class;
3182
            DB::require_table(
3183
                $table,
3184
                $fields,
3185
                $indexes,
3186
                $hasAutoIncPK,
3187
                $this->stat('create_table_options'),
3188
                $extensions
0 ignored issues
show
Security Bug introduced by
It seems like $extensions defined by self::database_extensions(static::class) on line 3170 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...
3189
            );
3190
        } else {
3191
            DB::dont_require_table($table);
3192
        }
3193
3194
        // Build any child tables for many_many items
3195
        if ($manyMany = $this->uninherited('many_many')) {
3196
            $extras = $this->uninherited('many_many_extraFields');
3197
            foreach ($manyMany as $component => $spec) {
3198
                // Get many_many spec
3199
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3200
                list(
3201
                    $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...
3202
                    $parentField, $childField, $tableOrClass
3203
                ) = $manyManyComponent;
3204
3205
                // Skip if backed by actual class
3206
                if (class_exists($tableOrClass)) {
3207
                    continue;
3208
                }
3209
3210
                // Build fields
3211
                $manymanyFields = array(
3212
                    $parentField => "Int",
3213
                    $childField => "Int",
3214
                );
3215
                if (isset($extras[$component])) {
3216
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3217
                }
3218
3219
                // Build index list
3220
                $manymanyIndexes = array(
3221
                    $parentField => true,
3222
                    $childField => true,
3223
                );
3224
                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 3170 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...
3225
            }
3226
        }
3227
3228
        // Let any extentions make their own database fields
3229
        $this->extend('augmentDatabase', $dummy);
3230
    }
3231
3232
    /**
3233
     * Add default records to database. This function is called whenever the
3234
     * database is built, after the database tables have all been created. Overload
3235
     * this to add default records when the database is built, but make sure you
3236
     * call parent::requireDefaultRecords().
3237
     *
3238
     * @uses DataExtension->requireDefaultRecords()
3239
     */
3240
    public function requireDefaultRecords()
3241
    {
3242
        $defaultRecords = $this->config()->get('default_records', Config::UNINHERITED);
3243
3244
        if (!empty($defaultRecords)) {
3245
            $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...
3246
            if (!$hasData) {
3247
                $className = $this->class;
3248
                foreach ($defaultRecords as $record) {
3249
                    $obj = $this->model->$className->newObject($record);
3250
                    $obj->write();
3251
                }
3252
                DB::alteration_message("Added default records to $className table", "created");
3253
            }
3254
        }
3255
3256
        // Let any extentions make their own database default data
3257
        $this->extend('requireDefaultRecords', $dummy);
3258
    }
3259
3260
    /**
3261
     * Get the default searchable fields for this object, as defined in the
3262
     * $searchable_fields list. If searchable fields are not defined on the
3263
     * data object, uses a default selection of summary fields.
3264
     *
3265
     * @return array
3266
     */
3267
    public function searchableFields()
3268
    {
3269
        // can have mixed format, need to make consistent in most verbose form
3270
        $fields = $this->stat('searchable_fields');
3271
        $labels = $this->fieldLabels();
3272
3273
        // fallback to summary fields (unless empty array is explicitly specified)
3274
        if (! $fields && ! is_array($fields)) {
3275
            $summaryFields = array_keys($this->summaryFields());
3276
            $fields = array();
3277
3278
            // remove the custom getters as the search should not include them
3279
            $schema = static::getSchema();
3280
            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...
3281
                foreach ($summaryFields as $key => $name) {
3282
                    $spec = $name;
3283
3284
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3285
                    if (($fieldPos = strpos($name, '.')) !== false) {
3286
                        $name = substr($name, 0, $fieldPos);
3287
                    }
3288
3289
                    if ($schema->fieldSpec($this, $name)) {
3290
                        $fields[] = $name;
3291
                    } elseif ($this->relObject($spec)) {
3292
                        $fields[] = $spec;
3293
                    }
3294
                }
3295
            }
3296
        }
3297
3298
        // we need to make sure the format is unified before
3299
        // augmenting fields, so extensions can apply consistent checks
3300
        // but also after augmenting fields, because the extension
3301
        // might use the shorthand notation as well
3302
3303
        // rewrite array, if it is using shorthand syntax
3304
        $rewrite = array();
3305
        foreach ($fields as $name => $specOrName) {
3306
            $identifer = (is_int($name)) ? $specOrName : $name;
3307
3308
            if (is_int($name)) {
3309
                // 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...
3310
                $rewrite[$identifer] = array();
3311
            } elseif (is_array($specOrName)) {
3312
                // 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...
3313
                //   'filter => 'ExactMatchFilter',
3314
                //   '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...
3315
                //   '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...
3316
                // ))
3317
                $rewrite[$identifer] = array_merge(
3318
                    array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3319
                    (array)$specOrName
3320
                );
3321
            } else {
3322
                // 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...
3323
                $rewrite[$identifer] = array(
3324
                    'filter' => $specOrName,
3325
                );
3326
            }
3327
            if (!isset($rewrite[$identifer]['title'])) {
3328
                $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3329
                    ? $labels[$identifer] : FormField::name_to_label($identifer);
3330
            }
3331
            if (!isset($rewrite[$identifer]['filter'])) {
3332
                /** @skipUpgrade */
3333
                $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3334
            }
3335
        }
3336
3337
        $fields = $rewrite;
3338
3339
        // apply DataExtensions if present
3340
        $this->extend('updateSearchableFields', $fields);
3341
3342
        return $fields;
3343
    }
3344
3345
    /**
3346
     * Get any user defined searchable fields labels that
3347
     * exist. Allows overriding of default field names in the form
3348
     * interface actually presented to the user.
3349
     *
3350
     * The reason for keeping this separate from searchable_fields,
3351
     * which would be a logical place for this functionality, is to
3352
     * avoid bloating and complicating the configuration array. Currently
3353
     * much of this system is based on sensible defaults, and this property
3354
     * would generally only be set in the case of more complex relationships
3355
     * between data object being required in the search interface.
3356
     *
3357
     * Generates labels based on name of the field itself, if no static property
3358
     * {@link self::field_labels} exists.
3359
     *
3360
     * @uses $field_labels
3361
     * @uses FormField::name_to_label()
3362
     *
3363
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3364
     *
3365
     * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3366
     */
3367
    public function fieldLabels($includerelations = true)
3368
    {
3369
        $cacheKey = $this->class . '_' . $includerelations;
3370
3371
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
3372
            $customLabels = $this->stat('field_labels');
3373
            $autoLabels = array();
3374
3375
            // get all translated static properties as defined in i18nCollectStatics()
3376
            $ancestry = ClassInfo::ancestry($this->class);
3377
            $ancestry = array_reverse($ancestry);
3378
            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...
3379
                foreach ($ancestry as $ancestorClass) {
3380
                    if ($ancestorClass === ViewableData::class) {
3381
                        break;
3382
                    }
3383
                    $types = array(
3384
                    'db'        => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3385
                                    );
3386
                    if ($includerelations) {
3387
                        $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3388
                        $types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3389
                        $types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3390
                        $types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3391
                    }
3392
                    foreach ($types as $type => $attrs) {
3393
                        foreach ($attrs as $name => $spec) {
3394
                            $autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}", FormField::name_to_label($name));
3395
                        }
3396
                    }
3397
                }
3398
            }
3399
3400
            $labels = array_merge((array)$autoLabels, (array)$customLabels);
3401
            $this->extend('updateFieldLabels', $labels);
3402
            self::$_cache_field_labels[$cacheKey] = $labels;
3403
        }
3404
3405
        return self::$_cache_field_labels[$cacheKey];
3406
    }
3407
3408
    /**
3409
     * Get a human-readable label for a single field,
3410
     * see {@link fieldLabels()} for more details.
3411
     *
3412
     * @uses fieldLabels()
3413
     * @uses FormField::name_to_label()
3414
     *
3415
     * @param string $name Name of the field
3416
     * @return string Label of the field
3417
     */
3418
    public function fieldLabel($name)
3419
    {
3420
        $labels = $this->fieldLabels();
3421
        return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3422
    }
3423
3424
    /**
3425
     * Get the default summary fields for this object.
3426
     *
3427
     * @todo use the translation apparatus to return a default field selection for the language
3428
     *
3429
     * @return array
3430
     */
3431
    public function summaryFields()
3432
    {
3433
        $fields = $this->stat('summary_fields');
3434
3435
        // if fields were passed in numeric array,
3436
        // convert to an associative array
3437
        if ($fields && array_key_exists(0, $fields)) {
3438
            $fields = array_combine(array_values($fields), array_values($fields));
3439
        }
3440
3441
        if (!$fields) {
3442
            $fields = array();
3443
            // try to scaffold a couple of usual suspects
3444
            if ($this->hasField('Name')) {
3445
                $fields['Name'] = 'Name';
3446
            }
3447
            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...
3448
                $fields['Title'] = 'Title';
3449
            }
3450
            if ($this->hasField('Description')) {
3451
                $fields['Description'] = 'Description';
3452
            }
3453
            if ($this->hasField('FirstName')) {
3454
                $fields['FirstName'] = 'First Name';
3455
            }
3456
        }
3457
        $this->extend("updateSummaryFields", $fields);
3458
3459
        // Final fail-over, just list ID field
3460
        if (!$fields) {
3461
            $fields['ID'] = 'ID';
3462
        }
3463
3464
        // Localize fields (if possible)
3465
        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...
3466
            // only attempt to localize if the label definition is the same as the field name.
3467
            // this will preserve any custom labels set in the summary_fields configuration
3468
            if (isset($fields[$name]) && $name === $fields[$name]) {
3469
                $fields[$name] = $label;
3470
            }
3471
        }
3472
3473
        return $fields;
3474
    }
3475
3476
    /**
3477
     * Defines a default list of filters for the search context.
3478
     *
3479
     * If a filter class mapping is defined on the data object,
3480
     * it is constructed here. Otherwise, the default filter specified in
3481
     * {@link DBField} is used.
3482
     *
3483
     * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3484
     *
3485
     * @return array
3486
     */
3487
    public function defaultSearchFilters()
3488
    {
3489
        $filters = array();
3490
3491
        foreach ($this->searchableFields() as $name => $spec) {
3492
            if (empty($spec['filter'])) {
3493
                /** @skipUpgrade */
3494
                $filters[$name] = 'PartialMatchFilter';
3495
            } elseif ($spec['filter'] instanceof SearchFilter) {
3496
                $filters[$name] = $spec['filter'];
3497
            } else {
3498
                $filters[$name] = Injector::inst()->create($spec['filter'], $name);
3499
            }
3500
        }
3501
3502
        return $filters;
3503
    }
3504
3505
    /**
3506
     * @return boolean True if the object is in the database
3507
     */
3508
    public function isInDB()
3509
    {
3510
        return is_numeric($this->ID) && $this->ID > 0;
3511
    }
3512
3513
    /*
3514
	 * @ignore
3515
	 */
3516
    private static $subclass_access = true;
3517
3518
    /**
3519
     * Temporarily disable subclass access in data object qeur
3520
     */
3521
    public static function disable_subclass_access()
3522
    {
3523
        self::$subclass_access = false;
3524
    }
3525
    public static function enable_subclass_access()
3526
    {
3527
        self::$subclass_access = true;
3528
    }
3529
3530
    //-------------------------------------------------------------------------------------------//
3531
3532
    /**
3533
     * Database field definitions.
3534
     * This is a map from field names to field type. The field
3535
     * type should be a class that extends .
3536
     * @var array
3537
     * @config
3538
     */
3539
    private static $db = null;
3540
3541
    /**
3542
     * Use a casting object for a field. This is a map from
3543
     * field name to class name of the casting object.
3544
     *
3545
     * @var array
3546
     */
3547
    private static $casting = array(
3548
        "Title" => 'Text',
3549
    );
3550
3551
    /**
3552
     * Specify custom options for a CREATE TABLE call.
3553
     * Can be used to specify a custom storage engine for specific database table.
3554
     * All options have to be keyed for a specific database implementation,
3555
     * identified by their class name (extending from {@link SS_Database}).
3556
     *
3557
     * <code>
3558
     * array(
3559
     *  'MySQLDatabase' => 'ENGINE=MyISAM'
3560
     * )
3561
     * </code>
3562
     *
3563
     * Caution: This API is experimental, and might not be
3564
     * included in the next major release. Please use with care.
3565
     *
3566
     * @var array
3567
     * @config
3568
     */
3569
    private static $create_table_options = array(
3570
        'SilverStripe\ORM\Connect\MySQLDatabase' => 'ENGINE=InnoDB'
3571
    );
3572
3573
    /**
3574
     * If a field is in this array, then create a database index
3575
     * on that field. This is a map from fieldname to index type.
3576
     * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3577
     *
3578
     * @var array
3579
     * @config
3580
     */
3581
    private static $indexes = null;
3582
3583
    /**
3584
     * Inserts standard column-values when a DataObject
3585
     * is instanciated. Does not insert default records {@see $default_records}.
3586
     * This is a map from fieldname to default value.
3587
     *
3588
     *  - If you would like to change a default value in a sub-class, just specify it.
3589
     *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3590
     *    or false in your subclass.  Setting it to null won't work.
3591
     *
3592
     * @var array
3593
     * @config
3594
     */
3595
    private static $defaults = null;
3596
3597
    /**
3598
     * Multidimensional array which inserts default data into the database
3599
     * on a db/build-call as long as the database-table is empty. Please use this only
3600
     * for simple constructs, not for SiteTree-Objects etc. which need special
3601
     * behaviour such as publishing and ParentNodes.
3602
     *
3603
     * Example:
3604
     * array(
3605
     *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3606
     *  array('Title' => "DefaultPage2")
3607
     * ).
3608
     *
3609
     * @var array
3610
     * @config
3611
     */
3612
    private static $default_records = null;
3613
3614
    /**
3615
     * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3616
     * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3617
     *
3618
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3619
     *
3620
     *  @var array
3621
     * @config
3622
     */
3623
    private static $has_one = null;
3624
3625
    /**
3626
     * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3627
     *
3628
     * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3629
     * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3630
     * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3631
     *
3632
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3633
     *
3634
     * @var array
3635
     * @config
3636
     */
3637
    private static $belongs_to;
3638
3639
    /**
3640
     * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3641
     *
3642
     * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3643
     * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3644
     * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3645
     * which foreign key to use.
3646
     *
3647
     * @var array
3648
     * @config
3649
     */
3650
    private static $has_many = null;
3651
3652
    /**
3653
     * many-many relationship definitions.
3654
     * This is a map from component name to data type.
3655
     * @var array
3656
     * @config
3657
     */
3658
    private static $many_many = null;
3659
3660
    /**
3661
     * Extra fields to include on the connecting many-many table.
3662
     * This is a map from field name to field type.
3663
     *
3664
     * Example code:
3665
     * <code>
3666
     * public static $many_many_extraFields = array(
3667
     *  'Members' => array(
3668
     *          'Role' => 'Varchar(100)'
3669
     *      )
3670
     * );
3671
     * </code>
3672
     *
3673
     * @var array
3674
     * @config
3675
     */
3676
    private static $many_many_extraFields = null;
3677
3678
    /**
3679
     * The inverse side of a many-many relationship.
3680
     * This is a map from component name to data type.
3681
     * @var array
3682
     * @config
3683
     */
3684
    private static $belongs_many_many = null;
3685
3686
    /**
3687
     * The default sort expression. This will be inserted in the ORDER BY
3688
     * clause of a SQL query if no other sort expression is provided.
3689
     * @var string
3690
     * @config
3691
     */
3692
    private static $default_sort = null;
3693
3694
    /**
3695
     * Default list of fields that can be scaffolded by the ModelAdmin
3696
     * search interface.
3697
     *
3698
     * Overriding the default filter, with a custom defined filter:
3699
     * <code>
3700
     *  static $searchable_fields = array(
3701
     *     "Name" => "PartialMatchFilter"
3702
     *  );
3703
     * </code>
3704
     *
3705
     * Overriding the default form fields, with a custom defined field.
3706
     * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3707
     * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3708
     * <code>
3709
     *  static $searchable_fields = array(
3710
     *    "Name" => array(
3711
     *      "field" => "TextField"
3712
     *    )
3713
     *  );
3714
     * </code>
3715
     *
3716
     * Overriding the default form field, filter and title:
3717
     * <code>
3718
     *  static $searchable_fields = array(
3719
     *    "Organisation.ZipCode" => array(
3720
     *      "field" => "TextField",
3721
     *      "filter" => "PartialMatchFilter",
3722
     *      "title" => 'Organisation ZIP'
3723
     *    )
3724
     *  );
3725
     * </code>
3726
     * @config
3727
     */
3728
    private static $searchable_fields = null;
3729
3730
    /**
3731
     * User defined labels for searchable_fields, used to override
3732
     * default display in the search form.
3733
     * @config
3734
     */
3735
    private static $field_labels = null;
3736
3737
    /**
3738
     * Provides a default list of fields to be used by a 'summary'
3739
     * view of this object.
3740
     * @config
3741
     */
3742
    private static $summary_fields = null;
3743
3744
    /**
3745
     * Collect all static properties on the object
3746
     * which contain natural language, and need to be translated.
3747
     * The full entity name is composed from the class name and a custom identifier.
3748
     *
3749
     * @return array A numerical array which contains one or more entities in array-form.
3750
     * Each numeric entity array contains the "arguments" for a _t() call as array values:
3751
     * $entity, $string, $priority, $context.
3752
     */
3753
    public function provideI18nEntities()
3754
    {
3755
        $entities = array();
3756
3757
        $entities["{$this->class}.SINGULARNAME"] = array(
3758
            $this->singular_name(),
3759
3760
            'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
3761
        );
3762
3763
        $entities["{$this->class}.PLURALNAME"] = array(
3764
            $this->plural_name(),
3765
3766
            'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the'
3767
            . ' interface'
3768
        );
3769
3770
        return $entities;
3771
    }
3772
3773
    /**
3774
     * Returns true if the given method/parameter has a value
3775
     * (Uses the DBField::hasValue if the parameter is a database field)
3776
     *
3777
     * @param string $field The field name
3778
     * @param array $arguments
3779
     * @param bool $cache
3780
     * @return boolean
3781
     */
3782
    public function hasValue($field, $arguments = null, $cache = true)
3783
    {
3784
        // has_one fields should not use dbObject to check if a value is given
3785
        $hasOne = static::getSchema()->hasOneComponent(static::class, $field);
3786
        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...
3787
            return $obj->exists();
3788
        } else {
3789
            return parent::hasValue($field, $arguments, $cache);
0 ignored issues
show
Bug introduced by
It seems like $arguments defined by parameter $arguments on line 3782 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...
3790
        }
3791
    }
3792
3793
    /**
3794
     * If selected through a many_many through relation, this is the instance of the joined record
3795
     *
3796
     * @return DataObject
3797
     */
3798
    public function getJoin()
3799
    {
3800
        return $this->joinRecord;
3801
    }
3802
3803
    /**
3804
     * Set joining object
3805
     *
3806
     * @param DataObject $object
3807
     * @param string $alias Alias
3808
     * @return $this
3809
     */
3810
    public function setJoin(DataObject $object, $alias = null)
3811
    {
3812
        $this->joinRecord = $object;
3813
        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...
3814
            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...
3815
                throw new InvalidArgumentException(
3816
                    "Joined record $alias cannot also be a db field"
3817
                );
3818
            }
3819
            $this->record[$alias] = $object;
3820
        }
3821
        return $this;
3822
    }
3823
}
3824