GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 855522...fe5688 )
by Robert
20:39
created

BaseActiveRecord::__get()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 11
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 20
ccs 12
cts 12
cp 1
crap 5
rs 9.6111
1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db;
9
10
use Yii;
11
use yii\base\InvalidArgumentException;
12
use yii\base\InvalidCallException;
13
use yii\base\InvalidConfigException;
14
use yii\base\InvalidParamException;
15
use yii\base\Model;
16
use yii\base\ModelEvent;
17
use yii\base\NotSupportedException;
18
use yii\base\UnknownMethodException;
19
use yii\helpers\ArrayHelper;
20
21
/**
22
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
23
 *
24
 * See [[\yii\db\ActiveRecord]] for a concrete implementation.
25
 *
26
 * @property-read array $dirtyAttributes The changed attribute values (name-value pairs).
27
 * @property bool $isNewRecord Whether the record is new and should be inserted when calling [[save()]].
28
 * @property array $oldAttributes The old attribute values (name-value pairs). Note that the type of this
29
 * property differs in getter and setter. See [[getOldAttributes()]] and [[setOldAttributes()]] for details.
30
 * @property-read mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is
31
 * returned if the primary key is composite or `$asArray` is `true`. A string is returned otherwise (null will be
32
 * returned if the key value is null).
33
 * @property-read mixed $primaryKey The primary key value. An array (column name => column value) is returned
34
 * if the primary key is composite or `$asArray` is `true`. A string is returned otherwise (null will be returned
35
 * if the key value is null).
36
 * @property-read array $relatedRecords An array of related records indexed by relation names.
37
 *
38
 * @author Qiang Xue <[email protected]>
39
 * @author Carsten Brandt <[email protected]>
40
 * @since 2.0
41
 */
