Completed
Push — master ( b9e345...56a3d2 )
by Alexander
11:11
created

framework/db/BaseActiveRecord.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 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 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 mixed $primaryKey The primary key value. An array (column name => column value) is returned if
35
 * the primary key is composite. A string is returned otherwise (null will be returned if the key value is null).
36
 * This property is read-only.
37
 * @property array $relatedRecords An array of related records indexed by relation names. This property is
38
 * 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 190
    public static function findOne($condition)
111
    {
112 190
        return static::findByCondition($condition)->one();
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)) {
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. Add a `required` validation rule for the version column to ensure the version value is submitted.
226
     * 3. In the Web form that collects the user input, add a hidden field that stores
227
     *    the lock version of the recording 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 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 36
    public function optimisticLock()
236
    {
237 36
        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 382
    public function __get($name)
284
    {
285 382
        if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
286 364
            return $this->_attributes[$name];
287
        }
288
289 192
        if ($this->hasAttribute($name)) {
290 47
            return null;
291
        }
292
293 166
        if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
294 93
            return $this->_related[$name];
295
        }
296 118
        $value = parent::__get($name);
297 118
        if ($value instanceof ActiveQueryInterface) {
298 73
            $this->setRelationDependencies($name, $value);
299 73
            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 188
    public function __set($name, $value)
312
    {
313 188
        if ($this->hasAttribute($name)) {
314
            if (
315 188
                !empty($this->_relationsDependencies[$name])
316 188
                && (!array_key_exists($name, $this->_attributes) || $this->_attributes[$name] !== $value)
317
            ) {
318 15
                $this->resetDependentRelations($name);
319
            }
320 188
            $this->_attributes[$name] = $value;
321
        } else {
322 5
            parent::__set($name, $value);
323
        }
324 188
    }
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 59
    public function __isset($name)
333
    {
334
        try {
335 59
            return $this->__get($name) !== null;
336
        } catch (\Exception $e) {
337
            return false;
338
        }
339
    }
340
341
    /**
342
     * Sets a component property to be null.
343
     * This method overrides the parent implementation by clearing
344
     * the specified attribute value.
345
     * @param string $name the property name or the event name
346
     */
347 15
    public function __unset($name)
348
    {
349 15
        if ($this->hasAttribute($name)) {
350 9
            unset($this->_attributes[$name]);
351 9
            if (!empty($this->_relationsDependencies[$name])) {
352 9
                $this->resetDependentRelations($name);
353
            }
354 6
        } elseif (array_key_exists($name, $this->_related)) {
355 6
            unset($this->_related[$name]);
356
        } elseif ($this->getRelation($name, false) === null) {
357
            parent::__unset($name);
358
        }
359 15
    }
360
361
    /**
362
     * Declares a `has-one` relation.
363
     * The declaration is returned in terms of a relational [[ActiveQuery]] instance
364
     * through which the related record can be queried and retrieved back.
365
     *
366
     * A `has-one` relation means that there is at most one related record matching
367
     * the criteria set by this relation, e.g., a customer has one country.
368
     *
369
     * For example, to declare the `country` relation for `Customer` class, we can write
370
     * the following code in the `Customer` class:
371
     *
372
     * ```php
373
     * public function getCountry()
374
     * {
375
     *     return $this->hasOne(Country::className(), ['id' => 'country_id']);
376
     * }
377
     * ```
378
     *
379
     * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name
380
     * in the related class `Country`, while the 'country_id' value refers to an attribute name
381
     * in the current AR class.
382
     *
383
     * Call methods declared in [[ActiveQuery]] to further customize the relation.
384
     *
385
     * @param string $class the class name of the related record
386
     * @param array $link the primary-foreign key constraint. The keys of the array refer to
387
     * the attributes of the record associated with the `$class` model, while the values of the
388
     * array refer to the corresponding attributes in **this** AR class.
389
     * @return ActiveQueryInterface the relational query object.
390
     */
391 73
    public function hasOne($class, $link)
392
    {
393 73
        return $this->createRelationQuery($class, $link, false);
394
    }
395
396
    /**
397
     * Declares a `has-many` relation.
398
     * The declaration is returned in terms of a relational [[ActiveQuery]] instance
399
     * through which the related record can be queried and retrieved back.
400
     *
401
     * A `has-many` relation means that there are multiple related records matching
402
     * the criteria set by this relation, e.g., a customer has many orders.
403
     *
404
     * For example, to declare the `orders` relation for `Customer` class, we can write
405
     * the following code in the `Customer` class:
406
     *
407
     * ```php
408
     * public function getOrders()
409
     * {
410
     *     return $this->hasMany(Order::className(), ['customer_id' => 'id']);
411
     * }
412
     * ```
413
     *
414
     * Note that in the above, the 'customer_id' key in the `$link` parameter refers to
415
     * an attribute name in the related class `Order`, while the 'id' value refers to
416
     * an attribute name in the current AR class.
417
     *
418
     * Call methods declared in [[ActiveQuery]] to further customize the relation.
419
     *
420
     * @param string $class the class name of the related record
421
     * @param array $link the primary-foreign key constraint. The keys of the array refer to
422
     * the attributes of the record associated with the `$class` model, while the values of the
423
     * array refer to the corresponding attributes in **this** AR class.
424
     * @return ActiveQueryInterface the relational query object.
425
     */
426 150
    public function hasMany($class, $link)
427
    {
428 150
        return $this->createRelationQuery($class, $link, true);
429
    }
430
431
    /**
432
     * Creates a query instance for `has-one` or `has-many` relation.
433
     * @param string $class the class name of the related record.
434
     * @param array $link the primary-foreign key constraint.
435
     * @param bool $multiple whether this query represents a relation to more than one record.
436
     * @return ActiveQueryInterface the relational query object.
437
     * @since 2.0.12
438
     * @see hasOne()
439
     * @see hasMany()
440
     */
441 181
    protected function createRelationQuery($class, $link, $multiple)
442
    {
443
        /* @var $class ActiveRecordInterface */
444
        /* @var $query ActiveQuery */
445 181
        $query = $class::find();
446 181
        $query->primaryModel = $this;
447 181
        $query->link = $link;
448 181
        $query->multiple = $multiple;
449 181
        return $query;
450
    }
451
452
    /**
453
     * Populates the named relation with the related records.
454
     * Note that this method does not check if the relation exists or not.
455
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method (case-sensitive).
456
     * @param ActiveRecordInterface|array|null $records the related records to be populated into the relation.
457
     * @see getRelation()
458
     */
459 123
    public function populateRelation($name, $records)
460
    {
461 123
        foreach ($this->_relationsDependencies as $attribute => $relationNames) {
462 27
            if (isset($relationNames[$name])) {
463 27
                unset($this->_relationsDependencies[$attribute][$name]);
464
            }
465
        }
466
467 123
        $this->_related[$name] = $records;
468 123
    }
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 48
    public function isRelationPopulated($name)
477
    {
478 48
        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 294
    public function hasAttribute($name)
497
    {
498 294
        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
    public function getAttribute($name)
510
    {
511
        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 89
    public function setAttribute($name, $value)
522
    {
523 89
        if ($this->hasAttribute($name)) {
524
            if (
525 89
                !empty($this->_relationsDependencies[$name])
526 89
                && (!array_key_exists($name, $this->_attributes) || $this->_attributes[$name] !== $value)
527
            ) {
528 6
                $this->resetDependentRelations($name);
529
            }
530 89
            $this->_attributes[$name] = $value;
531
        } else {
532
            throw new InvalidArgumentException(get_class($this) . ' has no attribute named "' . $name . '".');
533
        }
534 89
    }
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 100
    public function setOldAttributes($values)
552
    {
553 100
        $this->_oldAttributes = $values;
554 100
    }
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
    public function setOldAttribute($name, $value)
578
    {
579
        if (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name)) {
580
            $this->_oldAttributes[$name] = $value;
581
        } else {
582
            throw new InvalidArgumentException(get_class($this) . ' has no attribute named "' . $name . '".');
583
        }
584
    }
585
586
    /**
587
     * Marks an attribute dirty.
588
     * This method may be called to force updating a record when calling [[update()]],
589
     * even if there is no change being made to the record.
590
     * @param string $name the attribute name
591
     */
592 6
    public function markAttributeDirty($name)
593
    {
594 6
        unset($this->_oldAttributes[$name]);
595 6
    }
596
597
    /**
598
     * Returns a value indicating whether the named attribute has been changed.
599
     * @param string $name the name of the attribute.
600
     * @param bool $identical whether the comparison of new and old value is made for
601
     * identical values using `===`, defaults to `true`. Otherwise `==` is used for comparison.
602
     * This parameter is available since version 2.0.4.
603
     * @return bool whether the attribute has been changed
604
     */
605 2
    public function isAttributeChanged($name, $identical = true)
606
    {
607 2
        if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) {
608 1
            if ($identical) {
609 1
                return $this->_attributes[$name] !== $this->_oldAttributes[$name];
610
            }
611
612
            return $this->_attributes[$name] != $this->_oldAttributes[$name];
613
        }
614
615 1
        return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]);
616
    }
617
618
    /**
619
     * Returns the attribute values that have been modified since they are loaded or saved most recently.
620
     *
621
     * The comparison of new and old values is made for identical values using `===`.
622
     *
623
     * @param string[]|null $names the names of the attributes whose values may be returned if they are
624
     * changed recently. If null, [[attributes()]] will be used.
625
     * @return array the changed attribute values (name-value pairs)
626
     */
627 110
    public function getDirtyAttributes($names = null)
628
    {
629 110
        if ($names === null) {
630 107
            $names = $this->attributes();
631
        }
632 110
        $names = array_flip($names);
633 110
        $attributes = [];
634 110
        if ($this->_oldAttributes === null) {
635 97
            foreach ($this->_attributes as $name => $value) {
636 93
                if (isset($names[$name])) {
637 97
                    $attributes[$name] = $value;
638
                }
639
            }
640
        } else {
641 42
            foreach ($this->_attributes as $name => $value) {
642 42
                if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) {
643 42
                    $attributes[$name] = $value;
644
                }
645
            }
646
        }
647
648 110
        return $attributes;
649
    }
