Completed
Push — authenticator-refactor ( 0a18bb...b9e528 )
by Simon
08:12
created

DataObject::defineMethods()   D

Complexity

Conditions 10
Paths 25

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 16
nc 25
nop 0
dl 0
loc 30
rs 4.8196
c 0
b 0
f 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
418
        $clone->ID = 0;
419
420
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $manyMany);
421
        if ($manyMany) {
422
            $this->duplicateManyManyRelations($this, $clone, $manyMany);
423
        }
424
        if ($doWrite) {
425
            $clone->write();
426
        }
427
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $manyMany);
428
429
        return $clone;
430
    }
431
432
    /**
433
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
434
     *
435
     * @param DataObject $sourceObject the source object to duplicate from
436
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
437
     * @param bool|string $filter
438
     */
439
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
440
    {
441
        // Get list of relations to duplicate
442
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
443
            $relations = $sourceObject->config()->get($filter);
444
        } elseif ($filter === true) {
445
            $relations = $sourceObject->manyMany();
446
        } else {
447
            throw new InvalidArgumentException("Invalid many_many duplication filter");
448
        }
449
        foreach ($relations as $manyManyName => $type) {
450
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
451
        }
452
    }
453
454
    /**
455
     * Duplicates a single many_many relation from one object to another
456
     *
457
     * @param DataObject $sourceObject
458
     * @param DataObject $destinationObject
459
     * @param string $manyManyName
460
     */
461
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName)
462
    {
463
        // Ensure this component exists on the destination side as well
464
        if (!static::getSchema()->manyManyComponent(get_class($destinationObject), $manyManyName)) {
465
            return;
466
        }
467
468
        // Copy all components from source to destination
469
        $source = $sourceObject->getManyManyComponents($manyManyName);
470
        $dest = $destinationObject->getManyManyComponents($manyManyName);
471
        foreach ($source as $item) {
472
            $dest->add($item);
473
        }
474
    }
475
476
    /**
477
     * Return obsolete class name, if this is no longer a valid class
478
     *
479
     * @return string
480
     */
481
    public function getObsoleteClassName()
482
    {
483
        $className = $this->getField("ClassName");
484
        if (!ClassInfo::exists($className)) {
485
            return $className;
486
        }
487
        return null;
488
    }
489
490
    /**
491
     * Gets name of this class
492
     *
493
     * @return string
494
     */
495
    public function getClassName()
496
    {
497
        $className = $this->getField("ClassName");
498
        if (!ClassInfo::exists($className)) {
499
            return static::class;
500
        }
501
        return $className;
502
    }
503
504
    /**
505
     * Set the ClassName attribute. {@link $class} is also updated.
506
     * Warning: This will produce an inconsistent record, as the object
507
     * instance will not automatically switch to the new subclass.
508
     * Please use {@link newClassInstance()} for this purpose,
509
     * or destroy and reinstanciate the record.
510
     *
511
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
512
     * @return $this
513
     */
514
    public function setClassName($className)
515
    {
516
        $className = trim($className);
517
        if (!$className || !is_subclass_of($className, self::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if self::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
518
            return $this;
519
        }
520
521
        $this->setField("ClassName", $className);
522
        $this->setField('RecordClassName', $className);
523
        return $this;
524
    }
525
526
    /**
527
     * Create a new instance of a different class from this object's record.
528
     * This is useful when dynamically changing the type of an instance. Specifically,
529
     * it ensures that the instance of the class is a match for the className of the
530
     * record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
531
     * property manually before calling this method, as it will confuse change detection.
532
     *
533
     * If the new class is different to the original class, defaults are populated again
534
     * because this will only occur automatically on instantiation of a DataObject if
535
     * there is no record, or the record has no ID. In this case, we do have an ID but
536
     * we still need to repopulate the defaults.
537
     *
538
     * @param string $newClassName The name of the new class
539
     *
540
     * @return DataObject The new instance of the new class, The exact type will be of the class name provided.
541
     */
542
    public function newClassInstance($newClassName)
543
    {
544
        if (!is_subclass_of($newClassName, self::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if self::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
545
            throw new InvalidArgumentException("$newClassName is not a valid subclass of DataObject");
546
        }
547
548
        $originalClass = $this->ClassName;
549
550
        /** @var DataObject $newInstance */
551
        $newInstance = Injector::inst()->create($newClassName, $this->record, false, $this->model);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method create() does only exist in the following implementations of said interface: SilverStripe\Core\Injector\Injector.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
552
553
        // Modify ClassName
554
        if ($newClassName != $originalClass) {
555
            $newInstance->setClassName($newClassName);
556
            $newInstance->populateDefaults();
557
            $newInstance->forceChange();
558
        }
559
560
        return $newInstance;
561
    }
562
563
    /**
564
     * Adds methods from the extensions.
565
     * Called by Object::__construct() once per class.
566
     */
567
    public function defineMethods()
568
    {
569
        parent::defineMethods();
570
571
        if (static::class === self::class) {
572
             return;
573
        }
574
575
        // Set up accessors for joined items
576
        if ($manyMany = $this->manyMany()) {
577
            foreach ($manyMany as $relationship => $class) {
578
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
579
            }
580
        }
581
        if ($hasMany = $this->hasMany()) {
582
            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...
583
                $this->addWrapperMethod($relationship, 'getComponents');
584
            }
585
        }
586
        if ($hasOne = $this->hasOne()) {
587
            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...
588
                $this->addWrapperMethod($relationship, 'getComponent');
589
            }
590
        }
591
        if ($belongsTo = $this->belongsTo()) {
592
            foreach (array_keys($belongsTo) as $relationship) {
593
                $this->addWrapperMethod($relationship, 'getComponent');
594
            }
595
        }
596
    }
597
598
    /**
599
     * Returns true if this object "exists", i.e., has a sensible value.
600
     * The default behaviour for a DataObject is to return true if
601
     * the object exists in the database, you can override this in subclasses.
602
     *
603
     * @return boolean true if this object exists
604
     */
605
    public function exists()
606
    {
607
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
608
    }
609
610
    /**
611
     * Returns TRUE if all values (other than "ID") are
612
     * considered empty (by weak boolean comparison).
613
     *
614
     * @return boolean
615
     */
616
    public function isEmpty()
617
    {
618
        $fixed = DataObject::config()->uninherited('fixed_fields');
619
        foreach ($this->toMap() as $field => $value) {
620
            // only look at custom fields
621
            if (isset($fixed[$field])) {
622
                continue;
623
            }
624
625
            $dbObject = $this->dbObject($field);
626
            if (!$dbObject) {
627
                continue;
628
            }
629
            if ($dbObject->exists()) {
630
                return false;
631
            }
632
        }
633
        return true;
634
    }
635
636
    /**
637
     * Pluralise this item given a specific count.
638
     *
639
     * E.g. "0 Pages", "1 File", "3 Images"
640
     *
641
     * @param string $count
642
     * @return string
643
     */
644
    public function i18n_pluralise($count)
645
    {
646
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
647
        return i18n::_t(
648
            static::class.'.PLURALS',
649
            $default,
650
            [ 'count' => $count ]
651
        );
652
    }
653
654
    /**
655
     * Get the user friendly singular name of this DataObject.
656
     * If the name is not defined (by redefining $singular_name in the subclass),
657
     * this returns the class name.
658
     *
659
     * @return string User friendly singular name of this DataObject
660
     */
661
    public function singular_name()
662
    {
663
        $name = $this->stat('singular_name');
664
        if ($name) {
665
            return $name;
666
        }
667
        return ucwords(trim(strtolower(preg_replace(
668
            '/_?([A-Z])/',
669
            ' $1',
670
            ClassInfo::shortName($this)
671
        ))));
672
    }
673
674
    /**
675
     * Get the translated user friendly singular name of this DataObject
676
     * same as singular_name() but runs it through the translating function
677
     *
678
     * Translating string is in the form:
679
     *     $this->class.SINGULARNAME
680
     * Example:
681
     *     Page.SINGULARNAME
682
     *
683
     * @return string User friendly translated singular name of this DataObject
684
     */
