Passed
Pull Request — 2.2 (#20357)
by Wilmer
12:52 queued 05:12
created

ActiveRecord::updateAllCounters()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 3
dl 0
loc 11
ccs 0
cts 8
cp 0
crap 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\db;
10
11
use Yii;
12
use yii\base\InvalidArgumentException;
13
use yii\base\InvalidConfigException;
14
use yii\helpers\ArrayHelper;
15
use yii\helpers\Inflector;
16
use yii\helpers\StringHelper;
17
18
/**
19
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
20
 *
21
 * Active Record implements the [Active Record design pattern](https://en.wikipedia.org/wiki/Active_record_pattern).
22
 * The premise behind Active Record is that an individual [[ActiveRecord]] object is associated with a specific
23
 * row in a database table. The object's attributes are mapped to the columns of the corresponding table.
24
 * Referencing an Active Record attribute is equivalent to accessing the corresponding table column for that record.
25
 *
26
 * As an example, say that the `Customer` ActiveRecord class is associated with the `customer` table.
27
 * This would mean that the class's `name` attribute is automatically mapped to the `name` column in `customer` table.
28
 * Thanks to Active Record, assuming the variable `$customer` is an object of type `Customer`, to get the value of
29
 * the `name` column for the table row, you can use the expression `$customer->name`.
30
 * In this example, Active Record is providing an object-oriented interface for accessing data stored in the database.
31
 * But Active Record provides much more functionality than this.
32
 *
33
 * To declare an ActiveRecord class you need to extend [[\yii\db\ActiveRecord]] and
34
 * implement the `tableName` method:
35
 *
36
 * ```php
37
 * <?php
38
 *
39
 * class Customer extends \yii\db\ActiveRecord
40
 * {
41
 *     public static function tableName()
42
 *     {
43
 *         return 'customer';
44
 *     }
45
 * }
46
 * ```
47
 *
48
 * The `tableName` method only has to return the name of the database table associated with the class.
49
 *
50
 * > Tip: You may also use the [Gii code generator](guide:start-gii) to generate ActiveRecord classes from your
51
 * > database tables.
52
 *
53
 * Class instances are obtained in one of two ways:
54
 *
55
 * * Using the `new` operator to create a new, empty object
56
 * * Using a method to fetch an existing record (or records) from the database
57
 *
58
 * Below is an example showing some typical usage of ActiveRecord:
59
 *
60
 * ```php
61
 * $user = new User();
62
 * $user->name = 'Qiang';
63
 * $user->save();  // a new row is inserted into user table
64
 *
65
 * // the following will retrieve the user 'CeBe' from the database
66
 * $user = User::find()->where(['name' => 'CeBe'])->one();
67
 *
68
 * // this will get related records from orders table when relation is defined
69
 * $orders = $user->orders;
70
 * ```
71
 *
72
 * For more details and usage information on ActiveRecord, see the [guide article on ActiveRecord](guide:db-active-record).
73
 *
74
 * @method ActiveQuery hasMany($class, array $link) See [[BaseActiveRecord::hasMany()]] for more info.
75
 * @method ActiveQuery hasOne($class, array $link) See [[BaseActiveRecord::hasOne()]] for more info.
76
 *
77
 * @author Qiang Xue <[email protected]>
78
 * @author Carsten Brandt <[email protected]>
79
 * @since 2.0
80
 */
81
class ActiveRecord extends BaseActiveRecord
82
{
83
    /**
84
     * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
85
     */
86
    const OP_INSERT = 0x01;
87
    /**
88
     * The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
89
     */
90
    const OP_UPDATE = 0x02;
91
    /**
92
     * The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
93
     */
94
    const OP_DELETE = 0x04;
95
    /**
96
     * All three operations: insert, update, delete.
97
     * This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE.
98
     */
99
    const OP_ALL = 0x07;
100
101
102
    /**
103
     * Loads default values from database table schema.
104
     *
105
     * You may call this method to load default values after creating a new instance:
106
     *
107
     * ```php
108
     * // class Customer extends \yii\db\ActiveRecord
109
     * $customer = new Customer();
110
     * $customer->loadDefaultValues();
111
     * ```
112
     *
113
     * @param bool $skipIfSet whether existing value should be preserved.
114
     * This will only set defaults for attributes that are `null`.
115
     * @return $this the model instance itself.
116
     */
117
    public function loadDefaultValues($skipIfSet = true)
118
    {
119
        $columns = static::getTableSchema()->columns;
120
        foreach ($this->attributes() as $name) {
121
            if (isset($columns[$name])) {
122
                $defaultValue = $columns[$name]->defaultValue;
123
                if ($defaultValue !== null && (!$skipIfSet || $this->getAttribute($name) === null)) {
124
                    $this->setAttribute($name, $defaultValue);
125
                }
126
            }
127
        }
128
129
        return $this;
130
    }
131
132
    /**
133
     * Returns the database connection used by this AR class.
134
     * By default, the "db" application component is used as the database connection.
135
     * You may override this method if you want to use a different database connection.
136
     * @return Connection the database connection used by this AR class.
137
     */
138 55
    public static function getDb()
139
    {
140 55
        return Yii::$app->getDb();
141
    }
142
143
    /**
144
     * Creates an [[ActiveQuery]] instance with a given SQL statement.
145
     *
146
     * Note that because the SQL statement is already specified, calling additional
147
     * query modification methods (such as `where()`, `order()`) on the created [[ActiveQuery]]
148
     * instance will have no effect. However, calling `with()`, `asArray()` or `indexBy()` is
149
     * still fine.
150
     *
151
     * Below is an example:
152
     *
153
     * ```php
154
     * $customers = Customer::findBySql('SELECT * FROM customer')->all();
155
     * ```
156
     *
157
     * @param string $sql the SQL statement to be executed
158
     * @param array $params parameters to be bound to the SQL statement during execution.
159
     * @return ActiveQuery the newly created [[ActiveQuery]] instance
160
     */
161
    public static function findBySql($sql, $params = [])
162
    {
163
        $query = static::find();
164
        $query->sql = $sql;
165
166
        return $query->params($params);
167
    }
168
169
    /**
170
     * Finds ActiveRecord instance(s) by the given condition.
171
     * This method is internally called by [[findOne()]] and [[findAll()]].
172
     * @param mixed $condition please refer to [[findOne()]] for the explanation of this parameter
173
     * @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance.
174
     * @throws InvalidConfigException if there is no primary key defined.
175
     * @internal
176
     */
177 1
    protected static function findByCondition($condition)
178
    {
179 1
        $query = static::find();
180
181 1
        if (!ArrayHelper::isAssociative($condition) && !$condition instanceof ExpressionInterface) {
182
            // query by primary key
183
            $primaryKey = static::primaryKey();
184
            if (isset($primaryKey[0])) {
185
                $pk = $primaryKey[0];
186
                if (!empty($query->join) || !empty($query->joinWith)) {
187
                    $pk = static::tableName() . '.' . $pk;
188
                }
189
                // if condition is scalar, search for a single primary key, if it is array, search for multiple primary key values
190
                $condition = [$pk => is_array($condition) ? array_values($condition) : $condition];
191
            } else {
192
                throw new InvalidConfigException('"' . get_called_class() . '" must have a primary key.');
193
            }
194 1
        } elseif (is_array($condition)) {
195 1
            $aliases = static::filterValidAliases($query);
196 1
            $condition = static::filterCondition($condition, $aliases);
197
        }
198
199 1
        return $query->andWhere($condition);
200
    }
201
202
    /**
203
     * Returns table aliases which are not the same as the name of the tables.
204
     *
205
     * @param Query $query
206
     * @return array
207
     * @throws InvalidConfigException
208
     * @since 2.0.17
209
     * @internal
210
     */
211 1
    protected static function filterValidAliases(Query $query)
212
    {
213 1
        $tables = $query->getTablesUsedInFrom();
214
215 1
        $aliases = array_diff(array_keys($tables), $tables);
216
217 1
        return array_map(function ($alias) {
218
            return preg_replace('/{{(\w+)}}/', '$1', $alias);
219 1
        }, array_values($aliases));
220
    }
221
222
    /**
223
     * Filters array condition before it is assiged to a Query filter.
224
     *
225
     * This method will ensure that an array condition only filters on existing table columns.
226
     *
227
     * @param array $condition condition to filter.
228
     * @param array $aliases
229
     * @return array filtered condition.
230
     * @throws InvalidArgumentException in case array contains unsafe values.
231
     * @throws InvalidConfigException
232
     * @since 2.0.15
233
     * @internal
234
     */
235 1
    protected static function filterCondition(array $condition, array $aliases = [])
236
    {
237 1
        $result = [];
238 1
        $db = static::getDb();
239 1
        $columnNames = static::filterValidColumnNames($db, $aliases);
240
241 1
        foreach ($condition as $key => $value) {
242 1
            if (is_string($key) && !in_array($db->quoteSql($key), $columnNames, true)) {
243
                throw new InvalidArgumentException('Key "' . $key . '" is not a column name and can not be used as a filter');
244
            }
245 1
            $result[$key] = is_array($value) ? array_values($value) : $value;
246
        }
247
248 1
        return $result;
249
    }
250
251
    /**
252
     * Valid column names are table column names or column names prefixed with table name or table alias
253
     *
254
     * @param Connection $db
255
     * @param array $aliases
256
     * @return array
257
     * @throws InvalidConfigException
258
     * @since 2.0.17
259
     * @internal
260
     */
261 1
    protected static function filterValidColumnNames($db, array $aliases)
262
    {
263 1
        $columnNames = [];
264 1
        $tableName = static::tableName();
265 1
        $quotedTableName = $db->quoteTableName($tableName);
266
267 1
        foreach (static::getTableSchema()->getColumnNames() as $columnName) {
268 1
            $columnNames[] = $columnName;
269 1
            $columnNames[] = $db->quoteColumnName($columnName);
270 1
            $columnNames[] = "$tableName.$columnName";
271 1
            $columnNames[] = $db->quoteSql("$quotedTableName.[[$columnName]]");
272 1
            foreach ($aliases as $tableAlias) {
273
                $columnNames[] = "$tableAlias.$columnName";
274
                $quotedTableAlias = $db->quoteTableName($tableAlias);
275
                $columnNames[] = $db->quoteSql("$quotedTableAlias.[[$columnName]]");
276
            }
277
        }
278
279 1
        return $columnNames;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285 4
    public function refresh()
286
    {
287 4
        $query = static::find();
288 4
        $tableName = key($query->getTablesUsedInFrom());
289 4
        $pk = [];
290
        // disambiguate column names in case ActiveQuery adds a JOIN
291 4
        foreach ($this->getPrimaryKey(true) as $key => $value) {
292 4
            $pk[$tableName . '.' . $key] = $value;
293
        }
294 4
        $query->where($pk);
295
296
        /* @var $record BaseActiveRecord */
297 4
        $record = $query->noCache()->one();
298 4
        return $this->refreshInternal($record);
299
    }
300
301
    /**
302
     * Updates the whole table using the provided attribute values and conditions.
303
     *
304
     * For example, to change the status to be 1 for all customers whose status is 2:
305
     *
306
     * ```php
307
     * Customer::updateAll(['status' => 1], 'status = 2');
308
     * ```
309
     *
310
     * > Warning: If you do not specify any condition, this method will update **all** rows in the table.
311
     *
312
     * Note that this method will not trigger any events. If you need [[EVENT_BEFORE_UPDATE]] or
313
     * [[EVENT_AFTER_UPDATE]] to be triggered, you need to [[find()|find]] the models first and then
314
     * call [[update()]] on each of them. For example an equivalent of the example above would be:
315
     *
316
     * ```php
317
     * $models = Customer::find()->where('status = 2')->all();
318
     * foreach ($models as $model) {
319
     *     $model->status = 1;
320
     *     $model->update(false); // skipping validation as no user input is involved
321
     * }
322
     * ```
323
     *
324
     * For a large set of models you might consider using [[ActiveQuery::each()]] to keep memory usage within limits.
325
     *
326
     * @param array $attributes attribute values (name-value pairs) to be saved into the table
327
     * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
328
     * Please refer to [[Query::where()]] on how to specify this parameter.
329
     * @param array $params the parameters (name => value) to be bound to the query.
330
     * @return int the number of rows updated
331
     */
332 11
    public static function updateAll($attributes, $condition = '', $params = [])
333
    {
334 11
        $command = static::getDb()->createCommand();
335 11
        $command->update(static::tableName(), $attributes, $condition, $params);
336
337 11
        return $command->execute();
338
    }
339
340
    /**
341
     * Updates the whole table using the provided counter changes and conditions.
342
     *
343
     * For example, to increment all customers' age by 1,
344
     *
345
     * ```php
346
     * Customer::updateAllCounters(['age' => 1]);
347
     * ```
348
     *
349
     * Note that this method will not trigger any events.
350
     *
351
     * @param array $counters the counters to be updated (attribute name => increment value).
352
     * Use negative values if you want to decrement the counters.
353
     * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
354
     * Please refer to [[Query::where()]] on how to specify this parameter.
355
     * @param array $params the parameters (name => value) to be bound to the query.
356
     * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method.
357
     * @return int the number of rows updated
358
     */
359
    public static function updateAllCounters($counters, $condition = '', $params = [])
360
    {
361
        $n = 0;
362
        foreach ($counters as $name => $value) {
363
            $counters[$name] = new Expression("[[$name]]+:bp{$n}", [":bp{$n}" => $value]);
364
            $n++;
365
        }
366
        $command = static::getDb()->createCommand();
367
        $command->update(static::tableName(), $counters, $condition, $params);
368
369
        return $command->execute();
370
    }
371
372
    /**
373
     * Deletes rows in the table using the provided conditions.
374
     *
375
     * For example, to delete all customers whose status is 3:
376
     *
377
     * ```php
378
     * Customer::deleteAll('status = 3');
379
     * ```
380
     *
381
     * > Warning: If you do not specify any condition, this method will delete **all** rows in the table.
382
     *
383
     * Note that this method will not trigger any events. If you need [[EVENT_BEFORE_DELETE]] or
384
     * [[EVENT_AFTER_DELETE]] to be triggered, you need to [[find()|find]] the models first and then
385
     * call [[delete()]] on each of them. For example an equivalent of the example above would be:
386
     *
387
     * ```php
388
     * $models = Customer::find()->where('status = 3')->all();
389
     * foreach ($models as $model) {
390
     *     $model->delete();
391
     * }
392
     * ```
393
     *
394
     * For a large set of models you might consider using [[ActiveQuery::each()]] to keep memory usage within limits.
395
     *
396
     * @param string|array|null $condition the conditions that will be put in the WHERE part of the DELETE SQL.
397
     * Please refer to [[Query::where()]] on how to specify this parameter.
398
     * @param array $params the parameters (name => value) to be bound to the query.
399
     * @return int the number of rows deleted
400
     */
401 1
    public static function deleteAll($condition = null, $params = [])
402
    {
403 1
        $command = static::getDb()->createCommand();
404 1
        $command->delete(static::tableName(), $condition, $params);
405
406 1
        return $command->execute();
407
    }
408
409
    /**
410
     * {@inheritdoc}
411
     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
412
     */
413 17
    public static function find()
414
    {
415 17
        return Yii::createObject(ActiveQuery::class, [get_called_class()]);
416
    }
417
418
    /**
419
     * Declares the name of the database table associated with this AR class.
420
     * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]
421
     * with prefix [[Connection::tablePrefix]]. For example if [[Connection::tablePrefix]] is `tbl_`,
422
     * `Customer` becomes `tbl_customer`, and `OrderItem` becomes `tbl_order_item`. You may override this method
423
     * if the table is not named after this convention.
424
     * @return string the table name
425
     */
426
    public static function tableName()
427
    {
428
        return '{{%' . Inflector::camel2id(StringHelper::basename(get_called_class()), '_') . '}}';
429
    }
430
431
    /**
432
     * Returns the schema information of the DB table associated with this AR class.
433
     * @return TableSchema the schema information of the DB table associated with this AR class.
434
     * @throws InvalidConfigException if the table for the AR class does not exist.
435
     */
436 55
    public static function getTableSchema()
437
    {
438 55
        $tableSchema = static::getDb()
439 55
            ->getSchema()
440 55
            ->getTableSchema(static::tableName());
441
442 55
        if ($tableSchema === null) {
443
            throw new InvalidConfigException('The table does not exist: ' . static::tableName());
444
        }
445
446 55
        return $tableSchema;
447
    }
448
449
    /**
450
     * Returns the primary key name(s) for this AR class.
451
     * The default implementation will return the primary key(s) as declared
452
     * in the DB table that is associated with this AR class.
453
     *
454
     * If the DB table does not declare any primary key, you should override
455
     * this method to return the attributes that you want to use as primary keys
456
     * for this AR class.
457
     *
458
     * Note that an array should be returned even for a table with single primary key.
459
     *
460
     * @return string[] the primary keys of the associated database table.
461
     */
462 13
    public static function primaryKey()
463
    {
464 13
        return static::getTableSchema()->primaryKey;
465
    }
466
467
    /**
468
     * Returns the list of all attribute names of the model.
469
     * The default implementation will return all column names of the table associated with this AR class.
470
     * @return array list of attribute names.
471
     */
472 55
    public function attributes()
473
    {
474 55
        return static::getTableSchema()->getColumnNames();
475
    }
476
477
    /**
478
     * Declares which DB operations should be performed within a transaction in different scenarios.
479
     * The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]],
480
     * which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively.
481
     * By default, these methods are NOT enclosed in a DB transaction.
482
     *
483
     * In some scenarios, to ensure data consistency, you may want to enclose some or all of them
484
     * in transactions. You can do so by overriding this method and returning the operations
485
     * that need to be transactional. For example,
486
     *
487
     * ```php
488
     * return [
489
     *     'admin' => self::OP_INSERT,
490
     *     'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
491
     *     // the above is equivalent to the following:
492
     *     // 'api' => self::OP_ALL,
493
     *
494
     * ];
495
     * ```
496
     *
497
     * The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]])
498
     * should be done in a transaction; and in the "api" scenario, all the operations should be done
499
     * in a transaction.
500
     *
501
     * @return array the declarations of transactional operations. The array keys are scenarios names,
502
     * and the array values are the corresponding transaction operations.
503
     */
504 25
    public function transactions()
505
    {
506 25
        return [];
507
    }
508
509
    /**
510
     * {@inheritdoc}
511
     */
512 8
    public static function populateRecord($record, $row)
513
    {
514 8
        $columns = static::getTableSchema()->columns;
515 8
        foreach ($row as $name => $value) {
516 8
            if (isset($columns[$name])) {
517 8
                $row[$name] = $columns[$name]->phpTypecast($value);
518
            }
519
        }
520 8
        parent::populateRecord($record, $row);
521
    }
522
523
    /**
524
     * Inserts a row into the associated database table using the attribute values of this record.
525
     *
526
     * This method performs the following steps in order:
527
     *
528
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
529
     *    returns `false`, the rest of the steps will be skipped;
530
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
531
     *    failed, the rest of the steps will be skipped;
532
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
533
     *    the rest of the steps will be skipped;
534
     * 4. insert the record into database. If this fails, it will skip the rest of the steps;
535
     * 5. call [[afterSave()]];
536
     *
537
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
538
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_INSERT]], and [[EVENT_AFTER_INSERT]]
539
     * will be raised by the corresponding methods.
540
     *
541
     * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database.
542
     *
543
     * If the table's primary key is auto-incremental and is `null` during insertion,
544
     * it will be populated with the actual value after insertion.
545
     *
546
     * For example, to insert a customer record:
547
     *
548
     * ```php
549
     * $customer = new Customer;
550
     * $customer->name = $name;
551
     * $customer->email = $email;
552
     * $customer->insert();
553
     * ```
554
     *
555
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
556
     * before saving the record. Defaults to `true`. If the validation fails, the record
557
     * will not be saved to the database and this method will return `false`.
558
     * @param array|null $attributes list of attributes that need to be saved. Defaults to `null`,
559
     * meaning all attributes that are loaded from DB will be saved.
560
     * @return bool whether the attributes are valid and the record is inserted successfully.
561
     * @throws \Throwable in case insert failed.
562
     */
563 25
    public function insert($runValidation = true, $attributes = null)
564
    {
565 25
        if ($runValidation && !$this->validate($attributes)) {
566
            Yii::info('Model not inserted due to validation error.', __METHOD__);
567
            return false;
568
        }
569
570 25
        if (!$this->isTransactional(self::OP_INSERT)) {
571 25
            return $this->insertInternal($attributes);
572
        }
573
574
        $transaction = static::getDb()->beginTransaction();
575
        try {
576
            $result = $this->insertInternal($attributes);
577
            if ($result === false) {
578
                $transaction->rollBack();
579
            } else {
580
                $transaction->commit();
581
            }
582
583
            return $result;
584
        } catch (\Exception $e) {
585
            $transaction->rollBack();
586
            throw $e;
587
        } catch (\Throwable $e) {
588
            $transaction->rollBack();
589
            throw $e;
590
        }
591
    }
592
593
    /**
594
     * Inserts an ActiveRecord into DB without considering transaction.
595
     * @param array|null $attributes list of attributes that need to be saved. Defaults to `null`,
596
     * meaning all attributes that are loaded from DB will be saved.
597
     * @return bool whether the record is inserted successfully.
598
     */
599 25
    protected function insertInternal($attributes = null)
600
    {
601 25
        if (!$this->beforeSave(true)) {
602
            return false;
603
        }
604 25
        $values = $this->getDirtyAttributes($attributes);
605 25
        if (($primaryKeys = static::getDb()->schema->insert(static::tableName(), $values)) === false) {
606
            return false;
607
        }
608 25
        foreach ($primaryKeys as $name => $value) {
609 24
            $id = static::getTableSchema()->columns[$name]->phpTypecast($value);
610 24
            $this->setAttribute($name, $id);
611 24
            $values[$name] = $id;
612
        }
613
614 25
        $changedAttributes = array_fill_keys(array_keys($values), null);
615 25
        $this->setOldAttributes($values);
616 25
        $this->afterSave(true, $changedAttributes);
617
618 25
        return true;
619
    }
620
621
    /**
622
     * Saves the changes to this active record into the associated database table.
623
     *
624
     * This method performs the following steps in order:
625
     *
626
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
627
     *    returns `false`, the rest of the steps will be skipped;
628
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
629
     *    failed, the rest of the steps will be skipped;
630
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
631
     *    the rest of the steps will be skipped;
632
     * 4. save the record into database. If this fails, it will skip the rest of the steps;
633
     * 5. call [[afterSave()]];
634
     *
635
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
636
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]]
637
     * will be raised by the corresponding methods.
638
     *
639
     * Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
640
     *
641
     * For example, to update a customer record:
642
     *
643
     * ```php
644
     * $customer = Customer::findOne($id);
645
     * $customer->name = $name;
646
     * $customer->email = $email;
647
     * $customer->update();
648
     * ```
649
     *
650
     * Note that it is possible the update does not affect any row in the table.
651
     * In this case, this method will return 0. For this reason, you should use the following
652
     * code to check if update() is successful or not:
653
     *
654
     * ```php
655
     * if ($customer->update() !== false) {
656
     *     // update successful
657
     * } else {
658
     *     // update failed
659
     * }
660
     * ```
661
     *
662
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
663
     * before saving the record. Defaults to `true`. If the validation fails, the record
664
     * will not be saved to the database and this method will return `false`.
665
     * @param array|null $attributeNames list of attributes that need to be saved. Defaults to `null`,
666
     * meaning all attributes that are loaded from DB will be saved.
667
     * @return int|false the number of rows affected, or false if validation fails
668
     * or [[beforeSave()]] stops the updating process.
669
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
670
     * being updated is outdated.
671
     * @throws \Throwable in case update failed.
672
     */
673 11
    public function update($runValidation = true, $attributeNames = null)
674
    {
675 11
        if ($runValidation && !$this->validate($attributeNames)) {
676
            Yii::info('Model not updated due to validation error.', __METHOD__);
677
            return false;
678
        }
679
680 11
        if (!$this->isTransactional(self::OP_UPDATE)) {
681 11
            return $this->updateInternal($attributeNames);
682
        }
683
684
        $transaction = static::getDb()->beginTransaction();
685
        try {
686
            $result = $this->updateInternal($attributeNames);
687
            if ($result === false) {
688
                $transaction->rollBack();
689
            } else {
690
                $transaction->commit();
691
            }
692
693
            return $result;
694
        } catch (\Exception $e) {
695
            $transaction->rollBack();
696
            throw $e;
697
        } catch (\Throwable $e) {
698
            $transaction->rollBack();
699
            throw $e;
700
        }
701
    }
702
703
    /**
704
     * Deletes the table row corresponding to this active record.
705
     *
706
     * This method performs the following steps in order:
707
     *
708
     * 1. call [[beforeDelete()]]. If the method returns `false`, it will skip the
709
     *    rest of the steps;
710
     * 2. delete the record from the database;
711
     * 3. call [[afterDelete()]].
712
     *
713
     * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
714
     * will be raised by the corresponding methods.
715
     *
716
     * @return int|false the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
717
     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
718
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
719
     * being deleted is outdated.
720
     * @throws \Throwable in case delete failed.
721
     */
722 1
    public function delete()
723
    {
724 1
        if (!$this->isTransactional(self::OP_DELETE)) {
725 1
            return $this->deleteInternal();
726
        }
727
728
        $transaction = static::getDb()->beginTransaction();
729
        try {
730
            $result = $this->deleteInternal();
731
            if ($result === false) {
732
                $transaction->rollBack();
733
            } else {
734
                $transaction->commit();
735
            }
736
737
            return $result;
738
        } catch (\Exception $e) {
739
            $transaction->rollBack();
740
            throw $e;
741
        } catch (\Throwable $e) {
742
            $transaction->rollBack();
743
            throw $e;
744
        }
745
    }
746
747
    /**
748
     * Deletes an ActiveRecord without considering transaction.
749
     * @return int|false the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
750
     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
751
     * @throws StaleObjectException
752
     */
753 1
    protected function deleteInternal()
754
    {
755 1
        if (!$this->beforeDelete()) {
756
            return false;
757
        }
758
759
        // we do not check the return value of deleteAll() because it's possible
760
        // the record is already deleted in the database and thus the method will return 0
761 1
        $condition = $this->getOldPrimaryKey(true);
762 1
        $lock = $this->optimisticLock();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $lock is correct as $this->optimisticLock() targeting yii\db\BaseActiveRecord::optimisticLock() seems to always return null.

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

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

}

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

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

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

Loading history...
763 1
        if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
764 1
            $condition[$lock] = $this->$lock;
765
        }
766 1
        $result = static::deleteAll($condition);
767 1
        if ($lock !== null && !$result) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
768 1
            throw new StaleObjectException('The object being deleted is outdated.');
769
        }
770 1
        $this->setOldAttributes(null);
771 1
        $this->afterDelete();
772
773 1
        return $result;
774
    }
775
776
    /**
777
     * Returns a value indicating whether the given active record is the same as the current one.
778
     * The comparison is made by comparing the table names and the primary key values of the two active records.
779
     * If one of the records [[isNewRecord|is new]] they are also considered not equal.
780
     * @param ActiveRecord $record record to compare to
781
     * @return bool whether the two active records refer to the same row in the same database table.
782
     */
783
    public function equals($record)
784
    {
785
        if ($this->isNewRecord || $record->isNewRecord) {
786
            return false;
787
        }
788
789
        return static::tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
790
    }
791
792
    /**
793
     * Returns a value indicating whether the specified operation is transactional in the current [[$scenario]].
794
     * @param int $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]].
795
     * @return bool whether the specified operation is transactional in the current [[scenario]].
796
     */
797 25
    public function isTransactional($operation)
798
    {
799 25
        $scenario = $this->getScenario();
800 25
        $transactions = $this->transactions();
801
802 25
        return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation);
803
    }
804
}
805