650
651
    /**
652
     * Saves the current record.
653
     *
654
     * This method will call [[insert()]] when [[isNewRecord]] is `true`, or [[update()]]
655
     * when [[isNewRecord]] is `false`.
656
     *
657
     * For example, to save a customer record:
658
     *
659
     * ```php
660
     * $customer = new Customer; // or $customer = Customer::findOne($id);
661
     * $customer->name = $name;
662
     * $customer->email = $email;
663
     * $customer->save();
664
     * ```
665
     *
666
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
667
     * before saving the record. Defaults to `true`. If the validation fails, the record
668
     * will not be saved to the database and this method will return `false`.
669
     * @param array $attributeNames list of attribute names that need to be saved. Defaults to null,
670
     * meaning all attributes that are loaded from DB will be saved.
671
     * @return bool whether the saving succeeded (i.e. no validation errors occurred).
672
     */
673 104
    public function save($runValidation = true, $attributeNames = null)
674
    {
675 104
        if ($this->getIsNewRecord()) {
676 91
            return $this->insert($runValidation, $attributeNames);
677
        }
678
679 28
        return $this->update($runValidation, $attributeNames) !== false;
680
    }
681
682
    /**
683
     * Saves the changes to this active record into the associated database table.
684
     *
685
     * This method performs the following steps in order:
686
     *
687
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
688
     *    returns `false`, the rest of the steps will be skipped;
689
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
690
     *    failed, the rest of the steps will be skipped;
691
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
692
     *    the rest of the steps will be skipped;
693
     * 4. save the record into database. If this fails, it will skip the rest of the steps;
694
     * 5. call [[afterSave()]];
695
     *
696
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
697
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]]
698
     * will be raised by the corresponding methods.
699
     *
700
     * Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
701
     *
702
     * For example, to update a customer record:
703
     *
704
     * ```php
705
     * $customer = Customer::findOne($id);
706
     * $customer->name = $name;
707
     * $customer->email = $email;
708
     * $customer->update();
709
     * ```
710
     *
711
     * Note that it is possible the update does not affect any row in the table.
712
     * In this case, this method will return 0. For this reason, you should use the following
713
     * code to check if update() is successful or not:
714
     *
715
     * ```php
716
     * if ($customer->update() !== false) {
717
     *     // update successful
718
     * } else {
719
     *     // update failed
720
     * }
721
     * ```
722
     *
723
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
724
     * before saving the record. Defaults to `true`. If the validation fails, the record
725
     * will not be saved to the database and this method will return `false`.
726
     * @param array $attributeNames list of attribute names that need to be saved. Defaults to null,
727
     * meaning all attributes that are loaded from DB will be saved.
728
     * @return int|false the number of rows affected, or `false` if validation fails
729
     * or [[beforeSave()]] stops the updating process.
730
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
731
     * being updated is outdated.
732
     * @throws Exception in case update failed.
733
     */
