Completed
Push — fix-2494 ( 3153ee...40d9bb )
by Sam
13:43 queued 06:38
created

DataObject::newClassInstance()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 1
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Object;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Core\Resettable;
10
use SilverStripe\Dev\Deprecation;
11
use SilverStripe\Dev\Debug;
12
use SilverStripe\Control\HTTP;
13
use SilverStripe\Forms\FieldList;
14
use SilverStripe\Forms\FormField;
15
use SilverStripe\Forms\FormScaffolder;
16
use SilverStripe\i18n\i18n;
17
use SilverStripe\i18n\i18nEntityProvider;
18
use SilverStripe\ORM\Filters\SearchFilter;
19
use SilverStripe\ORM\Search\SearchContext;
20
use SilverStripe\ORM\Queries\SQLInsert;
21
use SilverStripe\ORM\Queries\SQLDelete;
22
use SilverStripe\ORM\FieldType\DBField;
23
use SilverStripe\ORM\FieldType\DBDatetime;
24
use SilverStripe\ORM\FieldType\DBComposite;
25
use SilverStripe\ORM\FieldType\DBClassName;
26
use SilverStripe\Security\Member;
27
use SilverStripe\Security\Permission;
28
use SilverStripe\View\ViewableData;
29
use LogicException;
30
use InvalidArgumentException;
31
use BadMethodCallException;
32
use Exception;
33
34
/**
35
 * A single database record & abstract class for the data-access-model.
36
 *
37
 * <h2>Extensions</h2>
38
 *
39
 * See {@link Extension} and {@link DataExtension}.
40
 *
41
 * <h2>Permission Control</h2>
42
 *
43
 * Object-level access control by {@link Permission}. Permission codes are arbitrary
44
 * strings which can be selected on a group-by-group basis.
45
 *
46
 * <code>
47
 * class Article extends DataObject implements PermissionProvider {
48
 *  static $api_access = true;
49
 *
50
 *  function canView($member = false) {
51
 *    return Permission::check('ARTICLE_VIEW');
52
 *  }
53
 *  function canEdit($member = false) {
54
 *    return Permission::check('ARTICLE_EDIT');
55
 *  }
56
 *  function canDelete() {
57
 *    return Permission::check('ARTICLE_DELETE');
58
 *  }
59
 *  function canCreate() {
60
 *    return Permission::check('ARTICLE_CREATE');
61
 *  }
62
 *  function providePermissions() {
63
 *    return array(
64
 *      'ARTICLE_VIEW' => 'Read an article object',
65
 *      'ARTICLE_EDIT' => 'Edit an article object',
66
 *      'ARTICLE_DELETE' => 'Delete an article object',
67
 *      'ARTICLE_CREATE' => 'Create an article object',
68
 *    );
69
 *  }
70
 * }
71
 * </code>
72
 *
73
 * Object-level access control by {@link Group} membership:
74
 * <code>
75
 * class Article extends DataObject {
76
 *   static $api_access = true;
77
 *
78
 *   function canView($member = false) {
79
 *     if(!$member) $member = Member::currentUser();
80
 *     return $member->inGroup('Subscribers');
81
 *   }
82
 *   function canEdit($member = false) {
83
 *     if(!$member) $member = Member::currentUser();
84
 *     return $member->inGroup('Editors');
85
 *   }
86
 *
87
 *   // ...
88
 * }
89
 * </code>
90
 *
91
 * If any public method on this class is prefixed with an underscore,
92
 * the results are cached in memory through {@link cachedCall()}.
93
 *
94
 *
95
 * @todo Add instance specific removeExtension() which undos loadExtraStatics()
96
 *  and defineMethods()
97
 *
98
 * @property integer ID ID of the DataObject, 0 if the DataObject doesn't exist in database.
99
 * @property string ClassName Class name of the DataObject
100
 * @property string LastEdited Date and time of DataObject's last modification.
101
 * @property string Created Date and time of DataObject creation.
102
 */
103
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider, Resettable
104
{
105
106
    /**
107
     * Human-readable singular name.
108
     * @var string
109
     * @config
110
     */
111
    private static $singular_name = null;
112
113
    /**
114
     * Human-readable plural name
115
     * @var string
116
     * @config
117
     */
118
    private static $plural_name = null;
119
120
    /**
121
     * Allow API access to this object?
122
     * @todo Define the options that can be set here
123
     * @config
124
     */
125
    private static $api_access = false;
126
127
    /**
128
     * Allows specification of a default value for the ClassName field.
129
     * Configure this value only in subclasses of DataObject.
130
     *
131
     * @config
132
     * @var string
133
     */
134
    private static $default_classname = null;
135
136
    /**
137
     * True if this DataObject has been destroyed.
138
     * @var boolean
139
     */
140
    public $destroyed = false;
141
142
    /**
143
     * The DataModel from this this object comes
144
     */
145
    protected $model;
146
147
    /**
148
     * Data stored in this objects database record. An array indexed by fieldname.
149
     *
150
     * Use {@link toMap()} if you want an array representation
151
     * of this object, as the $record array might contain lazy loaded field aliases.
152
     *
153
     * @var array
154
     */
155
    protected $record;
156
157
    /**
158
     * If selected through a many_many through relation, this is the instance of the through record
159
     *
160
     * @var DataObject
161
     */
162
    protected $joinRecord;
163
164
    /**
165
     * Represents a field that hasn't changed (before === after, thus before == after)
166
     */
167
    const CHANGE_NONE = 0;
168
169
    /**
170
     * Represents a field that has changed type, although not the loosely defined value.
171
     * (before !== after && before == after)
172
     * E.g. change 1 to true or "true" to true, but not true to 0.
173
     * Value changes are by nature also considered strict changes.
174
     */
175
    const CHANGE_STRICT = 1;
176
177
    /**
178
     * Represents a field that has changed the loosely defined value
179
     * (before != after, thus, before !== after))
180
     * E.g. change false to true, but not false to 0
181
     */
182
    const CHANGE_VALUE = 2;
183
184
    /**
185
     * An array indexed by fieldname, true if the field has been changed.
186
     * Use {@link getChangedFields()} and {@link isChanged()} to inspect
187
     * the changed state.
188
     *
189
     * @var array
190
     */
191
    private $changed;
192
193
    /**
194
     * The database record (in the same format as $record), before
195
     * any changes.
196
     * @var array
197
     */
198
    protected $original;
199
200
    /**
201
     * Used by onBeforeDelete() to ensure child classes call parent::onBeforeDelete()
202
     * @var boolean
203
     */
204
    protected $brokenOnDelete = false;
205
206
    /**
207
     * Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
208
     * @var boolean
209
     */
210
    protected $brokenOnWrite = false;
211
212
    /**
213
     * @config
214
     * @var boolean Should dataobjects be validated before they are written?
215
     * Caution: Validation can contain safeguards against invalid/malicious data,
216
     * and check permission levels (e.g. on {@link Group}). Therefore it is recommended
217
     * to only disable validation for very specific use cases.
218
     */
219
    private static $validation_enabled = true;
220
221
    /**
222
     * Static caches used by relevant functions.
223
     *
224
     * @var array
225
     */
226
    protected static $_cache_get_one;
227
228
    /**
229
     * Cache of field labels
230
     *
231
     * @var array
232
     */
233
    protected static $_cache_field_labels = array();
234
235
    /**
236
     * Base fields which are not defined in static $db
237
     *
238
     * @config
239
     * @var array
240
     */
241
    private static $fixed_fields = array(
242
        'ID' => 'PrimaryKey',
243
        'ClassName' => 'DBClassName',
244
        'LastEdited' => 'DBDatetime',
245
        'Created' => 'DBDatetime',
246
    );
247
248
    /**
249
     * Override table name for this class. If ignored will default to FQN of class.
250
     * This option is not inheritable, and must be set on each class.
251
     * If left blank naming will default to the legacy (3.x) behaviour.
252
     *
253
     * @var string
254
     */
255
    private static $table_name = null;
256
257
    /**
258
     * Non-static relationship cache, indexed by component name.
259
     *
260
     * @var DataObject[]
261
     */
262
    protected $components;
263
264
    /**
265
     * Non-static cache of has_many and many_many relations that can't be written until this object is saved.
266
     *
267
     * @var UnsavedRelationList[]
268
     */
269
    protected $unsavedRelations;
270
271
    /**
272
     * Get schema object
273
     *
274
     * @return DataObjectSchema
275
     */
276
    public static function getSchema()
277
    {
278
        return Injector::inst()->get(DataObjectSchema::class);
279
    }
280
281
    /**
282
     * Construct a new DataObject.
283
     *
284
285
     * @param array|null $record Used internally for rehydrating an object from database content.
286
     *                           Bypasses setters on this class, and hence should not be used
287
     *                           for populating data on new records.
288
     * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
289
     *                             Singletons don't have their defaults set.
290
     * @param DataModel $model
291
     * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
292
     */
293
    public function __construct($record = null, $isSingleton = false, $model = null, $queryParams = array())
294
    {
295
        parent::__construct();
296
297
        // Set query params on the DataObject to tell the lazy loading mechanism the context the object creation context
298
        $this->setSourceQueryParams($queryParams);
299
300
        // Set the fields data.
301
        if (!$record) {
302
            $record = array(
303
                'ID' => 0,
304
                'ClassName' => static::class,
305
                'RecordClassName' => static::class
306
            );
307
        }
308
309
        if (!is_array($record) && !is_a($record, "stdClass")) {
310
            if (is_object($record)) {
311
                $passed = "an object of type '".get_class($record)."'";
312
            } else {
313
                $passed = "The value '$record'";
314
            }
315
316
            user_error(
317
                "DataObject::__construct passed $passed.  It's supposed to be passed an array,"
318
                . " taken straight from the database.  Perhaps you should use DataList::create()->First(); instead?",
319
                E_USER_WARNING
320
            );
321
            $record = null;
322
        }
323
324
        if (is_a($record, "stdClass")) {
325
            $record = (array)$record;
326
        }
327
328
        // Set $this->record to $record, but ignore NULLs
329
        $this->record = array();
330
        foreach ($record as $k => $v) {
0 ignored issues
show
Bug introduced by
The expression $record of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
331
            // Ensure that ID is stored as a number and not a string
332
            // To do: this kind of clean-up should be done on all numeric fields, in some relatively
333
            // performant manner
334
            if ($v !== null) {
335
                if ($k == 'ID' && is_numeric($v)) {
336
                    $this->record[$k] = (int)$v;
337
                } else {
338
                    $this->record[$k] = $v;
339
                }
340
            }
341
        }
342
343
        // Identify fields that should be lazy loaded, but only on existing records
344
        if (!empty($record['ID'])) {
345
            // Get all field specs scoped to class for later lazy loading
346
            $fields = static::getSchema()->fieldSpecs(
347
                static::class,
348
                DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
349
            );
350
            foreach ($fields as $field => $fieldSpec) {
351
                $fieldClass = strtok($fieldSpec, ".");
352
                if (!array_key_exists($field, $record)) {
353
                    $this->record[$field.'_Lazy'] = $fieldClass;
354
                }
355
            }
356
        }
357
358
        $this->original = $this->record;
359
360
        // Keep track of the modification date of all the data sourced to make this page
361
        // From this we create a Last-Modified HTTP header
362
        if (isset($record['LastEdited'])) {
363
            HTTP::register_modification_date($record['LastEdited']);
364
        }
365
366
        // this must be called before populateDefaults(), as field getters on a DataObject
367
        // may call getComponent() and others, which rely on $this->model being set.
368
        $this->model = $model ? $model : DataModel::inst();
369
370
        // Must be called after parent constructor
371
        if (!$isSingleton && (!isset($this->record['ID']) || !$this->record['ID'])) {
372
            $this->populateDefaults();
373
        }
374
375
        // prevent populateDefaults() and setField() from marking overwritten defaults as changed
376
        $this->changed = array();
377
    }
378
379
    /**
380
     * Set the DataModel
381
     * @param DataModel $model
382
     * @return DataObject $this
383
     */
384
    public function setDataModel(DataModel $model)
385
    {
386
        $this->model = $model;
387
        return $this;
388
    }
389
390
    /**
391
     * Destroy all of this objects dependant objects and local caches.
392
     * You'll need to call this to get the memory of an object that has components or extensions freed.
393
     */
394
    public function destroy()
395
    {
396
        //$this->destroyed = true;
397
        gc_collect_cycles();
398
        $this->flushCache(false);
399
    }
400
401
    /**
402
     * Create a duplicate of this node. Can duplicate many_many relations
403
     *
404
     * @param bool $doWrite Perform a write() operation before returning the object.
405
     * If this is true, it will create the duplicate in the database.
406
     * @param bool|string $manyMany Which many-many to duplicate. Set to true to duplicate all, false to duplicate none.
407
     * Alternatively set to the string of the relation config to duplicate
408
     * (supports 'many_many', or 'belongs_many_many')
409
     * @return static A duplicate of this node. The exact type will be the type of this node.
410
     */
411
    public function duplicate($doWrite = true, $manyMany = 'many_many')
412
    {
413
        $map = $this->toMap();
414
        unset($map['Created']);
415
        /** @var static $clone */
416
        $clone = Injector::inst()->create(static::class, $map, false, $this->model);
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...
417
        $clone->ID = 0;
418
419
        $clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite, $manyMany);
420
        if ($manyMany) {
421
            $this->duplicateManyManyRelations($this, $clone, $manyMany);
422
        }
423
        if ($doWrite) {
424
            $clone->write();
425
        }
426
        $clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite, $manyMany);