42
abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
43
{
44
    /**
45
     * @event Event an event that is triggered when the record is initialized via [[init()]].
46
     */
47
    const EVENT_INIT = 'init';
48
    /**
49
     * @event Event an event that is triggered after the record is created and populated with query result.
50
     */
51
    const EVENT_AFTER_FIND = 'afterFind';
52
    /**
53
     * @event ModelEvent an event that is triggered before inserting a record.
54
     * You may set [[ModelEvent::isValid]] to be `false` to stop the insertion.
55
     */
56
    const EVENT_BEFORE_INSERT = 'beforeInsert';
57
    /**
58
     * @event AfterSaveEvent an event that is triggered after a record is inserted.
59
     */
60
    const EVENT_AFTER_INSERT = 'afterInsert';
61
    /**
62
     * @event ModelEvent an event that is triggered before updating a record.
63
     * You may set [[ModelEvent::isValid]] to be `false` to stop the update.
64
     */
65
    const EVENT_BEFORE_UPDATE = 'beforeUpdate';
66
    /**
67
     * @event AfterSaveEvent an event that is triggered after a record is updated.
68
     */
69
    const EVENT_AFTER_UPDATE = 'afterUpdate';
70
    /**
71
     * @event ModelEvent an event that is triggered before deleting a record.
72
     * You may set [[ModelEvent::isValid]] to be `false` to stop the deletion.
73
     */
74
    const EVENT_BEFORE_DELETE = 'beforeDelete';
75
    /**
76
     * @event Event an event that is triggered after a record is deleted.
77
     */
78
    const EVENT_AFTER_DELETE = 'afterDelete';
79
    /**
80
     * @event Event an event that is triggered after a record is refreshed.
81
     * @since 2.0.8
82
     */
83
    const EVENT_AFTER_REFRESH = 'afterRefresh';
84
85
    /**
86
     * @var array attribute values indexed by attribute names
87
     */
88
    private $_attributes = [];
89
    /**
90
     * @var array|null old attribute values indexed by attribute names.
91
     * This is `null` if the record [[isNewRecord|is new]].
92
     */
93
    private $_oldAttributes;
94
    /**
95
     * @var array related models indexed by the relation names
96
     */
97
    private $_related = [];
98
    /**
99
     * @var array relation names indexed by their link attributes
100
     */
101
    private $_relationsDependencies = [];
102
103
104
    /**
105
     * {@inheritdoc}
106
     * @return static|null ActiveRecord instance matching the condition, or `null` if nothing matches.
107
     */
108 202
    public static function findOne($condition)
109
    {
110 202
        return static::findByCondition($condition)->one();
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::findByCondition($condition)->one() also could return the type array which is incompatible with the documented return type null|yii\db\BaseActiveRecord.
Loading history...
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     * @return static[] an array of ActiveRecord instances, or an empty array if nothing matches.
116
     */
117 6
    public static function findAll($condition)
118
    {
119 6
        return static::findByCondition($condition)->all();
120
    }
121
122
    /**
123
     * Finds ActiveRecord instance(s) by the given condition.
124
     * This method is internally called by [[findOne()]] and [[findAll()]].
125
     * @param mixed $condition please refer to [[findOne()]] for the explanation of this parameter
126
     * @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance.
127
     * @throws InvalidConfigException if there is no primary key defined
128
     * @internal
129
     */
130
    protected static function findByCondition($condition)
131
    {
132
        $query = static::find();
133
134
        if (!ArrayHelper::isAssociative($condition) && !$condition instanceof ExpressionInterface) {
135
            // query by primary key
136
            $primaryKey = static::primaryKey();
137
            if (isset($primaryKey[0])) {
138
                // if condition is scalar, search for a single primary key, if it is array, search for multiple primary key values
139
                $condition = [$primaryKey[0] => is_array($condition) ? array_values($condition) : $condition];
140
            } else {
141
                throw new InvalidConfigException('"' . get_called_class() . '" must have a primary key.');
142
            }
143
        }
144
145
        return $query->andWhere($condition);
146
    }
147
148
    /**
149
     * Updates the whole table using the provided attribute values and conditions.
150
     *
151
     * For example, to change the status to be 1 for all customers whose status is 2:
152
     *
153
     * ```php
154
     * Customer::updateAll(['status' => 1], 'status = 2');
155
     * ```
156
     *
157
     * @param array $attributes attribute values (name-value pairs) to be saved into the table
158
     * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
159
     * Please refer to [[Query::where()]] on how to specify this parameter.
160
     * @return int the number of rows updated
161
     * @throws NotSupportedException if not overridden
162
     */
163
    public static function updateAll($attributes, $condition = '')
164
    {
165
        throw new NotSupportedException(__METHOD__ . ' is not supported.');
166
    }
167
168
    /**
169
     * Updates the whole table using the provided counter changes and conditions.
170
     *
171
     * For example, to increment all customers' age by 1,
172
     *
173
     * ```php
174
     * Customer::updateAllCounters(['age' => 1]);
175
     * ```
176
     *
177
     * @param array $counters the counters to be updated (attribute name => increment value).
178
     * Use negative values if you want to decrement the counters.
179
     * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
180
     * Please refer to [[Query::where()]] on how to specify this parameter.
181
     * @return int the number of rows updated
182
     * @throws NotSupportedException if not overrided
183
     */
184
    public static function updateAllCounters($counters, $condition = '')
185
    {
186
        throw new NotSupportedException(__METHOD__ . ' is not supported.');
187
    }
188
189
    /**
190
     * Deletes rows in the table using the provided conditions.
191
     * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
192
     *
193
     * For example, to delete all customers whose status is 3:
194
     *
195
     * ```php
196
     * Customer::deleteAll('status = 3');
197
     * ```
198
     *
199
     * @param string|array|null $condition the conditions that will be put in the WHERE part of the DELETE SQL.
200
     * Please refer to [[Query::where()]] on how to specify this parameter.
201
     * @return int the number of rows deleted
202
     * @throws NotSupportedException if not overridden.
203
     */
204
    public static function deleteAll($condition = null)
205
    {
206
        throw new NotSupportedException(__METHOD__ . ' is not supported.');
207
    }
208
209
    /**
210
     * Returns the name of the column that stores the lock version for implementing optimistic locking.
211
     *
212
     * Optimistic locking allows multiple users to access the same record for edits and avoids
213
     * potential conflicts. In case when a user attempts to save the record upon some staled data
214
     * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown,
215
     * and the update or deletion is skipped.
216
     *
217
     * Optimistic locking is only supported by [[update()]] and [[delete()]].
218
     *
219
     * To use Optimistic locking:
220
     *
221
     * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
222
     *    Override this method to return the name of this column.
223
     * 2. Ensure the version value is submitted and loaded to your model before any update or delete.
224
     *    Or add [[\yii\behaviors\OptimisticLockBehavior|OptimisticLockBehavior]] to your model
225
     *    class in order to automate the process.
226
     * 3. In the Web form that collects the user input, add a hidden field that stores
227
     *    the lock version of the record being updated.
228
     * 4. In the controller action that does the data updating, try to catch the [[StaleObjectException]]
229
     *    and implement necessary business logic (e.g. merging the changes, prompting stated data)
230
     *    to resolve the conflict.
231
     *
232
     * @return string|null the column name that stores the lock version of a table row.
233
     * If `null` is returned (default implemented), optimistic locking will not be supported.
234
     */
235 37
    public function optimisticLock()
236
    {
237 37
        return null;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 3
    public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
244
    {
245 3
        if (parent::canGetProperty($name, $checkVars, $checkBehaviors)) {
246 3
            return true;
247
        }
248
249
        try {
250 3
            return $this->hasAttribute($name);
251
        } catch (\Exception $e) {
252
            // `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used
253
            return false;
254
        }
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260 9
    public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
261
    {
262 9
        if (parent::canSetProperty($name, $checkVars, $checkBehaviors)) {
263 6
            return true;
264
        }
265
266
        try {
267 3
            return $this->hasAttribute($name);
268
        } catch (\Exception $e) {
269
            // `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used
270
            return false;
271
        }
272
    }
273
274
    /**
275
     * PHP getter magic method.
276
     * This method is overridden so that attributes and related objects can be accessed like properties.
277
     *
278
     * @param string $name property name
279
     * @throws InvalidArgumentException if relation name is wrong
280
     * @return mixed property value
281
     * @see getAttribute()
282
     */
283 465
    public function __get($name)
284
    {
285 465
        if (array_key_exists($name, $this->_attributes)) {
286 441
            return $this->_attributes[$name];
287
        }
288
289 237
        if ($this->hasAttribute($name)) {
290 44
            return null;
291
        }
292
293 214
        if (array_key_exists($name, $this->_related)) {
294 133
            return $this->_related[$name];
295
        }
296 139
        $value = parent::__get($name);
297 133
        if ($value instanceof ActiveQueryInterface) {
298 88
            $this->setRelationDependencies($name, $value);
299 88
            return $this->_related[$name] = $value->findFor($name, $this);
300
        }
301
302 54
        return $value;
303
    }
304
305
    /**
306
     * PHP setter magic method.
307
     * This method is overridden so that AR attributes can be accessed like properties.
308
     * @param string $name property name
309
     * @param mixed $value property value
310
     */
311 306
    public function __set($name, $value)
312
    {
313 306
        if ($this->hasAttribute($name)) {
314
            if (
315 306
                !empty($this->_relationsDependencies[$name])
316 306
                && (!array_key_exists($name, $this->_attributes) || $this->_attributes[$name] !== $value)
317
            ) {
318 15
                $this->resetDependentRelations($name);
319
            }
320 306
            $this->_attributes[$name] = $value;
321
        } else {
322 6
            parent::__set($name, $value);
323
        }
324
    }
325
326
    /**
327
     * Checks if a property value is null.
328
     * This method overrides the parent implementation by checking if the named attribute is `null` or not.
329
     * @param string $name the property name or the event name
330
     * @return bool whether the property value is null
331
     */
332 251
    public function __isset($name)
333
    {
334
        try {
335 251
            return $this->__get($name) !== null;
336 10
        } catch (\Exception $t) {
337 7
            return false;
338 3
        } catch (\Throwable $e) {
339 3
            return false;
340
        }
341
    }
342
343
    /**
344
     * Sets a component property to be null.
345
     * This method overrides the parent implementation by clearing
346
     * the specified attribute value.
347
     * @param string $name the property name or the event name
348
     */
349 15
    public function __unset($name)
350
    {
351 15
        if ($this->hasAttribute($name)) {
352 9
            unset($this->_attributes[$name]);
353 9
            if (!empty($this->_relationsDependencies[$name])) {
354 9
                $this->resetDependentRelations($name);
355
            }
356 6
        } elseif (array_key_exists($name, $this->_related)) {
357 6
            unset($this->_related[$name]);
358
        } elseif ($this->getRelation($name, false) === null) {
359
            parent::__unset($name);
360
        }
361
    }
362
363
    /**
364
     * Declares a `has-one` relation.
365
     * The declaration is returned in terms of a relational [[ActiveQuery]] instance
366
     * through which the related record can be queried and retrieved back.
367
     *
368
     * A `has-one` relation means that there is at most one related record matching
369
     * the criteria set by this relation, e.g., a customer has one country.
370
     *
371
     * For example, to declare the `country` relation for `Customer` class, we can write
372
     * the following code in the `Customer` class:
373
     *
374
     * ```php
375
     * public function getCountry()
376
     * {
377
     *     return $this->hasOne(Country::class, ['id' => 'country_id']);
378
     * }
379
     * ```
380
     *
381
     * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name
382
     * in the related class `Country`, while the 'country_id' value refers to an attribute name
383
     * in the current AR class.
384
     *
385
     * Call methods declared in [[ActiveQuery]] to further customize the relation.
386
     *
387
     * @param string $class the class name of the related record
388
     * @param array $link the primary-foreign key constraint. The keys of the array refer to
389
     * the attributes of the record associated with the `$class` model, while the values of the
390
     * array refer to the corresponding attributes in **this** AR class.
391
     * @return ActiveQueryInterface the relational query object.
392
     */
393 109
    public function hasOne($class, $link)
394
    {
395 109
        return $this->createRelationQuery($class, $link, false);
396
    }
397
398
    /**
399
     * Declares a `has-many` relation.
400
     * The declaration is returned in terms of a relational [[ActiveQuery]] instance
401
     * through which the related record can be queried and retrieved back.
402
     *
403
     * A `has-many` relation means that there are multiple related records matching
404
     * the criteria set by this relation, e.g., a customer has many orders.
405
     *
406
     * For example, to declare the `orders` relation for `Customer` class, we can write
407
     * the following code in the `Customer` class:
408
     *
409
     * ```php
410
     * public function getOrders()
411
     * {
412
     *     return $this->hasMany(Order::class, ['customer_id' => 'id']);
413
     * }
414
     * ```
415
     *
416
     * Note that in the above, the 'customer_id' key in the `$link` parameter refers to
417
     * an attribute name in the related class `Order`, while the 'id' value refers to
418
     * an attribute name in the current AR class.
419
     *
420
     * Call methods declared in [[ActiveQuery]] to further customize the relation.
421
     *
422
     * @param string $class the class name of the related record
423
     * @param array $link the primary-foreign key constraint. The keys of the array refer to
424
     * the attributes of the record associated with the `$class` model, while the values of the
425
     * array refer to the corresponding attributes in **this** AR class.
426
     * @return ActiveQueryInterface the relational query object.
427
     */
428 183
    public function hasMany($class, $link)
429
    {
430 183
        return $this->createRelationQuery($class, $link, true);
431
    }
432
433
    /**
434
     * Creates a query instance for `has-one` or `has-many` relation.
435
     * @param string $class the class name of the related record.
436
     * @param array $link the primary-foreign key constraint.
437
     * @param bool $multiple whether this query represents a relation to more than one record.
438
     * @return ActiveQueryInterface the relational query object.
439
     * @since 2.0.12
440
     * @see hasOne()
441
     * @see hasMany()
442
     */
443 241
    protected function createRelationQuery($class, $link, $multiple)
444
    {
445
        /* @var $class ActiveRecordInterface */
446
        /* @var $query ActiveQuery */
447 241
        $query = $class::find();
448 241
        $query->primaryModel = $this;
0 ignored issues
show
Documentation Bug introduced by
$this is of type yii\db\BaseActiveRecord, but the property $primaryModel was declared to be of type yii\db\ActiveRecord. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
449 241
        $query->link = $link;
450 241
        $query->multiple = $multiple;
451 241
        return $query;
452
    }
453
454
    /**
455
     * Populates the named relation with the related records.
456
     * Note that this method does not check if the relation exists or not.
457
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method (case-sensitive).
458
     * @param ActiveRecordInterface|array|null $records the related records to be populated into the relation.
459
     * @see getRelation()
460
     */
461 153
    public function populateRelation($name, $records)
462
    {
463 153
        foreach ($this->_relationsDependencies as &$relationNames) {
464 21
            unset($relationNames[$name]);
465
        }
466
467 153
        $this->_related[$name] = $records;
468
    }
469
470
    /**
471
     * Check whether the named relation has been populated with records.
472
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method (case-sensitive).
473
     * @return bool whether relation has been populated with records.
474
     * @see getRelation()
475
     */
476 102
    public function isRelationPopulated($name)
477
    {
478 102
        return array_key_exists($name, $this->_related);
479
    }
480
481
    /**
482
     * Returns all populated related records.
483
     * @return array an array of related records indexed by relation names.
484
     * @see getRelation()
485
     */
486 6
    public function getRelatedRecords()
487
    {
488 6
        return $this->_related;
489
    }
490
491
    /**
492
     * Returns a value indicating whether the model has an attribute with the specified name.
493
     * @param string $name the name of the attribute
494
     * @return bool whether the model has an attribute with the specified name.
495
     */
496 377
    public function hasAttribute($name)
497
    {
498 377
        return isset($this->_attributes[$name]) || in_array($name, $this->attributes(), true);
499
    }
500
501
    /**
502
     * Returns the named attribute value.
503
     * If this record is the result of a query and the attribute is not loaded,
504
     * `null` will be returned.
505
     * @param string $name the attribute name
506
     * @return mixed the attribute value. `null` if the attribute is not set or does not exist.
507
     * @see hasAttribute()
508
     */
509 4
    public function getAttribute($name)
510
    {
511 4
        return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
512
    }
513
514
    /**
515
     * Sets the named attribute value.
516
     * @param string $name the attribute name
517
     * @param mixed $value the attribute value.
518
     * @throws InvalidArgumentException if the named attribute does not exist.
519
     * @see hasAttribute()
520
     */
521 109
    public function setAttribute($name, $value)
522
    {
523 109
        if ($this->hasAttribute($name)) {
524
            if (
525 109
                !empty($this->_relationsDependencies[$name])
526 109
                && (!array_key_exists($name, $this->_attributes) || $this->_attributes[$name] !== $value)
527
            ) {
528 6
                $this->resetDependentRelations($name);
529
            }
530 109
            $this->_attributes[$name] = $value;
531
        } else {
532
            throw new InvalidArgumentException(get_class($this) . ' has no attribute named "' . $name . '".');
533
        }
534
    }
535
536
    /**
537
     * Returns the old attribute values.
538
     * @return array the old attribute values (name-value pairs)
539
     */
540
    public function getOldAttributes()
541
    {
542
        return $this->_oldAttributes === null ? [] : $this->_oldAttributes;
543
    }
544
545
    /**
546
     * Sets the old attribute values.
547
     * All existing old attribute values will be discarded.
548
     * @param array|null $values old attribute values to be set.
549
     * If set to `null` this record is considered to be [[isNewRecord|new]].
550
     */
551 117
    public function setOldAttributes($values)
552
    {
553 117
        $this->_oldAttributes = $values;
554
    }
555
556
    /**
557
     * Returns the old value of the named attribute.
558
     * If this record is the result of a query and the attribute is not loaded,
559
     * `null` will be returned.
560
     * @param string $name the attribute name
561
     * @return mixed the old attribute value. `null` if the attribute is not loaded before
562
     * or does not exist.
563
     * @see hasAttribute()
564
     */
565
    public function getOldAttribute($name)
566
    {
567
        return isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
568
    }
569
570
    /**
571
     * Sets the old value of the named attribute.
572
     * @param string $name the attribute name
573
     * @param mixed $value the old attribute value.
574
     * @throws InvalidArgumentException if the named attribute does not exist.
575
     * @see hasAttribute()
576
     */
577 123
    public function setOldAttribute($name, $value)
578
    {
579 123
        if ($this->canSetOldAttribute($name)) {
580 123
            $this->_oldAttributes[$name] = $value;
581
        } else {
582
            throw new InvalidArgumentException(get_class($this) . ' has no attribute named "' . $name . '".');
583
        }
584
    }
585
586
    /**
587
     * Returns if the old named attribute can be set.
588
     * @param string $name the attribute name
589
     * @return bool whether the old attribute can be set
590
     * @see setOldAttribute()
591
     */
592 123
    public function canSetOldAttribute($name)
593
    {
594 123
        return (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name));
595
    }
596
597
    /**
598
     * Marks an attribute dirty.
599
     * This method may be called to force updating a record when calling [[update()]],
600
     * even if there is no change being made to the record.
601
     * @param string $name the attribute name
602
     */
603 7
    public function markAttributeDirty($name)
604
    {
605 7
        unset($this->_oldAttributes[$name]);
606
    }
607
608
    /**
609
     * Returns a value indicating whether the named attribute has been changed.
610
     * @param string $name the name of the attribute.
611
     * @param bool $identical whether the comparison of new and old value is made for
612
     * identical values using `===`, defaults to `true`. Otherwise `==` is used for comparison.
613
     * This parameter is available since version 2.0.4.
614
     * @return bool whether the attribute has been changed
615
     */
616 2
    public function isAttributeChanged($name, $identical = true)
617
    {
618 2
        if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) {
619 1
            if ($identical) {
620 1
                return $this->_attributes[$name] !== $this->_oldAttributes[$name];
621
            }
622
623
            return $this->_attributes[$name] != $this->_oldAttributes[$name];
624
        }
625
626 1
        return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]);
627
    }
