Passed
Push — master ( 3a5802...52cf02 )
by Alexander
04:09
created

BaseActiveRecord::unlinkAll()   D

Complexity

Conditions 18
Paths 41

Size

Total Lines 72
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 18.3238

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 49
nc 41
nop 2
dl 0
loc 72
ccs 36
cts 40
cp 0.9
crap 18.3238
rs 4.8666
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://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). This property is
27
 * read-only.
28
 * @property bool $isNewRecord Whether the record is new and should be inserted when calling [[save()]].
29
 * @property array $oldAttributes The old attribute values (name-value pairs). Note that the type of this
30
 * property differs in getter and setter. See [[getOldAttributes()]] and [[setOldAttributes()]] for details.
31
 * @property-read mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is
32
 * returned if the primary key is composite. A string is returned otherwise (null will be returned if the key
33
 * value is null). This property is read-only.
34
 * @property-read mixed $primaryKey The primary key value. An array (column name => column value) is returned
35
 * if the primary key is composite. A string is returned otherwise (null will be returned if the key value is
36
 * null). This property is read-only.
37
 * @property-read array $relatedRecords An array of related records indexed by relation names. This property
38
 * is read-only.
39
 *
40
 * @author Qiang Xue <[email protected]>
41
 * @author Carsten Brandt <[email protected]>
42
 * @since 2.0
43
 */