685
    public function i18n_singular_name()
686
    {
687
        return _t(static::class.'.SINGULARNAME', $this->singular_name());
688
    }
689
690
    /**
691
     * Get the user friendly plural name of this DataObject
692
     * If the name is not defined (by renaming $plural_name in the subclass),
693
     * this returns a pluralised version of the class name.
694
     *
695
     * @return string User friendly plural name of this DataObject
696
     */
697
    public function plural_name()
698
    {
699
        if ($name = $this->stat('plural_name')) {
700
            return $name;
701
        }
702
        $name = $this->singular_name();
703
        //if the penultimate character is not a vowel, replace "y" with "ies"
704
        if (preg_match('/[^aeiou]y$/i', $name)) {
705
            $name = substr($name, 0, -1) . 'ie';
706
        }
707
        return ucfirst($name . 's');
708
    }
709
710
    /**
711
     * Get the translated user friendly plural name of this DataObject
712
     * Same as plural_name but runs it through the translation function
713
     * Translation string is in the form:
714
     *      $this->class.PLURALNAME
715
     * Example:
716
     *      Page.PLURALNAME
717
     *
718
     * @return string User friendly translated plural name of this DataObject
719
     */
720
    public function i18n_plural_name()
721
    {
722
        return _t(static::class.'.PLURALNAME', $this->plural_name());
723
    }
724
725
    /**
726
     * Standard implementation of a title/label for a specific
727
     * record. Tries to find properties 'Title' or 'Name',
728
     * and falls back to the 'ID'. Useful to provide
729
     * user-friendly identification of a record, e.g. in errormessages
730
     * or UI-selections.
731
     *
732
     * Overload this method to have a more specialized implementation,
733
     * e.g. for an Address record this could be:
734
     * <code>
735
     * function getTitle() {
736
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
737
     * }
738
     * </code>
739
     *
740
     * @return string
741
     */
742
    public function getTitle()
743
    {
744
        $schema = static::getSchema();
745
        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...
746
            return $this->getField('Title');
747
        }
748
        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...
749
            return $this->getField('Name');
750
        }
751
752
        return "#{$this->ID}";
753
    }
754
755
    /**
756
     * Returns the associated database record - in this case, the object itself.
757
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
758
     *
759
     * @return DataObject Associated database record
760
     */
761
    public function data()
762
    {
763
        return $this;
764
    }
765
766
    /**
767
     * Convert this object to a map.
768
     *
769
     * @return array The data as a map.
770
     */
771
    public function toMap()
772
    {
773
        $this->loadLazyFields();
774
        return $this->record;
775
    }
776
777
    /**
778
     * Return all currently fetched database fields.
779
     *
780
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
781
     * Obviously, this makes it a lot faster.
782
     *
783
     * @return array The data as a map.
784
     */
785
    public function getQueriedDatabaseFields()
786
    {
787
        return $this->record;
788
    }
789
790
    /**
791
     * Update a number of fields on this object, given a map of the desired changes.
792
     *
793
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
794
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
795
     *
796
     * update() doesn't write the main object, but if you use the dot syntax, it will write()
797
     * the related objects that it alters.
798
     *
799
     * @param array $data A map of field name to data values to update.
800
     * @return DataObject $this
801
     */
802
    public function update($data)
803
    {
804
        foreach ($data as $key => $value) {
805
            // Implement dot syntax for updates
806
            if (strpos($key, '.') !== false) {
807
                $relations = explode('.', $key);
808
                $fieldName = array_pop($relations);
809
                /** @var static $relObj */
810
                $relObj = $this;
811
                $relation = null;
812
                foreach ($relations as $i => $relation) {
813
                    // no support for has_many or many_many relationships,
814
                    // as the updater wouldn't know which object to write to (or create)
815
                    if ($relObj->$relation() instanceof DataObject) {
816
                        $parentObj = $relObj;
817
                        $relObj = $relObj->$relation();
818
                        // If the intermediate relationship objects have been created, then write them
819
                        if ($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj !== $this)) {
820
                            $relObj->write();
821
                            $relatedFieldName = $relation."ID";
822
                            $parentObj->$relatedFieldName = $relObj->ID;
823
                            $parentObj->write();
824
                        }
825
                    } else {
826
                        user_error(
827
                            "DataObject::update(): Can't traverse relationship '$relation'," .
828
                            "it has to be a has_one relationship or return a single DataObject",
829
                            E_USER_NOTICE
830
                        );
831
                        // unset relation object so we don't write properties to the wrong object
832
                        $relObj = null;
833
                        break;
834
                    }
835
                }
836
837
                if ($relObj) {
838
                    $relObj->$fieldName = $value;
839
                    $relObj->write();
840
                    $relatedFieldName = $relation."ID";
841
                    $this->$relatedFieldName = $relObj->ID;
842
                    $relObj->flushCache();
843
                } else {
844
                    $class = static::class;
845
                    user_error("Couldn't follow dot syntax '{$key}' on '{$class}' object", E_USER_WARNING);
846
                }
847
            } else {
848
                $this->$key = $value;
849
            }
850
        }
851
        return $this;
852
    }
853
854
    /**
855
     * Pass changes as a map, and try to
856
     * get automatic casting for these fields.
857
     * Doesn't write to the database. To write the data,
858
     * use the write() method.
859
     *
860
     * @param array $data A map of field name to data values to update.
861
     * @return DataObject $this
862
     */
863
    public function castedUpdate($data)
864
    {
865
        foreach ($data as $k => $v) {
866
            $this->setCastedField($k, $v);
867
        }
868
        return $this;
869
    }
870
871
    /**
872
     * Merges data and relations from another object of same class,
873
     * without conflict resolution. Allows to specify which
874
     * dataset takes priority in case its not empty.
875
     * has_one-relations are just transferred with priority 'right'.
876
     * has_many and many_many-relations are added regardless of priority.
877
     *
878
     * Caution: has_many/many_many relations are moved rather than duplicated,
879
     * meaning they are not connected to the merged object any longer.
880
     * Caution: Just saves updated has_many/many_many relations to the database,
881
     * doesn't write the updated object itself (just writes the object-properties).
882
     * Caution: Does not delete the merged object.
883
     * Caution: Does now overwrite Created date on the original object.
884
     *
885
     * @param DataObject $rightObj
886
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
887
     * @param bool $includeRelations Merge any existing relations (optional)
888
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
889
     *                            Only applicable with $priority='right'. (optional)
890
     * @return Boolean
891
     */
892
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
893
    {
894
        $leftObj = $this;
895
896
        if ($leftObj->ClassName != $rightObj->ClassName) {
897
            // we can't merge similiar subclasses because they might have additional relations
898
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
899
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
900
            return false;
901
        }
902
903
        if (!$rightObj->ID) {
904
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
905
				to make sure all relations are transferred properly.').", E_USER_WARNING);
906
            return false;
907
        }
908
909
        // makes sure we don't merge data like ID or ClassName
910
        $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
911
        foreach ($rightData as $key => $rightSpec) {
912
            // Don't merge ID
913
            if ($key === 'ID') {
914
                continue;
915
            }
916
917
            // Only merge relations if allowed
918
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
919
                continue;
920
            }
921
922
            // don't merge conflicting values if priority is 'left'
923
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
924
                continue;
925
            }
926
927
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
928
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
929
                continue;
930
            }
931
932
            // TODO remove redundant merge of has_one fields
933
            $leftObj->{$key} = $rightObj->{$key};
934
        }
935
936
        // merge relations