628
629
    /**
630
     * Returns the attribute values that have been modified since they are loaded or saved most recently.
631
     *
632
     * The comparison of new and old values is made for identical values using `===`.
633
     *
634
     * @param string[]|null $names the names of the attributes whose values may be returned if they are
635
     * changed recently. If null, [[attributes()]] will be used.
636
     * @return array the changed attribute values (name-value pairs)
637
     */
638 128
    public function getDirtyAttributes($names = null)
639
    {
640 128
        if ($names === null) {
641 125
            $names = $this->attributes();
642
        }
643 128
        $names = array_flip($names);
644 128
        $attributes = [];
645 128
        if ($this->_oldAttributes === null) {
646 114
            foreach ($this->_attributes as $name => $value) {
647 110
                if (isset($names[$name])) {
648 110
                    $attributes[$name] = $value;
649
                }
650
            }
651
        } else {
652 59
            foreach ($this->_attributes as $name => $value) {
653 59
                if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $this->isValueDifferent($value, $this->_oldAttributes[$name]))) {
654 55
                    $attributes[$name] = $value;
655
                }
656
            }
657
        }
658
659 128
        return $attributes;
660
    }
661
662
    /**
663
     * Saves the current record.
664
     *
665
     * This method will call [[insert()]] when [[isNewRecord]] is `true`, or [[update()]]
666
     * when [[isNewRecord]] is `false`.
667
     *
668
     * For example, to save a customer record:
669
     *
670
     * ```php
671
     * $customer = new Customer; // or $customer = Customer::findOne($id);
672
     * $customer->name = $name;
673
     * $customer->email = $email;
674
     * $customer->save();
675
     * ```
676
     *
677
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
678
     * before saving the record. Defaults to `true`. If the validation fails, the record
679
     * will not be saved to the database and this method will return `false`.
680
     * @param array|null $attributeNames list of attribute names that need to be saved. Defaults to null,
681
     * meaning all attributes that are loaded from DB will be saved.
682
     * @return bool whether the saving succeeded (i.e. no validation errors occurred).
683
     * @throws Exception in case update or insert failed.
684
     */
685 122
    public function save($runValidation = true, $attributeNames = null)
686
    {
687 122
        if ($this->getIsNewRecord()) {
688 108
            return $this->insert($runValidation, $attributeNames);
689
        }
690
691 31
        return $this->update($runValidation, $attributeNames) !== false;
692
    }
693
694
    /**
695
     * Saves the changes to this active record into the associated database table.
696
     *
697
     * This method performs the following steps in order:
698
     *
699
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
700
     *    returns `false`, the rest of the steps will be skipped;
701
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
702
     *    failed, the rest of the steps will be skipped;
703
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
704
     *    the rest of the steps will be skipped;
705
     * 4. save the record into database. If this fails, it will skip the rest of the steps;
706
     * 5. call [[afterSave()]];
707
     *
708
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
709
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]]
710
     * will be raised by the corresponding methods.
711
     *
712
     * Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
713
     *
714
     * For example, to update a customer record:
715
     *
716
     * ```php
717
     * $customer = Customer::findOne($id);
718
     * $customer->name = $name;
719
     * $customer->email = $email;
720
     * $customer->update();
721
     * ```
722
     *
723
     * Note that it is possible the update does not affect any row in the table.
724
     * In this case, this method will return 0. For this reason, you should use the following
725
     * code to check if update() is successful or not:
726
     *
727
     * ```php
728
     * if ($customer->update() !== false) {
729
     *     // update successful
730
     * } else {
731
     *     // update failed
732
     * }
733
     * ```
734
     *
735
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
736
     * before saving the record. Defaults to `true`. If the validation fails, the record
737
     * will not be saved to the database and this method will return `false`.
738
     * @param array|null $attributeNames list of attribute names that need to be saved. Defaults to null,
739
     * meaning all attributes that are loaded from DB will be saved.
740
     * @return int|false the number of rows affected, or `false` if validation fails
741
     * or [[beforeSave()]] stops the updating process.
742
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
743
     * being updated is outdated.
744
     * @throws Exception in case update failed.
745
     */
746
    public function update($runValidation = true, $attributeNames = null)
747
    {
748
        if ($runValidation && !$this->validate($attributeNames)) {
749
            return false;
750
        }
751
752
        return $this->updateInternal($attributeNames);
753
    }