44
abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
45
{
46
    /**
47
     * @event Event an event that is triggered when the record is initialized via [[init()]].
48
     */
49
    const EVENT_INIT = 'init';
50
    /**
51
     * @event Event an event that is triggered after the record is created and populated with query result.
52
     */
53
    const EVENT_AFTER_FIND = 'afterFind';
54
    /**
55
     * @event ModelEvent an event that is triggered before inserting a record.
56
     * You may set [[ModelEvent::isValid]] to be `false` to stop the insertion.
57
     */
58
    const EVENT_BEFORE_INSERT = 'beforeInsert';
59
    /**
60
     * @event AfterSaveEvent an event that is triggered after a record is inserted.
61
     */
62
    const EVENT_AFTER_INSERT = 'afterInsert';
63
    /**
64
     * @event ModelEvent an event that is triggered before updating a record.
65
     * You may set [[ModelEvent::isValid]] to be `false` to stop the update.
66
     */
67
    const EVENT_BEFORE_UPDATE = 'beforeUpdate';
68
    /**
69
     * @event AfterSaveEvent an event that is triggered after a record is updated.
70
     */
71
    const EVENT_AFTER_UPDATE = 'afterUpdate';
72
    /**
73
     * @event ModelEvent an event that is triggered before deleting a record.
74
     * You may set [[ModelEvent::isValid]] to be `false` to stop the deletion.
75
     */
76
    const EVENT_BEFORE_DELETE = 'beforeDelete';
77
    /**
78
     * @event Event an event that is triggered after a record is deleted.
79
     */
80
    const EVENT_AFTER_DELETE = 'afterDelete';
81
    /**
82
     * @event Event an event that is triggered after a record is refreshed.
83
     * @since 2.0.8
84
     */
85
    const EVENT_AFTER_REFRESH = 'afterRefresh';
86
87
    /**
88
     * @var array attribute values indexed by attribute names
89
     */
90
    private $_attributes = [];
91
    /**
92
     * @var array|null old attribute values indexed by attribute names.
93
     * This is `null` if the record [[isNewRecord|is new]].
94
     */
95
    private $_oldAttributes;
96
    /**
97
     * @var array related models indexed by the relation names
98
     */
99
    private $_related = [];
100
    /**
101
     * @var array relation names indexed by their link attributes
102
     */
103
    private $_relationsDependencies = [];
104
105
106
    /**
107
     * {@inheritdoc}
108
     * @return static|null ActiveRecord instance matching the condition, or `null` if nothing matches.
109
     */
110 193
    public static function findOne($condition)
111
    {
112 193
        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...
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     * @return static[] an array of ActiveRecord instances, or an empty array if nothing matches.
118
     */
119
    public static function findAll($condition)
120
    {
121
        return static::findByCondition($condition)->all();
122
    }
123
124
    /**
125
     * Finds ActiveRecord instance(s) by the given condition.
126
     * This method is internally called by [[findOne()]] and [[findAll()]].
127
     * @param mixed $condition please refer to [[findOne()]] for the explanation of this parameter
128
     * @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance.
129
     * @throws InvalidConfigException if there is no primary key defined
130
     * @internal
131
     */
132
    protected static function findByCondition($condition)
133
    {
134
        $query = static::find();
135
136
        if (!ArrayHelper::isAssociative($condition) && !$condition instanceof ExpressionInterface) {
137
            // query by primary key
138
            $primaryKey = static::primaryKey();
139
            if (isset($primaryKey[0])) {
140
                // if condition is scalar, search for a single primary key, if it is array, search for multiple primary key values
141
                $condition = [$primaryKey[0] => is_array($condition) ? array_values($condition) : $condition];
142
            } else {
143
                throw new InvalidConfigException('"' . get_called_class() . '" must have a primary key.');
144
            }
145
        }
146
147
        return $query->andWhere($condition);
148
    }
149
150
    /**
151
     * Updates the whole table using the provided attribute values and conditions.
152
     *
153
     * For example, to change the status to be 1 for all customers whose status is 2:
154
     *
155
     * ```php
156
     * Customer::updateAll(['status' => 1], 'status = 2');
157
     * ```
158
     *
159
     * @param array $attributes attribute values (name-value pairs) to be saved into the table
160
     * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
161
     * Please refer to [[Query::where()]] on how to specify this parameter.
162
     * @return int the number of rows updated
163
     * @throws NotSupportedException if not overridden
164
     */
165
    public static function updateAll($attributes, $condition = '')
166
    {
167
        throw new NotSupportedException(__METHOD__ . ' is not supported.');
168
    }
169
170
    /**
171
     * Updates the whole table using the provided counter changes and conditions.
172
     *
173
     * For example, to increment all customers' age by 1,
174
     *
175
     * ```php
176
     * Customer::updateAllCounters(['age' => 1]);
177
     * ```
178
     *
179
     * @param array $counters the counters to be updated (attribute name => increment value).
180
     * Use negative values if you want to decrement the counters.
181
     * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
182
     * Please refer to [[Query::where()]] on how to specify this parameter.
183
     * @return int the number of rows updated
184
     * @throws NotSupportedException if not overrided
185
     */
186
    public static function updateAllCounters($counters, $condition = '')
187
    {
188
        throw new NotSupportedException(__METHOD__ . ' is not supported.');
189
    }
190
191
    /**
192
     * Deletes rows in the table using the provided conditions.
193
     * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
194
     *
195
     * For example, to delete all customers whose status is 3:
196
     *
197
     * ```php
198
     * Customer::deleteAll('status = 3');
199
     * ```
200
     *
201
     * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL.
202
     * Please refer to [[Query::where()]] on how to specify this parameter.
203
     * @return int the number of rows deleted
204
     * @throws NotSupportedException if not overridden.
205
     */
206
    public static function deleteAll($condition = null)
207
    {
208
        throw new NotSupportedException(__METHOD__ . ' is not supported.');
209
    }
210
211
    /**
212
     * Returns the name of the column that stores the lock version for implementing optimistic locking.
213
     *
214
     * Optimistic locking allows multiple users to access the same record for edits and avoids
215
     * potential conflicts. In case when a user attempts to save the record upon some staled data
216
     * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown,
217
     * and the update or deletion is skipped.
218
     *
219
     * Optimistic locking is only supported by [[update()]] and [[delete()]].
220
     *
221
     * To use Optimistic locking:
222
     *
223
     * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
224
     *    Override this method to return the name of this column.
225
     * 2. Ensure the version value is submitted and loaded to your model before any update or delete.
226
     *    Or add [[\yii\behaviors\OptimisticLockBehavior|OptimisticLockBehavior]] to your model
227
     *    class in order to automate the process.
228
     * 3. In the Web form that collects the user input, add a hidden field that stores
229
     *    the lock version of the recording being updated.
230
     * 4. In the controller action that does the data updating, try to catch the [[StaleObjectException]]
231
     *    and implement necessary business logic (e.g. merging the changes, prompting stated data)
232
     *    to resolve the conflict.
233
     *
234
     * @return string the column name that stores the lock version of a table row.
235
     * If `null` is returned (default implemented), optimistic locking will not be supported.
236
     */
237 36
    public function optimisticLock()
238
    {
239 36
        return null;
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245 3
    public function canGetProperty($name, $checkVars = true, $checkBehaviors = true)
246
    {
247 3
        if (parent::canGetProperty($name, $checkVars, $checkBehaviors)) {
248 3
            return true;
249
        }
250
251
        try {
252 3
            return $this->hasAttribute($name);
253
        } catch (\Exception $e) {
254
            // `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used
255
            return false;
256
        }
257
    }
258
259
    /**
260
     * {@inheritdoc}
261
     */
262 9
    public function canSetProperty($name, $checkVars = true, $checkBehaviors = true)
263
    {
264 9
        if (parent::canSetProperty($name, $checkVars, $checkBehaviors)) {
265 6
            return true;
266
        }
267
268
        try {
269 3
            return $this->hasAttribute($name);
270
        } catch (\Exception $e) {
271
            // `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used
272
            return false;
273
        }
274
    }
275
276
    /**
277
     * PHP getter magic method.
278
     * This method is overridden so that attributes and related objects can be accessed like properties.
279
     *
280
     * @param string $name property name
281
     * @throws InvalidArgumentException if relation name is wrong
282
     * @return mixed property value
283
     * @see getAttribute()
284
     */
285 439
    public function __get($name)
286
    {
287 439
        if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
288 418
            return $this->_attributes[$name];
289
        }
290
291 222
        if ($this->hasAttribute($name)) {
292 47
            return null;
293
        }
294
295 196
        if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
296 115
            return $this->_related[$name];
297
        }
298 130
        $value = parent::__get($name);
299 121
        if ($value instanceof ActiveQueryInterface) {
300 76
            $this->setRelationDependencies($name, $value);
301 76
            return $this->_related[$name] = $value->findFor($name, $this);
302
        }
303
304 54
        return $value;
305
    }
306
307
    /**
308
     * PHP setter magic method.
309
     * This method is overridden so that AR attributes can be accessed like properties.
310
     * @param string $name property name
311
     * @param mixed $value property value
312
     */
313 284
    public function __set($name, $value)
314
    {
315 284
        if ($this->hasAttribute($name)) {
316
            if (
317 284
                !empty($this->_relationsDependencies[$name])
318 284
                && (!array_key_exists($name, $this->_attributes) || $this->_attributes[$name] !== $value)
319
            ) {
320 15
                $this->resetDependentRelations($name);
321
            }
322 284
            $this->_attributes[$name] = $value;
323
        } else {
324 6
            parent::__set($name, $value);
325
        }
326 284
    }
327
328
    /**
329
     * Checks if a property value is null.
330
     * This method overrides the parent implementation by checking if the named attribute is `null` or not.
331
     * @param string $name the property name or the event name
332
     * @return bool whether the property value is null
333
     */
334 116
    public function __isset($name)
335
    {
336
        try {
337 116
            return $this->__get($name) !== null;
338 10
        } catch (\Throwable $t) {
339 10
            return false;
340
        } catch (\Exception $e) {
341
            return false;
342
        }
343
    }
344
345
    /**
346
     * Sets a component property to be null.
347
     * This method overrides the parent implementation by clearing
348
     * the specified attribute value.
349
     * @param string $name the property name or the event name
350
     */
351 15
    public function __unset($name)
352
    {
353 15
        if ($this->hasAttribute($name)) {
354 9
            unset($this->_attributes[$name]);
355 9
            if (!empty($this->_relationsDependencies[$name])) {
356 9
                $this->resetDependentRelations($name);
357
            }
358 6
        } elseif (array_key_exists($name, $this->_related)) {
359 6
            unset($this->_related[$name]);
360
        } elseif ($this->getRelation($name, false) === null) {
361
            parent::__unset($name);
362
        }
363 15
    }
364
365
    /**
366
     * Declares a `has-one` relation.
367
     * The declaration is returned in terms of a relational [[ActiveQuery]] instance
368
     * through which the related record can be queried and retrieved back.
369
     *
370
     * A `has-one` relation means that there is at most one related record matching
371
     * the criteria set by this relation, e.g., a customer has one country.
372
     *
373
     * For example, to declare the `country` relation for `Customer` class, we can write
374
     * the following code in the `Customer` class:
375
     *
376
     * ```php
377
     * public function getCountry()
378
     * {
379
     *     return $this->hasOne(Country::className(), ['id' => 'country_id']);
380
     * }
381
     * ```
382
     *
383
     * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name
384
     * in the related class `Country`, while the 'country_id' value refers to an attribute name
385
     * in the current AR class.
386
     *
387
     * Call methods declared in [[ActiveQuery]] to further customize the relation.
388
     *
389
     * @param string $class the class name of the related record
390
     * @param array $link the primary-foreign key constraint. The keys of the array refer to
391
     * the attributes of the record associated with the `$class` model, while the values of the
392
     * array refer to the corresponding attributes in **this** AR class.
393
     * @return ActiveQueryInterface the relational query object.
394
     */
395 103
    public function hasOne($class, $link)
396
    {
397 103
        return $this->createRelationQuery($class, $link, false);
398
    }
399
400
    /**
401
     * Declares a `has-many` relation.
402
     * The declaration is returned in terms of a relational [[ActiveQuery]] instance
403
     * through which the related record can be queried and retrieved back.
404
     *
405
     * A `has-many` relation means that there are multiple related records matching
406
     * the criteria set by this relation, e.g., a customer has many orders.
407
     *
408
     * For example, to declare the `orders` relation for `Customer` class, we can write
409
     * the following code in the `Customer` class:
410
     *
411
     * ```php
412
     * public function getOrders()
413
     * {
414
     *     return $this->hasMany(Order::className(), ['customer_id' => 'id']);
415
     * }
416
     * ```
417
     *
418
     * Note that in the above, the 'customer_id' key in the `$link` parameter refers to
419
     * an attribute name in the related class `Order`, while the 'id' value refers to
420
     * an attribute name in the current AR class.
421
     *
422
     * Call methods declared in [[ActiveQuery]] to further customize the relation.
423
     *
424
     * @param string $class the class name of the related record
425
     * @param array $link the primary-foreign key constraint. The keys of the array refer to
426
     * the attributes of the record associated with the `$class` model, while the values of the
427
     * array refer to the corresponding attributes in **this** AR class.
428
     * @return ActiveQueryInterface the relational query object.
429
     */
430 171
    public function hasMany($class, $link)
431
    {
432 171
        return $this->createRelationQuery($class, $link, true);
433
    }
434
435
    /**
436
     * Creates a query instance for `has-one` or `has-many` relation.
437
     * @param string $class the class name of the related record.
438
     * @param array $link the primary-foreign key constraint.
439
     * @param bool $multiple whether this query represents a relation to more than one record.
440
     * @return ActiveQueryInterface the relational query object.
441
     * @since 2.0.12
442
     * @see hasOne()
443
     * @see hasMany()
444
     */
445 223
    protected function createRelationQuery($class, $link, $multiple)
446
    {
447
        /* @var $class ActiveRecordInterface */
448
        /* @var $query ActiveQuery */
449 223
        $query = $class::find();
450 223
        $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...
451 223
        $query->link = $link;
452 223
        $query->multiple = $multiple;
453 223
        return $query;
454
    }
455
456
    /**
457
     * Populates the named relation with the related records.
458
     * Note that this method does not check if the relation exists or not.
459
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method (case-sensitive).
460
     * @param ActiveRecordInterface|array|null $records the related records to be populated into the relation.
461
     * @see getRelation()
462
     */
463 141
    public function populateRelation($name, $records)
464
    {
465 141
        foreach ($this->_relationsDependencies as &$relationNames) {
466 12
            unset($relationNames[$name]);
467
        }
468
469 141
        $this->_related[$name] = $records;
470 141
    }
471
472
    /**
473
     * Check whether the named relation has been populated with records.
474
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method (case-sensitive).
475
     * @return bool whether relation has been populated with records.
476
     * @see getRelation()
477
     */
478 84
    public function isRelationPopulated($name)
479
    {
480 84
        return array_key_exists($name, $this->_related);
481
    }
482
483
    /**
484
     * Returns all populated related records.
485
     * @return array an array of related records indexed by relation names.
486
     * @see getRelation()
487
     */
488 6
    public function getRelatedRecords()
489
    {
490 6
        return $this->_related;
491
    }
492
493
    /**
494
     * Returns a value indicating whether the model has an attribute with the specified name.
495
     * @param string $name the name of the attribute
496
     * @return bool whether the model has an attribute with the specified name.
497
     */
498 339
    public function hasAttribute($name)
499
    {
500 339
        return isset($this->_attributes[$name]) || in_array($name, $this->attributes(), true);
501
    }
502
503
    /**
504
     * Returns the named attribute value.
505
     * If this record is the result of a query and the attribute is not loaded,
506
     * `null` will be returned.
507
     * @param string $name the attribute name
508
     * @return mixed the attribute value. `null` if the attribute is not set or does not exist.
509
     * @see hasAttribute()
510
     */
511
    public function getAttribute($name)
512
    {
513
        return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
514
    }
515
516
    /**
517
     * Sets the named attribute value.
518
     * @param string $name the attribute name
519
     * @param mixed $value the attribute value.
520
     * @throws InvalidArgumentException if the named attribute does not exist.
521
     * @see hasAttribute()
522
     */
523 93
    public function setAttribute($name, $value)
524
    {
525 93
        if ($this->hasAttribute($name)) {
526
            if (
527 93
                !empty($this->_relationsDependencies[$name])
528 93
                && (!array_key_exists($name, $this->_attributes) || $this->_attributes[$name] !== $value)
529
            ) {
530 6
                $this->resetDependentRelations($name);
531
            }
532 93
            $this->_attributes[$name] = $value;
533
        } else {
534
            throw new InvalidArgumentException(get_class($this) . ' has no attribute named "' . $name . '".');
535
        }
536 93
    }
537
538
    /**
539
     * Returns the old attribute values.
540
     * @return array the old attribute values (name-value pairs)
541
     */
542
    public function getOldAttributes()
543
    {
544
        return $this->_oldAttributes === null ? [] : $this->_oldAttributes;
545
    }
546
547
    /**
548
     * Sets the old attribute values.
549
     * All existing old attribute values will be discarded.
550
     * @param array|null $values old attribute values to be set.
551
     * If set to `null` this record is considered to be [[isNewRecord|new]].
552
     */
553 104
    public function setOldAttributes($values)
554
    {
555 104
        $this->_oldAttributes = $values;
556 104
    }
557
558
    /**
559
     * Returns the old value of the named attribute.
560
     * If this record is the result of a query and the attribute is not loaded,
561
     * `null` will be returned.
562
     * @param string $name the attribute name
563
     * @return mixed the old attribute value. `null` if the attribute is not loaded before
564
     * or does not exist.
565
     * @see hasAttribute()
566
     */
567
    public function getOldAttribute($name)
568
    {
569
        return isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
570
    }
571
572
    /**
573
     * Sets the old value of the named attribute.
574
     * @param string $name the attribute name
575
     * @param mixed $value the old attribute value.
576
     * @throws InvalidArgumentException if the named attribute does not exist.
577
     * @see hasAttribute()
578
     */
579
    public function setOldAttribute($name, $value)
580
    {
581
        if (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name)) {
582
            $this->_oldAttributes[$name] = $value;
583
        } else {
584
            throw new InvalidArgumentException(get_class($this) . ' has no attribute named "' . $name . '".');
585
        }
586
    }