734
    public function update($runValidation = true, $attributeNames = null)
735
    {
736
        if ($runValidation && !$this->validate($attributeNames)) {
737
            return false;
738
        }
739
740
        return $this->updateInternal($attributeNames);
741
    }
742
743
    /**
744
     * Updates the specified attributes.
745
     *
746
     * This method is a shortcut to [[update()]] when data validation is not needed
747
     * and only a small set attributes need to be updated.
748
     *
749
     * You may specify the attributes to be updated as name list or name-value pairs.
750
     * If the latter, the corresponding attribute values will be modified accordingly.
751
     * The method will then save the specified attributes into database.
752
     *
753
     * Note that this method will **not** perform data validation and will **not** trigger events.
754
     *
755
     * @param array $attributes the attributes (names or name-value pairs) to be updated
756
     * @return int the number of rows affected.
757
     */
758 4
    public function updateAttributes($attributes)
759
    {
760 4
        $attrs = [];
761 4
        foreach ($attributes as $name => $value) {
762 4
            if (is_int($name)) {
763
                $attrs[] = $value;
764
            } else {
765 4
                $this->$name = $value;
766 4
                $attrs[] = $name;
767
            }
768
        }
769
770 4
        $values = $this->getDirtyAttributes($attrs);
771 4
        if (empty($values) || $this->getIsNewRecord()) {
772 4
            return 0;
773
        }
774
775 3
        $rows = static::updateAll($values, $this->getOldPrimaryKey(true));
776
777 3
        foreach ($values as $name => $value) {
778 3
            $this->_oldAttributes[$name] = $this->_attributes[$name];
779
        }
780
781 3
        return $rows;
782
    }
783
784
    /**
785
     * @see update()
786
     * @param array $attributes attributes to update
787
     * @return int|false the number of rows affected, or false if [[beforeSave()]] stops the updating process.
788
     * @throws StaleObjectException
789
     */
790 38
    protected function updateInternal($attributes = null)
791
    {
792 38
        if (!$this->beforeSave(false)) {
793
            return false;
794
        }
795 38
        $values = $this->getDirtyAttributes($attributes);
796 38
        if (empty($values)) {
797 3
            $this->afterSave(false, $values);
798 3
            return 0;
799
        }
800 36
        $condition = $this->getOldPrimaryKey(true);
801 36
        $lock = $this->optimisticLock();
802 36
        if ($lock !== null) {
803 3
            $values[$lock] = $this->$lock + 1;
804 3
            $condition[$lock] = $this->$lock;
805
        }
806
        // We do not check the return value of updateAll() because it's possible
807
        // that the UPDATE statement doesn't change anything and thus returns 0.
808 36
        $rows = static::updateAll($values, $condition);
809
810 36
        if ($lock !== null && !$rows) {
811 3
            throw new StaleObjectException('The object being updated is outdated.');
812
        }
813
814 36
        if (isset($values[$lock])) {
815 3
            $this->$lock = $values[$lock];
816
        }
817
818 36
        $changedAttributes = [];
819 36
        foreach ($values as $name => $value) {
820 36
            $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
821 36
            $this->_oldAttributes[$name] = $value;
822
        }
823 36
        $this->afterSave(false, $changedAttributes);
824
825 36
        return $rows;
826
    }
827
828
    /**
829
     * Updates one or several counter columns for the current AR object.
830
     * Note that this method differs from [[updateAllCounters()]] in that it only
831
     * saves counters for the current AR object.
832
     *
833
     * An example usage is as follows:
834
     *
835
     * ```php
836
     * $post = Post::findOne($id);
837
     * $post->updateCounters(['view_count' => 1]);
838
     * ```
839
     *
840
     * @param array $counters the counters to be updated (attribute name => increment value)
841
     * Use negative values if you want to decrement the counters.
842
     * @return bool whether the saving is successful
843
     * @see updateAllCounters()
844
     */
845 6
    public function updateCounters($counters)
846
    {
847 6
        if (static::updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) {
848 6
            foreach ($counters as $name => $value) {
849 6
                if (!isset($this->_attributes[$name])) {
850 3
                    $this->_attributes[$name] = $value;
851
                } else {
852 3
                    $this->_attributes[$name] += $value;
853
                }
854 6
                $this->_oldAttributes[$name] = $this->_attributes[$name];
855
            }
856
857 6
            return true;
858
        }
859
860
        return false;
861
    }
862
863
    /**
864
     * Deletes the table row corresponding to this active record.
865
     *
866
     * This method performs the following steps in order:
867
     *
868
     * 1. call [[beforeDelete()]]. If the method returns `false`, it will skip the
869
     *    rest of the steps;
870
     * 2. delete the record from the database;
871
     * 3. call [[afterDelete()]].
872
     *
873
     * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
874
     * will be raised by the corresponding methods.
875
     *
876
     * @return int|false the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
877
     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
878
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
879
     * being deleted is outdated.
880
     * @throws Exception in case delete failed.
881
     */
882
    public function delete()
883
    {
884
        $result = false;
885
        if ($this->beforeDelete()) {
886
            // we do not check the return value of deleteAll() because it's possible
887
            // the record is already deleted in the database and thus the method will return 0
888
            $condition = $this->getOldPrimaryKey(true);
889
            $lock = $this->optimisticLock();
890
            if ($lock !== null) {
891
                $condition[$lock] = $this->$lock;
892
            }
893
            $result = static::deleteAll($condition);
894
            if ($lock !== null && !$result) {
895
                throw new StaleObjectException('The object being deleted is outdated.');
896
            }
897
            $this->_oldAttributes = null;
898
            $this->afterDelete();
899
        }
900
901
        return $result;
902
    }
903
904
    /**
905
     * Returns a value indicating whether the current record is new.
906
     * @return bool whether the record is new and should be inserted when calling [[save()]].
907
     */
908 147
    public function getIsNewRecord()
909
    {
910 147
        return $this->_oldAttributes === null;
911
    }
912
913
    /**
914
     * Sets the value indicating whether the record is new.
915
     * @param bool $value whether the record is new and should be inserted when calling [[save()]].
916
     * @see getIsNewRecord()
917
     */
918
    public function setIsNewRecord($value)
919
    {
920
        $this->_oldAttributes = $value ? null : $this->_attributes;
921
    }
922
923
    /**
924
     * Initializes the object.
925
     * This method is called at the end of the constructor.
926
     * The default implementation will trigger an [[EVENT_INIT]] event.
927
     */