427
428
        return $clone;
429
    }
430
431
    /**
432
     * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object.
433
     *
434
     * @param DataObject $sourceObject the source object to duplicate from
435
     * @param DataObject $destinationObject the destination object to populate with the duplicated relations
436
     * @param bool|string $filter
437
     */
438
    protected function duplicateManyManyRelations($sourceObject, $destinationObject, $filter)
439
    {
440
        // Get list of relations to duplicate
441
        if ($filter === 'many_many' || $filter === 'belongs_many_many') {
442
            $relations = $sourceObject->config()->get($filter);
443
        } elseif ($filter === true) {
444
            $relations = $sourceObject->manyMany();
445
        } else {
446
            throw new InvalidArgumentException("Invalid many_many duplication filter");
447
        }
448
        foreach ($relations as $manyManyName => $type) {
449
            $this->duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName);
450
        }
451
    }
452
453
    /**
454
     * Duplicates a single many_many relation from one object to another
455
     *
456
     * @param DataObject $sourceObject
457
     * @param DataObject $destinationObject
458
     * @param string $manyManyName
459
     */
460
    protected function duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName)
461
    {
462
        // Ensure this component exists on the destination side as well
463
        if (!static::getSchema()->manyManyComponent(get_class($destinationObject), $manyManyName)) {
464
            return;
465
        }
466
467
        // Copy all components from source to destination
468
        $source = $sourceObject->getManyManyComponents($manyManyName);
469
        $dest = $destinationObject->getManyManyComponents($manyManyName);
470
        foreach ($source as $item) {
471
            $dest->add($item);
472
        }
473
    }
474
475
    /**
476
     * Return obsolete class name, if this is no longer a valid class
477
     *
478
     * @return string
479
     */
480
    public function getObsoleteClassName()
481
    {
482
        $className = $this->getField("ClassName");
483
        if (!ClassInfo::exists($className)) {
484
            return $className;
485
        }
486
        return null;
487
    }
488
489
    /**
490
     * Gets name of this class
491
     *
492
     * @return string
493
     */
494
    public function getClassName()
495
    {
496
        $className = $this->getField("ClassName");
497
        if (!ClassInfo::exists($className)) {
498
            return static::class;
499
        }
500
        return $className;
501
    }
502
503
    /**
504
     * Set the ClassName attribute. {@link $class} is also updated.
505
     * Warning: This will produce an inconsistent record, as the object
506
     * instance will not automatically switch to the new subclass.
507
     * Please use {@link newClassInstance()} for this purpose,
508
     * or destroy and reinstanciate the record.
509
     *
510
     * @param string $className The new ClassName attribute (a subclass of {@link DataObject})
511
     * @return $this
512
     */
513
    public function setClassName($className)
514
    {
515
        $className = trim($className);
516
        if (!$className || !is_subclass_of($className, self::class)) {
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...
517
            return $this;
518
        }
519
520
        $this->class = $className;
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
        // Define the extra db fields - this is only necessary for extensions added in the
572
        // class definition.  Object::add_extension() will call this at definition time for
573
        // those objects, which is a better mechanism.  Perhaps extensions defined inside the
574
        // class def can somehow be applied at definiton time also?
575
        if ($this->extension_instances) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extension_instances of type SilverStripe\Core\Extension[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
576
            foreach ($this->extension_instances as $i => $instance) {
577
                if (!$instance->class) {
578
                    $class = get_class($instance);
579
                    user_error("DataObject::defineMethods(): Please ensure {$class}::__construct() calls"
580
                    . " parent::__construct()", E_USER_ERROR);
581
                }
582
            }
583
        }
584
585
        if (static::class === self::class) {
586
             return;
587
        }
588
589
        // Set up accessors for joined items
590
        if ($manyMany = $this->manyMany()) {
591
            foreach ($manyMany as $relationship => $class) {
592
                $this->addWrapperMethod($relationship, 'getManyManyComponents');
593
            }
594
        }
595
        if ($hasMany = $this->hasMany()) {
596
            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...
597
                $this->addWrapperMethod($relationship, 'getComponents');
598
            }
599
        }
600
        if ($hasOne = $this->hasOne()) {
601
            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...
602
                $this->addWrapperMethod($relationship, 'getComponent');
603
            }
604
        }
605
        if ($belongsTo = $this->belongsTo()) {
606
            foreach (array_keys($belongsTo) as $relationship) {
607
                $this->addWrapperMethod($relationship, 'getComponent');
608
            }
609
        }
610
    }
611
612
    /**
613
     * Returns true if this object "exists", i.e., has a sensible value.
614
     * The default behaviour for a DataObject is to return true if
615
     * the object exists in the database, you can override this in subclasses.
616
     *
617
     * @return boolean true if this object exists
618
     */
619
    public function exists()
620
    {
621
        return (isset($this->record['ID']) && $this->record['ID'] > 0);
622
    }
623
624
    /**
625
     * Returns TRUE if all values (other than "ID") are
626
     * considered empty (by weak boolean comparison).
627
     *
628
     * @return boolean
629
     */
630
    public function isEmpty()