587
588
    /**
589
     * Marks an attribute dirty.
590
     * This method may be called to force updating a record when calling [[update()]],
591
     * even if there is no change being made to the record.
592
     * @param string $name the attribute name
593
     */
594 7
    public function markAttributeDirty($name)
595
    {
596 7
        unset($this->_oldAttributes[$name]);
597 7
    }
598
599
    /**
600
     * Returns a value indicating whether the named attribute has been changed.
601
     * @param string $name the name of the attribute.
602
     * @param bool $identical whether the comparison of new and old value is made for
603
     * identical values using `===`, defaults to `true`. Otherwise `==` is used for comparison.
604
     * This parameter is available since version 2.0.4.
605
     * @return bool whether the attribute has been changed
606
     */
607 2
    public function isAttributeChanged($name, $identical = true)
608
    {
609 2
        if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) {
610 1
            if ($identical) {
611 1
                return $this->_attributes[$name] !== $this->_oldAttributes[$name];
612
            }
613
614
            return $this->_attributes[$name] != $this->_oldAttributes[$name];
615
        }
616
617 1
        return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]);
618
    }
619
620
    /**
621
     * Returns the attribute values that have been modified since they are loaded or saved most recently.
622
     *
623
     * The comparison of new and old values is made for identical values using `===`.
624
     *
625
     * @param string[]|null $names the names of the attributes whose values may be returned if they are
626
     * changed recently. If null, [[attributes()]] will be used.
627
     * @return array the changed attribute values (name-value pairs)
628
     */
629 114
    public function getDirtyAttributes($names = null)
630
    {
631 114
        if ($names === null) {
632 111
            $names = $this->attributes();
633
        }
634 114
        $names = array_flip($names);
635 114
        $attributes = [];
636 114
        if ($this->_oldAttributes === null) {
637 101
            foreach ($this->_attributes as $name => $value) {
638 97
                if (isset($names[$name])) {
639 101
                    $attributes[$name] = $value;
640
                }
641
            }
642
        } else {
643 45
            foreach ($this->_attributes as $name => $value) {
644 45
                if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) {
645 45
                    $attributes[$name] = $value;
646
                }
647
            }
648
        }
649
650 114
        return $attributes;
651
    }
652
653
    /**
654
     * Saves the current record.
655
     *
656
     * This method will call [[insert()]] when [[isNewRecord]] is `true`, or [[update()]]
657
     * when [[isNewRecord]] is `false`.
658
     *
659
     * For example, to save a customer record:
660
     *
661
     * ```php
662
     * $customer = new Customer; // or $customer = Customer::findOne($id);
663
     * $customer->name = $name;
664
     * $customer->email = $email;
665
     * $customer->save();
666
     * ```
667
     *
668
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
669
     * before saving the record. Defaults to `true`. If the validation fails, the record
670
     * will not be saved to the database and this method will return `false`.
671
     * @param array $attributeNames list of attribute names that need to be saved. Defaults to null,
672
     * meaning all attributes that are loaded from DB will be saved.
673
     * @return bool whether the saving succeeded (i.e. no validation errors occurred).
674
     */
675 108
    public function save($runValidation = true, $attributeNames = null)
676
    {
677 108
        if ($this->getIsNewRecord()) {
678 95
            return $this->insert($runValidation, $attributeNames);
679
        }
680
681 30
        return $this->update($runValidation, $attributeNames) !== false;
682
    }