937
        if ($includeRelations) {
938 View Code Duplication
            if ($manyMany = $this->manyMany()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
939
                foreach ($manyMany as $relationship => $class) {
940
                    /** @var DataObject $leftComponents */
941
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
942
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
943
                    if ($rightComponents && $rightComponents->exists()) {
944
                        $leftComponents->addMany($rightComponents->column('ID'));
945
                    }
946
                    $leftComponents->write();
947
                }
948
            }
949
950 View Code Duplication
            if ($hasMany = $this->hasMany()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

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

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

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

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

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

Loading history...
1641
                return substr($remoteClass, 0, $fieldPos);
1642
            }
1643
1644
            // Otherwise just return the class
1645
            return $remoteClass;
1646
        }
1647
1648
        return null;
1649
    }
1650
1651
    /**
1652
     * Given a relation name, determine the relation type
1653
     *
1654
     * @param string $component Name of component
1655
     * @return string has_one, has_many, many_many, belongs_many_many or belongs_to
1656
     */
1657
    public function getRelationType($component)
1658
    {
1659
        $types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
1660
        $config = $this->config();
1661
        foreach ($types as $type) {
1662
            $relations = $config->get($type);
1663
            if ($relations && isset($relations[$component])) {
1664
                return $type;
1665
            }
1666
        }
1667
        return null;
1668
    }
1669
1670
    /**
1671
     * Given a relation declared on a remote class, generate a substitute component for the opposite
1672
     * side of the relation.
1673
     *
1674
     * Notes on behaviour:
1675
     *  - This can still be used on components that are defined on both sides, but do not need to be.
1676
     *  - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
1677
     *  - Cannot be used on polymorphic relationships
1678
     *  - Cannot be used on unsaved objects.
1679
     *
1680
     * @param string $remoteClass
1681
     * @param string $remoteRelation
1682
     * @return DataList|DataObject The component, either as a list or single object
1683
     * @throws BadMethodCallException
1684
     * @throws InvalidArgumentException
1685
     */
1686
    public function inferReciprocalComponent($remoteClass, $remoteRelation)
1687
    {
1688
        $remote = DataObject::singleton($remoteClass);
1689
        $class = $remote->getRelationClass($remoteRelation);
1690
        $schema = static::getSchema();
1691
1692
        // Validate arguments
1693
        if (!$this->isInDB()) {
1694
            throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
1695
        }
1696
        if (empty($class)) {
1697
            throw new InvalidArgumentException(sprintf(
1698
                "%s invoked with invalid relation %s.%s",
1699
                __METHOD__,
1700
                $remoteClass,
1701
                $remoteRelation
1702
            ));
1703
        }
1704
        if ($class === self::class) {
1705
            throw new InvalidArgumentException(sprintf(
1706
                "%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
1707
                "This method does not support polymorphic relationships",
1708
                __METHOD__,
1709
                $remoteClass,
1710
                $remoteRelation
1711
            ));
1712
        }
1713
        if (!is_a($this, $class, true)) {
1714
            throw new InvalidArgumentException(sprintf(
1715
                "Relation %s on %s does not refer to objects of type %s",
1716
                $remoteRelation,
1717
                $remoteClass,
1718
                static::class
1719
            ));
1720
        }
1721
1722
        // Check the relation type to mock
1723
        $relationType = $remote->getRelationType($remoteRelation);
1724
        switch ($relationType) {
1725
            case 'has_one': {
1726
                // Mock has_many
1727
                $joinField = "{$remoteRelation}ID";
1728
                $componentClass = $schema->classForField($remoteClass, $joinField);
1729
                $result = HasManyList::create($componentClass, $joinField);
1730
                if ($this->model) {
1731
                    $result->setDataModel($this->model);
1732
                }
1733
                return $result
1734
                    ->setDataQueryParam($this->getInheritableQueryParams())
1735
                    ->forForeignID($this->ID);
1736
            }
1737
            case 'belongs_to':
1738
            case 'has_many': {
1739
                // These relations must have a has_one on the other end, so find it
1740
                $joinField = $schema->getRemoteJoinField($remoteClass, $remoteRelation, $relationType, $polymorphic);
1741
                if ($polymorphic) {
1742
                    throw new InvalidArgumentException(sprintf(
1743
                        "%s cannot generate opposite component of relation %s.%s, as the other end appears" .
1744
                        "to be a has_one polymorphic. This method does not support polymorphic relationships",
1745
                        __METHOD__,
1746
                        $remoteClass,
1747
                        $remoteRelation
1748
                    ));
1749
                }
1750
                $joinID = $this->getField($joinField);
1751
                if (empty($joinID)) {
1752
                    return null;
1753
                }
1754
                // Get object by joined ID
1755
                return DataObject::get($remoteClass)
1756
                    ->filter('ID', $joinID)
1757
                    ->setDataQueryParam($this->getInheritableQueryParams())
1758
                    ->first();
1759
            }
1760
            case 'many_many':
1761
            case 'belongs_many_many': {
1762
                // Get components and extra fields from parent
1763
                $manyMany = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1764
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1765
1766
                // Reverse parent and component fields and create an inverse ManyManyList
1767
                /** @var RelationList $result */
1768
                $result = Injector::inst()->create(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method create() does only exist in the following implementations of said interface: SilverStripe\Core\Injector\Injector.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1769
                    $manyMany['relationClass'],
1770
                    $manyMany['parentClass'], // Substitute parent class for dataClass
1771
                    $manyMany['join'],
1772
                    $manyMany['parentField'], // Reversed parent / child field
1773
                    $manyMany['childField'], // Reversed parent / child field
1774
                    $extraFields
1775
                );
1776
                if ($this->model) {
1777
                    $result->setDataModel($this->model);
1778
                }
1779
                $this->extend('updateManyManyComponents', $result);
1780
1781
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1782
                // foreignID set elsewhere.
1783
                return $result
1784
                    ->setDataQueryParam($this->getInheritableQueryParams())
1785
                    ->forForeignID($this->ID);
1786
            }
1787
            default: {
1788
                return null;
1789
            }
1790
        }
1791
    }
1792
1793
    /**
1794
     * Returns a many-to-many component, as a ManyManyList.
1795
     * @param string $componentName Name of the many-many component
1796
     * @return RelationList|UnsavedRelationList The set of components
1797
     */
1798
    public function getManyManyComponents($componentName)
1799
    {
1800
        $schema = static::getSchema();
1801
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
1802
        if (!$manyManyComponent) {
1803
            throw new InvalidArgumentException(sprintf(
1804
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1805
                $componentName,
1806
                static::class
1807
            ));
1808
        }
1809
1810
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1811
        if (!$this->ID) {
1812
            if (!isset($this->unsavedRelations[$componentName])) {
1813
                $this->unsavedRelations[$componentName] =
1814
                    new UnsavedRelationList($manyManyComponent['parentClass'], $componentName, $manyManyComponent['childClass']);
1815
            }
1816
            return $this->unsavedRelations[$componentName];
1817
        }
1818
1819
        $extraFields = $schema->manyManyExtraFieldsForComponent(static::class, $componentName) ?: array();
1820
        /** @var RelationList $result */
1821
        $result = Injector::inst()->create(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method create() does only exist in the following implementations of said interface: SilverStripe\Core\Injector\Injector.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1822
            $manyManyComponent['relationClass'],
1823
            $manyManyComponent['childClass'],
1824
            $manyManyComponent['join'],
1825
            $manyManyComponent['childField'],
1826
            $manyManyComponent['parentField'],
1827
            $extraFields
1828
        );
1829
1830
1831
        // Store component data in query meta-data
1832
        $result = $result->alterDataQuery(function ($query) use ($extraFields) {
1833
            /** @var DataQuery $query */
1834
            $query->setQueryParam('Component.ExtraFields', $extraFields);
1835
        });
1836
1837
        if ($this->model) {
1838
            $result->setDataModel($this->model);
1839
        }
1840
1841
        $this->extend('updateManyManyComponents', $result);
1842
1843
        // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1844
        // foreignID set elsewhere.
1845
        return $result
1846
            ->setDataQueryParam($this->getInheritableQueryParams())
1847
            ->forForeignID($this->ID);
1848
    }