631
    {
632
        $fixed = DataObject::config()->uninherited('fixed_fields');
633
        foreach ($this->toMap() as $field => $value) {
634
            // only look at custom fields
635
            if (isset($fixed[$field])) {
636
                continue;
637
            }
638
639
            $dbObject = $this->dbObject($field);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $dbObject is correct as $this->dbObject($field) (which targets SilverStripe\ORM\DataObject::dbObject()) seems to always return null.

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

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

}

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

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

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

Loading history...
640
            if (!$dbObject) {
641
                continue;
642
            }
643
            if ($dbObject->exists()) {
644
                return false;
645
            }
646
        }
647
        return true;
648
    }
649
650
    /**
651
     * Pluralise this item given a specific count.
652
     *
653
     * E.g. "0 Pages", "1 File", "3 Images"
654
     *
655
     * @param string $count
656
     * @return string
657
     */
658
    public function i18n_pluralise($count)
659
    {
660
        $default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
661
        return i18n::_t(
662
            static::class.'.PLURALS',
663
            $default,
664
            [ 'count' => $count ]
665
        );
666
    }
667
668
    /**
669
     * Get the user friendly singular name of this DataObject.
670
     * If the name is not defined (by redefining $singular_name in the subclass),
671
     * this returns the class name.
672
     *
673
     * @return string User friendly singular name of this DataObject
674
     */
675
    public function singular_name()
676
    {
677
        $name = $this->stat('singular_name');
678
        if ($name) {
679
            return $name;
680
        }
681
        return ucwords(trim(strtolower(preg_replace(
682
            '/_?([A-Z])/',
683
            ' $1',
684
            ClassInfo::shortName($this)
685
        ))));
686
    }
687
688
    /**
689
     * Get the translated user friendly singular name of this DataObject
690
     * same as singular_name() but runs it through the translating function
691
     *
692
     * Translating string is in the form:
693
     *     $this->class.SINGULARNAME
694
     * Example:
695
     *     Page.SINGULARNAME
696
     *
697
     * @return string User friendly translated singular name of this DataObject
698
     */
699
    public function i18n_singular_name()
700
    {
701
        return _t(static::class.'.SINGULARNAME', $this->singular_name());
702
    }
703
704
    /**
705
     * Get the user friendly plural name of this DataObject
706
     * If the name is not defined (by renaming $plural_name in the subclass),
707
     * this returns a pluralised version of the class name.
708
     *
709
     * @return string User friendly plural name of this DataObject
710
     */
711
    public function plural_name()
712
    {
713
        if ($name = $this->stat('plural_name')) {
714
            return $name;
715
        }
716
        $name = $this->singular_name();
717
        //if the penultimate character is not a vowel, replace "y" with "ies"
718
        if (preg_match('/[^aeiou]y$/i', $name)) {
719
            $name = substr($name, 0, -1) . 'ie';
720
        }
721
        return ucfirst($name . 's');
722
    }
723
724
    /**
725
     * Get the translated user friendly plural name of this DataObject
726
     * Same as plural_name but runs it through the translation function
727
     * Translation string is in the form:
728
     *      $this->class.PLURALNAME
729
     * Example:
730
     *      Page.PLURALNAME
731
     *
732
     * @return string User friendly translated plural name of this DataObject
733
     */
734
    public function i18n_plural_name()
735
    {
736
        return _t(static::class.'.PLURALNAME', $this->plural_name());
737
    }
738
739
    /**
740
     * Standard implementation of a title/label for a specific
741
     * record. Tries to find properties 'Title' or 'Name',
742
     * and falls back to the 'ID'. Useful to provide
743
     * user-friendly identification of a record, e.g. in errormessages
744
     * or UI-selections.
745
     *
746
     * Overload this method to have a more specialized implementation,
747
     * e.g. for an Address record this could be:
748
     * <code>
749
     * function getTitle() {
750
     *   return "{$this->StreetNumber} {$this->StreetName} {$this->City}";
751
     * }
752
     * </code>
753
     *
754
     * @return string
755
     */
756
    public function getTitle()
757
    {
758
        $schema = static::getSchema();
759
        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...
760
            return $this->getField('Title');
761
        }
762
        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...
763
            return $this->getField('Name');
764
        }
765
766
        return "#{$this->ID}";
767
    }
768
769
    /**
770
     * Returns the associated database record - in this case, the object itself.
771
     * This is included so that you can call $dataOrController->data() and get a DataObject all the time.
772
     *
773
     * @return DataObject Associated database record
774
     */
775
    public function data()
776
    {
777
        return $this;
778
    }
779
780
    /**
781
     * Convert this object to a map.
782
     *
783
     * @return array The data as a map.
784
     */
785
    public function toMap()
786
    {
787
        $this->loadLazyFields();
788
        return $this->record;
789
    }
790
791
    /**
792
     * Return all currently fetched database fields.
793
     *
794
     * This function is similar to toMap() but doesn't trigger the lazy-loading of all unfetched fields.
795
     * Obviously, this makes it a lot faster.
796
     *
797
     * @return array The data as a map.
798
     */
799
    public function getQueriedDatabaseFields()
800
    {
801
        return $this->record;
802
    }
803
804
    /**
805
     * Update a number of fields on this object, given a map of the desired changes.
806
     *
807
     * The field names can be simple names, or you can use a dot syntax to access $has_one relations.
808
     * For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
809
     *
810
     * update() doesn't write the main object, but if you use the dot syntax, it will write()
811
     * the related objects that it alters.
812
     *
813
     * @param array $data A map of field name to data values to update.
814
     * @return DataObject $this
815
     */
816
    public function update($data)
817
    {
818
        foreach ($data as $k => $v) {
819
            // Implement dot syntax for updates
820
            if (strpos($k, '.') !== false) {
821
                $relations = explode('.', $k);
822
                $fieldName = array_pop($relations);
823
                $relObj = $this;
824
                $relation = null;
825
                foreach ($relations as $i => $relation) {
826
                    // no support for has_many or many_many relationships,
827
                    // as the updater wouldn't know which object to write to (or create)
828
                    if ($relObj->$relation() instanceof DataObject) {
829
                        $parentObj = $relObj;
830
                        $relObj = $relObj->$relation();
831
                        // If the intermediate relationship objects have been created, then write them
832
                        if ($i<sizeof($relation)-1 && !$relObj->ID || (!$relObj->ID && $parentObj != $this)) {
833
                            $relObj->write();
834
                            $relatedFieldName = $relation."ID";
835
                            $parentObj->$relatedFieldName = $relObj->ID;
836
                            $parentObj->write();
837
                        }
838
                    } else {
839
                        user_error(
840
                            "DataObject::update(): Can't traverse relationship '$relation'," .
841
                            "it has to be a has_one relationship or return a single DataObject",
842
                            E_USER_NOTICE
843
                        );
844
                        // unset relation object so we don't write properties to the wrong object
845
                        $relObj = null;
846
                        break;
847
                    }
848
                }
849
850
                if ($relObj) {
851
                    $relObj->$fieldName = $v;
852
                    $relObj->write();
853
                    $relatedFieldName = $relation."ID";
854
                    $this->$relatedFieldName = $relObj->ID;
855
                    $relObj->flushCache();
856
                } else {
857
                    user_error("Couldn't follow dot syntax '$k' on '$this->class' object", E_USER_WARNING);
858
                }
859
            } else {
860
                $this->$k = $v;
861
            }
862
        }
863
        return $this;
864
    }
865
866
    /**
867
     * Pass changes as a map, and try to
868
     * get automatic casting for these fields.
869
     * Doesn't write to the database. To write the data,
870
     * use the write() method.
871
     *
872
     * @param array $data A map of field name to data values to update.
873
     * @return DataObject $this
874
     */
875
    public function castedUpdate($data)
876
    {
877
        foreach ($data as $k => $v) {
878
            $this->setCastedField($k, $v);
879
        }
880
        return $this;
881
    }
882
883
    /**
884
     * Merges data and relations from another object of same class,
885
     * without conflict resolution. Allows to specify which
886
     * dataset takes priority in case its not empty.
887
     * has_one-relations are just transferred with priority 'right'.
888
     * has_many and many_many-relations are added regardless of priority.
889
     *
890
     * Caution: has_many/many_many relations are moved rather than duplicated,
891
     * meaning they are not connected to the merged object any longer.
892
     * Caution: Just saves updated has_many/many_many relations to the database,
893
     * doesn't write the updated object itself (just writes the object-properties).
894
     * Caution: Does not delete the merged object.
895
     * Caution: Does now overwrite Created date on the original object.
896
     *
897
     * @param DataObject $rightObj
898
     * @param string $priority left|right Determines who wins in case of a conflict (optional)
899
     * @param bool $includeRelations Merge any existing relations (optional)
900
     * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values.
901
     *                            Only applicable with $priority='right'. (optional)
902
     * @return Boolean
903
     */
904
    public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false)
905
    {
906
        $leftObj = $this;
907
908
        if ($leftObj->ClassName != $rightObj->ClassName) {
909
            // we can't merge similiar subclasses because they might have additional relations
910
            user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'
911
			(expected '{$leftObj->ClassName}').", E_USER_WARNING);
912
            return false;
913
        }
914
915
        if (!$rightObj->ID) {
916
            user_error("DataObject->merge(): Please write your merged-in object to the database before merging,
917
				to make sure all relations are transferred properly.').", E_USER_WARNING);
918
            return false;
919
        }
920
921
        // makes sure we don't merge data like ID or ClassName
922
        $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj));