754
755
    /**
756
     * Updates the specified attributes.
757
     *
758
     * This method is a shortcut to [[update()]] when data validation is not needed
759
     * and only a small set attributes need to be updated.
760
     *
761
     * You may specify the attributes to be updated as name list or name-value pairs.
762
     * If the latter, the corresponding attribute values will be modified accordingly.
763
     * The method will then save the specified attributes into database.
764
     *
765
     * Note that this method will **not** perform data validation and will **not** trigger events.
766
     *
767
     * @param array $attributes the attributes (names or name-value pairs) to be updated
768
     * @return int the number of rows affected.
769
     */
770 7
    public function updateAttributes($attributes)
771
    {
772 7
        $attrs = [];
773 7
        foreach ($attributes as $name => $value) {
774 7
            if (is_int($name)) {
775
                $attrs[] = $value;
776
            } else {
777 7
                $this->$name = $value;
778 7
                $attrs[] = $name;
779
            }
780
        }
781
782 7
        $values = $this->getDirtyAttributes($attrs);
783 7
        if (empty($values) || $this->getIsNewRecord()) {
784 4
            return 0;
785
        }
786
787 6
        $rows = static::updateAll($values, $this->getOldPrimaryKey(true));
788
789 6
        foreach ($values as $name => $value) {
790 6
            $this->_oldAttributes[$name] = $this->_attributes[$name];
791
        }
792
793 6
        return $rows;
794
    }
795
796
    /**
797
     * @see update()
798
     * @param array|null $attributes attributes to update
799
     * @return int|false the number of rows affected, or false if [[beforeSave()]] stops the updating process.
800
     * @throws StaleObjectException
801
     */
802 41
    protected function updateInternal($attributes = null)
803
    {
804 41
        if (!$this->beforeSave(false)) {
805
            return false;
806
        }
807 41
        $values = $this->getDirtyAttributes($attributes);
808 41
        if (empty($values)) {
809 3
            $this->afterSave(false, $values);
810 3
            return 0;
811
        }
812 39
        $condition = $this->getOldPrimaryKey(true);
813 39
        $lock = $this->optimisticLock();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $lock is correct as $this->optimisticLock() targeting yii\db\BaseActiveRecord::optimisticLock() 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...
814 39
        if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
815 5
            $values[$lock] = $this->$lock + 1;
816 5
            $condition[$lock] = $this->$lock;
817
        }
818
        // We do not check the return value of updateAll() because it's possible
819
        // that the UPDATE statement doesn't change anything and thus returns 0.
820 39
        $rows = static::updateAll($values, $condition);
821
822 39
        if ($lock !== null && !$rows) {
823 4
            throw new StaleObjectException('The object being updated is outdated.');
824
        }
825
826 39
        if (isset($values[$lock])) {
827 5
            $this->$lock = $values[$lock];
828
        }
829
830 39
        $changedAttributes = [];
831 39
        foreach ($values as $name => $value) {
832 39
            $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
833 39
            $this->_oldAttributes[$name] = $value;
834
        }
835 39
        $this->afterSave(false, $changedAttributes);
836
837 39
        return $rows;
838
    }
839
840
    /**
841
     * Updates one or several counter columns for the current AR object.
842
     * Note that this method differs from [[updateAllCounters()]] in that it only
843
     * saves counters for the current AR object.
844
     *
845
     * An example usage is as follows:
846
     *
847
     * ```php
848
     * $post = Post::findOne($id);
849
     * $post->updateCounters(['view_count' => 1]);
850
     * ```
851
     *
852
     * @param array $counters the counters to be updated (attribute name => increment value)
853
     * Use negative values if you want to decrement the counters.
854
     * @return bool whether the saving is successful
855
     * @see updateAllCounters()
856
     */
857 6
    public function updateCounters($counters)
858
    {
859 6
        if (static::updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) {
860 6
            foreach ($counters as $name => $value) {
861 6
                if (!isset($this->_attributes[$name])) {
862 3
                    $this->_attributes[$name] = $value;
863
                } else {
864 3
                    $this->_attributes[$name] += $value;
865
                }
866 6
                $this->_oldAttributes[$name] = $this->_attributes[$name];
867
            }
868
869 6
            return true;
870
        }
871
872
        return false;
873
    }
874
875
    /**
876
     * Deletes the table row corresponding to this active record.
877
     *
878
     * This method performs the following steps in order:
879
     *
880
     * 1. call [[beforeDelete()]]. If the method returns `false`, it will skip the
881
     *    rest of the steps;
882
     * 2. delete the record from the database;
883
     * 3. call [[afterDelete()]].
884
     *
885
     * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
886
     * will be raised by the corresponding methods.
887
     *
888
     * @return int|false the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
889
     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
890
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
891
     * being deleted is outdated.
892
     * @throws Exception in case delete failed.
893
     */
894
    public function delete()
895
    {
896
        $result = false;
897
        if ($this->beforeDelete()) {
898
            // we do not check the return value of deleteAll() because it's possible
899
            // the record is already deleted in the database and thus the method will return 0
900
            $condition = $this->getOldPrimaryKey(true);
901
            $lock = $this->optimisticLock();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $lock is correct as $this->optimisticLock() targeting yii\db\BaseActiveRecord::optimisticLock() 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...
902
            if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
903
                $condition[$lock] = $this->$lock;
904
            }
905
            $result = static::deleteAll($condition);
906
            if ($lock !== null && !$result) {
907
                throw new StaleObjectException('The object being deleted is outdated.');
908
            }
909
            $this->_oldAttributes = null;
910
            $this->afterDelete();
911
        }
912
913
        return $result;
914
    }
915
916
    /**
917
     * Returns a value indicating whether the current record is new.
918
     * @return bool whether the record is new and should be inserted when calling [[save()]].
919
     */
920 171
    public function getIsNewRecord()
921
    {
922 171
        return $this->_oldAttributes === null;
923
    }
924
925
    /**
926
     * Sets the value indicating whether the record is new.
927
     * @param bool $value whether the record is new and should be inserted when calling [[save()]].
928
     * @see getIsNewRecord()
929
     */
930
    public function setIsNewRecord($value)
931
    {
932
        $this->_oldAttributes = $value ? null : $this->_attributes;
933
    }
934
935
    /**
936
     * Initializes the object.
937
     * This method is called at the end of the constructor.
938
     * The default implementation will trigger an [[EVENT_INIT]] event.
939
     */
940 585
    public function init()
941
    {
942 585
        parent::init();
943 585
        $this->trigger(self::EVENT_INIT);
944
    }
945
946
    /**
947
     * This method is called when the AR object is created and populated with the query result.
948
     * The default implementation will trigger an [[EVENT_AFTER_FIND]] event.
949
     * When overriding this method, make sure you call the parent implementation to ensure the
950
     * event is triggered.
951
     */
952 396
    public function afterFind()
953
    {
954 396
        $this->trigger(self::EVENT_AFTER_FIND);
955
    }
956
957
    /**
958
     * This method is called at the beginning of inserting or updating a record.
959
     *
960
     * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is `true`,
961
     * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is `false`.
962
     * When overriding this method, make sure you call the parent implementation like the following:
963
     *
964
     * ```php
965
     * public function beforeSave($insert)
966
     * {
967
     *     if (!parent::beforeSave($insert)) {
968
     *         return false;
969
     *     }
970
     *
971
     *     // ...custom code here...
972
     *     return true;
973
     * }
974
     * ```
975
     *
976
     * @param bool $insert whether this method called while inserting a record.
977
     * If `false`, it means the method is called while updating a record.
978
     * @return bool whether the insertion or updating should continue.
979
     * If `false`, the insertion or updating will be cancelled.
980
     */
981 134
    public function beforeSave($insert)
982
    {
983 134
        $event = new ModelEvent();
984 134
        $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
985
986 134
        return $event->isValid;
987
    }
988
989
    /**
990
     * This method is called at the end of inserting or updating a record.
991
     * The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is `true`,
992
     * or an [[EVENT_AFTER_UPDATE]] event if `$insert` is `false`. The event class used is [[AfterSaveEvent]].
993
     * When overriding this method, make sure you call the parent implementation so that
994
     * the event is triggered.
995
     * @param bool $insert whether this method called while inserting a record.
996
     * If `false`, it means the method is called while updating a record.
997
     * @param array $changedAttributes The old values of attributes that had changed and were saved.
998
     * You can use this parameter to take action based on the changes made for example send an email
999
     * when the password had changed or implement audit trail that tracks all the changes.
1000
     * `$changedAttributes` gives you the old attribute values while the active record (`$this`) has
1001
     * already the new, updated values.
1002
     *
1003
     * Note that no automatic type conversion performed by default. You may use
1004
     * [[\yii\behaviors\AttributeTypecastBehavior]] to facilitate attribute typecasting.
1005
     * See https://www.yiiframework.com/doc-2.0/guide-db-active-record.html#attributes-typecasting.
1006
     */
1007 125
    public function afterSave($insert, $changedAttributes)
1008
    {
1009 125
        $this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE, new AfterSaveEvent([
1010 125
            'changedAttributes' => $changedAttributes,
1011 125
        ]));