1849
1850
    /**
1851
     * Return the class of a one-to-one component.  If $component is null, return all of the one-to-one components and
1852
     * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
1853
     *
1854
     * @return string|array The class of the one-to-one component, or an array of all one-to-one components and
1855
     *                          their classes.
1856
     */
1857
    public function hasOne()
1858
    {
1859
        return (array)$this->config()->get('has_one');
1860
    }
1861
1862
    /**
1863
     * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
1864
     * their class name will be returned.
1865
     *
1866
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1867
     *        the field data stripped off. It defaults to TRUE.
1868
     * @return string|array
1869
     */
1870 View Code Duplication
    public function belongsTo($classOnly = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1871
    {
1872
        $belongsTo = (array)$this->config()->get('belongs_to');
1873
        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...
1874
            return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
1875
        } else {
1876
            return $belongsTo ? $belongsTo : array();
1877
        }
1878
    }
1879
1880
    /**
1881
     * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many
1882
     * relationships and their classes will be returned.
1883
     *
1884
     * @param bool $classOnly If this is TRUE, than any has_many relationships in the form "ClassName.Field" will have
1885
     *        the field data stripped off. It defaults to TRUE.
1886
     * @return string|array|false
1887
     */
1888 View Code Duplication
    public function hasMany($classOnly = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

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

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

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

Loading history...
2364
            $tableClass = $this->record[$fieldName.'_Lazy'];
2365
            $this->loadLazyFields($tableClass);
2366
        }
2367
2368
        // Situation 1: Passing an DBField
2369
        if ($val instanceof DBField) {
2370
            $val->setName($fieldName);
2371
            $val->saveInto($this);
2372
2373
            // Situation 1a: Composite fields should remain bound in case they are
2374
            // later referenced to update the parent dataobject
2375
            if ($val instanceof DBComposite) {
2376
                $val->bindTo($this);
2377
                $this->record[$fieldName] = $val;
2378
            }
2379
        // Situation 2: Passing a literal or non-DBField object
2380
        } else {
2381
            // If this is a proper database field, we shouldn't be getting non-DBField objects
2382
            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...
2383
                throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField');
2384
            }
2385
2386
            // if a field is not existing or has strictly changed
2387
            if (!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
2388
                // TODO Add check for php-level defaults which are not set in the db
2389
                // TODO Add check for hidden input-fields (readonly) which are not set in the db
2390
                // At the very least, the type has changed
2391
                $this->changed[$fieldName] = self::CHANGE_STRICT;
2392
2393
                if ((!isset($this->record[$fieldName]) && $val)
2394
                    || (isset($this->record[$fieldName]) && $this->record[$fieldName] != $val)
2395
                ) {
2396
                    // Value has changed as well, not just the type
2397
                    $this->changed[$fieldName] = self::CHANGE_VALUE;
2398
                }
2399
2400
                // Value is always saved back when strict check succeeds.
2401
                $this->record[$fieldName] = $val;
2402
            }
2403
        }
2404
        return $this;
2405
    }
2406
2407
    /**
2408
     * Set the value of the field, using a casting object.
2409
     * This is useful when you aren't sure that a date is in SQL format, for example.
2410
     * setCastedField() can also be used, by forms, to set related data.  For example, uploaded images
2411
     * can be saved into the Image table.
2412
     *
2413
     * @param string $fieldName Name of the field
2414
     * @param mixed $value New field value
2415
     * @return $this
2416
     */
2417
    public function setCastedField($fieldName, $value)
2418
    {
2419
        if (!$fieldName) {
2420
            user_error("DataObject::setCastedField: Called without a fieldName", E_USER_ERROR);
2421
        }
2422
        $fieldObj = $this->dbObject($fieldName);
2423
        if ($fieldObj) {
2424
            $fieldObj->setValue($value);
2425
            $fieldObj->saveInto($this);
2426
        } else {
2427
            $this->$fieldName = $value;
2428
        }
2429
        return $this;
2430
    }
2431
2432
    /**
2433
     * {@inheritdoc}
2434
     */
2435
    public function castingHelper($field)
2436
    {
2437
        $fieldSpec = static::getSchema()->fieldSpec(static::class, $field);
2438
        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...
2439
            return $fieldSpec;
2440
        }
2441
2442
        // many_many_extraFields aren't presented by db(), so we check if the source query params
2443
        // provide us with meta-data for a many_many relation we can inspect for extra fields.
2444
        $queryParams = $this->getSourceQueryParams();
2445
        if (!empty($queryParams['Component.ExtraFields'])) {
2446
            $extraFields = $queryParams['Component.ExtraFields'];
2447
2448
            if (isset($extraFields[$field])) {
2449
                return $extraFields[$field];
2450
            }
2451
        }
2452
2453
        return parent::castingHelper($field);
2454
    }
2455
2456
    /**
2457
     * Returns true if the given field exists in a database column on any of
2458
     * the objects tables and optionally look up a dynamic getter with
2459
     * get<fieldName>().
2460
     *
2461
     * @param string $field Name of the field
2462
     * @return boolean True if the given field exists
2463
     */
2464
    public function hasField($field)
2465
    {
2466
        $schema = static::getSchema();
2467
        return (
2468
            array_key_exists($field, $this->record)
2469
            || $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...
2470
            || (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...
2471
            || $this->hasMethod("get{$field}")
2472
        );
2473
    }
2474
2475
    /**
2476
     * Returns true if the given field exists as a database column
2477
     *
2478
     * @param string $field Name of the field
2479
     *
2480
     * @return boolean
2481
     */
2482
    public function hasDatabaseField($field)
2483
    {
2484
        $spec = static::getSchema()->fieldSpec(static::class, $field, DataObjectSchema::DB_ONLY);
2485
        return !empty($spec);
2486
    }
2487
2488
    /**
2489
     * Returns true if the member is allowed to do the given action.
2490
     * See {@link extendedCan()} for a more versatile tri-state permission control.
2491
     *
2492
     * @param string $perm The permission to be checked, such as 'View'.
2493
     * @param Member $member The member whose permissions need checking.  Defaults to the currently logged
2494
     * in user.
2495
     * @param array $context Additional $context to pass to extendedCan()
2496
     *
2497
     * @return boolean True if the the member is allowed to do the given action
2498
     */
2499
    public function can($perm, $member = null, $context = array())
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

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

Loading history...
2500
    {
2501
        if (!$member) {
2502
            $member = Security::getCurrentUser();
2503
        }
2504
2505
        if ($member && Permission::checkMember($member, "ADMIN")) {
2506
            return true;
2507
        }
2508
2509
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
2510
            $method = 'can' . ucfirst($perm);
2511
            return $this->$method($member);
2512
        }
2513
2514
        $results = $this->extendedCan('can', $member);
2515
        if (isset($results)) {
2516
            return $results;
2517
        }
2518
2519
        return ($member && Permission::checkMember($member, $perm));
2520
    }
2521
2522
    /**
2523
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2524
     * expected to return one of three values:
2525
     *
2526
     *  - false: Disallow this permission, regardless of what other extensions say
2527
     *  - true: Allow this permission, as long as no other extensions return false
2528
     *  - NULL: Don't affect the outcome
2529
     *
2530
     * This method itself returns a tri-state value, and is designed to be used like this:
2531
     *
2532
     * <code>
2533
     * $extended = $this->extendedCan('canDoSomething', $member);
2534
     * if($extended !== null) return $extended;
2535
     * else return $normalValue;
2536
     * </code>
2537
     *
2538
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2539
     * @param Member|int $member
2540
     * @param array $context Optional context
2541
     * @return boolean|null
2542
     */
2543
    public function extendedCan($methodName, $member, $context = array())
2544
    {
2545
        $results = $this->extend($methodName, $member, $context);
2546
        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...
2547
            // Remove NULLs
2548
            $results = array_filter($results, function ($v) {
2549
                return !is_null($v);
2550
            });
2551
            // If there are any non-NULL responses, then return the lowest one of them.
2552
            // If any explicitly deny the permission, then we don't get access
2553
            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...
2554
                return min($results);
2555
            }