928 423
    public function init()
929
    {
930 423
        parent::init();
931 423
        $this->trigger(self::EVENT_INIT);
932 423
    }
933
934
    /**
935
     * This method is called when the AR object is created and populated with the query result.
936
     * The default implementation will trigger an [[EVENT_AFTER_FIND]] event.
937
     * When overriding this method, make sure you call the parent implementation to ensure the
938
     * event is triggered.
939
     */
940 322
    public function afterFind()
941
    {
942 322
        $this->trigger(self::EVENT_AFTER_FIND);
943 322
    }
944
945
    /**
946
     * This method is called at the beginning of inserting or updating a record.
947
     *
948
     * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is `true`,
949
     * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is `false`.
950
     * When overriding this method, make sure you call the parent implementation like the following:
951
     *
952
     * ```php
953
     * public function beforeSave($insert)
954
     * {
955
     *     if (!parent::beforeSave($insert)) {
956
     *         return false;
957
     *     }
958
     *
959
     *     // ...custom code here...
960
     *     return true;
961
     * }
962
     * ```
963
     *
964
     * @param bool $insert whether this method called while inserting a record.
965
     * If `false`, it means the method is called while updating a record.
966
     * @return bool whether the insertion or updating should continue.
967
     * If `false`, the insertion or updating will be cancelled.
968
     */
969 114
    public function beforeSave($insert)
970
    {
971 114
        $event = new ModelEvent();
972 114
        $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event);
973
974 114
        return $event->isValid;
975
    }
976
977
    /**
978
     * This method is called at the end of inserting or updating a record.
979
     * The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is `true`,
980
     * or an [[EVENT_AFTER_UPDATE]] event if `$insert` is `false`. The event class used is [[AfterSaveEvent]].
981
     * When overriding this method, make sure you call the parent implementation so that
982
     * the event is triggered.
983
     * @param bool $insert whether this method called while inserting a record.
984
     * If `false`, it means the method is called while updating a record.
985
     * @param array $changedAttributes The old values of attributes that had changed and were saved.
986
     * You can use this parameter to take action based on the changes made for example send an email
987
     * when the password had changed or implement audit trail that tracks all the changes.
988
     * `$changedAttributes` gives you the old attribute values while the active record (`$this`) has
989
     * already the new, updated values.
990
     *
991
     * Note that no automatic type conversion performed by default. You may use
992
     * [[\yii\behaviors\AttributeTypecastBehavior]] to facilitate attribute typecasting.
993
     * See http://www.yiiframework.com/doc-2.0/guide-db-active-record.html#attributes-typecasting.
994
     */
995 107
    public function afterSave($insert, $changedAttributes)
996
    {
997 107
        $this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE, new AfterSaveEvent([
998 107
            'changedAttributes' => $changedAttributes,
999
        ]));
1000 107
    }
1001
1002
    /**
1003
     * This method is invoked before deleting a record.
1004
     *
1005
     * The default implementation raises the [[EVENT_BEFORE_DELETE]] event.
1006
     * When overriding this method, make sure you call the parent implementation like the following:
1007
     *
1008
     * ```php
1009
     * public function beforeDelete()
1010
     * {
1011
     *     if (!parent::beforeDelete()) {
1012
     *         return false;
1013
     *     }
1014
     *
1015
     *     // ...custom code here...
1016
     *     return true;
1017
     * }
1018
     * ```
1019
     *
1020
     * @return bool whether the record should be deleted. Defaults to `true`.
1021
     */
1022 6
    public function beforeDelete()
1023
    {
1024 6
        $event = new ModelEvent();
1025 6
        $this->trigger(self::EVENT_BEFORE_DELETE, $event);
1026
1027 6
        return $event->isValid;
1028
    }
1029
1030
    /**
1031
     * This method is invoked after deleting a record.
1032
     * The default implementation raises the [[EVENT_AFTER_DELETE]] event.
1033
     * You may override this method to do postprocessing after the record is deleted.
1034
     * Make sure you call the parent implementation so that the event is raised properly.
1035
     */
1036 6
    public function afterDelete()
1037
    {
1038 6
        $this->trigger(self::EVENT_AFTER_DELETE);
1039 6
    }
1040
1041
    /**
1042
     * Repopulates this active record with the latest data.
1043
     *
1044
     * If the refresh is successful, an [[EVENT_AFTER_REFRESH]] event will be triggered.
1045
     * This event is available since version 2.0.8.
1046
     *
1047
     * @return bool whether the row still exists in the database. If `true`, the latest data
1048
     * will be populated to this active record. Otherwise, this record will remain unchanged.
1049
     */
1050
    public function refresh()
1051
    {
1052
        /* @var $record BaseActiveRecord */
1053
        $record = static::findOne($this->getPrimaryKey(true));
1054
        return $this->refreshInternal($record);
1055
    }
1056
1057
    /**
1058
     * Repopulates this active record with the latest data from a newly fetched instance.
1059
     * @param BaseActiveRecord $record the record to take attributes from.
1060
     * @return bool whether refresh was successful.
1061
     * @see refresh()
1062
     * @since 2.0.13
1063
     */
1064 29
    protected function refreshInternal($record)
1065
    {
1066 29
        if ($record === null) {
1067 3
            return false;
1068
        }
1069 29
        foreach ($this->attributes() as $name) {
1070 29
            $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null;
1071
        }
1072 29
        $this->_oldAttributes = $record->_oldAttributes;
1073 29
        $this->_related = [];
1074 29
        $this->_relationsDependencies = [];
1075 29
        $this->afterRefresh();
1076
1077 29
        return true;
1078
    }
1079
1080
    /**
1081
     * This method is called when the AR object is refreshed.
1082
     * The default implementation will trigger an [[EVENT_AFTER_REFRESH]] event.
1083
     * When overriding this method, make sure you call the parent implementation to ensure the
1084
     * event is triggered.
1085
     * @since 2.0.8
1086
     */
1087 29
    public function afterRefresh()
1088
    {
1089 29
        $this->trigger(self::EVENT_AFTER_REFRESH);
1090 29
    }
1091
1092
    /**
1093
     * Returns a value indicating whether the given active record is the same as the current one.
1094
     * The comparison is made by comparing the table names and the primary key values of the two active records.
1095
     * If one of the records [[isNewRecord|is new]] they are also considered not equal.
1096
     * @param ActiveRecordInterface $record record to compare to
1097
     * @return bool whether the two active records refer to the same row in the same database table.
1098
     */