1012
    }
1013
1014
    /**
1015
     * This method is invoked before deleting a record.
1016
     *
1017
     * The default implementation raises the [[EVENT_BEFORE_DELETE]] event.
1018
     * When overriding this method, make sure you call the parent implementation like the following:
1019
     *
1020
     * ```php
1021
     * public function beforeDelete()
1022
     * {
1023
     *     if (!parent::beforeDelete()) {
1024
     *         return false;
1025
     *     }
1026
     *
1027
     *     // ...custom code here...
1028
     *     return true;
1029
     * }
1030
     * ```
1031
     *
1032
     * @return bool whether the record should be deleted. Defaults to `true`.
1033
     */
1034 7
    public function beforeDelete()
1035
    {
1036 7
        $event = new ModelEvent();
1037 7
        $this->trigger(self::EVENT_BEFORE_DELETE, $event);
1038
1039 7
        return $event->isValid;
1040
    }
1041
1042
    /**
1043
     * This method is invoked after deleting a record.
1044
     * The default implementation raises the [[EVENT_AFTER_DELETE]] event.
1045
     * You may override this method to do postprocessing after the record is deleted.
1046
     * Make sure you call the parent implementation so that the event is raised properly.
1047
     */
1048 7
    public function afterDelete()
1049
    {
1050 7
        $this->trigger(self::EVENT_AFTER_DELETE);
1051
    }
1052
1053
    /**
1054
     * Repopulates this active record with the latest data.
1055
     *
1056
     * If the refresh is successful, an [[EVENT_AFTER_REFRESH]] event will be triggered.
1057
     * This event is available since version 2.0.8.
1058
     *
1059
     * @return bool whether the row still exists in the database. If `true`, the latest data
1060
     * will be populated to this active record. Otherwise, this record will remain unchanged.
1061
     */
1062
    public function refresh()
1063
    {
1064
        /* @var $record BaseActiveRecord */
1065
        $record = static::findOne($this->getPrimaryKey(true));
1066
        return $this->refreshInternal($record);
1067
    }
1068
1069
    /**
1070
     * Repopulates this active record with the latest data from a newly fetched instance.
1071
     * @param BaseActiveRecord $record the record to take attributes from.
1072
     * @return bool whether refresh was successful.
1073
     * @see refresh()
1074
     * @since 2.0.13
1075
     */
1076 29
    protected function refreshInternal($record)
1077
    {
1078 29
        if ($record === null) {
1079 3
            return false;
1080
        }
1081 29
        foreach ($this->attributes() as $name) {
1082 29
            $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null;
1083
        }
1084 29
        $this->_oldAttributes = $record->_oldAttributes;
1085 29
        $this->_related = [];
1086 29
        $this->_relationsDependencies = [];
1087 29
        $this->afterRefresh();
1088
1089 29
        return true;
1090
    }
1091
1092
    /**
1093
     * This method is called when the AR object is refreshed.
1094
     * The default implementation will trigger an [[EVENT_AFTER_REFRESH]] event.
1095
     * When overriding this method, make sure you call the parent implementation to ensure the
1096
     * event is triggered.
1097
     * @since 2.0.8
1098
     */
1099 29
    public function afterRefresh()
1100
    {
1101 29
        $this->trigger(self::EVENT_AFTER_REFRESH);
1102
    }
1103
1104
    /**
1105
     * Returns a value indicating whether the given active record is the same as the current one.
1106
     * The comparison is made by comparing the table names and the primary key values of the two active records.
1107
     * If one of the records [[isNewRecord|is new]] they are also considered not equal.
1108
     * @param ActiveRecordInterface $record record to compare to
1109
     * @return bool whether the two active records refer to the same row in the same database table.
1110
     */
1111
    public function equals($record)
1112
    {
1113
        if ($this->getIsNewRecord() || $record->getIsNewRecord()) {
1114
            return false;
1115
        }
1116
1117
        return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey();
1118
    }
1119
1120
    /**
1121
     * Returns the primary key value(s).
1122
     * @param bool $asArray whether to return the primary key value as an array. If `true`,
1123
     * the return value will be an array with column names as keys and column values as values.
1124
     * Note that for composite primary keys, an array will always be returned regardless of this parameter value.
1125
     * @return mixed the primary key value. An array (column name => column value) is returned if the primary key
1126
     * is composite or `$asArray` is `true`. A string is returned otherwise (null will be returned if
1127
     * the key value is null).
1128
     */
1129 51
    public function getPrimaryKey($asArray = false)
1130
    {
1131 51
        $keys = static::primaryKey();
1132 51
        if (!$asArray && count($keys) === 1) {
1133 25
            return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
1134
        }
1135
1136 29
        $values = [];
1137 29
        foreach ($keys as $name) {
1138 29
            $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
1139
        }
1140
1141 29
        return $values;
1142
    }
1143
1144
    /**
1145
     * Returns the old primary key value(s).
1146
     * This refers to the primary key value that is populated into the record
1147
     * after executing a find method (e.g. find(), findOne()).
1148
     * The value remains unchanged even if the primary key attribute is manually assigned with a different value.
1149
     * @param bool $asArray whether to return the primary key value as an array. If `true`,
1150
     * the return value will be an array with column name as key and column value as value.
1151
     * If this is `false` (default), a scalar value will be returned for non-composite primary key.
1152
     * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
1153
     * is composite or `$asArray` is `true`. A string is returned otherwise (null will be returned if
1154
     * the key value is null).
1155
     * @throws Exception if the AR model does not have a primary key
1156
     */
1157 77
    public function getOldPrimaryKey($asArray = false)
1158
    {
1159 77
        $keys = static::primaryKey();
1160 77
        if (empty($keys)) {
1161
            throw new Exception(get_class($this) . ' does not have a primary key. You should either define a primary key for the corresponding table or override the primaryKey() method.');
1162
        }
1163 77
        if (!$asArray && count($keys) === 1) {
1164
            return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
1165
        }
1166
1167 77
        $values = [];
1168 77
        foreach ($keys as $name) {
1169 77
            $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
1170
        }
1171
1172 77
        return $values;
1173
    }
1174
1175
    /**
1176
     * Populates an active record object using a row of data from the database/storage.
1177
     *
1178
     * This is an internal method meant to be called to create active record objects after
1179
     * fetching data from the database. It is mainly used by [[ActiveQuery]] to populate
1180
     * the query results into active records.
1181
     *
1182
     * When calling this method manually you should call [[afterFind()]] on the created
1183
     * record to trigger the [[EVENT_AFTER_FIND|afterFind Event]].
1184
     *
1185
     * @param BaseActiveRecord $record the record to be populated. In most cases this will be an instance
1186
     * created by [[instantiate()]] beforehand.
1187
     * @param array $row attribute values (name => value)
1188
     */
1189 396
    public static function populateRecord($record, $row)
1190
    {
1191 396
        $columns = array_flip($record->attributes());
1192 396
        foreach ($row as $name => $value) {
1193 396
            if (isset($columns[$name])) {
1194 396
                $record->_attributes[$name] = $value;
1195 6
            } elseif ($record->canSetProperty($name)) {
1196 6
                $record->$name = $value;
1197
            }
1198
        }
1199 396
        $record->_oldAttributes = $record->_attributes;
1200 396
        $record->_related = [];
1201 396
        $record->_relationsDependencies = [];
1202
    }
1203
1204
    /**
1205
     * Creates an active record instance.
1206
     *
1207
     * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
1208
     * It is not meant to be used for creating new records directly.
1209
     *
1210
     * You may override this method if the instance being created
1211
     * depends on the row data to be populated into the record.
1212
     * For example, by creating a record based on the value of a column,
1213
     * you may implement the so-called single-table inheritance mapping.
1214
     * @param array $row row data to be populated into the record.
1215
     * @return static the newly created active record
1216
     */
1217 390
    public static function instantiate($row)
1218
    {
1219 390
        return new static();
1220
    }
1221
1222
    /**
1223
     * Returns whether there is an element at the specified offset.
1224
     * This method is required by the interface [[\ArrayAccess]].
1225
     * @param mixed $offset the offset to check on
1226
     * @return bool whether there is an element at the specified offset.
1227
     */
1228 239
    #[\ReturnTypeWillChange]
1229
    public function offsetExists($offset)
1230
    {
1231 239
        return $this->__isset($offset);
1232
    }
1233
1234
    /**
1235
     * Returns the relation object with the specified name.
1236
     * A relation is defined by a getter method which returns an [[ActiveQueryInterface]] object.
1237
     * It can be declared in either the Active Record class itself or one of its behaviors.
1238
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method (case-sensitive).
1239
     * @param bool $throwException whether to throw exception if the relation does not exist.
1240
     * @return ActiveQueryInterface|ActiveQuery|null the relational query object. If the relation does not exist
1241
     * and `$throwException` is `false`, `null` will be returned.
1242
     * @throws InvalidArgumentException if the named relation does not exist.
1243
     */