2556
        }
2557
        return null;
2558
    }
2559
2560
    /**
2561
     * @param Member $member
2562
     * @return boolean
2563
     */
2564 View Code Duplication
    public function canView($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2565
    {
2566
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2564 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...
2567
        if ($extended !== null) {
2568
            return $extended;
2569
        }
2570
        return Permission::check('ADMIN', 'any', $member);
2571
    }
2572
2573
    /**
2574
     * @param Member $member
2575
     * @return boolean
2576
     */
2577 View Code Duplication
    public function canEdit($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2578
    {
2579
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2577 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...
2580
        if ($extended !== null) {
2581
            return $extended;
2582
        }
2583
        return Permission::check('ADMIN', 'any', $member);
2584
    }
2585
2586
    /**
2587
     * @param Member $member
2588
     * @return boolean
2589
     */
2590 View Code Duplication
    public function canDelete($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2591
    {
2592
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2590 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...
2593
        if ($extended !== null) {
2594
            return $extended;
2595
        }
2596
        return Permission::check('ADMIN', 'any', $member);
2597
    }
2598
2599
    /**
2600
     * @param Member $member
2601
     * @param array $context Additional context-specific data which might
2602
     * affect whether (or where) this object could be created.
2603
     * @return boolean
2604
     */
2605 View Code Duplication
    public function canCreate($member = null, $context = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2606
    {
2607
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2605 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...
2608
        if ($extended !== null) {
2609
            return $extended;
2610
        }
2611
        return Permission::check('ADMIN', 'any', $member);
2612
    }
2613
2614
    /**
2615
     * Debugging used by Debug::show()
2616
     *
2617
     * @return string HTML data representing this object
2618
     */
2619
    public function debug()
2620
    {
2621
        $class = static::class;
2622
        $val = "<h3>Database record: {$class}</h3>\n<ul>\n";
2623
        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...
2624
            foreach ($this->record as $fieldName => $fieldVal) {
2625
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2626
            }
2627
        }
2628
        $val .= "</ul>\n";
2629
        return $val;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $val; (string) is incompatible with the return type of the parent method SilverStripe\View\ViewableData::Debug of type SilverStripe\View\ViewableData_Debugger.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
2630
    }
2631
2632
    /**
2633
     * Return the DBField object that represents the given field.
2634
     * This works similarly to obj() with 2 key differences:
2635
     *   - it still returns an object even when the field has no value.
2636
     *   - it only matches fields and not methods
2637
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2638
     *
2639
     * @param string $fieldName Name of the field
2640
     * @return DBField The field as a DBField object
2641
     */
2642
    public function dbObject($fieldName)
2643
    {
2644
        // Check for field in DB
2645
        $schema = static::getSchema();
2646
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2647
        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...
2648
            return null;
2649
        }
2650
2651 View Code Duplication
        if (!isset($this->record[$fieldName]) && isset($this->record[$fieldName . '_Lazy'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2652
            $tableClass = $this->record[$fieldName . '_Lazy'];
2653
            $this->loadLazyFields($tableClass);
2654
        }
2655
2656
        $value = isset($this->record[$fieldName])
2657
            ? $this->record[$fieldName]
2658
            : null;
2659
2660
        // If we have a DBField object in $this->record, then return that
2661
        if ($value instanceof DBField) {
2662
            return $value;
2663
        }
2664
2665
        list($class, $spec) = explode('.', $helper);
2666
        /** @var DBField $obj */
2667
        $table = $schema->tableName($class);
2668
        $obj = Injector::inst()->create($spec, $fieldName);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method create() does only exist in the following implementations of said interface: SilverStripe\Core\Injector\Injector.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
2669
        $obj->setTable($table);
2670
        $obj->setValue($value, $this, false);
2671
        return $obj;
2672
    }
2673
2674
    /**
2675
     * Traverses to a DBField referenced by relationships between data objects.
2676
     *
2677
     * The path to the related field is specified with dot separated syntax
2678
     * (eg: Parent.Child.Child.FieldName).
2679
     *
2680
     * @param string $fieldPath
2681
     *
2682
     * @return mixed DBField of the field on the object or a DataList instance.
2683
     */
2684
    public function relObject($fieldPath)
2685
    {
2686
        $object = null;
0 ignored issues
show
Unused Code introduced by
$object is not used, you could remove the assignment.

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

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

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

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

Loading history...
2687
2688
        if (strpos($fieldPath, '.') !== false) {
2689
            $parts = explode('.', $fieldPath);
2690
            $fieldName = array_pop($parts);
2691
2692
            // Traverse dot syntax
2693
            $component = $this;
2694
2695
            foreach ($parts as $relation) {
2696
                if ($component instanceof SS_List) {
2697
                    if (method_exists($component, $relation)) {
2698
                        $component = $component->$relation();
2699
                    } else {
2700
                        /** @var DataList $component */
2701
                        $component = $component->relation($relation);
2702
                    }
2703
                } else {
2704
                    $component = $component->$relation();
2705
                }
2706
            }
2707
2708
            $object = $component->dbObject($fieldName);
2709
        } else {
2710
            $object = $this->dbObject($fieldPath);
2711
        }
2712
2713
        return $object;
2714
    }
2715
2716
    /**
2717
     * Traverses to a field referenced by relationships between data objects, returning the value
2718
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2719
     *
2720
     * @param $fieldName string
2721
     * @return string | null - will return null on a missing value
2722
     */
2723
    public function relField($fieldName)
2724
    {
2725
        $component = $this;
2726
2727
        // We're dealing with relations here so we traverse the dot syntax
2728
        if (strpos($fieldName, '.') !== false) {
2729
            $relations = explode('.', $fieldName);
2730
            $fieldName = array_pop($relations);
2731
            foreach ($relations as $relation) {
2732
                // Inspect $component for element $relation
2733
                if ($component->hasMethod($relation)) {
2734
                    // Check nested method
2735
                    $component = $component->$relation();
2736
                } elseif ($component instanceof SS_List) {
2737
                    // Select adjacent relation from DataList
2738
                    /** @var DataList $component */
2739
                    $component = $component->relation($relation);
2740
                } elseif ($component instanceof DataObject
2741
                    && ($dbObject = $component->dbObject($relation))
2742
                ) {
2743
                    // Select db object
2744
                    $component = $dbObject;
2745
                } else {
2746
                    user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
2747
                }
2748
            }
2749
        }
2750
2751
        // Bail if the component is null
2752
        if (!$component) {
2753
            return null;
2754
        }
2755
        if ($component->hasMethod($fieldName)) {
2756
            return $component->$fieldName();
2757
        }
2758
        return $component->$fieldName;
2759
    }
2760
2761
    /**
2762
     * Temporary hack to return an association name, based on class, to get around the mangle
2763
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2764
     *
2765
     * @param string $className
2766
     * @return string
2767
     */
2768
    public function getReverseAssociation($className)
2769
    {
2770
        if (is_array($this->manyMany())) {
2771
            $many_many = array_flip($this->manyMany());
2772
            if (array_key_exists($className, $many_many)) {
2773
                return $many_many[$className];
2774
            }
2775
        }
2776
        if (is_array($this->hasMany())) {
2777
            $has_many = array_flip($this->hasMany());
2778
            if (array_key_exists($className, $has_many)) {
2779
                return $has_many[$className];
2780
            }
2781
        }
2782
        if (is_array($this->hasOne())) {
2783
            $has_one = array_flip($this->hasOne());
2784
            if (array_key_exists($className, $has_one)) {
2785
                return $has_one[$className];
2786
            }
2787
        }
2788
2789
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\ORM\DataObject::getReverseAssociation of type string.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
2790
    }
2791
2792
    /**
2793
     * Return all objects matching the filter
2794
     * sub-classes are automatically selected and included
2795
     *
2796
     * @param string $callerClass The class of objects to be returned
2797
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2798
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2799
     * @param string|array $sort A sort expression to be inserted into the ORDER
2800
     * BY clause.  If omitted, self::$default_sort will be used.
2801
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
2802
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2803
     * @param string $containerClass The container class to return the results in.
2804
     *
2805
     * @todo $containerClass is Ignored, why?
2806
     *
2807
     * @return DataList The objects matching the filter, in the class specified by $containerClass
2808
     */
2809
    public static function get(
2810
        $callerClass = null,
2811
        $filter = "",
2812
        $sort = "",
2813
        $join = "",
2814
        $limit = null,
2815
        $containerClass = DataList::class
2816
    ) {
2817
2818
        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...
2819
            $callerClass = get_called_class();
2820
            if ($callerClass == self::class) {
2821
                throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
2822
            }
2823
2824
            if ($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
2825
                throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
2826
                    . ' arguments');
2827
            }
2828
2829
            $result = DataList::create(get_called_class());
2830
            $result->setDataModel(DataModel::inst());
2831
            return $result;
2832
        }
2833
2834
        if ($join) {
2835
            throw new \InvalidArgumentException(
2836
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
2837
            );
2838
        }
2839
2840
        $result = DataList::create($callerClass)->where($filter)->sort($sort);
2841
2842
        if ($limit && strpos($limit, ',') !== false) {
2843
            $limitArguments = explode(',', $limit);
2844
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
2845
        } elseif ($limit) {
2846
            $result = $result->limit($limit);
0 ignored issues
show
Documentation introduced by
$limit is of type string|array, but the function expects a integer.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
3115
                $hasAutoIncPK,
3116
                $this->stat('create_table_options'),
3117
                $extensions
0 ignored issues
show
Security Bug introduced by
It seems like $extensions defined by self::database_extensions(static::class) on line 3099 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...
3118
            );
3119
        } else {
3120
            DB::dont_require_table($table);
3121
        }
3122
3123
        // Build any child tables for many_many items
3124
        if ($manyMany = $this->uninherited('many_many')) {
3125
            $extras = $this->uninherited('many_many_extraFields');
3126
            foreach ($manyMany as $component => $spec) {
3127
                // Get many_many spec
3128
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3129
                $parentField = $manyManyComponent['parentField'];
3130
                $childField = $manyManyComponent['childField'];
3131
                $tableOrClass = $manyManyComponent['join'];
3132
3133
                // Skip if backed by actual class
3134
                if (class_exists($tableOrClass)) {
3135
                    continue;
3136
                }
3137
3138
                // Build fields
3139
                $manymanyFields = array(
3140
                    $parentField => "Int",
3141
                    $childField => "Int",
3142
                );
3143
                if (isset($extras[$component])) {
3144
                    $manymanyFields = array_merge($manymanyFields, $extras[$component]);
3145
                }
3146
3147
                // Build index list
3148
                $manymanyIndexes = array(
3149
                    $parentField => true,
3150
                    $childField => true,
3151
                );
3152
                DB::require_table($tableOrClass, $manymanyFields, $manymanyIndexes, true, null, $extensions);
0 ignored issues
show
Documentation introduced by
$manymanyFields is of type array<?,string>, but the function expects a string|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Security Bug introduced by
It seems like $extensions defined by self::database_extensions(static::class) on line 3099 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...
3153
            }
3154
        }
3155
3156
        // Let any extentions make their own database fields
3157
        $this->extend('augmentDatabase', $dummy);
3158
    }
3159
3160
    /**
3161
     * Add default records to database. This function is called whenever the
3162
     * database is built, after the database tables have all been created. Overload
3163
     * this to add default records when the database is built, but make sure you
3164
     * call parent::requireDefaultRecords().
3165
     *
3166
     * @uses DataExtension->requireDefaultRecords()
3167
     */
3168
    public function requireDefaultRecords()
3169
    {
3170
        $defaultRecords = $this->config()->uninherited('default_records');
3171
3172
        if (!empty($defaultRecords)) {
3173
            $hasData = DataObject::get_one(static::class);
3174
            if (!$hasData) {
3175
                $className = static::class;
3176
                foreach ($defaultRecords as $record) {
3177
                    $obj = $this->model->$className->newObject($record);
3178
                    $obj->write();
3179
                }
3180
                DB::alteration_message("Added default records to $className table", "created");
3181
            }
3182
        }
3183
3184
        // Let any extentions make their own database default data
3185
        $this->extend('requireDefaultRecords', $dummy);
3186
    }
3187
3188
    /**
3189
     * Get the default searchable fields for this object, as defined in the
3190
     * $searchable_fields list. If searchable fields are not defined on the
3191
     * data object, uses a default selection of summary fields.
3192
     *
3193
     * @return array
3194
     */
3195
    public function searchableFields()
3196
    {
3197
        // can have mixed format, need to make consistent in most verbose form
3198
        $fields = $this->stat('searchable_fields');
3199
        $labels = $this->fieldLabels();
3200
3201
        // fallback to summary fields (unless empty array is explicitly specified)
3202
        if (! $fields && ! is_array($fields)) {
3203
            $summaryFields = array_keys($this->summaryFields());
3204
            $fields = array();
3205
3206
            // remove the custom getters as the search should not include them
3207
            $schema = static::getSchema();
3208
            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...
3209
                foreach ($summaryFields as $key => $name) {
3210
                    $spec = $name;
3211
3212
                    // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
3213 View Code Duplication
                    if (($fieldPos = strpos($name, '.')) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
3214
                        $name = substr($name, 0, $fieldPos);
3215
                    }
3216
3217
                    if ($schema->fieldSpec($this, $name)) {
3218
                        $fields[] = $name;
3219
                    } elseif ($this->relObject($spec)) {
3220
                        $fields[] = $spec;
3221
                    }
3222
                }
3223
            }
3224
        }
3225
3226
        // we need to make sure the format is unified before
3227
        // augmenting fields, so extensions can apply consistent checks
3228
        // but also after augmenting fields, because the extension
3229
        // might use the shorthand notation as well
3230
3231
        // rewrite array, if it is using shorthand syntax
3232
        $rewrite = array();
3233
        foreach ($fields as $name => $specOrName) {
3234
            $identifer = (is_int($name)) ? $specOrName : $name;
3235
3236
            if (is_int($name)) {
3237
                // Format: array('MyFieldName')
3238
                $rewrite[$identifer] = array();
3239
            } elseif (is_array($specOrName)) {
3240
                // Format: array('MyFieldName' => array(
3241
                //   'filter => 'ExactMatchFilter',
3242
                //   'field' => 'NumericField', // optional
3243
                //   'title' => 'My Title', // optional
3244
                // ))
3245
                $rewrite[$identifer] = array_merge(
3246
                    array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
3247
                    (array)$specOrName
3248
                );
3249
            } else {
3250
                // Format: array('MyFieldName' => 'ExactMatchFilter')
3251
                $rewrite[$identifer] = array(
3252
                    'filter' => $specOrName,
3253
                );
3254
            }
3255
            if (!isset($rewrite[$identifer]['title'])) {
3256
                $rewrite[$identifer]['title'] = (isset($labels[$identifer]))
3257
                    ? $labels[$identifer] : FormField::name_to_label($identifer);
3258
            }
3259
            if (!isset($rewrite[$identifer]['filter'])) {
3260
                /** @skipUpgrade */
3261
                $rewrite[$identifer]['filter'] = 'PartialMatchFilter';
3262
            }
3263
        }
3264
3265
        $fields = $rewrite;
3266
3267
        // apply DataExtensions if present
3268
        $this->extend('updateSearchableFields', $fields);
3269
3270
        return $fields;
3271
    }