1099
    public function equals($record)
1100
    {
1101
        if ($this->getIsNewRecord() || $record->getIsNewRecord()) {
1102
            return false;
1103
        }
1104
1105
        return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey();
1106
    }
1107
1108
    /**
1109
     * Returns the primary key value(s).
1110
     * @param bool $asArray whether to return the primary key value as an array. If `true`,
1111
     * the return value will be an array with column names as keys and column values as values.
1112
     * Note that for composite primary keys, an array will always be returned regardless of this parameter value.
1113
     * @property mixed The primary key value. An array (column name => column value) is returned if
1114
     * the primary key is composite. A string is returned otherwise (null will be returned if
1115
     * the key value is null).
1116
     * @return mixed the primary key value. An array (column name => column value) is returned if the primary key
1117
     * is composite or `$asArray` is `true`. A string is returned otherwise (null will be returned if
1118
     * the key value is null).
1119
     */
1120 45
    public function getPrimaryKey($asArray = false)
1121
    {
1122 45
        $keys = $this->primaryKey();
1123 45
        if (!$asArray && count($keys) === 1) {
1124 19
            return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null;
1125
        }
1126
1127 29
        $values = [];
1128 29
        foreach ($keys as $name) {
1129 29
            $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null;
1130
        }
1131
1132 29
        return $values;
1133
    }
1134
1135
    /**
1136
     * Returns the old primary key value(s).
1137
     * This refers to the primary key value that is populated into the record
1138
     * after executing a find method (e.g. find(), findOne()).
1139
     * The value remains unchanged even if the primary key attribute is manually assigned with a different value.
1140
     * @param bool $asArray whether to return the primary key value as an array. If `true`,
1141
     * the return value will be an array with column name as key and column value as value.
1142
     * If this is `false` (default), a scalar value will be returned for non-composite primary key.
1143
     * @property mixed The old primary key value. An array (column name => column value) is
1144
     * returned if the primary key is composite. A string is returned otherwise (null will be
1145
     * returned if the key value is null).
1146
     * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
1147
     * is composite or `$asArray` is `true`. A string is returned otherwise (null will be returned if
1148
     * the key value is null).
1149
     * @throws Exception if the AR model does not have a primary key
1150
     */
1151 70
    public function getOldPrimaryKey($asArray = false)
1152
    {
1153 70
        $keys = $this->primaryKey();
1154 70
        if (empty($keys)) {
1155
            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.');
1156
        }
1157 70
        if (!$asArray && count($keys) === 1) {
1158
            return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null;
1159
        }
1160
1161 70
        $values = [];
1162 70
        foreach ($keys as $name) {
1163 70
            $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
1164
        }
1165
1166 70
        return $values;
1167
    }
1168
1169
    /**
1170
     * Populates an active record object using a row of data from the database/storage.
1171
     *
1172
     * This is an internal method meant to be called to create active record objects after
1173
     * fetching data from the database. It is mainly used by [[ActiveQuery]] to populate
1174
     * the query results into active records.
1175
     *
1176
     * When calling this method manually you should call [[afterFind()]] on the created
1177
     * record to trigger the [[EVENT_AFTER_FIND|afterFind Event]].
1178
     *
1179
     * @param BaseActiveRecord $record the record to be populated. In most cases this will be an instance
1180
     * created by [[instantiate()]] beforehand.
1181
     * @param array $row attribute values (name => value)
1182
     */
1183 322
    public static function populateRecord($record, $row)
1184
    {
1185 322
        $columns = array_flip($record->attributes());
1186 322
        foreach ($row as $name => $value) {
1187 322
            if (isset($columns[$name])) {
1188 322
                $record->_attributes[$name] = $value;
1189 6
            } elseif ($record->canSetProperty($name)) {
1190 322
                $record->$name = $value;
1191
            }
1192
        }
1193 322
        $record->_oldAttributes = $record->_attributes;
1194 322
        $record->_related = [];
1195 322
        $record->_relationsDependencies = [];
1196 322
    }
1197
1198
    /**
1199
     * Creates an active record instance.
1200
     *
1201
     * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
1202
     * It is not meant to be used for creating new records directly.
1203
     *
1204
     * You may override this method if the instance being created
1205
     * depends on the row data to be populated into the record.
1206
     * For example, by creating a record based on the value of a column,
1207
     * you may implement the so-called single-table inheritance mapping.
1208
     * @param array $row row data to be populated into the record.
1209
     * @return static the newly created active record
1210
     */
1211 316
    public static function instantiate($row)
1212
    {
1213 316
        return new static();
1214
    }
1215
1216
    /**
1217
     * Returns whether there is an element at the specified offset.
1218
     * This method is required by the interface [[\ArrayAccess]].
1219
     * @param mixed $offset the offset to check on
1220
     * @return bool whether there is an element at the specified offset.
1221
     */
1222 33
    public function offsetExists($offset)
1223
    {
1224 33
        return $this->__isset($offset);
1225
    }
1226
1227
    /**
1228
     * Returns the relation object with the specified name.
1229
     * A relation is defined by a getter method which returns an [[ActiveQueryInterface]] object.
1230
     * It can be declared in either the Active Record class itself or one of its behaviors.
1231
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method (case-sensitive).
1232
     * @param bool $throwException whether to throw exception if the relation does not exist.
1233
     * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist
1234
     * and `$throwException` is `false`, `null` will be returned.
1235
     * @throws InvalidArgumentException if the named relation does not exist.
1236
     */
1237 150
    public function getRelation($name, $throwException = true)
1238
    {
1239 150
        $getter = 'get' . $name;
1240
        try {
1241
            // the relation could be defined in a behavior
1242 150
            $relation = $this->$getter();
1243
        } catch (UnknownMethodException $e) {
1244
            if ($throwException) {
1245
                throw new InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
1246
            }
1247
1248
            return null;
1249
        }
1250 150
        if (!$relation instanceof ActiveQueryInterface) {
1251
            if ($throwException) {
1252
                throw new InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".');
1253
            }
1254
1255
            return null;
1256
        }
1257
1258 150
        if (method_exists($this, $getter)) {
1259
            // relation name is case sensitive, trying to validate it when the relation is defined within this class
1260 150
            $method = new \ReflectionMethod($this, $getter);
1261 150
            $realName = lcfirst(substr($method->getName(), 3));
1262 150
            if ($realName !== $name) {
1263
                if ($throwException) {
1264
                    throw new InvalidArgumentException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\".");
1265
                }
1266
1267
                return null;
1268
            }
1269
        }