923
        foreach ($rightData as $key => $rightSpec) {
924
            // Don't merge ID
925
            if ($key === 'ID') {
926
                continue;
927
            }
928
929
            // Only merge relations if allowed
930
            if ($rightSpec === 'ForeignKey' && !$includeRelations) {
931
                continue;
932
            }
933
934
            // don't merge conflicting values if priority is 'left'
935
            if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) {
936
                continue;
937
            }
938
939
            // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set)
940
            if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) {
941
                continue;
942
            }
943
944
            // TODO remove redundant merge of has_one fields
945
            $leftObj->{$key} = $rightObj->{$key};
946
        }
947
948
        // merge relations
949
        if ($includeRelations) {
950 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...
951
                foreach ($manyMany as $relationship => $class) {
952
                    $leftComponents = $leftObj->getManyManyComponents($relationship);
953
                    $rightComponents = $rightObj->getManyManyComponents($relationship);
954
                    if ($rightComponents && $rightComponents->exists()) {
955
                        $leftComponents->addMany($rightComponents->column('ID'));
956
                    }
957
                    $leftComponents->write();
958
                }
959
            }
960
961 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...
962
                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...
963
                    $leftComponents = $leftObj->getComponents($relationship);
964
                    $rightComponents = $rightObj->getComponents($relationship);
965
                    if ($rightComponents && $rightComponents->exists()) {
966
                        $leftComponents->addMany($rightComponents->column('ID'));
967
                    }
968
                    $leftComponents->write();
969
                }
970
            }
971
        }
972
973
        return true;
974
    }
975
976
    /**
977
     * Forces the record to think that all its data has changed.
978
     * Doesn't write to the database. Only sets fields as changed
979
     * if they are not already marked as changed.
980
     *
981
     * @return $this
982
     */
983
    public function forceChange()
984
    {
985
        // Ensure lazy fields loaded
986
        $this->loadLazyFields();
987
        $fields = static::getSchema()->fieldSpecs(static::class);
988
989
        // $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
990
        $fieldNames = array_unique(array_merge(
991
            array_keys($this->record),
992
            array_keys($fields)
993
        ));
994
995
        foreach ($fieldNames as $fieldName) {
996
            if (!isset($this->changed[$fieldName])) {
997
                $this->changed[$fieldName] = self::CHANGE_STRICT;
998
            }
999
            // Populate the null values in record so that they actually get written
1000
            if (!isset($this->record[$fieldName])) {
1001
                $this->record[$fieldName] = null;
1002
            }
1003
        }
1004
1005
        // @todo Find better way to allow versioned to write a new version after forceChange
1006
        if ($this->isChanged('Version')) {
1007
            unset($this->changed['Version']);
1008
        }
1009
        return $this;
1010
    }
1011
1012
    /**
1013
     * Validate the current object.
1014
     *
1015
     * By default, there is no validation - objects are always valid!  However, you can overload this method in your
1016
     * DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
1017
     *
1018
     * Invalid objects won't be able to be written - a warning will be thrown and no write will occur.  onBeforeWrite()
1019
     * and onAfterWrite() won't get called either.
1020
     *
1021
     * It is expected that you call validate() in your own application to test that an object is valid before
1022
     * attempting a write, and respond appropriately if it isn't.
1023
     *
1024
     * @see {@link ValidationResult}
1025
     * @return ValidationResult
1026
     */
1027
    public function validate()
1028
    {
1029
        $result = ValidationResult::create();
1030
        $this->extend('validate', $result);
1031
        return $result;
1032
    }
1033
1034
    /**
1035
     * Public accessor for {@see DataObject::validate()}
1036
     *
1037
     * @return ValidationResult
1038
     */
1039
    public function doValidate()
1040
    {
1041
        Deprecation::notice('5.0', 'Use validate');
1042
        return $this->validate();
1043
    }
1044
1045
    /**
1046
     * Event handler called before writing to the database.
1047
     * You can overload this to clean up or otherwise process data before writing it to the
1048
     * database.  Don't forget to call parent::onBeforeWrite(), though!
1049
     *
1050
     * This called after {@link $this->validate()}, so you can be sure that your data is valid.
1051
     *
1052
     * @uses DataExtension->onBeforeWrite()
1053
     */
1054
    protected function onBeforeWrite()
1055
    {
1056
        $this->brokenOnWrite = false;
1057
1058
        $dummy = null;
1059
        $this->extend('onBeforeWrite', $dummy);
1060
    }
1061
1062
    /**
1063
     * Event handler called after writing to the database.
1064
     * You can overload this to act upon changes made to the data after it is written.
1065
     * $this->changed will have a record
1066
     * database.  Don't forget to call parent::onAfterWrite(), though!
1067
     *
1068
     * @uses DataExtension->onAfterWrite()
1069
     */
1070
    protected function onAfterWrite()
1071
    {
1072
        $dummy = null;
1073
        $this->extend('onAfterWrite', $dummy);
1074
    }
1075
1076
    /**
1077
     * Event handler called before deleting from the database.
1078
     * You can overload this to clean up or otherwise process data before delete this
1079
     * record.  Don't forget to call parent::onBeforeDelete(), though!
1080
     *
1081
     * @uses DataExtension->onBeforeDelete()
1082
     */
1083
    protected function onBeforeDelete()
1084
    {
1085
        $this->brokenOnDelete = false;
1086
1087
        $dummy = null;
1088
        $this->extend('onBeforeDelete', $dummy);
1089
    }
1090
1091
    protected function onAfterDelete()
1092
    {
1093
        $this->extend('onAfterDelete');
1094
    }
1095
1096
    /**
1097
     * Load the default values in from the self::$defaults array.
1098
     * Will traverse the defaults of the current class and all its parent classes.
1099
     * Called by the constructor when creating new records.
1100
     *
1101
     * @uses DataExtension->populateDefaults()
1102
     * @return DataObject $this
1103
     */
1104
    public function populateDefaults()
1105
    {
1106
        $classes = array_reverse(ClassInfo::ancestry($this));
1107
1108
        foreach ($classes as $class) {
1109
            $defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
1110
1111
            if ($defaults && !is_array($defaults)) {
1112
                user_error(
1113
                    "Bad '$this->class' defaults given: " . var_export($defaults, true),
1114
                    E_USER_WARNING
1115
                );
1116
                $defaults = null;
1117
            }
1118
1119
            if ($defaults) {
1120
                foreach ($defaults as $fieldName => $fieldValue) {
1121
                // SRM 2007-03-06: Stricter check
1122
                    if (!isset($this->$fieldName) || $this->$fieldName === null) {
1123
                        $this->$fieldName = $fieldValue;
1124
                    }
1125
                // Set many-many defaults with an array of ids
1126
                    if (is_array($fieldValue) && $this->getSchema()->manyManyComponent(static::class, $fieldName)) {
1127
                        /** @var ManyManyList $manyManyJoin */
1128
                        $manyManyJoin = $this->$fieldName();
1129
                        $manyManyJoin->setByIDList($fieldValue);
1130
                    }
1131
                }
1132
            }
1133
            if ($class == self::class) {
1134
                break;
1135
            }
1136
        }
1137
1138
        $this->extend('populateDefaults');
1139
        return $this;
1140
    }
1141
1142
    /**
1143
     * Determine validation of this object prior to write
1144
     *
1145
     * @return ValidationException Exception generated by this write, or null if valid
1146
     */
1147
    protected function validateWrite()
1148
    {
1149
        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...
1150
            return new ValidationException(
1151
                "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...
1152
                "you need to change the ClassName before you can write it"
1153
            );
1154
        }
1155
1156
        // Note: Validation can only be disabled at the global level, not per-model
1157
        if (DataObject::config()->uninherited('validation_enabled')) {
1158
            $result = $this->validate();
1159
            if (!$result->isValid()) {
1160
                return new ValidationException($result);
1161
            }
1162
        }
1163
        return null;
1164
    }
1165
1166
    /**
1167
     * Prepare an object prior to write
1168
     *
1169
     * @throws ValidationException
1170
     */
1171
    protected function preWrite()
1172
    {
1173
        // Validate this object
1174
        if ($writeException = $this->validateWrite()) {
1175
            // Used by DODs to clean up after themselves, eg, Versioned
1176
            $this->invokeWithExtensions('onAfterSkippedWrite');
1177
            throw $writeException;
1178
        }
1179
1180
        // Check onBeforeWrite
1181
        $this->brokenOnWrite = true;
1182
        $this->onBeforeWrite();
1183
        if ($this->brokenOnWrite) {
1184
            user_error("$this->class has a broken onBeforeWrite() function."
1185
                . " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
1186
        }
1187
    }
1188
1189
    /**
1190
     * Detects and updates all changes made to this object
1191
     *
1192
     * @param bool $forceChanges If set to true, force all fields to be treated as changed
1193
     * @return bool True if any changes are detected
1194
     */
1195
    protected function updateChanges($forceChanges = false)
1196
    {
1197
        if ($forceChanges) {
1198
            // Force changes, but only for loaded fields
1199
            foreach ($this->record as $field => $value) {
1200
                $this->changed[$field] = static::CHANGE_VALUE;
1201
            }
1202
            return true;
1203
        }
1204
        return $this->isChanged();
1205
    }
1206
1207
    /**
1208
     * Writes a subset of changes for a specific table to the given manipulation
1209
     *
1210
     * @param string $baseTable Base table
1211
     * @param string $now Timestamp to use for the current time
1212
     * @param bool $isNewRecord Whether this should be treated as a new record write
1213
     * @param array $manipulation Manipulation to write to
1214
     * @param string $class Class of table to manipulate
1215
     */
1216
    protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class)