3272
3273
    /**
3274
     * Get any user defined searchable fields labels that
3275
     * exist. Allows overriding of default field names in the form
3276
     * interface actually presented to the user.
3277
     *
3278
     * The reason for keeping this separate from searchable_fields,
3279
     * which would be a logical place for this functionality, is to
3280
     * avoid bloating and complicating the configuration array. Currently
3281
     * much of this system is based on sensible defaults, and this property
3282
     * would generally only be set in the case of more complex relationships
3283
     * between data object being required in the search interface.
3284
     *
3285
     * Generates labels based on name of the field itself, if no static property
3286
     * {@link self::field_labels} exists.
3287
     *
3288
     * @uses $field_labels
3289
     * @uses FormField::name_to_label()
3290
     *
3291
     * @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
3292
     *
3293
     * @return array|string Array of all element labels if no argument given, otherwise the label of the field
3294
     */
3295
    public function fieldLabels($includerelations = true)
3296
    {
3297
        $cacheKey = static::class . '_' . $includerelations;
3298
3299
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
3300
            $customLabels = $this->stat('field_labels');
3301
            $autoLabels = array();
3302
3303
            // get all translated static properties as defined in i18nCollectStatics()
3304
            $ancestry = ClassInfo::ancestry(static::class);
3305
            $ancestry = array_reverse($ancestry);
3306
            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...
3307
                foreach ($ancestry as $ancestorClass) {
3308
                    if ($ancestorClass === ViewableData::class) {
3309
                        break;
3310
                    }
3311
                    $types = [
3312
                        'db' => (array)Config::inst()->get($ancestorClass, 'db', Config::UNINHERITED)
3313
                    ];
3314
                    if ($includerelations) {
3315
                        $types['has_one'] = (array)Config::inst()->get($ancestorClass, 'has_one', Config::UNINHERITED);
3316
                        $types['has_many'] = (array)Config::inst()->get($ancestorClass, 'has_many', Config::UNINHERITED);
3317
                        $types['many_many'] = (array)Config::inst()->get($ancestorClass, 'many_many', Config::UNINHERITED);
3318
                        $types['belongs_many_many'] = (array)Config::inst()->get($ancestorClass, 'belongs_many_many', Config::UNINHERITED);
3319
                    }
3320
                    foreach ($types as $type => $attrs) {
3321
                        foreach ($attrs as $name => $spec) {
3322
                            $autoLabels[$name] = _t("{$ancestorClass}.{$type}_{$name}", FormField::name_to_label($name));
3323
                        }
3324
                    }
3325
                }
3326
            }