1244 210
    public function getRelation($name, $throwException = true)
1245
    {
1246 210
        $getter = 'get' . $name;
1247
        try {
1248
            // the relation could be defined in a behavior
1249 210
            $relation = $this->$getter();
1250 6
        } catch (UnknownMethodException $e) {
1251 6
            if ($throwException) {
1252 6
                throw new InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
1253
            }
1254
1255
            return null;
1256
        }
1257 207
        if (!$relation instanceof ActiveQueryInterface) {
1258
            if ($throwException) {
1259
                throw new InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".');
1260
            }
1261
1262
            return null;
1263
        }
1264
1265 207
        if (method_exists($this, $getter)) {
1266
            // relation name is case sensitive, trying to validate it when the relation is defined within this class
1267 207
            $method = new \ReflectionMethod($this, $getter);
1268 207
            $realName = lcfirst(substr($method->getName(), 3));
1269 207
            if ($realName !== $name) {
1270
                if ($throwException) {
1271
                    throw new InvalidArgumentException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\".");
1272
                }
1273
1274
                return null;
1275
            }
1276
        }
1277
1278 207
        return $relation;
1279
    }
1280
1281
    /**
1282
     * Establishes the relationship between two models.
1283
     *
1284
     * The relationship is established by setting the foreign key value(s) in one model
1285
     * to be the corresponding primary key value(s) in the other model.
1286
     * The model with the foreign key will be saved into database **without** performing validation
1287
     * and **without** events/behaviors.
1288
     *
1289
     * If the relationship involves a junction table, a new row will be inserted into the
1290
     * junction table which contains the primary key values from both models.
1291
     *
1292
     * Note that this method requires that the primary key value is not null.
1293
     *
1294
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via `getOrders()` method.
1295
     * @param ActiveRecordInterface $model the model to be linked with the current one.
1296
     * @param array $extraColumns additional column values to be saved into the junction table.
1297
     * This parameter is only meaningful for a relationship involving a junction table
1298
     * (i.e., a relation set with [[ActiveRelationTrait::via()]] or [[ActiveQuery::viaTable()]].)
1299
     * @throws InvalidCallException if the method is unable to link two models.
1300
     */
1301 9
    public function link($name, $model, $extraColumns = [])
1302
    {
1303
        /* @var $relation ActiveQueryInterface|ActiveQuery */
1304 9
        $relation = $this->getRelation($name);
1305
1306 9
        if ($relation->via !== null) {
0 ignored issues
show
Bug introduced by
Accessing via on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1307 3
            if ($this->getIsNewRecord() || $model->getIsNewRecord()) {
1308
                throw new InvalidCallException('Unable to link models: the models being linked cannot be newly created.');
1309
            }
1310 3
            if (is_array($relation->via)) {
1311
                /* @var $viaRelation ActiveQuery */
1312 3
                list($viaName, $viaRelation) = $relation->via;
1313 3
                $viaClass = $viaRelation->modelClass;
1314
                // unset $viaName so that it can be reloaded to reflect the change
1315 3
                unset($this->_related[$viaName]);
1316
            } else {
1317
                $viaRelation = $relation->via;
1318
                $viaTable = reset($relation->via->from);
1319
            }
1320 3
            $columns = [];
1321 3
            foreach ($viaRelation->link as $a => $b) {
1322 3
                $columns[$a] = $this->$b;
1323
            }
1324 3
            foreach ($relation->link as $a => $b) {
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1325 3
                $columns[$b] = $model->$a;
1326
            }
1327 3
            foreach ($extraColumns as $k => $v) {
1328 3
                $columns[$k] = $v;
1329
            }
1330 3
            if (is_array($relation->via)) {
1331
                /* @var $viaClass ActiveRecordInterface */
1332
                /* @var $record ActiveRecordInterface */
1333 3
                $record = Yii::createObject($viaClass);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaClass does not seem to be defined for all execution paths leading up to this point.
Loading history...
1334 3
                foreach ($columns as $column => $value) {
1335 3
                    $record->$column = $value;
1336
                }
1337 3
                $record->insert(false);
1338
            } else {
1339
                /* @var $viaTable string */
1340 3
                static::getDb()->createCommand()->insert($viaTable, $columns)->execute();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaTable does not seem to be defined for all execution paths leading up to this point.
Loading history...
1341
            }
1342
        } else {
1343 9
            $p1 = $model->isPrimaryKey(array_keys($relation->link));
1344 9
            $p2 = static::isPrimaryKey(array_values($relation->link));
1345 9
            if ($p1 && $p2) {
1346
                if ($this->getIsNewRecord()) {
1347
                    if ($model->getIsNewRecord()) {
1348
                        throw new InvalidCallException('Unable to link models: at most one model can be newly created.');
1349
                    }
1350
                    $this->bindModels(array_flip($relation->link), $this, $model);
1351
                } else {
1352
                    $this->bindModels($relation->link, $model, $this);
1353
                }
1354 9
            } elseif ($p1) {
1355 3
                $this->bindModels(array_flip($relation->link), $this, $model);
1356 9
            } elseif ($p2) {
1357 9
                $this->bindModels($relation->link, $model, $this);
1358
            } else {
1359
                throw new InvalidCallException('Unable to link models: the link defining the relation does not involve any primary key.');
1360
            }
1361
        }
1362
1363
        // update lazily loaded related objects
1364 9
        if (!$relation->multiple) {
0 ignored issues
show
Bug introduced by
Accessing multiple on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1365 3
            $this->_related[$name] = $model;
1366 9
        } elseif (isset($this->_related[$name])) {
1367 9
            if ($relation->indexBy !== null) {
0 ignored issues
show
Bug introduced by
Accessing indexBy on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1368 6
                if ($relation->indexBy instanceof \Closure) {
1369 3
                    $index = call_user_func($relation->indexBy, $model);
1370
                } else {
1371 3
                    $index = $model->{$relation->indexBy};
1372
                }
1373 6
                $this->_related[$name][$index] = $model;
1374
            } else {
1375 3
                $this->_related[$name][] = $model;
1376
            }
1377
        }
1378
    }
1379
1380
    /**
1381
     * Destroys the relationship between two models.
1382
     *
1383
     * The model with the foreign key of the relationship will be deleted if `$delete` is `true`.
1384
     * Otherwise, the foreign key will be set `null` and the model will be saved without validation.
1385
     *
1386
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via `getOrders()` method.
1387
     * @param ActiveRecordInterface $model the model to be unlinked from the current one.
1388
     * You have to make sure that the model is really related with the current model as this method
1389
     * does not check this.
1390
     * @param bool $delete whether to delete the model that contains the foreign key.
1391
     * If `false`, the model's foreign key will be set `null` and saved.
1392
     * If `true`, the model containing the foreign key will be deleted.
1393
     * @throws InvalidCallException if the models cannot be unlinked
1394
     * @throws Exception
1395
     * @throws StaleObjectException
1396
     */
1397 9
    public function unlink($name, $model, $delete = false)
1398
    {
1399
        /* @var $relation ActiveQueryInterface|ActiveQuery */
1400 9
        $relation = $this->getRelation($name);
1401
1402 9
        if ($relation->via !== null) {
0 ignored issues
show
Bug introduced by
Accessing via on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1403 9
            if (is_array($relation->via)) {
1404
                /* @var $viaRelation ActiveQuery */
1405 9
                list($viaName, $viaRelation) = $relation->via;
1406 9
                $viaClass = $viaRelation->modelClass;
1407 9
                unset($this->_related[$viaName]);
1408
            } else {
1409 3
                $viaRelation = $relation->via;
1410 3
                $viaTable = reset($relation->via->from);
1411
            }
1412 9
            $columns = [];
1413 9
            foreach ($viaRelation->link as $a => $b) {
1414 9
                $columns[$a] = $this->$b;
1415
            }
1416 9
            foreach ($relation->link as $a => $b) {
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1417 9
                $columns[$b] = $model->$a;
1418
            }
1419 9
            $nulls = [];
1420 9
            foreach (array_keys($columns) as $a) {
1421 9
                $nulls[$a] = null;
1422
            }
1423 9
            if (property_exists($viaRelation, 'on') && $viaRelation->on !== null) {
1424 6
                $columns = ['and', $columns, $viaRelation->on];
1425
            }
1426 9
            if (is_array($relation->via)) {
1427
                /* @var $viaClass ActiveRecordInterface */
1428 9
                if ($delete) {
1429 6
                    $viaClass::deleteAll($columns);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaClass does not seem to be defined for all execution paths leading up to this point.
Loading history...
1430
                } else {
1431 9
                    $viaClass::updateAll($nulls, $columns);
1432
                }
1433
            } else {
1434
                /* @var $viaTable string */
1435
                /* @var $command Command */
1436 3
                $command = static::getDb()->createCommand();
1437 3
                if ($delete) {
1438
                    $command->delete($viaTable, $columns)->execute();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaTable does not seem to be defined for all execution paths leading up to this point.
Loading history...
1439
                } else {
1440 9
                    $command->update($viaTable, $nulls, $columns)->execute();
1441
                }
1442
            }
1443
        } else {
1444 3
            $p1 = $model->isPrimaryKey(array_keys($relation->link));
1445 3
            $p2 = static::isPrimaryKey(array_values($relation->link));
1446 3
            if ($p2) {
1447 3
                if ($delete) {
1448 3
                    $model->delete();
1449
                } else {
1450 3
                    foreach ($relation->link as $a => $b) {
1451 3
                        $model->$a = null;
1452
                    }
1453 3
                    $model->save(false);
1454
                }
1455
            } elseif ($p1) {
1456
                foreach ($relation->link as $a => $b) {
1457
                    if (is_array($this->$b)) { // relation via array valued attribute
1458
                        if (($key = array_search($model->$a, $this->$b, false)) !== false) {
1459
                            $values = $this->$b;
1460
                            unset($values[$key]);
1461
                            $this->$b = array_values($values);
1462
                        }
1463
                    } else {
1464
                        $this->$b = null;
1465
                    }
1466
                }
1467
                $delete ? $this->delete() : $this->save(false);
1468
            } else {
1469
                throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
1470
            }
1471
        }
1472
1473 9
        if (!$relation->multiple) {
0 ignored issues
show
Bug introduced by
Accessing multiple on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1474
            unset($this->_related[$name]);
1475 9
        } elseif (isset($this->_related[$name])) {
1476
            /* @var $b ActiveRecordInterface */
1477 9
            foreach ($this->_related[$name] as $a => $b) {
1478 9
                if ($model->getPrimaryKey() === $b->getPrimaryKey()) {
1479 9
                    unset($this->_related[$name][$a]);
1480
                }
1481
            }
1482
        }
1483
    }