1270
1271 150
        return $relation;
1272
    }
1273
1274
    /**
1275
     * Establishes the relationship between two models.
1276
     *
1277
     * The relationship is established by setting the foreign key value(s) in one model
1278
     * to be the corresponding primary key value(s) in the other model.
1279
     * The model with the foreign key will be saved into database without performing validation.
1280
     *
1281
     * If the relationship involves a junction table, a new row will be inserted into the
1282
     * junction table which contains the primary key values from both models.
1283
     *
1284
     * Note that this method requires that the primary key value is not null.
1285
     *
1286
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via `getOrders()` method.
1287
     * @param ActiveRecordInterface $model the model to be linked with the current one.
1288
     * @param array $extraColumns additional column values to be saved into the junction table.
1289
     * This parameter is only meaningful for a relationship involving a junction table
1290
     * (i.e., a relation set with [[ActiveRelationTrait::via()]] or [[ActiveQuery::viaTable()]].)
1291
     * @throws InvalidCallException if the method is unable to link two models.
1292
     */
1293 9
    public function link($name, $model, $extraColumns = [])
1294
    {
1295 9
        $relation = $this->getRelation($name);
1296
1297 9
        if ($relation->via !== null) {
1298 3
            if ($this->getIsNewRecord() || $model->getIsNewRecord()) {
1299
                throw new InvalidCallException('Unable to link models: the models being linked cannot be newly created.');
1300
            }
1301 3
            if (is_array($relation->via)) {
1302
                /* @var $viaRelation ActiveQuery */
1303 3
                list($viaName, $viaRelation) = $relation->via;
1304 3
                $viaClass = $viaRelation->modelClass;
1305
                // unset $viaName so that it can be reloaded to reflect the change
1306 3
                unset($this->_related[$viaName]);
1307
            } else {
1308
                $viaRelation = $relation->via;
1309
                $viaTable = reset($relation->via->from);
1310
            }
1311 3
            $columns = [];
1312 3
            foreach ($viaRelation->link as $a => $b) {
1313 3
                $columns[$a] = $this->$b;
1314
            }
1315 3
            foreach ($relation->link as $a => $b) {
1316 3
                $columns[$b] = $model->$a;
1317
            }
1318 3
            foreach ($extraColumns as $k => $v) {
1319 3
                $columns[$k] = $v;
1320
            }
1321 3
            if (is_array($relation->via)) {
1322
                /* @var $viaClass ActiveRecordInterface */
1323
                /* @var $record ActiveRecordInterface */
1324 3
                $record = Yii::createObject($viaClass);
1325 3
                foreach ($columns as $column => $value) {
1326 3
                    $record->$column = $value;
1327
                }
1328 3
                $record->insert(false);
1329
            } else {
1330
                /* @var $viaTable string */
1331
                static::getDb()->createCommand()
1332 3
                    ->insert($viaTable, $columns)->execute();
1333
            }
1334
        } else {
1335 9
            $p1 = $model->isPrimaryKey(array_keys($relation->link));
1336 9
            $p2 = static::isPrimaryKey(array_values($relation->link));
1337 9
            if ($p1 && $p2) {
1338
                if ($this->getIsNewRecord() && $model->getIsNewRecord()) {
1339
                    throw new InvalidCallException('Unable to link models: at most one model can be newly created.');
1340
                } elseif ($this->getIsNewRecord()) {
1341
                    $this->bindModels(array_flip($relation->link), $this, $model);
1342
                } else {
1343
                    $this->bindModels($relation->link, $model, $this);
1344
                }
1345 9
            } elseif ($p1) {
1346 3
                $this->bindModels(array_flip($relation->link), $this, $model);
1347 9
            } elseif ($p2) {
1348 9
                $this->bindModels($relation->link, $model, $this);
1349
            } else {
1350
                throw new InvalidCallException('Unable to link models: the link defining the relation does not involve any primary key.');
1351
            }
1352
        }
1353
1354
        // update lazily loaded related objects
1355 9
        if (!$relation->multiple) {
1356 3
            $this->_related[$name] = $model;
1357 9
        } elseif (isset($this->_related[$name])) {
1358 9
            if ($relation->indexBy !== null) {
1359 6
                if ($relation->indexBy instanceof \Closure) {
1360 3
                    $index = call_user_func($relation->indexBy, $model);
1361
                } else {
1362 3
                    $index = $model->{$relation->indexBy};
1363
                }
1364 6
                $this->_related[$name][$index] = $model;
1365
            } else {
1366 3
                $this->_related[$name][] = $model;
1367
            }
1368
        }
1369 9
    }
1370
1371
    /**
1372
     * Destroys the relationship between two models.
1373
     *
1374
     * The model with the foreign key of the relationship will be deleted if `$delete` is `true`.
1375
     * Otherwise, the foreign key will be set `null` and the model will be saved without validation.
1376
     *
1377
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via `getOrders()` method.
1378
     * @param ActiveRecordInterface $model the model to be unlinked from the current one.
1379
     * You have to make sure that the model is really related with the current model as this method
1380
     * does not check this.
1381
     * @param bool $delete whether to delete the model that contains the foreign key.
1382
     * If `false`, the model's foreign key will be set `null` and saved.
1383
     * If `true`, the model containing the foreign key will be deleted.
1384
     * @throws InvalidCallException if the models cannot be unlinked
1385
     */
1386 3
    public function unlink($name, $model, $delete = false)