683
684
    /**
685
     * Saves the changes to this active record into the associated database table.
686
     *
687
     * This method performs the following steps in order:
688
     *
689
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
690
     *    returns `false`, the rest of the steps will be skipped;
691
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
692
     *    failed, the rest of the steps will be skipped;
693
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
694
     *    the rest of the steps will be skipped;
695
     * 4. save the record into database. If this fails, it will skip the rest of the steps;
696
     * 5. call [[afterSave()]];
697
     *
698
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
699
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]]
700
     * will be raised by the corresponding methods.
701
     *
702
     * Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
703
     *
704
     * For example, to update a customer record:
705
     *
706
     * ```php
707
     * $customer = Customer::findOne($id);
708
     * $customer->name = $name;
709
     * $customer->email = $email;
710
     * $customer->update();
711
     * ```
712
     *
713
     * Note that it is possible the update does not affect any row in the table.
714
     * In this case, this method will return 0. For this reason, you should use the following
715
     * code to check if update() is successful or not:
716
     *
717
     * ```php
718
     * if ($customer->update() !== false) {
719
     *     // update successful
720
     * } else {
721
     *     // update failed
722
     * }
723
     * ```
724
     *
725
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
726
     * before saving the record. Defaults to `true`. If the validation fails, the record
727
     * will not be saved to the database and this method will return `false`.
728
     * @param array $attributeNames list of attribute names that need to be saved. Defaults to null,
729
     * meaning all attributes that are loaded from DB will be saved.
730
     * @return int|false the number of rows affected, or `false` if validation fails
731
     * or [[beforeSave()]] stops the updating process.
732
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
733
     * being updated is outdated.
734
     * @throws Exception in case update failed.
735
     */
736
    public function update($runValidation = true, $attributeNames = null)
737
    {
738
        if ($runValidation && !$this->validate($attributeNames)) {
739
            return false;
740
        }
741
742
        return $this->updateInternal($attributeNames);
743
    }
744
745
    /**
746
     * Updates the specified attributes.
747
     *
748
     * This method is a shortcut to [[update()]] when data validation is not needed
749
     * and only a small set attributes need to be updated.
750
     *
751
     * You may specify the attributes to be updated as name list or name-value pairs.
752
     * If the latter, the corresponding attribute values will be modified accordingly.
753
     * The method will then save the specified attributes into database.
754
     *
755
     * Note that this method will **not** perform data validation and will **not** trigger events.
756
     *
757
     * @param array $attributes the attributes (names or name-value pairs) to be updated
758
     * @return int the number of rows affected.
759
     */
760 7
    public function updateAttributes($attributes)
761
    {
762 7
        $attrs = [];
763 7
        foreach ($attributes as $name => $value) {
764 7
            if (is_int($name)) {
765
                $attrs[] = $value;
766
            } else {
767 7
                $this->$name = $value;
768 7
                $attrs[] = $name;
769
            }
770
        }
771
772 7
        $values = $this->getDirtyAttributes($attrs);
773 7
        if (empty($values) || $this->getIsNewRecord()) {
774 4
            return 0;
775
        }
776
777 6
        $rows = static::updateAll($values, $this->getOldPrimaryKey(true));
778
779 6
        foreach ($values as $name => $value) {
780 6
            $this->_oldAttributes[$name] = $this->_attributes[$name];
781
        }
782
783 6
        return $rows;
784
    }
785
786
    /**
787
     * @see update()
788
     * @param array $attributes attributes to update
789
     * @return int|false the number of rows affected, or false if [[beforeSave()]] stops the updating process.
790
     * @throws StaleObjectException
791
     */
792 40
    protected function updateInternal($attributes = null)
793
    {
794 40
        if (!$this->beforeSave(false)) {
795
            return false;
796
        }
797 40
        $values = $this->getDirtyAttributes($attributes);
798 40
        if (empty($values)) {
799 3
            $this->afterSave(false, $values);
800 3
            return 0;
801
        }
802 38
        $condition = $this->getOldPrimaryKey(true);
803 38
        $lock = $this->optimisticLock();
804 38
        if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always true.
Loading history...
805 5
            $values[$lock] = $this->$lock + 1;
806 5
            $condition[$lock] = $this->$lock;
807
        }
808
        // We do not check the return value of updateAll() because it's possible
809
        // that the UPDATE statement doesn't change anything and thus returns 0.
810 38
        $rows = static::updateAll($values, $condition);
811
812 38
        if ($lock !== null && !$rows) {
813 4
            throw new StaleObjectException('The object being updated is outdated.');
814
        }
815
816 38
        if (isset($values[$lock])) {
817 5
            $this->$lock = $values[$lock];
818
        }
819
820 38
        $changedAttributes = [];
821 38
        foreach ($values as $name => $value) {
822 38
            $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
823 38
            $this->_oldAttributes[$name] = $value;
824
        }
825 38
        $this->afterSave(false, $changedAttributes);
826
827 38
        return $rows;
828
    }
829
830
    /**
831
     * Updates one or several counter columns for the current AR object.
832
     * Note that this method differs from [[updateAllCounters()]] in that it only
833
     * saves counters for the current AR object.
834
     *
835
     * An example usage is as follows:
836
     *
837
     * ```php
838
     * $post = Post::findOne($id);
839
     * $post->updateCounters(['view_count' => 1]);
840
     * ```
841
     *
842
     * @param array $counters the counters to be updated (attribute name => increment value)
843
     * Use negative values if you want to decrement the counters.
844
     * @return bool whether the saving is successful
845
     * @see updateAllCounters()
846
     */
847 6
    public function updateCounters($counters)
848
    {
849 6
        if (static::updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) {
850 6
            foreach ($counters as $name => $value) {
851 6
                if (!isset($this->_attributes[$name])) {
852 3
                    $this->_attributes[$name] = $value;
853
                } else {
854 3
                    $this->_attributes[$name] += $value;
855
                }
856 6
                $this->_oldAttributes[$name] = $this->_attributes[$name];
857
            }
858
859 6
            return true;
860
        }
861
862
        return false;
863
    }
864
865
    /**
866
     * Deletes the table row corresponding to this active record.
867
     *
868
     * This method performs the following steps in order:
869
     *
870
     * 1. call [[beforeDelete()]]. If the method returns `false`, it will skip the
871
     *    rest of the steps;
872
     * 2. delete the record from the database;
873
     * 3. call [[afterDelete()]].
874
     *
875
     * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
876
     * will be raised by the corresponding methods.
877
     *
878
     * @return int|false the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
879
     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
880
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
881
     * being deleted is outdated.
882
     * @throws Exception in case delete failed.
883
     */
884
    public function delete()
885
    {
886
        $result = false;
887
        if ($this->beforeDelete()) {
888
            // we do not check the return value of deleteAll() because it's possible
889
            // the record is already deleted in the database and thus the method will return 0
890
            $condition = $this->getOldPrimaryKey(true);
891
            $lock = $this->optimisticLock();
892
            if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always true.
Loading history...
893
                $condition[$lock] = $this->$lock;
894
            }
895
            $result = static::deleteAll($condition);
896
            if ($lock !== null && !$result) {
897
                throw new StaleObjectException('The object being deleted is outdated.');
898
            }
899
            $this->_oldAttributes = null;
900
            $this->afterDelete();
901
        }
902
903
        return $result;
904
    }
905
906
    /**
907
     * Returns a value indicating whether the current record is new.
908
     * @return bool whether the record is new and should be inserted when calling [[save()]].
909
     */
910 154
    public function getIsNewRecord()
911
    {
912 154
        return $this->_oldAttributes === null;
913
    }
914
915
    /**
916
     * Sets the value indicating whether the record is new.
917
     * @param bool $value whether the record is new and should be inserted when calling [[save()]].
918
     * @see getIsNewRecord()
919
     */
920
    public function setIsNewRecord($value)
921
    {
922
        $this->_oldAttributes = $value ? null : $this->_attributes;
923
    }
924
925
    /**
926
     * Initializes the object.
927
     * This method is called at the end of the constructor.
928
     * The default implementation will trigger an [[EVENT_INIT]] event.
929
     */
930 550
    public function init()
931
    {
932 550
        parent::init();
933 550
        $this->trigger(self::EVENT_INIT);
934 550
    }
935
936
    /**
937
     * This method is called when the AR object is created and populated with the query result.
938
     * The default implementation will trigger an [[EVENT_AFTER_FIND]] event.
939
     * When overriding this method, make sure you call the parent implementation to ensure the
940
     * event is triggered.
941
     */