1484
1485
    /**
1486
     * Destroys the relationship in current model.
1487
     *
1488
     * The model with the foreign key of the relationship will be deleted if `$delete` is `true`.
1489
     * Otherwise, the foreign key will be set `null` and the model will be saved without validation.
1490
     *
1491
     * Note that to destroy the relationship without removing records make sure your keys can be set to null
1492
     *
1493
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via `getOrders()` method.
1494
     * @param bool $delete whether to delete the model that contains the foreign key.
1495
     *
1496
     * Note that the deletion will be performed using [[deleteAll()]], which will not trigger any events on the related models.
1497
     * If you need [[EVENT_BEFORE_DELETE]] or [[EVENT_AFTER_DELETE]] to be triggered, you need to [[find()|find]] the models first
1498
     * and then call [[delete()]] on each of them.
1499
     */
1500 18
    public function unlinkAll($name, $delete = false)
1501
    {
1502
        /* @var $relation ActiveQueryInterface|ActiveQuery */
1503 18
        $relation = $this->getRelation($name);
1504
1505 18
        if ($relation->via !== null) {
0 ignored issues
show
Bug introduced by
Accessing via on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1506 9
            if (is_array($relation->via)) {
1507
                /* @var $viaRelation ActiveQuery */
1508 6
                list($viaName, $viaRelation) = $relation->via;
1509 6
                $viaClass = $viaRelation->modelClass;
1510 6
                unset($this->_related[$viaName]);
1511
            } else {
1512 3
                $viaRelation = $relation->via;
1513 3
                $viaTable = reset($relation->via->from);
1514
            }
1515 9
            $condition = [];
1516 9
            $nulls = [];
1517 9
            foreach ($viaRelation->link as $a => $b) {
1518 9
                $nulls[$a] = null;
1519 9
                $condition[$a] = $this->$b;
1520
            }
1521 9
            if (!empty($viaRelation->where)) {
1522
                $condition = ['and', $condition, $viaRelation->where];
1523
            }
1524 9
            if (property_exists($viaRelation, 'on') && !empty($viaRelation->on)) {
1525
                $condition = ['and', $condition, $viaRelation->on];
1526
            }
1527 9
            if (is_array($relation->via)) {
1528
                /* @var $viaClass ActiveRecordInterface */
1529 6
                if ($delete) {
1530 6
                    $viaClass::deleteAll($condition);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaClass does not seem to be defined for all execution paths leading up to this point.
Loading history...
1531
                } else {
1532 6
                    $viaClass::updateAll($nulls, $condition);
1533
                }
1534
            } else {
1535
                /* @var $viaTable string */
1536
                /* @var $command Command */
1537 3
                $command = static::getDb()->createCommand();
1538 3
                if ($delete) {
1539 3
                    $command->delete($viaTable, $condition)->execute();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaTable does not seem to be defined for all execution paths leading up to this point.
Loading history...
1540
                } else {
1541 9
                    $command->update($viaTable, $nulls, $condition)->execute();
1542
                }
1543
            }
1544
        } else {
1545
            /* @var $relatedModel ActiveRecordInterface */
1546 12
            $relatedModel = $relation->modelClass;
0 ignored issues
show
Bug introduced by
Accessing modelClass on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1547 12
            if (!$delete && count($relation->link) === 1 && is_array($this->{$b = reset($relation->link)})) {
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1548
                // relation via array valued attribute
1549
                $this->$b = [];
1550
                $this->save(false);
1551
            } else {
1552 12
                $nulls = [];
1553 12
                $condition = [];
1554 12
                foreach ($relation->link as $a => $b) {
1555 12
                    $nulls[$a] = null;
1556 12
                    $condition[$a] = $this->$b;
1557
                }
1558 12
                if (!empty($relation->where)) {
0 ignored issues
show
Bug introduced by
Accessing where on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1559 6
                    $condition = ['and', $condition, $relation->where];
1560
                }
1561 12
                if (property_exists($relation, 'on') && !empty($relation->on)) {
1562 3
                    $condition = ['and', $condition, $relation->on];
1563
                }
1564 12
                if ($delete) {
1565 9
                    $relatedModel::deleteAll($condition);
1566
                } else {
1567 6
                    $relatedModel::updateAll($nulls, $condition);
1568
                }
1569
            }
1570
        }
1571
1572 18
        unset($this->_related[$name]);
1573
    }
1574
1575
    /**
1576
     * @param array $link
1577
     * @param ActiveRecordInterface $foreignModel
1578
     * @param ActiveRecordInterface $primaryModel
1579
     * @throws InvalidCallException
1580
     */
1581 9
    private function bindModels($link, $foreignModel, $primaryModel)
1582
    {
1583 9
        foreach ($link as $fk => $pk) {
1584 9
            $value = $primaryModel->$pk;
1585 9
            if ($value === null) {
1586
                throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.');
1587
            }
1588 9
            if (is_array($foreignModel->$fk)) { // relation via array valued attribute
1589
                $foreignModel->{$fk}[] = $value;
1590
            } else {
1591 9
                $foreignModel->{$fk} = $value;
1592
            }
1593
        }
1594 9
        $foreignModel->save(false);
1595
    }
1596
1597
    /**
1598
     * Returns a value indicating whether the given set of attributes represents the primary key for this model.
1599
     * @param array $keys the set of attributes to check
1600
     * @return bool whether the given set of attributes represents the primary key for this model
1601
     */
1602 15
    public static function isPrimaryKey($keys)
1603
    {
1604 15
        $pks = static::primaryKey();
1605 15
        if (count($keys) === count($pks)) {
1606 15
            return count(array_intersect($keys, $pks)) === count($pks);
1607
        }
1608
1609 9
        return false;
1610
    }
1611
1612
    /**
1613
     * Returns the text label for the specified attribute.
1614
     * The attribute may be specified in a dot format to retrieve the label from related model or allow this model to override the label defined in related model.
1615
     * For example, if the attribute is specified as 'relatedModel1.relatedModel2.attr' the function will return the first label definition it can find
1616
     * in the following order:
1617
     * - the label for 'relatedModel1.relatedModel2.attr' defined in [[attributeLabels()]] of this model;
1618
     * - the label for 'relatedModel2.attr' defined in related model represented by relation 'relatedModel1' of this model;
1619
     * - the label for 'attr' defined in related model represented by relation 'relatedModel2' of relation 'relatedModel1'.
1620
     *   If no label definition was found then the value of $this->generateAttributeLabel('relatedModel1.relatedModel2.attr') will be returned.
1621
     * @param string $attribute the attribute name
1622
     * @return string the attribute label
1623
     * @see attributeLabels()
1624
     * @see generateAttributeLabel()
1625
     */
1626 58
    public function getAttributeLabel($attribute)