3327
3328
            $labels = array_merge((array)$autoLabels, (array)$customLabels);
3329
            $this->extend('updateFieldLabels', $labels);
3330
            self::$_cache_field_labels[$cacheKey] = $labels;
3331
        }
3332
3333
        return self::$_cache_field_labels[$cacheKey];
3334
    }
3335
3336
    /**
3337
     * Get a human-readable label for a single field,
3338
     * see {@link fieldLabels()} for more details.
3339
     *
3340
     * @uses fieldLabels()
3341
     * @uses FormField::name_to_label()
3342
     *
3343
     * @param string $name Name of the field
3344
     * @return string Label of the field
3345
     */
3346
    public function fieldLabel($name)
3347
    {
3348
        $labels = $this->fieldLabels();
3349
        return (isset($labels[$name])) ? $labels[$name] : FormField::name_to_label($name);
3350
    }
3351
3352
    /**
3353
     * Get the default summary fields for this object.
3354
     *
3355
     * @todo use the translation apparatus to return a default field selection for the language
3356
     *
3357
     * @return array
3358
     */
3359
    public function summaryFields()
3360
    {
3361
        $fields = $this->stat('summary_fields');
3362
3363
        // if fields were passed in numeric array,
3364
        // convert to an associative array
3365
        if ($fields && array_key_exists(0, $fields)) {
3366
            $fields = array_combine(array_values($fields), array_values($fields));
3367
        }
3368
3369
        if (!$fields) {
3370
            $fields = array();
3371
            // try to scaffold a couple of usual suspects
3372
            if ($this->hasField('Name')) {
3373
                $fields['Name'] = 'Name';
3374
            }
3375
            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...
3376
                $fields['Title'] = 'Title';
3377
            }
3378
            if ($this->hasField('Description')) {
3379
                $fields['Description'] = 'Description';
3380
            }
3381
            if ($this->hasField('FirstName')) {
3382
                $fields['FirstName'] = 'First Name';
3383
            }
3384
        }
3385
        $this->extend("updateSummaryFields", $fields);
3386
3387
        // Final fail-over, just list ID field
3388
        if (!$fields) {
3389
            $fields['ID'] = 'ID';
3390
        }
3391
3392
        // Localize fields (if possible)
3393
        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...
3394
            // only attempt to localize if the label definition is the same as the field name.
3395
            // this will preserve any custom labels set in the summary_fields configuration
3396
            if (isset($fields[$name]) && $name === $fields[$name]) {
3397
                $fields[$name] = $label;
3398
            }
3399
        }
3400
3401
        return $fields;
3402
    }
3403
3404
    /**
3405
     * Defines a default list of filters for the search context.
3406
     *
3407
     * If a filter class mapping is defined on the data object,
3408
     * it is constructed here. Otherwise, the default filter specified in
3409
     * {@link DBField} is used.
3410
     *
3411
     * @todo error handling/type checking for valid FormField and SearchFilter subclasses?
3412
     *
3413
     * @return array
3414
     */
3415
    public function defaultSearchFilters()
3416
    {
3417
        $filters = array();
3418
3419
        foreach ($this->searchableFields() as $name => $spec) {
3420
            if (empty($spec['filter'])) {
3421
                /** @skipUpgrade */
3422
                $filters[$name] = 'PartialMatchFilter';
3423
            } elseif ($spec['filter'] instanceof SearchFilter) {
3424
                $filters[$name] = $spec['filter'];
3425
            } else {
3426
                $filters[$name] = Injector::inst()->create($spec['filter'], $name);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method create() does only exist in the following implementations of said interface: SilverStripe\Core\Injector\Injector.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
3427
            }
3428
        }
3429
3430
        return $filters;
3431
    }
3432
3433
    /**
3434
     * @return boolean True if the object is in the database
3435
     */
3436
    public function isInDB()
3437
    {
3438
        return is_numeric($this->ID) && $this->ID > 0;
3439
    }
3440
3441
    /*
3442
	 * @ignore
3443
	 */
3444
    private static $subclass_access = true;
3445
3446
    /**
3447
     * Temporarily disable subclass access in data object qeur
3448
     */
3449
    public static function disable_subclass_access()
3450
    {
3451
        self::$subclass_access = false;
3452
    }
3453
    public static function enable_subclass_access()
3454
    {
3455
        self::$subclass_access = true;
3456
    }
3457
3458
    //-------------------------------------------------------------------------------------------//
3459
3460
    /**
3461
     * Database field definitions.
3462
     * This is a map from field names to field type. The field
3463
     * type should be a class that extends .
3464
     * @var array
3465
     * @config
3466
     */
3467
    private static $db = [];
3468
3469
    /**
3470
     * Use a casting object for a field. This is a map from
3471
     * field name to class name of the casting object.
3472
     *
3473
     * @var array
3474
     */
3475
    private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
3476
        "Title" => 'Text',
3477
    );
3478
3479
    /**
3480
     * Specify custom options for a CREATE TABLE call.
3481
     * Can be used to specify a custom storage engine for specific database table.
3482
     * All options have to be keyed for a specific database implementation,
3483
     * identified by their class name (extending from {@link SS_Database}).
3484
     *
3485
     * <code>
3486
     * array(
3487
     *  'MySQLDatabase' => 'ENGINE=MyISAM'
3488
     * )
3489
     * </code>
3490
     *
3491
     * Caution: This API is experimental, and might not be
3492
     * included in the next major release. Please use with care.
3493
     *
3494
     * @var array
3495
     * @config
3496
     */
3497
    private static $create_table_options = array(
3498
        'SilverStripe\ORM\Connect\MySQLDatabase' => 'ENGINE=InnoDB'
3499
    );