1217
    {
1218
        $schema = $this->getSchema();
1219
        $table = $schema->tableName($class);
1220
        $manipulation[$table] = array();
1221
1222
        // Extract records for this table
1223
        foreach ($this->record as $fieldName => $fieldValue) {
1224
            // we're not attempting to reset the BaseTable->ID
1225
            // Ignore unchanged fields or attempts to reset the BaseTable->ID
1226
            if (empty($this->changed[$fieldName]) || ($table === $baseTable && $fieldName === 'ID')) {
1227
                continue;
1228
            }
1229
1230
            // Ensure this field pertains to this table
1231
            $specification = $schema->fieldSpec($class, $fieldName, DataObjectSchema::DB_ONLY | DataObjectSchema::UNINHERITED);
1232
            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...
1233
                continue;
1234
            }
1235
1236
            // if database column doesn't correlate to a DBField instance...
1237
            $fieldObj = $this->dbObject($fieldName);
1238
            if (!$fieldObj) {
1239
                $fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
1240
            }
1241
1242
            // Write to manipulation
1243
            $fieldObj->writeToManipulation($manipulation[$table]);
1244
        }
1245
1246
        // Ensure update of Created and LastEdited columns
1247
        if ($baseTable === $table) {
1248
            $manipulation[$table]['fields']['LastEdited'] = $now;
1249
            if ($isNewRecord) {
1250
                $manipulation[$table]['fields']['Created']
1251
                    = empty($this->record['Created'])
1252
                        ? $now
1253
                        : $this->record['Created'];
1254
                $manipulation[$table]['fields']['ClassName'] = $this->class;
1255
            }
1256
        }
1257
1258
        // Inserts done one the base table are performed in another step, so the manipulation should instead
1259
        // attempt an update, as though it were a normal update.
1260
        $manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
1261
        $manipulation[$table]['id'] = $this->record['ID'];
1262
        $manipulation[$table]['class'] = $class;
1263
    }
1264
1265
    /**
1266
     * Ensures that a blank base record exists with the basic fixed fields for this dataobject
1267
     *
1268
     * Does nothing if an ID is already assigned for this record
1269
     *
1270
     * @param string $baseTable Base table
1271
     * @param string $now Timestamp to use for the current time
1272
     */
1273
    protected function writeBaseRecord($baseTable, $now)
1274
    {
1275
        // Generate new ID if not specified
1276
        if ($this->isInDB()) {
1277
            return;
1278
        }
1279
1280
        // Perform an insert on the base table
1281
        $insert = new SQLInsert('"'.$baseTable.'"');
1282
        $insert
1283
            ->assign('"Created"', $now)
1284
            ->execute();
1285
        $this->changed['ID'] = self::CHANGE_VALUE;
1286
        $this->record['ID'] = DB::get_generated_id($baseTable);
1287
    }
1288
1289
    /**
1290
     * Generate and write the database manipulation for all changed fields
1291
     *
1292
     * @param string $baseTable Base table
1293
     * @param string $now Timestamp to use for the current time
1294
     * @param bool $isNewRecord If this is a new record
1295
     */
1296
    protected function writeManipulation($baseTable, $now, $isNewRecord)
1297
    {
1298
        // Generate database manipulations for each class
1299
        $manipulation = array();
1300
        foreach (ClassInfo::ancestry(static::class, true) as $class) {
1301
            $this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
1302
        }
1303
1304
        // Allow extensions to extend this manipulation
1305
        $this->extend('augmentWrite', $manipulation);
1306
1307
        // New records have their insert into the base data table done first, so that they can pass the
1308
        // generated ID on to the rest of the manipulation
1309
        if ($isNewRecord) {
1310
            $manipulation[$baseTable]['command'] = 'update';
1311
        }
1312
1313
        // Perform the manipulation
1314
        DB::manipulate($manipulation);
1315
    }
1316
1317
    /**
1318
     * Writes all changes to this object to the database.
1319
     *  - It will insert a record whenever ID isn't set, otherwise update.
1320
     *  - All relevant tables will be updated.
1321
     *  - $this->onBeforeWrite() gets called beforehand.
1322
     *  - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
1323
     *
1324
     *  @uses DataExtension->augmentWrite()
1325
     *
1326
     * @param boolean $showDebug Show debugging information
1327
     * @param boolean $forceInsert Run INSERT command rather than UPDATE, even if record already exists
1328
     * @param boolean $forceWrite Write to database even if there are no changes
1329
     * @param boolean $writeComponents Call write() on all associated component instances which were previously
1330
     *                                 retrieved through {@link getComponent()}, {@link getComponents()} or
1331
     *                                 {@link getManyManyComponents()} (Default: false)
1332
     * @return int The ID of the record
1333
     * @throws ValidationException Exception that can be caught and handled by the calling function
1334
     */
1335
    public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false)
1336
    {
1337
        $now = DBDatetime::now()->Rfc2822();
1338
1339
        // Execute pre-write tasks
1340
        $this->preWrite();
1341
1342
        // Check if we are doing an update or an insert
1343
        $isNewRecord = !$this->isInDB() || $forceInsert;
1344
1345
        // Check changes exist, abort if there are none
1346
        $hasChanges = $this->updateChanges($isNewRecord);
1347
        if ($hasChanges || $forceWrite || $isNewRecord) {
1348
            // New records have their insert into the base data table done first, so that they can pass the
1349
            // generated primary key on to the rest of the manipulation
1350
            $baseTable = $this->baseTable();
1351
            $this->writeBaseRecord($baseTable, $now);
1352
1353
            // Write the DB manipulation for all changed fields
1354
            $this->writeManipulation($baseTable, $now, $isNewRecord);
1355
1356
            // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1357
            $this->writeRelations();
1358
            $this->onAfterWrite();
1359
            $this->changed = array();
1360
        } else {
1361
            if ($showDebug) {
1362
                Debug::message("no changes for DataObject");
1363
            }
1364
1365
            // Used by DODs to clean up after themselves, eg, Versioned
1366
            $this->invokeWithExtensions('onAfterSkippedWrite');
1367
        }
1368
1369
        // Ensure Created and LastEdited are populated
1370
        if (!isset($this->record['Created'])) {
1371
            $this->record['Created'] = $now;
1372
        }
1373
        $this->record['LastEdited'] = $now;
1374
1375
        // Write relations as necessary
1376
        if ($writeComponents) {
1377
            $this->writeComponents(true);
1378
        }
1379
1380
        // Clears the cache for this object so get_one returns the correct object.
1381
        $this->flushCache();
1382
1383
        return $this->record['ID'];
1384
    }
1385
1386
    /**
1387
     * Writes cached relation lists to the database, if possible
1388
     */
1389
    public function writeRelations()
1390
    {
1391
        if (!$this->isInDB()) {
1392
            return;
1393
        }
1394
1395
        // If there's any relations that couldn't be saved before, save them now (we have an ID here)
1396
        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...
1397
            foreach ($this->unsavedRelations as $name => $list) {
1398
                $list->changeToList($this->$name());
1399
            }
1400
            $this->unsavedRelations = array();
1401
        }
1402
    }
1403
1404
    /**
1405
     * Write the cached components to the database. Cached components could refer to two different instances of the
1406
     * same record.
1407
     *
1408
     * @param bool $recursive Recursively write components
1409
     * @return DataObject $this
1410
     */
1411
    public function writeComponents($recursive = false)
1412
    {
1413
        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...
1414
            foreach ($this->components as $component) {
1415
                $component->write(false, false, false, $recursive);
1416
            }
1417
        }
1418
1419
        if ($join = $this->getJoin()) {
1420
            $join->write(false, false, false, $recursive);
1421
        }
1422
1423
        return $this;
1424
    }
1425
1426
    /**
1427
     * Delete this data object.
1428
     * $this->onBeforeDelete() gets called.
1429
     * Note that in Versioned objects, both Stage and Live will be deleted.
1430
     *  @uses DataExtension->augmentSQL()
1431
     */
1432
    public function delete()