942 367
    public function afterFind()
943
    {
944 367
        $this->trigger(self::EVENT_AFTER_FIND);
945 367
    }
946
947
    /**
948
     * This method is called at the beginning of inserting or updating a record.
949
     *
950
     * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is `true`,
951
     * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is `false`.
952
     * When overriding this method, make sure you call the parent implementation like the following:
953
     *
954
     * ```php
955
     * public function beforeSave($insert)
956
     * {
957
     *     if (!parent::beforeSave($insert)) {
958
     *         return false;
959
     *     }
960
     *
961
     *     // ...custom code here...
962
     *     return true;
963
     * }
964
     * ```
965
     *
966
     * @param bool $insert whether this method called while inserting a record.
967
     * If `false`, it means the method is called while updating a record.
968
     * @return bool whether the insertion or updating should continue.
969
     * If `false`, the insertion or updating will be cancelled.
970
     */
971 120
    public function beforeSave($insert)
972
    {
973 120
        $event = new ModelEvent();
974 120
        $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
975
976 120
        return $event->isValid;
977
    }
978
979
    /**
980
     * This method is called at the end of inserting or updating a record.
981
     * The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is `true`,
982
     * or an [[EVENT_AFTER_UPDATE]] event if `$insert` is `false`. The event class used is [[AfterSaveEvent]].
983
     * When overriding this method, make sure you call the parent implementation so that
984
     * the event is triggered.
985
     * @param bool $insert whether this method called while inserting a record.
986
     * If `false`, it means the method is called while updating a record.
987
     * @param array $changedAttributes The old values of attributes that had changed and were saved.
988
     * You can use this parameter to take action based on the changes made for example send an email
989
     * when the password had changed or implement audit trail that tracks all the changes.
990
     * `$changedAttributes` gives you the old attribute values while the active record (`$this`) has
991
     * already the new, updated values.
992
     *
993
     * Note that no automatic type conversion performed by default. You may use
994
     * [[\yii\behaviors\AttributeTypecastBehavior]] to facilitate attribute typecasting.
995
     * See http://www.yiiframework.com/doc-2.0/guide-db-active-record.html#attributes-typecasting.
996
     */
997 111
    public function afterSave($insert, $changedAttributes)
998
    {
999 111
        $this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE, new AfterSaveEvent([
1000 111
            'changedAttributes' => $changedAttributes,
1001
        ]));
1002 111
    }
1003
1004
    /**
1005
     * This method is invoked before deleting a record.
1006
     *
1007
     * The default implementation raises the [[EVENT_BEFORE_DELETE]] event.
1008
     * When overriding this method, make sure you call the parent implementation like the following:
1009
     *
1010
     * ```php
1011
     * public function beforeDelete()
1012
     * {
1013
     *     if (!parent::beforeDelete()) {
1014
     *         return false;
1015
     *     }
1016
     *
1017
     *     // ...custom code here...
1018
     *     return true;
1019
     * }
1020
     * ```
1021
     *
1022
     * @return bool whether the record should be deleted. Defaults to `true`.
1023
     */
1024 7
    public function beforeDelete()
1025
    {
1026 7
        $event = new ModelEvent();
1027 7
        $this->trigger(self::EVENT_BEFORE_DELETE, $event);
1028
1029 7
        return $event->isValid;
1030
    }
1031
1032
    /**
1033
     * This method is invoked after deleting a record.
1034
     * The default implementation raises the [[EVENT_AFTER_DELETE]] event.
1035
     * You may override this method to do postprocessing after the record is deleted.
1036
     * Make sure you call the parent implementation so that the event is raised properly.
1037
     */
1038 7
    public function afterDelete()
1039
    {
1040 7
        $this->trigger(self::EVENT_AFTER_DELETE);
1041 7
    }
1042
1043
    /**
1044
     * Repopulates this active record with the latest data.
1045
     *
1046
     * If the refresh is successful, an [[EVENT_AFTER_REFRESH]] event will be triggered.
1047
     * This event is available since version 2.0.8.
1048
     *
1049
     * @return bool whether the row still exists in the database. If `true`, the latest data
1050
     * will be populated to this active record. Otherwise, this record will remain unchanged.
1051
     */
1052
    public function refresh()
1053
    {
1054
        /* @var $record BaseActiveRecord */
1055
        $record = static::findOne($this->getPrimaryKey(true));
1056
        return $this->refreshInternal($record);
1057
    }
1058
1059
    /**
1060
     * Repopulates this active record with the latest data from a newly fetched instance.
1061
     * @param BaseActiveRecord $record the record to take attributes from.
1062
     * @return bool whether refresh was successful.
1063
     * @see refresh()
1064
     * @since 2.0.13
1065
     */
1066 29
    protected function refreshInternal($record)
1067
    {
1068 29
        if ($record === null) {
1069 3
            return false;
1070
        }
1071 29
        foreach ($this->attributes() as $name) {
1072 29
            $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null;
1073
        }
1074 29
        $this->_oldAttributes = $record->_oldAttributes;
1075 29
        $this->_related = [];
1076 29
        $this->_relationsDependencies = [];
1077 29
        $this->afterRefresh();
1078
1079 29
        return true;
1080
    }
1081
1082
    /**
1083
     * This method is called when the AR object is refreshed.
1084
     * The default implementation will trigger an [[EVENT_AFTER_REFRESH]] event.
1085
     * When overriding this method, make sure you call the parent implementation to ensure the
1086
     * event is triggered.
1087
     * @since 2.0.8
1088
     */
1089 29
    public function afterRefresh()
1090
    {
1091 29
        $this->trigger(self::EVENT_AFTER_REFRESH);
1092 29
    }
1093
1094
    /**
1095
     * Returns a value indicating whether the given active record is the same as the current one.
1096
     * The comparison is made by comparing the table names and the primary key values of the two active records.
1097
     * If one of the records [[isNewRecord|is new]] they are also considered not equal.
1098
     * @param ActiveRecordInterface $record record to compare to
1099
     * @return bool whether the two active records refer to the same row in the same database table.
1100
     */
1101
    public function equals($record)
1102
    {
1103
        if ($this->getIsNewRecord() || $record->getIsNewRecord()) {
1104
            return false;
1105
        }
1106
1107
        return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey();
1108
    }
1109
1110
    /**
1111
     * Returns the primary key value(s).
1112
     * @param bool $asArray whether to return the primary key value as an array. If `true`,
1113
     * the return value will be an array with column names as keys and column values as values.
1114
     * Note that for composite primary keys, an array will always be returned regardless of this parameter value.
1115
     * @property mixed The primary key value. An array (column name => column value) is returned if
1116
     * the primary key is composite. A string is returned otherwise (null will be returned if
1117
     * the key value is null).
1118
     * @return mixed the primary key value. An array (column name => column value) is returned if the primary key
1119
     * is composite or `$asArray` is `true`. A string is returned otherwise (null will be returned if
1120
     * the key value is null).
1121
     */
1122 45
    public function getPrimaryKey($asArray = false)
1123
    {
1124 45
        $keys = static::primaryKey();
1125 45
        if (!$asArray && count($keys) === 1) {
1126 19
            return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
1127
        }
1128
1129 29
        $values = [];
1130 29
        foreach ($keys as $name) {
1131 29
            $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
1132
        }
1133
1134 29
        return $values;
1135
    }
1136
1137
    /**
1138
     * Returns the old primary key value(s).
1139
     * This refers to the primary key value that is populated into the record
1140
     * after executing a find method (e.g. find(), findOne()).
1141
     * The value remains unchanged even if the primary key attribute is manually assigned with a different value.
1142
     * @param bool $asArray whether to return the primary key value as an array. If `true`,
1143
     * the return value will be an array with column name as key and column value as value.
1144
     * If this is `false` (default), a scalar value will be returned for non-composite primary key.
1145
     * @property mixed The old primary key value. An array (column name => column value) is
1146
     * returned if the primary key is composite. A string is returned otherwise (null will be
1147
     * returned if the key value is null).
1148
     * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
1149
     * is composite or `$asArray` is `true`. A string is returned otherwise (null will be returned if
1150
     * the key value is null).
1151
     * @throws Exception if the AR model does not have a primary key
1152
     */