1387
    {
1388 3
        $relation = $this->getRelation($name);
1389
1390 3
        if ($relation->via !== null) {
1391 3
            if (is_array($relation->via)) {
1392
                /* @var $viaRelation ActiveQuery */
1393 3
                list($viaName, $viaRelation) = $relation->via;
1394 3
                $viaClass = $viaRelation->modelClass;
1395 3
                unset($this->_related[$viaName]);
1396
            } else {
1397 3
                $viaRelation = $relation->via;
1398 3
                $viaTable = reset($relation->via->from);
1399
            }
1400 3
            $columns = [];
1401 3
            foreach ($viaRelation->link as $a => $b) {
1402 3
                $columns[$a] = $this->$b;
1403
            }
1404 3
            foreach ($relation->link as $a => $b) {
1405 3
                $columns[$b] = $model->$a;
1406
            }
1407 3
            $nulls = [];
1408 3
            foreach (array_keys($columns) as $a) {
1409 3
                $nulls[$a] = null;
1410
            }
1411 3
            if (is_array($relation->via)) {
1412
                /* @var $viaClass ActiveRecordInterface */
1413 3
                if ($delete) {
1414 3
                    $viaClass::deleteAll($columns);
1415
                } else {
1416 3
                    $viaClass::updateAll($nulls, $columns);
1417
                }
1418
            } else {
1419
                /* @var $viaTable string */
1420
                /* @var $command Command */
1421 3
                $command = static::getDb()->createCommand();
1422 3
                if ($delete) {
1423
                    $command->delete($viaTable, $columns)->execute();
1424
                } else {
1425 3
                    $command->update($viaTable, $nulls, $columns)->execute();
1426
                }
1427
            }
1428
        } else {
1429 3
            $p1 = $model->isPrimaryKey(array_keys($relation->link));
1430 3
            $p2 = static::isPrimaryKey(array_values($relation->link));
1431 3
            if ($p2) {
1432 3
                if ($delete) {
1433 3
                    $model->delete();
1434
                } else {
1435 3
                    foreach ($relation->link as $a => $b) {
1436 3
                        $model->$a = null;
1437
                    }
1438 3
                    $model->save(false);
1439
                }
1440
            } elseif ($p1) {
1441
                foreach ($relation->link as $a => $b) {
0 ignored issues
show
Accessing link on the interface yii\db\ActiveQueryInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1442
                    if (is_array($this->$b)) { // relation via array valued attribute
1443
                        if (($key = array_search($model->$a, $this->$b, false)) !== false) {
1444
                            $values = $this->$b;
1445
                            unset($values[$key]);
1446
                            $this->$b = array_values($values);
1447
                        }
1448
                    } else {
1449
                        $this->$b = null;
1450
                    }
1451
                }
1452
                $delete ? $this->delete() : $this->save(false);
1453
            } else {
1454
                throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
1455
            }
1456
        }
1457
1458 3
        if (!$relation->multiple) {
1459
            unset($this->_related[$name]);
1460 3
        } elseif (isset($this->_related[$name])) {
1461
            /* @var $b ActiveRecordInterface */
1462 3
            foreach ($this->_related[$name] as $a => $b) {
1463 3
                if ($model->getPrimaryKey() === $b->getPrimaryKey()) {
1464 3
                    unset($this->_related[$name][$a]);
1465
                }
1466
            }
1467
        }
1468 3
    }
1469
1470
    /**
1471
     * Destroys the relationship in current model.
1472
     *
1473
     * The model with the foreign key of the relationship will be deleted if `$delete` is `true`.
1474
     * Otherwise, the foreign key will be set `null` and the model will be saved without validation.
1475
     *
1476
     * Note that to destroy the relationship without removing records make sure your keys can be set to null
1477
     *
1478
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via `getOrders()` method.
1479
     * @param bool $delete whether to delete the model that contains the foreign key.
1480
     *
1481
     * Note that the deletion will be performed using [[deleteAll()]], which will not trigger any events on the related models.
1482
     * If you need [[EVENT_BEFORE_DELETE]] or [[EVENT_AFTER_DELETE]] to be triggered, you need to [[find()|find]] the models first
1483
     * and then call [[delete()]] on each of them.
1484
     */
1485 18
    public function unlinkAll($name, $delete = false)
1486
    {
1487 18
        $relation = $this->getRelation($name);
1488
1489 18
        if ($relation->via !== null) {
1490 9
            if (is_array($relation->via)) {
1491
                /* @var $viaRelation ActiveQuery */
1492 6
                list($viaName, $viaRelation) = $relation->via;
1493 6
                $viaClass = $viaRelation->modelClass;
1494 6
                unset($this->_related[$viaName]);
1495
            } else {
1496 3
                $viaRelation = $relation->via;
1497 3
                $viaTable = reset($relation->via->from);
1498
            }
1499 9
            $condition = [];
1500 9
            $nulls = [];
1501 9
            foreach ($viaRelation->link as $a => $b) {
1502 9
                $nulls[$a] = null;
1503 9
                $condition[$a] = $this->$b;
1504
            }
1505 9
            if (!empty($viaRelation->where)) {
1506
                $condition = ['and', $condition, $viaRelation->where];
1507
            }
1508 9
            if (!empty($viaRelation->on)) {
1509
                $condition = ['and', $condition, $viaRelation->on];
1510
            }
1511 9
            if (is_array($relation->via)) {
1512
                /* @var $viaClass ActiveRecordInterface */
1513 6
                if ($delete) {
1514 6
                    $viaClass::deleteAll($condition);
1515
                } else {
1516 6
                    $viaClass::updateAll($nulls, $condition);
1517
                }
1518
            } else {
1519
                /* @var $viaTable string */
1520
                /* @var $command Command */
1521 3
                $command = static::getDb()->createCommand();
1522 3
                if ($delete) {
1523 3
                    $command->delete($viaTable, $condition)->execute();
1524
                } else {
1525 9
                    $command->update($viaTable, $nulls, $condition)->execute();
1526
                }
1527
            }
1528
        } else {
1529
            /* @var $relatedModel ActiveRecordInterface */
1530 12
            $relatedModel = $relation->modelClass;
1531 12
            if (!$delete && count($relation->link) === 1 && is_array($this->{$b = reset($relation->link)})) {
1532
                // relation via array valued attribute
1533
                $this->$b = [];
1534
                $this->save(false);
1535
            } else {
1536 12
                $nulls = [];
1537 12
                $condition = [];
1538 12
                foreach ($relation->link as $a => $b) {
1539 12
                    $nulls[$a] = null;
1540 12
                    $condition[$a] = $this->$b;
1541
                }
1542 12
                if (!empty($relation->where)) {
1543 6
                    $condition = ['and', $condition, $relation->where];
1544
                }
1545 12
                if (!empty($relation->on)) {
1546 3
                    $condition = ['and', $condition, $relation->on];
1547
                }
1548 12
                if ($delete) {
1549 9
                    $relatedModel::deleteAll($condition);
1550
                } else {
1551 6
                    $relatedModel::updateAll($nulls, $condition);
1552
                }
1553
            }
1554
        }
1555
1556 18
        unset($this->_related[$name]);
1557 18
    }
1558
1559
    /**
1560
     * @param array $link
1561
     * @param ActiveRecordInterface $foreignModel
1562
     * @param ActiveRecordInterface $primaryModel
1563
     * @throws InvalidCallException
1564
     */
1565 9
    private function bindModels($link, $foreignModel, $primaryModel)