1433
    {
1434
        $this->brokenOnDelete = true;
1435
        $this->onBeforeDelete();
1436
        if ($this->brokenOnDelete) {
1437
            user_error("$this->class has a broken onBeforeDelete() function."
1438
                . " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
1439
        }
1440
1441
        // Deleting a record without an ID shouldn't do anything
1442
        if (!$this->ID) {
1443
            throw new LogicException("DataObject::delete() called on a DataObject without an ID");
1444
        }
1445
1446
        // TODO: This is quite ugly.  To improve:
1447
        //  - move the details of the delete code in the DataQuery system
1448
        //  - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest
1449
        //    obviously, that means getting requireTable() to configure cascading deletes ;-)
1450
        $srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
1451
        foreach ($srcQuery->queriedTables() as $table) {
1452
            $delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
1453
            $delete->execute();
1454
        }
1455
        // Remove this item out of any caches
1456
        $this->flushCache();
1457
1458
        $this->onAfterDelete();
1459
1460
        $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...
1461
        $this->ID = 0;
1462
    }
1463
1464
    /**
1465
     * Delete the record with the given ID.
1466
     *
1467
     * @param string $className The class name of the record to be deleted
1468
     * @param int $id ID of record to be deleted
1469
     */
1470
    public static function delete_by_id($className, $id)
1471
    {
1472
        $obj = DataObject::get_by_id($className, $id);
1473
        if ($obj) {
1474
            $obj->delete();
1475
        } else {
1476
            user_error("$className object #$id wasn't found when calling DataObject::delete_by_id", E_USER_WARNING);
1477
        }
1478
    }
1479
1480
    /**
1481
     * Get the class ancestry, including the current class name.
1482
     * The ancestry will be returned as an array of class names, where the 0th element
1483
     * will be the class that inherits directly from DataObject, and the last element
1484
     * will be the current class.
1485
     *
1486
     * @return array Class ancestry
1487
     */
1488
    public function getClassAncestry()
1489
    {
1490
        return ClassInfo::ancestry(static::class);
1491
    }
1492
1493
    /**
1494
     * Return a component object from a one to one relationship, as a DataObject.
1495
     * If no component is available, an 'empty component' will be returned for
1496
     * non-polymorphic relations, or for polymorphic relations with a class set.
1497
     *
1498
     * @param string $componentName Name of the component
1499
     * @return DataObject The component object. It's exact type will be that of the component.
1500
     * @throws Exception
1501
     */
1502
    public function getComponent($componentName)
1503
    {
1504
        if (isset($this->components[$componentName])) {
1505
            return $this->components[$componentName];
1506
        }
1507
1508
        $schema = static::getSchema();
1509
        if ($class = $schema->hasOneComponent(static::class, $componentName)) {
1510
            $joinField = $componentName . 'ID';
1511
            $joinID    = $this->getField($joinField);
1512
1513
            // Extract class name for polymorphic relations
1514
            if ($class === self::class) {
1515
                $class = $this->getField($componentName . 'Class');
1516
                if (empty($class)) {
1517
                    return null;
1518
                }
1519
            }
1520
1521
            if ($joinID) {
1522
                // Ensure that the selected object originates from the same stage, subsite, etc
1523
                $component = DataObject::get($class)
1524
                    ->filter('ID', $joinID)
1525
                    ->setDataQueryParam($this->getInheritableQueryParams())
1526
                    ->first();
1527
            }
1528
1529
            if (empty($component)) {
1530
                $component = $this->model->$class->newObject();
1531
            }
1532
        } elseif ($class = $schema->belongsToComponent(static::class, $componentName)) {
1533
            $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'belongs_to', $polymorphic);
1534
            $joinID = $this->ID;
1535
1536
            if ($joinID) {
1537
                // Prepare filter for appropriate join type
1538
                if ($polymorphic) {
1539
                    $filter = array(
1540
                        "{$joinField}ID" => $joinID,
1541
                        "{$joinField}Class" => $this->class
1542
                    );
1543
                } else {
1544
                    $filter = array(
1545
                        $joinField => $joinID
1546
                    );
1547
                }
1548
1549
                // Ensure that the selected object originates from the same stage, subsite, etc
1550
                $component = DataObject::get($class)
1551
                    ->filter($filter)
1552
                    ->setDataQueryParam($this->getInheritableQueryParams())
1553
                    ->first();
1554
            }
1555
1556
            if (empty($component)) {
1557
                $component = $this->model->$class->newObject();
1558
                if ($polymorphic) {
1559
                    $component->{$joinField.'ID'} = $this->ID;
1560
                    $component->{$joinField.'Class'} = $this->class;
1561
                } else {
1562
                    $component->$joinField = $this->ID;
1563
                }
1564
            }
1565
        } else {
1566
            throw new InvalidArgumentException(
1567
                "DataObject->getComponent(): Could not find component '$componentName'."
1568
            );
1569
        }
1570
1571
        $this->components[$componentName] = $component;
1572
        return $component;
1573
    }
1574
1575
    /**
1576
     * Returns a one-to-many relation as a HasManyList
1577
     *
1578
     * @param string $componentName Name of the component
1579
     * @return HasManyList|UnsavedRelationList The components of the one-to-many relationship.
1580
     */
1581
    public function getComponents($componentName)
1582
    {
1583
        $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...
1584
1585
        $schema = $this->getSchema();
1586
        $componentClass = $schema->hasManyComponent(static::class, $componentName);
1587
        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...
1588
            throw new InvalidArgumentException(sprintf(
1589
                "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'",
1590
                $componentName,
1591
                $this->class
1592
            ));
1593
        }
1594
1595
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1596 View Code Duplication
        if (!$this->ID) {
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...
1597
            if (!isset($this->unsavedRelations[$componentName])) {
1598
                $this->unsavedRelations[$componentName] =
1599
                    new UnsavedRelationList($this->class, $componentName, $componentClass);
0 ignored issues
show
Documentation introduced by
$this->class is of type string, but the function expects a array.

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...
1600
            }
1601
            return $this->unsavedRelations[$componentName];
1602
        }
1603
1604
        // Determine type and nature of foreign relation
1605
        $joinField = $schema->getRemoteJoinField(static::class, $componentName, 'has_many', $polymorphic);
1606
        /** @var HasManyList $result */
1607
        if ($polymorphic) {
1608
            $result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
1609
        } else {
1610
            $result = HasManyList::create($componentClass, $joinField);
1611
        }
1612
1613
        if ($this->model) {
1614
            $result->setDataModel($this->model);
1615
        }
1616
1617
        return $result
1618
            ->setDataQueryParam($this->getInheritableQueryParams())
1619
            ->forForeignID($this->ID);
1620
    }
1621
1622
    /**
1623
     * Find the foreign class of a relation on this DataObject, regardless of the relation type.
1624
     *
1625
     * @param string $relationName Relation name.
1626
     * @return string Class name, or null if not found.
1627
     */
1628
    public function getRelationClass($relationName)
1629
    {
1630
        // Parse many_many
1631
        $manyManyComponent = $this->getSchema()->manyManyComponent(static::class, $relationName);
1632
        if ($manyManyComponent) {
1633
            list(
1634
                $relationClass, $parentClass, $componentClass,
0 ignored issues
show
Unused Code introduced by
The assignment to $relationClass is unused. Consider omitting it like so list($first,,$third).

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

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

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

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

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

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

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

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

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

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

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1776
                    = $remote->getSchema()->manyManyComponent($remoteClass, $remoteRelation);
1777
                $extraFields = $schema->manyManyExtraFieldsForComponent($remoteClass, $remoteRelation) ?: array();
1778
1779
                // Reverse parent and component fields and create an inverse ManyManyList
1780
                /** @var RelationList $result */
1781
                $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...
1782
                    $relationClass,
1783
                    $componentClass,
1784
                    $table,
1785
                    $componentField,
1786
                    $parentField,
1787
                    $extraFields
1788
                );
1789
                if ($this->model) {
1790
                    $result->setDataModel($this->model);
1791
                }
1792
                $this->extend('updateManyManyComponents', $result);
1793
1794
                // If this is called on a singleton, then we return an 'orphaned relation' that can have the
1795
                // foreignID set elsewhere.
1796
                return $result
1797
                    ->setDataQueryParam($this->getInheritableQueryParams())
1798
                    ->forForeignID($this->ID);
1799
            }
1800
            default: {
1801
                return null;
1802
            }
1803
        }
1804
    }
1805
1806
    /**
1807
     * Returns a many-to-many component, as a ManyManyList.
1808
     * @param string $componentName Name of the many-many component
1809
     * @return RelationList|UnsavedRelationList The set of components
1810
     */
1811
    public function getManyManyComponents($componentName)
1812
    {
1813
        $schema = static::getSchema();
1814
        $manyManyComponent = $schema->manyManyComponent(static::class, $componentName);
1815
        if (!$manyManyComponent) {
1816
            throw new InvalidArgumentException(sprintf(
1817
                "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'",
1818
                $componentName,
1819
                $this->class
1820
            ));
1821
        }
1822
1823
        list($relationClass, $parentClass, $componentClass, $parentField, $componentField, $tableOrClass)
1824
            = $manyManyComponent;
1825
1826
        // If we haven't been written yet, we can't save these relations, so use a list that handles this case
1827 View Code Duplication
        if (!$this->ID) {
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...
1828
            if (!isset($this->unsavedRelations[$componentName])) {
1829
                $this->unsavedRelations[$componentName] =
1830
                    new UnsavedRelationList($parentClass, $componentName, $componentClass);
0 ignored issues
show
Documentation introduced by
$parentClass is of type string, but the function expects a array.

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

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2531
        if (isset($results)) {
2532
            return $results;
2533
        }
2534
2535
        return ($member && Permission::checkMember($member, $perm));
2536
    }
2537
2538
    /**
2539
     * Process tri-state responses from permission-alterting extensions.  The extensions are
2540
     * expected to return one of three values:
2541
     *
2542
     *  - false: Disallow this permission, regardless of what other extensions say
2543
     *  - true: Allow this permission, as long as no other extensions return false
2544
     *  - NULL: Don't affect the outcome
2545
     *
2546
     * This method itself returns a tri-state value, and is designed to be used like this:
2547
     *
2548
     * <code>
2549
     * $extended = $this->extendedCan('canDoSomething', $member);
2550
     * if($extended !== null) return $extended;
2551
     * else return $normalValue;
2552
     * </code>
2553
     *
2554
     * @param string $methodName Method on the same object, e.g. {@link canEdit()}
2555
     * @param Member|int $member
2556
     * @param array $context Optional context
2557
     * @return boolean|null
2558
     */
2559
    public function extendedCan($methodName, $member, $context = array())
2560
    {
2561
        $results = $this->extend($methodName, $member, $context);
2562
        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...
2563
            // Remove NULLs
2564
            $results = array_filter($results, function ($v) {
2565
                return !is_null($v);
2566
            });
2567
            // If there are any non-NULL responses, then return the lowest one of them.
2568
            // If any explicitly deny the permission, then we don't get access
2569
            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...
2570
                return min($results);
2571
            }
2572
        }
2573
        return null;
2574
    }