1153 76
    public function getOldPrimaryKey($asArray = false)
1154
    {
1155 76
        $keys = static::primaryKey();
1156 76
        if (empty($keys)) {
1157
            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.');
1158
        }
1159 76
        if (!$asArray && count($keys) === 1) {
1160
            return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
1161
        }
1162
1163 76
        $values = [];
1164 76
        foreach ($keys as $name) {
1165 76
            $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
1166
        }
1167
1168 76
        return $values;
1169
    }
1170
1171
    /**
1172
     * Populates an active record object using a row of data from the database/storage.
1173
     *
1174
     * This is an internal method meant to be called to create active record objects after
1175
     * fetching data from the database. It is mainly used by [[ActiveQuery]] to populate
1176
     * the query results into active records.
1177
     *
1178
     * When calling this method manually you should call [[afterFind()]] on the created
1179
     * record to trigger the [[EVENT_AFTER_FIND|afterFind Event]].
1180
     *
1181
     * @param BaseActiveRecord $record the record to be populated. In most cases this will be an instance
1182
     * created by [[instantiate()]] beforehand.
1183
     * @param array $row attribute values (name => value)
1184
     */
1185 367
    public static function populateRecord($record, $row)
1186
    {
1187 367
        $columns = array_flip($record->attributes());
1188 367
        foreach ($row as $name => $value) {
1189 367
            if (isset($columns[$name])) {
1190 367
                $record->_attributes[$name] = $value;
1191 6
            } elseif ($record->canSetProperty($name)) {
1192 367
                $record->$name = $value;
1193
            }
1194
        }
1195 367
        $record->_oldAttributes = $record->_attributes;
1196 367
        $record->_related = [];
1197 367
        $record->_relationsDependencies = [];
1198 367
    }
1199
1200
    /**
1201
     * Creates an active record instance.
1202
     *
1203
     * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
1204
     * It is not meant to be used for creating new records directly.
1205
     *
1206
     * You may override this method if the instance being created
1207
     * depends on the row data to be populated into the record.
1208
     * For example, by creating a record based on the value of a column,
1209
     * you may implement the so-called single-table inheritance mapping.
1210
     * @param array $row row data to be populated into the record.
1211
     * @return static the newly created active record
1212
     */
1213 361
    public static function instantiate($row)
1214
    {
1215 361
        return new static();
1216
    }
1217
1218
    /**
1219
     * Returns whether there is an element at the specified offset.
1220
     * This method is required by the interface [[\ArrayAccess]].
1221
     * @param mixed $offset the offset to check on
1222
     * @return bool whether there is an element at the specified offset.
1223
     */
1224 92
    public function offsetExists($offset)
1225
    {
1226 92
        return $this->__isset($offset);
1227
    }
1228
1229
    /**
1230
     * Returns the relation object with the specified name.
1231
     * A relation is defined by a getter method which returns an [[ActiveQueryInterface]] object.
1232
     * It can be declared in either the Active Record class itself or one of its behaviors.
1233
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method (case-sensitive).
1234
     * @param bool $throwException whether to throw exception if the relation does not exist.
1235
     * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist
1236
     * and `$throwException` is `false`, `null` will be returned.
1237
     * @throws InvalidArgumentException if the named relation does not exist.
1238
     */
1239 192
    public function getRelation($name, $throwException = true)
1240
    {
1241 192
        $getter = 'get' . $name;
1242
        try {
1243
            // the relation could be defined in a behavior
1244 192
            $relation = $this->$getter();
1245
        } catch (UnknownMethodException $e) {
1246
            if ($throwException) {
1247
                throw new InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
1248
            }
1249
1250
            return null;
1251
        }
1252 192
        if (!$relation instanceof ActiveQueryInterface) {
1253
            if ($throwException) {
1254
                throw new InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".');
1255
            }
1256
1257
            return null;
1258
        }
1259
1260 192
        if (method_exists($this, $getter)) {
1261
            // relation name is case sensitive, trying to validate it when the relation is defined within this class
1262 192
            $method = new \ReflectionMethod($this, $getter);
1263 192
            $realName = lcfirst(substr($method->getName(), 3));
1264 192
            if ($realName !== $name) {
1265
                if ($throwException) {
1266
                    throw new InvalidArgumentException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\".");
1267
                }
1268
1269
                return null;
1270
            }
1271
        }
1272
1273 192
        return $relation;
1274
    }
1275
1276
    /**
1277
     * Establishes the relationship between two models.
1278
     *
1279
     * The relationship is established by setting the foreign key value(s) in one model
1280
     * to be the corresponding primary key value(s) in the other model.
1281
     * The model with the foreign key will be saved into database without performing validation.
1282
     *
1283
     * If the relationship involves a junction table, a new row will be inserted into the
1284
     * junction table which contains the primary key values from both models.
1285
     *
1286
     * Note that this method requires that the primary key value is not null.
1287
     *
1288
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via `getOrders()` method.
1289
     * @param ActiveRecordInterface $model the model to be linked with the current one.
1290
     * @param array $extraColumns additional column values to be saved into the junction table.
1291
     * This parameter is only meaningful for a relationship involving a junction table
1292
     * (i.e., a relation set with [[ActiveRelationTrait::via()]] or [[ActiveQuery::viaTable()]].)
1293
     * @throws InvalidCallException if the method is unable to link two models.
1294
     */
1295 9
    public function link($name, $model, $extraColumns = [])
1296
    {
1297 9
        $relation = $this->getRelation($name);
1298
1299 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...
1300 3
            if ($this->getIsNewRecord() || $model->getIsNewRecord()) {
1301
                throw new InvalidCallException('Unable to link models: the models being linked cannot be newly created.');
1302
            }
1303 3
            if (is_array($relation->via)) {
1304
                /* @var $viaRelation ActiveQuery */
1305 3
                list($viaName, $viaRelation) = $relation->via;
1306 3
                $viaClass = $viaRelation->modelClass;
1307
                // unset $viaName so that it can be reloaded to reflect the change
1308 3
                unset($this->_related[$viaName]);
1309
            } else {
1310
                $viaRelation = $relation->via;
1311
                $viaTable = reset($relation->via->from);
1312
            }
1313 3
            $columns = [];
1314 3
            foreach ($viaRelation->link as $a => $b) {
1315 3
                $columns[$a] = $this->$b;
1316
            }
1317 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...
1318 3
                $columns[$b] = $model->$a;
1319
            }
1320 3
            foreach ($extraColumns as $k => $v) {
1321 3
                $columns[$k] = $v;
1322
            }
1323 3
            if (is_array($relation->via)) {
1324
                /* @var $viaClass ActiveRecordInterface */
1325
                /* @var $record ActiveRecordInterface */
1326 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...
1327 3
                foreach ($columns as $column => $value) {
1328 3
                    $record->$column = $value;
1329
                }
1330 3
                $record->insert(false);
1331
            } else {
1332
                /* @var $viaTable string */
1333
                static::getDb()->createCommand()
1334 3
                    ->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...
1335
            }
1336
        } else {
1337 9
            $p1 = $model->isPrimaryKey(array_keys($relation->link));
1338 9
            $p2 = static::isPrimaryKey(array_values($relation->link));
1339 9
            if ($p1 && $p2) {
1340
                if ($this->getIsNewRecord() && $model->getIsNewRecord()) {
1341
                    throw new InvalidCallException('Unable to link models: at most one model can be newly created.');
1342
                } elseif ($this->getIsNewRecord()) {
1343
                    $this->bindModels(array_flip($relation->link), $this, $model);
1344
                } else {
1345
                    $this->bindModels($relation->link, $model, $this);
1346
                }
1347 9
            } elseif ($p1) {
1348 3
                $this->bindModels(array_flip($relation->link), $this, $model);
1349 9
            } elseif ($p2) {
1350 9
                $this->bindModels($relation->link, $model, $this);
1351
            } else {
1352
                throw new InvalidCallException('Unable to link models: the link defining the relation does not involve any primary key.');
1353
            }
1354
        }