3500
3501
    /**
3502
     * If a field is in this array, then create a database index
3503
     * on that field. This is a map from fieldname to index type.
3504
     * See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
3505
     *
3506
     * @var array
3507
     * @config
3508
     */
3509
    private static $indexes = null;
3510
3511
    /**
3512
     * Inserts standard column-values when a DataObject
3513
     * is instanciated. Does not insert default records {@see $default_records}.
3514
     * This is a map from fieldname to default value.
3515
     *
3516
     *  - If you would like to change a default value in a sub-class, just specify it.
3517
     *  - If you would like to disable the default value given by a parent class, set the default value to 0,'',
3518
     *    or false in your subclass.  Setting it to null won't work.
3519
     *
3520
     * @var array
3521
     * @config
3522
     */
3523
    private static $defaults = [];
3524
3525
    /**
3526
     * Multidimensional array which inserts default data into the database
3527
     * on a db/build-call as long as the database-table is empty. Please use this only
3528
     * for simple constructs, not for SiteTree-Objects etc. which need special
3529
     * behaviour such as publishing and ParentNodes.
3530
     *
3531
     * Example:
3532
     * array(
3533
     *  array('Title' => "DefaultPage1", 'PageTitle' => 'page1'),
3534
     *  array('Title' => "DefaultPage2")
3535
     * ).
3536
     *
3537
     * @var array
3538
     * @config
3539
     */
3540
    private static $default_records = null;
3541
3542
    /**
3543
     * One-to-zero relationship defintion. This is a map of component name to data type. In order to turn this into a
3544
     * true one-to-one relationship you can add a {@link DataObject::$belongs_to} relationship on the child class.
3545
     *
3546
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3547
     *
3548
     *  @var array
3549
     * @config
3550
     */
3551
    private static $has_one = [];
3552
3553
    /**
3554
     * A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
3555
     *
3556
     * This does not actually create any data structures, but allows you to query the other object in a one-to-one
3557
     * relationship from the child object. If you have multiple belongs_to links to another object you can use the
3558
     * syntax "ClassName.HasOneName" to specify which foreign has_one key on the other object to use.
3559
     *
3560
     * Note that you cannot have a has_one and belongs_to relationship with the same name.
3561
     *
3562
     * @var array
3563
     * @config
3564
     */
3565
    private static $belongs_to = [];
3566
3567
    /**
3568
     * This defines a one-to-many relationship. It is a map of component name to the remote data class.
3569
     *
3570
     * This relationship type does not actually create a data structure itself - you need to define a matching $has_one
3571
     * relationship on the child class. Also, if the $has_one relationship on the child class has multiple links to this
3572
     * class you can use the syntax "ClassName.HasOneRelationshipName" in the remote data class definition to show
3573
     * which foreign key to use.
3574
     *
3575
     * @var array
3576
     * @config
3577
     */
3578
    private static $has_many = [];
3579
3580
    /**
3581
     * many-many relationship definitions.
3582
     * This is a map from component name to data type.
3583
     * @var array
3584
     * @config
3585
     */
3586
    private static $many_many = [];
3587
3588
    /**
3589
     * Extra fields to include on the connecting many-many table.
3590
     * This is a map from field name to field type.
3591
     *
3592
     * Example code:
3593
     * <code>
3594
     * public static $many_many_extraFields = array(
3595
     *  'Members' => array(
3596
     *          'Role' => 'Varchar(100)'
3597
     *      )
3598
     * );
3599
     * </code>
3600
     *
3601
     * @var array
3602
     * @config
3603
     */
3604
    private static $many_many_extraFields = [];
3605
3606
    /**
3607
     * The inverse side of a many-many relationship.
3608
     * This is a map from component name to data type.
3609
     * @var array
3610
     * @config
3611
     */
3612
    private static $belongs_many_many = [];
3613
3614
    /**
3615
     * The default sort expression. This will be inserted in the ORDER BY
3616
     * clause of a SQL query if no other sort expression is provided.
3617
     * @var string
3618
     * @config
3619
     */
3620
    private static $default_sort = null;
3621
3622
    /**
3623
     * Default list of fields that can be scaffolded by the ModelAdmin
3624
     * search interface.
3625
     *
3626
     * Overriding the default filter, with a custom defined filter:
3627
     * <code>
3628
     *  static $searchable_fields = array(
3629
     *     "Name" => "PartialMatchFilter"
3630
     *  );
3631
     * </code>
3632
     *
3633
     * Overriding the default form fields, with a custom defined field.
3634
     * The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
3635
     * The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
3636
     * <code>
3637
     *  static $searchable_fields = array(
3638
     *    "Name" => array(
3639
     *      "field" => "TextField"
3640
     *    )
3641
     *  );
3642
     * </code>
3643
     *
3644
     * Overriding the default form field, filter and title:
3645
     * <code>
3646
     *  static $searchable_fields = array(
3647
     *    "Organisation.ZipCode" => array(
3648
     *      "field" => "TextField",
3649
     *      "filter" => "PartialMatchFilter",
3650
     *      "title" => 'Organisation ZIP'
3651
     *    )
3652
     *  );
3653
     * </code>
3654
     * @config
3655
     */
3656
    private static $searchable_fields = null;
3657
3658
    /**
3659
     * User defined labels for searchable_fields, used to override
3660
     * default display in the search form.
3661
     * @config
3662
     */
3663
    private static $field_labels = [];
3664
3665
    /**
3666
     * Provides a default list of fields to be used by a 'summary'
3667
     * view of this object.
3668
     * @config
3669
     */
3670
    private static $summary_fields = [];
3671
3672
    public function provideI18nEntities()
3673
    {
3674
        // Note: see http://guides.rubyonrails.org/i18n.html#pluralization for rules
3675
        // Best guess for a/an rule. Better guesses require overriding in subclasses
3676
        $pluralName = $this->plural_name();
3677
        $singularName = $this->singular_name();
3678
        $conjunction = preg_match('/^[aeiou]/i', $singularName) ? 'An ' : 'A ';
3679
        return [
3680
            static::class.'.SINGULARNAME' => $this->singular_name(),
3681
            static::class.'.PLURALNAME' => $pluralName,
3682
            static::class.'.PLURALS' => [
3683
                'one' => $conjunction . $singularName,
3684
                'other' => '{count} ' . $pluralName
3685
            ]
3686
        ];
3687
    }
3688
3689
    /**
3690
     * Returns true if the given method/parameter has a value
3691
     * (Uses the DBField::hasValue if the parameter is a database field)
3692
     *
3693
     * @param string $field The field name
3694
     * @param array $arguments
3695
     * @param bool $cache
3696
     * @return boolean
3697
     */
3698
    public function hasValue($field, $arguments = null, $cache = true)
3699
    {
3700
        // has_one fields should not use dbObject to check if a value is given
3701
        $hasOne = static::getSchema()->hasOneComponent(static::class, $field);
3702
        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...
3703
            return $obj->exists();
3704
        } else {
3705
            return parent::hasValue($field, $arguments, $cache);
0 ignored issues
show
Bug introduced by
It seems like $arguments defined by parameter $arguments on line 3698 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...
3706
        }
3707
    }
3708
3709
    /**
3710
     * If selected through a many_many through relation, this is the instance of the joined record
3711
     *
3712
     * @return DataObject
3713
     */
3714
    public function getJoin()
3715
    {
3716
        return $this->joinRecord;
3717
    }
3718
3719
    /**
3720
     * Set joining object
3721
     *
3722
     * @param DataObject $object
3723
     * @param string $alias Alias
3724
     * @return $this
3725
     */
3726
    public function setJoin(DataObject $object, $alias = null)
3727
    {
3728
        $this->joinRecord = $object;
3729
        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...
3730
            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...
3731
                throw new InvalidArgumentException(
3732
                    "Joined record $alias cannot also be a db field"
3733
                );
3734
            }
3735
            $this->record[$alias] = $object;
3736
        }
3737
        return $this;
3738
    }
3739
}
3740