2575
2576
    /**
2577
     * @param Member $member
2578
     * @return boolean
2579
     */
2580 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...
2581
    {
2582
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2580 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...
2583
        if ($extended !== null) {
2584
            return $extended;
2585
        }
2586
        return Permission::check('ADMIN', 'any', $member);
2587
    }
2588
2589
    /**
2590
     * @param Member $member
2591
     * @return boolean
2592
     */
2593 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...
2594
    {
2595
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2593 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...
2596
        if ($extended !== null) {
2597
            return $extended;
2598
        }
2599
        return Permission::check('ADMIN', 'any', $member);
2600
    }
2601
2602
    /**
2603
     * @param Member $member
2604
     * @return boolean
2605
     */
2606 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...
2607
    {
2608
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2606 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...
2609
        if ($extended !== null) {
2610
            return $extended;
2611
        }
2612
        return Permission::check('ADMIN', 'any', $member);
2613
    }
2614
2615
    /**
2616
     * @param Member $member
2617
     * @param array $context Additional context-specific data which might
2618
     * affect whether (or where) this object could be created.
2619
     * @return boolean
2620
     */
2621 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...
2622
    {
2623
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 2621 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...
2624
        if ($extended !== null) {
2625
            return $extended;
2626
        }
2627
        return Permission::check('ADMIN', 'any', $member);
2628
    }
2629
2630
    /**
2631
     * Debugging used by Debug::show()
2632
     *
2633
     * @return string HTML data representing this object
2634
     */
2635
    public function debug()
2636
    {
2637
        $val = "<h3>Database record: $this->class</h3>\n<ul>\n";
2638
        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...
2639
            foreach ($this->record as $fieldName => $fieldVal) {
2640
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
2641
            }
2642
        }
2643
        $val .= "</ul>\n";
2644
        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...
2645
    }
2646
2647
    /**
2648
     * Return the DBField object that represents the given field.
2649
     * This works similarly to obj() with 2 key differences:
2650
     *   - it still returns an object even when the field has no value.
2651
     *   - it only matches fields and not methods
2652
     *   - it matches foreign keys generated by has_one relationships, eg, "ParentID"
2653
     *
2654
     * @param string $fieldName Name of the field
2655
     * @return DBField The field as a DBField object
2656
     */
2657
    public function dbObject($fieldName)
2658
    {
2659
        // Check for field in DB
2660
        $schema = static::getSchema();
2661
        $helper = $schema->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS);
2662
        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...
2663
            return null;
2664
        }
2665
2666 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...
2667
            $tableClass = $this->record[$fieldName . '_Lazy'];
2668
            $this->loadLazyFields($tableClass);
2669
        }
2670
2671
        $value = isset($this->record[$fieldName])
2672
            ? $this->record[$fieldName]
2673
            : null;
2674
2675
        // If we have a DBField object in $this->record, then return that
2676
        if ($value instanceof DBField) {
2677
            return $value;
2678
        }
2679
2680
        list($class, $spec) = explode('.', $helper);
2681
        /** @var DBField $obj */
2682
        $table = $schema->tableName($class);
2683
        $obj = Object::create_from_string($spec, $fieldName);
2684
        $obj->setTable($table);
2685
        $obj->setValue($value, $this, false);
2686
        return $obj;
2687
    }
2688
2689
    /**
2690
     * Traverses to a DBField referenced by relationships between data objects.
2691
     *
2692
     * The path to the related field is specified with dot separated syntax
2693
     * (eg: Parent.Child.Child.FieldName).
2694
     *
2695
     * @param string $fieldPath
2696
     *
2697
     * @return mixed DBField of the field on the object or a DataList instance.
2698
     */
2699
    public function relObject($fieldPath)
2700
    {
2701
        $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...
2702
2703
        if (strpos($fieldPath, '.') !== false) {
2704
            $parts = explode('.', $fieldPath);
2705
            $fieldName = array_pop($parts);
2706
2707
            // Traverse dot syntax
2708
            $component = $this;
2709
2710
            foreach ($parts as $relation) {
2711
                if ($component instanceof SS_List) {
2712
                    if (method_exists($component, $relation)) {
2713
                        $component = $component->$relation();
2714
                    } else {
2715
                        $component = $component->relation($relation);
2716
                    }
2717
                } else {
2718
                    $component = $component->$relation();
2719
                }
2720
            }
2721
2722
            $object = $component->dbObject($fieldName);
2723
        } else {
2724
            $object = $this->dbObject($fieldPath);
2725
        }
2726
2727
        return $object;
2728
    }
2729
2730
    /**
2731
     * Traverses to a field referenced by relationships between data objects, returning the value
2732
     * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2733
     *
2734
     * @param $fieldName string
2735
     * @return string | null - will return null on a missing value
2736
     */
2737
    public function relField($fieldName)
2738
    {
2739
        $component = $this;
2740
2741
        // We're dealing with relations here so we traverse the dot syntax
2742
        if (strpos($fieldName, '.') !== false) {
2743
            $relations = explode('.', $fieldName);
2744
            $fieldName = array_pop($relations);
2745
            foreach ($relations as $relation) {
2746
                // Inspect $component for element $relation
2747
                if ($component->hasMethod($relation)) {
2748
                    // Check nested method
2749
                    $component = $component->$relation();
2750
                } elseif ($component instanceof SS_List) {
2751
                    // Select adjacent relation from DataList
2752
                    $component = $component->relation($relation);
2753
                } elseif ($component instanceof DataObject
2754
                    && ($dbObject = $component->dbObject($relation))
2755
                ) {
2756
                    // Select db object
2757
                    $component = $dbObject;
2758
                } else {
2759
                    user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
2760
                }
2761
            }
2762
        }
2763
2764
        // Bail if the component is null
2765
        if (!$component) {
2766
            return null;
2767
        }
2768
        if ($component->hasMethod($fieldName)) {
2769
            return $component->$fieldName();
2770
        }
2771
        return $component->$fieldName;
2772
    }
2773
2774
    /**
2775
     * Temporary hack to return an association name, based on class, to get around the mangle
2776
     * of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
2777
     *
2778
     * @param string $className
2779
     * @return string
2780
     */
2781
    public function getReverseAssociation($className)
2782
    {
2783
        if (is_array($this->manyMany())) {
2784
            $many_many = array_flip($this->manyMany());
2785
            if (array_key_exists($className, $many_many)) {
2786
                return $many_many[$className];
2787
            }
2788
        }
2789
        if (is_array($this->hasMany())) {
2790
            $has_many = array_flip($this->hasMany());
2791
            if (array_key_exists($className, $has_many)) {
2792
                return $has_many[$className];
2793
            }
2794
        }
2795
        if (is_array($this->hasOne())) {
2796
            $has_one = array_flip($this->hasOne());
2797
            if (array_key_exists($className, $has_one)) {
2798
                return $has_one[$className];
2799
            }
2800
        }
2801
2802
        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...
2803
    }
2804
2805
    /**
2806
     * Return all objects matching the filter
2807
     * sub-classes are automatically selected and included
2808
     *
2809
     * @param string $callerClass The class of objects to be returned
2810
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2811
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2812
     * @param string|array $sort A sort expression to be inserted into the ORDER
2813
     * BY clause.  If omitted, self::$default_sort will be used.
2814
     * @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
2815
     * @param string|array $limit A limit expression to be inserted into the LIMIT clause.
2816
     * @param string $containerClass The container class to return the results in.
2817
     *
2818
     * @todo $containerClass is Ignored, why?
2819
     *
2820
     * @return DataList The objects matching the filter, in the class specified by $containerClass
2821
     */
2822
    public static function get(
2823
        $callerClass = null,
2824
        $filter = "",
2825
        $sort = "",
2826
        $join = "",
2827
        $limit = null,
2828
        $containerClass = DataList::class
2829
    ) {
2830
2831
        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...
2832
            $callerClass = get_called_class();
2833
            if ($callerClass == self::class) {
2834
                throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
2835
            }
2836
2837
            if ($filter || $sort || $join || $limit || ($containerClass != DataList::class)) {
2838
                throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
2839
                    . ' arguments');
2840
            }
2841
2842
            $result = DataList::create(get_called_class());
2843
            $result->setDataModel(DataModel::inst());
2844
            return $result;
2845
        }
2846
2847
        if ($join) {
2848
            throw new \InvalidArgumentException(
2849
                'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
2850
            );
2851
        }
2852
2853
        $result = DataList::create($callerClass)->where($filter)->sort($sort);