1627
    {
1628 58
        $model = $this;
1629 58
        $modelAttribute = $attribute;
1630
        for (;;) {
1631 58
            $labels = $model->attributeLabels();
1632 58
            if (isset($labels[$modelAttribute])) {
1633 11
                return $labels[$modelAttribute];
1634
            }
1635
1636 54
            $parts = explode('.', $modelAttribute, 2);
1637 54
            if (count($parts) < 2) {
1638 48
                break;
1639
            }
1640
1641 6
            list ($relationName, $modelAttribute) = $parts;
1642
1643 6
            if ($model->isRelationPopulated($relationName) && $model->$relationName instanceof self) {
1644 3
                $model = $model->$relationName;
1645
            } else {
1646
                try {
1647 6
                    $relation = $model->getRelation($relationName);
1648 6
                } catch (InvalidArgumentException $e) {
1649 6
                    break;
1650
                }
1651
                /* @var $modelClass ActiveRecordInterface */
1652 3
                $modelClass = $relation->modelClass;
0 ignored issues
show
Bug introduced by
Accessing modelClass on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1653 58
                $model = $modelClass::instance();
1654
            }
1655
        }
1656
1657 54
        return $this->generateAttributeLabel($attribute);
1658
    }
1659
1660
    /**
1661
     * Returns the text hint for the specified attribute.
1662
     * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
1663
     * @param string $attribute the attribute name
1664
     * @return string the attribute hint
1665
     * @see attributeHints()
1666
     * @since 2.0.4
1667
     */
1668
    public function getAttributeHint($attribute)
1669
    {
1670
        $hints = $this->attributeHints();
1671
        if (isset($hints[$attribute])) {
1672
            return $hints[$attribute];
1673
        } elseif (strpos($attribute, '.')) {
1674
            $attributeParts = explode('.', $attribute);
1675
            $neededAttribute = array_pop($attributeParts);
1676
1677
            $relatedModel = $this;
1678
            foreach ($attributeParts as $relationName) {
1679
                if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) {
1680
                    $relatedModel = $relatedModel->$relationName;
1681
                } else {
1682
                    try {
1683
                        $relation = $relatedModel->getRelation($relationName);
1684
                    } catch (InvalidParamException $e) {
1685
                        return '';
1686
                    }
1687
                    /* @var $modelClass ActiveRecordInterface */
1688
                    $modelClass = $relation->modelClass;
0 ignored issues
show
Bug introduced by
Accessing modelClass on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1689
                    $relatedModel = $modelClass::instance();
1690
                }
1691
            }
1692
1693
            $hints = $relatedModel->attributeHints();
1694
            if (isset($hints[$neededAttribute])) {
1695
                return $hints[$neededAttribute];
1696
            }
1697
        }
1698
1699
        return '';
1700
    }
1701
1702
    /**
1703
     * {@inheritdoc}
1704
     *
1705
     * The default implementation returns the names of the columns whose values have been populated into this record.
1706
     */
1707 3
    public function fields()
1708
    {
1709 3
        $fields = array_keys($this->_attributes);
1710
1711 3
        return array_combine($fields, $fields);
1712
    }
1713
1714
    /**
1715
     * {@inheritdoc}
1716
     *
1717
     * The default implementation returns the names of the relations that have been populated into this record.
1718
     */
1719
    public function extraFields()
1720
    {
1721
        $fields = array_keys($this->getRelatedRecords());
1722
1723
        return array_combine($fields, $fields);
1724
    }
1725
1726
    /**
1727
     * Sets the element value at the specified offset to null.
1728
     * This method is required by the SPL interface [[\ArrayAccess]].
1729
     * It is implicitly called when you use something like `unset($model[$offset])`.
1730
     * @param mixed $offset the offset to unset element
1731
     */
1732 3
    public function offsetUnset($offset)
1733
    {
1734 3
        if (property_exists($this, $offset)) {
1735
            $this->$offset = null;
1736
        } else {
1737 3
            unset($this->$offset);
1738
        }
1739
    }
1740
1741
    /**
1742
     * Resets dependent related models checking if their links contain specific attribute.
1743
     * @param string $attribute The changed attribute name.
1744
     */
1745 15
    private function resetDependentRelations($attribute)
1746
    {
1747 15
        foreach ($this->_relationsDependencies[$attribute] as $relation) {
1748 15
            unset($this->_related[$relation]);
1749
        }
1750 15
        unset($this->_relationsDependencies[$attribute]);
1751
    }
1752
1753
    /**
1754
     * Sets relation dependencies for a property
1755
     * @param string $name property name
1756
     * @param ActiveQueryInterface $relation relation instance
1757
     * @param string|null $viaRelationName intermediate relation
1758
     */
1759 88
    private function setRelationDependencies($name, $relation, $viaRelationName = null)
1760
    {
1761 88
        if (empty($relation->via) && $relation->link) {
0 ignored issues
show
Bug introduced by
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Bug introduced by
Accessing via on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1762 85
            foreach ($relation->link as $attribute) {
1763 85
                $this->_relationsDependencies[$attribute][$name] = $name;
1764 85
                if ($viaRelationName !== null) {
1765 36
                    $this->_relationsDependencies[$attribute][] = $viaRelationName;
1766
                }
1767
            }
1768 51
        } elseif ($relation->via instanceof ActiveQueryInterface) {
1769 15
            $this->setRelationDependencies($name, $relation->via);
1770 39
        } elseif (is_array($relation->via)) {
1771 36
            list($viaRelationName, $viaQuery) = $relation->via;
1772 36
            $this->setRelationDependencies($name, $viaQuery, $viaRelationName);
1773
        }
1774
    }
1775
1776
    /**
1777
     * @param mixed $newValue
1778
     * @param mixed $oldValue
1779
     * @return bool
1780
     * @since 2.0.48
1781
     */
1782 59
    private function isValueDifferent($newValue, $oldValue)
1783
    {
1784 59
        if (is_array($newValue) && is_array($oldValue)) {
1785
            // Only sort associative arrays
1786 13
            $sorter = function(&$array) {
1787 13
                if (ArrayHelper::isAssociative($array)) {
1788 11
                    ksort($array);
1789
                }
1790 13
            };
1791 13
            $newValue = ArrayHelper::recursiveSort($newValue, $sorter);
1792 13
            $oldValue = ArrayHelper::recursiveSort($oldValue, $sorter);
1793
        }
1794
1795 59
        return $newValue !== $oldValue;
1796
    }
1797
1798
    /**
1799
     * Eager loads related models for the already loaded primary models.
1800
     *
1801
     * Helps to reduce the number of queries performed against database if some related models are only used
1802
     * when a specific condition is met. For example:
1803
     *
1804
     * ```php
1805
     * $customers = Customer::find()->where(['country_id' => 123])->all();
1806
     * if (Yii:app()->getUser()->getIdentity()->canAccessOrders()) {
1807
     *     Customer::loadRelationsFor($customers, 'orders.items');
1808
     * }
1809
     * ```
1810
     *
1811
     * @param array|ActiveRecordInterface[] $models array of primary models. Each model should have the same type and can be:
1812
     * - an active record instance;
1813
     * - active record instance represented by array (i.e. active record was loaded using [[ActiveQuery::asArray()]]).
1814
     * @param string|array $relationNames the names of the relations of primary models to be loaded from database. See [[ActiveQueryInterface::with()]] on how to specify this argument.
1815
     * @param bool $asArray whether to load each related model as an array or an object (if the relation itself does not specify that).
1816
     * @since 2.0.50
1817
     */
1818 3
    public static function loadRelationsFor(&$models, $relationNames, $asArray = false)
1819
    {
1820
        // ActiveQueryTrait::findWith() called below assumes $models array is non-empty.
1821 3
        if (empty($models)) {
1822
            return;
1823
        }
1824
1825 3
        static::find()->asArray($asArray)->findWith((array)$relationNames, $models);
0 ignored issues
show
Bug introduced by
The method findWith() does not exist on yii\db\ActiveQueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to yii\db\ActiveQueryInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1825
        static::find()->asArray($asArray)->/** @scrutinizer ignore-call */ findWith((array)$relationNames, $models);
Loading history...
1826
    }
1827
1828
    /**
1829
     * Eager loads related models for the already loaded primary model.
1830
     *
1831
     * Helps to reduce the number of queries performed against database if some related models are only used
1832
     * when a specific condition is met. For example:
1833
     *
1834
     * ```php
1835
     * $customer = Customer::find()->where(['id' => 123])->one();
1836
     * if (Yii:app()->getUser()->getIdentity()->canAccessOrders()) {
1837
     *     $customer->loadRelations('orders.items');
1838
     * }
1839
     * ```
1840
     *
1841
     * @param string|array $relationNames the names of the relations of this model to be loaded from database. See [[ActiveQueryInterface::with()]] on how to specify this argument.
1842
     * @param bool $asArray whether to load each relation as an array or an object (if the relation itself does not specify that).
1843
     * @since 2.0.50
1844
     */
1845 3
    public function loadRelations($relationNames, $asArray = false)
1846
    {
1847 3
        $models = [$this];
1848 3
        static::loadRelationsFor($models, $relationNames, $asArray);
1849
    }
1850
}
1851