1355
1356
        // update lazily loaded related objects
1357 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...
1358 3
            $this->_related[$name] = $model;
1359 9
        } elseif (isset($this->_related[$name])) {
1360 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...
1361 6
                if ($relation->indexBy instanceof \Closure) {
1362 3
                    $index = call_user_func($relation->indexBy, $model);
1363
                } else {
1364 3
                    $index = $model->{$relation->indexBy};
1365
                }
1366 6
                $this->_related[$name][$index] = $model;
1367
            } else {
1368 3
                $this->_related[$name][] = $model;
1369
            }
1370
        }
1371 9
    }
1372
1373
    /**
1374
     * Destroys the relationship between two models.
1375
     *
1376
     * The model with the foreign key of the relationship will be deleted if `$delete` is `true`.
1377
     * Otherwise, the foreign key will be set `null` and the model will be saved without validation.
1378
     *
1379
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via `getOrders()` method.
1380
     * @param ActiveRecordInterface $model the model to be unlinked from the current one.
1381
     * You have to make sure that the model is really related with the current model as this method
1382
     * does not check this.
1383
     * @param bool $delete whether to delete the model that contains the foreign key.
1384
     * If `false`, the model's foreign key will be set `null` and saved.
1385
     * If `true`, the model containing the foreign key will be deleted.
1386
     * @throws InvalidCallException if the models cannot be unlinked
1387
     * @throws Exception
1388 3
     * @throws StaleObjectException
1389
     */
1390 3
    public function unlink($name, $model, $delete = false)
1391
    {
1392 3
        /* @var $relation ActiveQueryInterface|ActiveQuery */
1393 3
        $relation = $this->getRelation($name);
1394
1395 3
        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...
1396 3
            if (is_array($relation->via)) {
1397 3
                /* @var $viaRelation ActiveQuery */
1398
                list($viaName, $viaRelation) = $relation->via;
1399 3
                $viaClass = $viaRelation->modelClass;
1400 3
                unset($this->_related[$viaName]);
1401
            } else {
1402 3
                $viaRelation = $relation->via;
1403 3
                $viaTable = reset($relation->via->from);
1404 3
            }
1405
            $columns = [];
1406 3
            foreach ($viaRelation->link as $a => $b) {
1407 3
                $columns[$a] = $this->$b;
1408
            }
1409 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...
1410 3
                $columns[$b] = $model->$a;
1411 3
            }
1412
            $nulls = [];
1413 3
            foreach (array_keys($columns) as $a) {
1414
                $nulls[$a] = null;
1415 3
            }
1416 3
            if (property_exists($viaRelation, 'on') && $viaRelation->on !== null) {
1417
                $columns = ['and', $columns, $viaRelation->on];
1418 3
            }
1419
            if (is_array($relation->via)) {
1420
                /* @var $viaClass ActiveRecordInterface */
1421
                if ($delete) {
1422
                    $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...
1423 3
                } else {
1424 3
                    $viaClass::updateAll($nulls, $columns);
1425
                }
1426
            } else {
1427 3
                /* @var $viaTable string */
1428
                /* @var $command Command */
1429
                $command = static::getDb()->createCommand();
1430
                if ($delete) {
1431 3
                    $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...
1432 3
                } else {
1433 3
                    $command->update($viaTable, $nulls, $columns)->execute();
1434 3
                }
1435 3
            }
1436
        } else {
1437 3
            $p1 = $model->isPrimaryKey(array_keys($relation->link));
1438 3
            $p2 = static::isPrimaryKey(array_values($relation->link));
1439
            if ($p2) {
1440 3
                if ($delete) {
1441
                    $model->delete();
1442
                } else {
1443
                    foreach ($relation->link as $a => $b) {
1444
                        $model->$a = null;
1445
                    }
1446
                    $model->save(false);
1447
                }
1448
            } elseif ($p1) {
1449
                foreach ($relation->link as $a => $b) {
1450
                    if (is_array($this->$b)) { // relation via array valued attribute
1451
                        if (($key = array_search($model->$a, $this->$b, false)) !== false) {
1452
                            $values = $this->$b;
1453
                            unset($values[$key]);
1454
                            $this->$b = array_values($values);
1455
                        }
1456
                    } else {
1457
                        $this->$b = null;
1458
                    }
1459
                }
1460 3
                $delete ? $this->delete() : $this->save(false);
1461
            } else {
1462 3
                throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
1463
            }
1464 3
        }
1465 3
1466 3
        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...
1467
            unset($this->_related[$name]);
1468
        } elseif (isset($this->_related[$name])) {
1469
            /* @var $b ActiveRecordInterface */
1470 3
            foreach ($this->_related[$name] as $a => $b) {
1471
                if ($model->getPrimaryKey() === $b->getPrimaryKey()) {
1472
                    unset($this->_related[$name][$a]);
1473
                }
1474
            }
1475
        }
1476
    }
1477
1478
    /**
1479
     * Destroys the relationship in current model.
1480
     *
1481
     * The model with the foreign key of the relationship will be deleted if `$delete` is `true`.
1482
     * Otherwise, the foreign key will be set `null` and the model will be saved without validation.
1483
     *
1484
     * Note that to destroy the relationship without removing records make sure your keys can be set to null
1485
     *
1486
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via `getOrders()` method.
1487 18
     * @param bool $delete whether to delete the model that contains the foreign key.
1488
     *
1489 18
     * Note that the deletion will be performed using [[deleteAll()]], which will not trigger any events on the related models.
1490
     * If you need [[EVENT_BEFORE_DELETE]] or [[EVENT_AFTER_DELETE]] to be triggered, you need to [[find()|find]] the models first
1491 18
     * and then call [[delete()]] on each of them.
1492 9
     */
1493
    public function unlinkAll($name, $delete = false)
1494 6
    {
1495 6
        $relation = $this->getRelation($name);
1496 6
1497
        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...
1498 3
            if (is_array($relation->via)) {
1499 3
                /* @var $viaRelation ActiveQuery */
1500
                list($viaName, $viaRelation) = $relation->via;
1501 9
                $viaClass = $viaRelation->modelClass;
1502 9
                unset($this->_related[$viaName]);
1503 9
            } else {
1504 9
                $viaRelation = $relation->via;
1505 9
                $viaTable = reset($relation->via->from);
1506
            }
1507 9
            $condition = [];
1508
            $nulls = [];
1509
            foreach ($viaRelation->link as $a => $b) {
1510 9
                $nulls[$a] = null;
1511
                $condition[$a] = $this->$b;
1512
            }
1513 9
            if (!empty($viaRelation->where)) {
1514
                $condition = ['and', $condition, $viaRelation->where];
1515 6
            }
1516 6
            if (property_exists($viaRelation, 'on') && !empty($viaRelation->on)) {
1517
                $condition = ['and', $condition, $viaRelation->on];
1518 6
            }
1519
            if (is_array($relation->via)) {
1520
                /* @var $viaClass ActiveRecordInterface */
1521
                if ($delete) {
1522
                    $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...
1523 3
                } else {
1524 3
                    $viaClass::updateAll($nulls, $condition);
1525 3
                }
1526
            } else {
1527 9
                /* @var $viaTable string */
1528
                /* @var $command Command */
1529
                $command = static::getDb()->createCommand();
1530
                if ($delete) {
1531
                    $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...
1532 12
                } else {
1533 12
                    $command->update($viaTable, $nulls, $condition)->execute();
1534
                }
1535
            }
1536
        } else {
1537
            /* @var $relatedModel ActiveRecordInterface */
1538 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...
1539 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...
1540 12
                // relation via array valued attribute
1541 12
                $this->$b = [];
1542 12
                $this->save(false);
1543
            } else {
1544 12
                $nulls = [];
1545 6
                $condition = [];
1546
                foreach ($relation->link as $a => $b) {
1547 12
                    $nulls[$a] = null;
1548 3
                    $condition[$a] = $this->$b;
1549
                }
1550 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...
1551 9
                    $condition = ['and', $condition, $relation->where];
1552
                }