1566
    {
1567 9
        foreach ($link as $fk => $pk) {
1568 9
            $value = $primaryModel->$pk;
1569 9
            if ($value === null) {
1570
                throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.');
1571
            }
1572 9
            if (is_array($foreignModel->$fk)) { // relation via array valued attribute
1573
                $foreignModel->$fk = array_merge($foreignModel->$fk, [$value]);
1574
            } else {
1575 9
                $foreignModel->$fk = $value;
1576
            }
1577
        }
1578 9
        $foreignModel->save(false);
1579 9
    }
1580
1581
    /**
1582
     * Returns a value indicating whether the given set of attributes represents the primary key for this model.
1583
     * @param array $keys the set of attributes to check
1584
     * @return bool whether the given set of attributes represents the primary key for this model
1585
     */
1586 15
    public static function isPrimaryKey($keys)
1587
    {
1588 15
        $pks = static::primaryKey();
1589 15
        if (count($keys) === count($pks)) {
1590 15
            return count(array_intersect($keys, $pks)) === count($pks);
1591
        }
1592
1593 9
        return false;
1594
    }
1595
1596
    /**
1597
     * Returns the text label for the specified attribute.
1598
     * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
1599
     * @param string $attribute the attribute name
1600
     * @return string the attribute label
1601
     * @see generateAttributeLabel()
1602
     * @see attributeLabels()
1603
     */
1604 57
    public function getAttributeLabel($attribute)
1605
    {
1606 57
        $labels = $this->attributeLabels();
1607 57
        if (isset($labels[$attribute])) {
1608 10
            return $labels[$attribute];
1609 54
        } elseif (strpos($attribute, '.')) {
1610
            $attributeParts = explode('.', $attribute);
1611
            $neededAttribute = array_pop($attributeParts);
1612
1613
            $relatedModel = $this;
1614
            foreach ($attributeParts as $relationName) {
1615
                if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) {
1616
                    $relatedModel = $relatedModel->$relationName;
1617
                } else {
1618
                    try {
1619
                        $relation = $relatedModel->getRelation($relationName);
1620
                    } catch (InvalidParamException $e) {
1621
                        return $this->generateAttributeLabel($attribute);
1622
                    }
1623
                    /* @var $modelClass ActiveRecordInterface */
1624
                    $modelClass = $relation->modelClass;
1625
                    $relatedModel = $modelClass::instance();
1626
                }
1627
            }
1628
1629
            $labels = $relatedModel->attributeLabels();
1630
            if (isset($labels[$neededAttribute])) {
1631
                return $labels[$neededAttribute];
1632
            }
1633
        }
1634
1635 54
        return $this->generateAttributeLabel($attribute);
1636
    }
1637
1638
    /**
1639
     * Returns the text hint for the specified attribute.
1640
     * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
1641
     * @param string $attribute the attribute name
1642
     * @return string the attribute hint
1643
     * @see attributeHints()
1644
     * @since 2.0.4
1645
     */
1646
    public function getAttributeHint($attribute)
1647
    {
1648
        $hints = $this->attributeHints();
1649
        if (isset($hints[$attribute])) {
1650
            return $hints[$attribute];
1651
        } elseif (strpos($attribute, '.')) {
1652
            $attributeParts = explode('.', $attribute);
1653
            $neededAttribute = array_pop($attributeParts);
1654
1655
            $relatedModel = $this;
1656
            foreach ($attributeParts as $relationName) {
1657
                if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) {
1658
                    $relatedModel = $relatedModel->$relationName;
1659
                } else {
1660
                    try {
1661
                        $relation = $relatedModel->getRelation($relationName);
1662
                    } catch (InvalidParamException $e) {
1663
                        return '';
1664
                    }
1665
                    /* @var $modelClass ActiveRecordInterface */
1666
                    $modelClass = $relation->modelClass;
1667
                    $relatedModel = $modelClass::instance();
1668
                }
1669
            }
1670
1671
            $hints = $relatedModel->attributeHints();
1672
            if (isset($hints[$neededAttribute])) {
1673
                return $hints[$neededAttribute];
1674
            }
1675
        }
1676
1677
        return '';
1678
    }
1679
1680
    /**
1681
     * {@inheritdoc}
1682
     *
1683
     * The default implementation returns the names of the columns whose values have been populated into this record.
1684
     */
1685
    public function fields()
1686
    {
1687
        $fields = array_keys($this->_attributes);
1688
1689
        return array_combine($fields, $fields);
1690
    }
1691
1692
    /**
1693
     * {@inheritdoc}
1694
     *
1695
     * The default implementation returns the names of the relations that have been populated into this record.
1696
     */
1697
    public function extraFields()
1698
    {
1699
        $fields = array_keys($this->getRelatedRecords());
1700
1701
        return array_combine($fields, $fields);
1702
    }
1703
1704
    /**
1705
     * Sets the element value at the specified offset to null.
1706
     * This method is required by the SPL interface [[\ArrayAccess]].
1707
     * It is implicitly called when you use something like `unset($model[$offset])`.
1708
     * @param mixed $offset the offset to unset element
1709
     */
1710 3
    public function offsetUnset($offset)
1711
    {
1712 3
        if (property_exists($this, $offset)) {
1713
            $this->$offset = null;
1714
        } else {
1715 3
            unset($this->$offset);
1716
        }
1717 3
    }
1718
1719
    /**
1720
     * Resets dependent related models checking if their links contain specific attribute.
1721
     * @param string $attribute The changed attribute name.
1722
     */
1723 15
    private function resetDependentRelations($attribute)
1724
    {
1725 15
        foreach ($this->_relationsDependencies[$attribute] as $relation) {
1726 15
            unset($this->_related[$relation]);
1727
        }
1728 15
        unset($this->_relationsDependencies[$attribute]);
1729 15
    }
1730
1731
    /**
1732
     * Sets relation dependencies for a property
1733
     * @param string $name property name
1734
     * @param ActiveQueryInterface $relation relation instance
1735
     */
1736 73
    private function setRelationDependencies($name, $relation)
1737
    {
1738 73
        if (empty($relation->via) && $relation->link) {
1739 70
            foreach ($relation->link as $attribute) {
1740 70
                $this->_relationsDependencies[$attribute][$name] = $name;
1741
            }
1742 39
        } elseif ($relation->via instanceof ActiveQueryInterface) {
1743 15
            $this->setRelationDependencies($name, $relation->via);
1744 27
        } elseif (is_array($relation->via)) {
1745 24
            list(, $viaQuery) = $relation->via;
1746 24
            $this->setRelationDependencies($name, $viaQuery);
1747
        }
1748 73
    }
1749
}
1750