2854
2855
        if ($limit && strpos($limit, ',') !== false) {
2856
            $limitArguments = explode(',', $limit);
2857
            $result = $result->limit($limitArguments[1], $limitArguments[0]);
2858
        } elseif ($limit) {
2859
            $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...
2860
        }
2861
2862
        $result->setDataModel(DataModel::inst());
2863
        return $result;
2864
    }
2865
2866
2867
    /**
2868
     * Return the first item matching the given query.
2869
     * All calls to get_one() are cached.
2870
     *
2871
     * @param string $callerClass The class of objects to be returned
2872
     * @param string|array $filter A filter to be inserted into the WHERE clause.
2873
     * Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
2874
     * @param boolean $cache Use caching
2875
     * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
2876
     *
2877
     * @return DataObject The first item matching the query
2878
     */
2879
    public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "")
2880
    {
2881
        $SNG = singleton($callerClass);
2882
2883
        $cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
2884
        $cacheKey = md5(var_export($cacheComponents, true));
2885
2886
        // Flush destroyed items out of the cache
2887
        if ($cache && isset(self::$_cache_get_one[$callerClass][$cacheKey])
2888
                && self::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
2889
                && self::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
2890
            self::$_cache_get_one[$callerClass][$cacheKey] = false;
2891
        }
2892
        $item = null;
2893
        if (!$cache || !isset(self::$_cache_get_one[$callerClass][$cacheKey])) {
2894
            $dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
2895
            $item = $dl->first();
2896
2897
            if ($cache) {
2898
                self::$_cache_get_one[$callerClass][$cacheKey] = $item;
2899
                if (!self::$_cache_get_one[$callerClass][$cacheKey]) {
2900
                    self::$_cache_get_one[$callerClass][$cacheKey] = false;
2901
                }
2902
            }
2903
        }
2904
        return $cache ? self::$_cache_get_one[$callerClass][$cacheKey] : $item;
2905
    }
2906
2907
    /**
2908
     * Flush the cached results for all relations (has_one, has_many, many_many)
2909
     * Also clears any cached aggregate data.
2910
     *
2911
     * @param boolean $persistent When true will also clear persistent data stored in the Cache system.
2912
     *                            When false will just clear session-local cached data
2913
     * @return DataObject $this
2914
     */
2915
    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...
2916
    {
2917
        if ($this->class == self::class) {
2918
            self::$_cache_get_one = array();
2919
            return $this;
2920
        }
2921
2922
        $classes = ClassInfo::ancestry($this->class);
2923
        foreach ($classes as $class) {
2924
            if (isset(self::$_cache_get_one[$class])) {
2925
                unset(self::$_cache_get_one[$class]);
2926
            }
2927
        }
2928
2929
        $this->extend('flushCache');
2930
2931
        $this->components = array();
2932
        return $this;
2933
    }
2934
2935
    /**
2936
     * Flush the get_one global cache and destroy associated objects.
2937
     */
2938
    public static function flush_and_destroy_cache()
2939
    {
2940
        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...
2941
            foreach (self::$_cache_get_one as $class => $items) {
2942
                if (is_array($items)) {
2943
                    foreach ($items as $item) {
2944
                        if ($item) {
2945
                            $item->destroy();
2946
                        }
2947
                    }
2948
                }
2949
            }
2950
        }
2951
        self::$_cache_get_one = array();
2952
    }
2953
2954
    /**
2955
     * Reset all global caches associated with DataObject.
2956
     */
2957
    public static function reset()
2958
    {
2959
        // @todo Decouple these
2960
        DBClassName::clear_classname_cache();
2961
        ClassInfo::reset_db_cache();
2962
        static::getSchema()->reset();
2963
        self::$_cache_get_one = array();
2964
        self::$_cache_field_labels = array();
2965
    }
2966
2967
    /**
2968
     * Return the given element, searching by ID
2969
     *
2970
     * @param string $callerClass The class of the object to be returned
2971
     * @param int $id The id of the element
2972
     * @param boolean $cache See {@link get_one()}
2973
     *
2974
     * @return DataObject The element
2975
     */
2976
    public static function get_by_id($callerClass, $id, $cache = true)
2977
    {
2978
        if (!is_numeric($id)) {
2979
            user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
2980
        }
2981
2982
        // Pass to get_one
2983
        $column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
2984
        return DataObject::get_one($callerClass, array($column => $id), $cache);
2985
    }
2986
2987
    /**
2988
     * Get the name of the base table for this object
2989
     *
2990
     * @return string
2991
     */
2992
    public function baseTable()
2993
    {
2994
        return static::getSchema()->baseDataTable($this);
2995
    }
2996
2997
    /**
2998
     * Get the base class for this object
2999
     *
3000
     * @return string
3001
     */
3002
    public function baseClass()
3003
    {
3004
        return static::getSchema()->baseDataClass($this);
3005
    }
3006
3007
    /**
3008
     * @var array Parameters used in the query that built this object.
3009
     * This can be used by decorators (e.g. lazy loading) to
3010
     * run additional queries using the same context.
3011
     */
3012
    protected $sourceQueryParams;
3013
3014
    /**
3015
     * @see $sourceQueryParams
3016
     * @return array
3017
     */
3018
    public function getSourceQueryParams()
3019
    {
3020
        return $this->sourceQueryParams;
3021
    }
3022
3023
    /**
3024
     * Get list of parameters that should be inherited to relations on this object
3025
     *
3026
     * @return array
3027
     */
3028
    public function getInheritableQueryParams()
3029
    {
3030
        $params = $this->getSourceQueryParams();
3031
        $this->extend('updateInheritableQueryParams', $params);
3032
        return $params;
3033
    }
3034
3035
    /**
3036
     * @see $sourceQueryParams
3037
     * @param array
3038
     */
3039
    public function setSourceQueryParams($array)
3040
    {
3041
        $this->sourceQueryParams = $array;
3042
    }
3043
3044
    /**
3045
     * @see $sourceQueryParams
3046
     * @param string $key
3047
     * @param string $value
3048
     */
3049
    public function setSourceQueryParam($key, $value)
3050
    {
3051
        $this->sourceQueryParams[$key] = $value;
3052
    }
3053
3054
    /**
3055
     * @see $sourceQueryParams
3056
     * @param string $key
3057
     * @return string
3058
     */
3059
    public function getSourceQueryParam($key)
3060
    {
3061
        if (isset($this->sourceQueryParams[$key])) {
3062
            return $this->sourceQueryParams[$key];
3063
        }
3064
        return null;
3065
    }
3066
3067
    //-------------------------------------------------------------------------------------------//
3068
3069
    /**
3070
     * Return the database indexes on this table.
3071
     * This array is indexed by the name of the field with the index, and
3072
     * the value is the type of index.
3073
     */
3074
    public function databaseIndexes()
3075
    {
3076
        $has_one = $this->uninherited('has_one');
3077
        $classIndexes = $this->uninherited('indexes');
3078
        //$fileIndexes = $this->uninherited('fileIndexes', true);
3079
3080
        $indexes = array();
3081
3082
        if ($has_one) {
3083
            foreach ($has_one as $relationshipName => $fieldType) {
3084
                $indexes[$relationshipName . 'ID'] = true;
3085
            }
3086
        }
3087
3088
        if ($classIndexes) {
3089
            foreach ($classIndexes as $indexName => $indexType) {
3090
                $indexes[$indexName] = $indexType;
3091
            }
3092
        }
3093
3094
        if (get_parent_class($this) == self::class) {
3095
            $indexes['ClassName'] = true;
3096
        }
3097
3098
        return $indexes;
3099
    }
3100
3101
    /**
3102
     * Check the database schema and update it as necessary.
3103
     *
3104
     * @uses DataExtension->augmentDatabase()
3105
     */
3106
    public function requireTable()
3107
    {
3108
        // Only build the table if we've actually got fields
3109
        $schema = static::getSchema();
3110
        $fields = $schema->databaseFields(static::class, false);
3111
        $table = $schema->tableName(static::class);
3112
        $extensions = self::database_extensions(static::class);
3113
3114
        $indexes = $this->databaseIndexes();
3115
3116
        if (empty($table)) {
3117
            throw new LogicException(
3118
                "Class " . static::class . " not loaded by manifest, or no database table configured"
3119
            );
3120
        }
3121
3122
        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...
3123
            $hasAutoIncPK = get_parent_class($this) === self::class;
3124
            DB::require_table(
3125
                $table,
3126
                $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...
3127
                $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...
3128
                $hasAutoIncPK,
3129
                $this->stat('create_table_options'),
3130
                $extensions
0 ignored issues
show
Security Bug introduced by
It seems like $extensions defined by self::database_extensions(static::class) on line 3112 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...
3131
            );
3132
        } else {
3133
            DB::dont_require_table($table);
3134
        }
3135
3136
        // Build any child tables for many_many items
3137
        if ($manyMany = $this->uninherited('many_many')) {
3138
            $extras = $this->uninherited('many_many_extraFields');
3139
            foreach ($manyMany as $component => $spec) {
3140
                // Get many_many spec
3141
                $manyManyComponent = $schema->manyManyComponent(static::class, $component);
3142
                list(
3143
                    $relationClass, $parentClass, $componentClass,
0 ignored issues
show
Unused Code introduced by
The assignment to $relationClass is unused. Consider omitting it like so list($first,,$third).

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

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

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

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

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

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