1553 6
                if (property_exists($relation, 'on') && !empty($relation->on)) {
1554
                    $condition = ['and', $condition, $relation->on];
1555
                }
1556
                if ($delete) {
1557
                    $relatedModel::deleteAll($condition);
1558 18
                } else {
1559 18
                    $relatedModel::updateAll($nulls, $condition);
1560
                }
1561
            }
1562
        }
1563
1564
        unset($this->_related[$name]);
1565
    }
1566
1567 9
    /**
1568
     * @param array $link
1569 9
     * @param ActiveRecordInterface $foreignModel
1570 9
     * @param ActiveRecordInterface $primaryModel
1571 9
     * @throws InvalidCallException
1572
     */
1573
    private function bindModels($link, $foreignModel, $primaryModel)
1574 9
    {
1575
        foreach ($link as $fk => $pk) {
1576
            $value = $primaryModel->$pk;
1577 9
            if ($value === null) {
1578
                throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.');
1579
            }
1580 9
            if (is_array($foreignModel->$fk)) { // relation via array valued attribute
1581 9
                $foreignModel->{$fk}[] = $value;
1582
            } else {
1583
                $foreignModel->{$fk} = $value;
1584
            }
1585
        }
1586
        $foreignModel->save(false);
1587
    }
1588 15
1589
    /**
1590 15
     * Returns a value indicating whether the given set of attributes represents the primary key for this model.
1591 15
     * @param array $keys the set of attributes to check
1592 15
     * @return bool whether the given set of attributes represents the primary key for this model
1593
     */
1594
    public static function isPrimaryKey($keys)
1595 9
    {
1596
        $pks = static::primaryKey();
1597
        if (count($keys) === count($pks)) {
1598
            return count(array_intersect($keys, $pks)) === count($pks);
1599
        }
1600
1601
        return false;
1602
    }
1603
1604
    /**
1605
     * Returns the text label for the specified attribute.
1606 57
     * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
1607
     * @param string $attribute the attribute name
1608 57
     * @return string the attribute label
1609 57
     * @see generateAttributeLabel()
1610 10
     * @see attributeLabels()
1611 54
     */
1612
    public function getAttributeLabel($attribute)
1613
    {
1614
        $labels = $this->attributeLabels();
1615
        if (isset($labels[$attribute])) {
1616
            return $labels[$attribute];
1617
        } elseif (strpos($attribute, '.')) {
1618
            $attributeParts = explode('.', $attribute);
1619
            $neededAttribute = array_pop($attributeParts);
1620
1621
            $relatedModel = $this;
1622
            foreach ($attributeParts as $relationName) {
1623
                if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) {
1624
                    $relatedModel = $relatedModel->$relationName;
1625
                } else {
1626
                    try {
1627
                        $relation = $relatedModel->getRelation($relationName);
1628
                    } catch (InvalidParamException $e) {
1629
                        return $this->generateAttributeLabel($attribute);
1630
                    }
1631
                    /* @var $modelClass ActiveRecordInterface */
1632
                    $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...
1633
                    $relatedModel = $modelClass::instance();
1634
                }
1635
            }
1636
1637 54
            $labels = $relatedModel->attributeLabels();
1638
            if (isset($labels[$neededAttribute])) {
1639
                return $labels[$neededAttribute];
1640
            }
1641
        }
1642
1643
        return $this->generateAttributeLabel($attribute);
1644
    }
1645
1646
    /**
1647
     * Returns the text hint for the specified attribute.
1648
     * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
1649
     * @param string $attribute the attribute name
1650
     * @return string the attribute hint
1651
     * @see attributeHints()
1652
     * @since 2.0.4
1653
     */
1654
    public function getAttributeHint($attribute)
1655
    {
1656
        $hints = $this->attributeHints();
1657
        if (isset($hints[$attribute])) {
1658
            return $hints[$attribute];
1659
        } elseif (strpos($attribute, '.')) {
1660
            $attributeParts = explode('.', $attribute);
1661
            $neededAttribute = array_pop($attributeParts);
1662
1663
            $relatedModel = $this;
1664
            foreach ($attributeParts as $relationName) {
1665
                if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) {
1666
                    $relatedModel = $relatedModel->$relationName;
1667
                } else {
1668
                    try {
1669
                        $relation = $relatedModel->getRelation($relationName);
1670
                    } catch (InvalidParamException $e) {
1671
                        return '';
1672
                    }
1673
                    /* @var $modelClass ActiveRecordInterface */
1674
                    $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...
1675
                    $relatedModel = $modelClass::instance();
1676
                }
1677
            }
1678
1679
            $hints = $relatedModel->attributeHints();
1680
            if (isset($hints[$neededAttribute])) {
1681
                return $hints[$neededAttribute];
1682
            }
1683
        }
1684
1685
        return '';
1686
    }
1687
1688
    /**
1689
     * {@inheritdoc}
1690
     *
1691
     * The default implementation returns the names of the columns whose values have been populated into this record.
1692
     */
1693
    public function fields()
1694
    {
1695
        $fields = array_keys($this->_attributes);
1696
1697
        return array_combine($fields, $fields);
1698
    }
1699
1700
    /**
1701
     * {@inheritdoc}
1702
     *
1703
     * The default implementation returns the names of the relations that have been populated into this record.
1704
     */
1705
    public function extraFields()
1706
    {
1707
        $fields = array_keys($this->getRelatedRecords());
1708
1709
        return array_combine($fields, $fields);
1710
    }
1711
1712 3
    /**
1713
     * Sets the element value at the specified offset to null.
1714 3
     * This method is required by the SPL interface [[\ArrayAccess]].
1715
     * It is implicitly called when you use something like `unset($model[$offset])`.
1716
     * @param mixed $offset the offset to unset element
1717 3
     */
1718
    public function offsetUnset($offset)
1719 3
    {
1720
        if (property_exists($this, $offset)) {
1721
            $this->$offset = null;
1722
        } else {
1723
            unset($this->$offset);
1724
        }
1725 15
    }
1726
1727 15
    /**
1728 15
     * Resets dependent related models checking if their links contain specific attribute.
1729
     * @param string $attribute The changed attribute name.
1730 15
     */
1731 15
    private function resetDependentRelations($attribute)
1732
    {
1733
        foreach ($this->_relationsDependencies[$attribute] as $relation) {
1734
            unset($this->_related[$relation]);
1735
        }
1736
        unset($this->_relationsDependencies[$attribute]);
1737
    }
1738
1739 76
    /**
1740
     * Sets relation dependencies for a property
1741 76
     * @param string $name property name
1742 73
     * @param ActiveQueryInterface $relation relation instance
1743 73
     * @param string|null $viaRelationName intermediate relation
1744 73
     */
1745 73
    private function setRelationDependencies($name, $relation, $viaRelationName = null)
1746
    {
1747
        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...
1748 42
            foreach ($relation->link as $attribute) {
1749 15
                $this->_relationsDependencies[$attribute][$name] = $name;
1750 30
                if ($viaRelationName !== null) {
1751 27
                    $this->_relationsDependencies[$attribute][] = $viaRelationName;
1752 27
                }
1753
            }
1754 76
        } elseif ($relation->via instanceof ActiveQueryInterface) {
1755
            $this->setRelationDependencies($name, $relation->via);
1756
        } elseif (is_array($relation->via)) {
1757
            list($viaRelationName, $viaQuery) = $relation->via;
1758
            $this->setRelationDependencies($name, $viaQuery, $viaRelationName);
1759
        }
1760
    }
1761
}
1762