Completed
Push — master ( b9ee17...adf8f9 )
by Alexander
13:00
created

ActiveRecord::filterCondition()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 16
cts 16
cp 1
rs 8.9137
c 0
b 0
f 0
cc 6
nc 8
nop 1
crap 6
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\InvalidConfigException;
13
use yii\helpers\ArrayHelper;
14
use yii\helpers\Inflector;
15
use yii\helpers\StringHelper;
16
17
/**
18
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
19
 *
20
 * Active Record implements the [Active Record design pattern](http://en.wikipedia.org/wiki/Active_record).
21
 * The premise behind Active Record is that an individual [[ActiveRecord]] object is associated with a specific
22
 * row in a database table. The object's attributes are mapped to the columns of the corresponding table.
23
 * Referencing an Active Record attribute is equivalent to accessing the corresponding table column for that record.
24
 *
25
 * As an example, say that the `Customer` ActiveRecord class is associated with the `customer` table.
26
 * This would mean that the class's `name` attribute is automatically mapped to the `name` column in `customer` table.
27
 * Thanks to Active Record, assuming the variable `$customer` is an object of type `Customer`, to get the value of
28
 * the `name` column for the table row, you can use the expression `$customer->name`.
29
 * In this example, Active Record is providing an object-oriented interface for accessing data stored in the database.
30
 * But Active Record provides much more functionality than this.
31
 *
32
 * To declare an ActiveRecord class you need to extend [[\yii\db\ActiveRecord]] and
33
 * implement the `tableName` method:
34
 *
35
 * ```php
36
 * <?php
37
 *
38
 * class Customer extends \yii\db\ActiveRecord
39
 * {
40
 *     public static function tableName()
41
 *     {
42
 *         return 'customer';
43
 *     }
44
 * }
45
 * ```
46
 *
47
 * The `tableName` method only has to return the name of the database table associated with the class.
48
 *
49
 * > Tip: You may also use the [Gii code generator](guide:start-gii) to generate ActiveRecord classes from your
50
 * > database tables.
51
 *
52
 * Class instances are obtained in one of two ways:
53
 *
54
 * * Using the `new` operator to create a new, empty object
55
 * * Using a method to fetch an existing record (or records) from the database
56
 *
57
 * Below is an example showing some typical usage of ActiveRecord:
58
 *
59
 * ```php
60
 * $user = new User();
61
 * $user->name = 'Qiang';
62
 * $user->save();  // a new row is inserted into user table
63
 *
64
 * // the following will retrieve the user 'CeBe' from the database
65
 * $user = User::find()->where(['name' => 'CeBe'])->one();
66
 *
67
 * // this will get related records from orders table when relation is defined
68
 * $orders = $user->orders;
69
 * ```
70
 *
71
 * For more details and usage information on ActiveRecord, see the [guide article on ActiveRecord](guide:db-active-record).
72
 *
73
 * @method ActiveQuery hasMany($class, array $link) see [[BaseActiveRecord::hasMany()]] for more info
74
 * @method ActiveQuery hasOne($class, array $link) see [[BaseActiveRecord::hasOne()]] for more info
75
 *
76
 * @author Qiang Xue <[email protected]>
77
 * @author Carsten Brandt <[email protected]>
78
 * @since 2.0
79
 */
80
class ActiveRecord extends BaseActiveRecord
81
{
82
    /**
83
     * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
84
     */
85
    const OP_INSERT = 0x01;
86
    /**
87
     * The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
88
     */
89
    const OP_UPDATE = 0x02;
90
    /**
91
     * The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
92
     */
93
    const OP_DELETE = 0x04;
94
    /**
95
     * All three operations: insert, update, delete.
96
     * This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE.
97
     */
98
    const OP_ALL = 0x07;
99
100
101
    /**
102
     * Loads default values from database table schema.
103
     *
104
     * You may call this method to load default values after creating a new instance:
105
     *
106
     * ```php
107
     * // class Customer extends \yii\db\ActiveRecord
108
     * $customer = new Customer();
109
     * $customer->loadDefaultValues();
110
     * ```
111
     *
112
     * @param bool $skipIfSet whether existing value should be preserved.
113
     * This will only set defaults for attributes that are `null`.
114
     * @return $this the model instance itself.
115
     */
116 4
    public function loadDefaultValues($skipIfSet = true)
117
    {
118 4
        foreach (static::getTableSchema()->columns as $column) {
119 4
            if ($column->defaultValue !== null && (!$skipIfSet || $this->{$column->name} === null)) {
120 4
                $this->{$column->name} = $column->defaultValue;
121
            }
122
        }
123
124 4
        return $this;
125
    }
126
127
    /**
128
     * Returns the database connection used by this AR class.
129
     * By default, the "db" application component is used as the database connection.
130
     * You may override this method if you want to use a different database connection.
131
     * @return Connection the database connection used by this AR class.
132
     */
133 48
    public static function getDb()
134
    {
135 48
        return Yii::$app->getDb();
136
    }
137
138
    /**
139
     * Creates an [[ActiveQuery]] instance with a given SQL statement.
140
     *
141
     * Note that because the SQL statement is already specified, calling additional
142
     * query modification methods (such as `where()`, `order()`) on the created [[ActiveQuery]]
143
     * instance will have no effect. However, calling `with()`, `asArray()` or `indexBy()` is
144
     * still fine.
145
     *
146
     * Below is an example:
147
     *
148
     * ```php
149
     * $customers = Customer::findBySql('SELECT * FROM customer')->all();
150
     * ```
151
     *
152
     * @param string $sql the SQL statement to be executed
153
     * @param array $params parameters to be bound to the SQL statement during execution.
154
     * @return ActiveQuery the newly created [[ActiveQuery]] instance
155
     */
156 6
    public static function findBySql($sql, $params = [])
157
    {
158 6
        $query = static::find();
159 6
        $query->sql = $sql;
160
161 6
        return $query->params($params);
162
    }
163
164
    /**
165
     * Finds ActiveRecord instance(s) by the given condition.
166
     * This method is internally called by [[findOne()]] and [[findAll()]].
167
     * @param mixed $condition please refer to [[findOne()]] for the explanation of this parameter
168
     * @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance.
169
     * @throws InvalidConfigException if there is no primary key defined.
170
     * @internal
171
     */
172 217
    protected static function findByCondition($condition)
173
    {
174 217
        $query = static::find();
175
176 217
        if (!ArrayHelper::isAssociative($condition)) {
177
            // query by primary key
178 161
            $primaryKey = static::primaryKey();
179 161
            if (isset($primaryKey[0])) {
180 161
                $pk = $primaryKey[0];
181 161
                if (!empty($query->join) || !empty($query->joinWith)) {
182 3
                    $pk = static::tableName() . '.' . $pk;
183
                }
184
                // if condition is scalar, search for a single primary key, if it is array, search for multiple primary key values
185 161
                $condition = [$pk => is_array($condition) ? array_values($condition) : $condition];
186
            } else {
187 161
                throw new InvalidConfigException('"' . get_called_class() . '" must have a primary key.');
188
            }
189 68
        } elseif (is_array($condition)) {
190 68
            $condition = static::filterCondition($condition);
191
        }
192
193 205
        return $query->andWhere($condition);
194
    }
195
196
    /**
197
     * Filters array condition before it is assiged to a Query filter.
198
     *
199
     * This method will ensure that an array condition only filters on existing table columns.
200
     *
201
     * @param array $condition condition to filter.
202
     * @return array filtered condition.
203
     * @throws InvalidArgumentException in case array contains unsafe values.
204
     * @since 2.0.15
205
     * @internal
206
     */
207 68
    protected static function filterCondition(array $condition)
208
    {
209 68
        $result = [];
210 68
        $db = static::getDb();
211
        // valid column names are table column names or column names prefixed with table name
212 68
        $columnNames = [];
213 68
        $tableName = static::tableName();
214 68
        $quotedTableName = $db->quoteTableName($tableName);
215
216 68
        foreach (static::getTableSchema()->getColumnNames() as $columnName) {
217 68
            $columnNames[] = $columnName;
218 68
            $columnNames[] = $db->quoteColumnName($columnName);
219 68
            $columnNames[] = "$tableName.$columnName";
220 68
            $columnNames[] = $db->quoteSql("$quotedTableName.[[$columnName]]");
221
        }
222 68
        foreach ($condition as $key => $value) {
223 68
            if (is_string($key) && !in_array($db->quoteSql($key), $columnNames, true)) {
224 12
                throw new InvalidArgumentException('Key "' . $key . '" is not a column name and can not be used as a filter');
225
            }
226 56
            $result[$key] = is_array($value) ? array_values($value) : $value;
227
        }
228
229 56
        return $result;
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235 29
    public function refresh()
236
    {
237 29
        $query = static::find();
238 29
        $tableName = key($query->getTablesUsedInFrom());
239 29
        $pk = [];
240
        // disambiguate column names in case ActiveQuery adds a JOIN
241 29
        foreach ($this->getPrimaryKey(true) as $key => $value) {
242 29
            $pk[$tableName . '.' . $key] = $value;
243
        }
244 29
        $query->where($pk);
245
246
        /* @var $record BaseActiveRecord */
247 29
        $record = $query->one();
248 29
        return $this->refreshInternal($record);
249
    }
250
251
    /**
252
     * Updates the whole table using the provided attribute values and conditions.
253
     *
254
     * For example, to change the status to be 1 for all customers whose status is 2:
255
     *
256
     * ```php
257
     * Customer::updateAll(['status' => 1], 'status = 2');
258
     * ```
259
     *
260
     * > Warning: If you do not specify any condition, this method will update **all** rows in the table.
261
     *
262
     * Note that this method will not trigger any events. If you need [[EVENT_BEFORE_UPDATE]] or
263
     * [[EVENT_AFTER_UPDATE]] to be triggered, you need to [[find()|find]] the models first and then
264
     * call [[update()]] on each of them. For example an equivalent of the example above would be:
265
     *
266
     * ```php
267
     * $models = Customer::find()->where('status = 2')->all();
268
     * foreach ($models as $model) {
269
     *     $model->status = 1;
270
     *     $model->update(false); // skipping validation as no user input is involved
271
     * }
272
     * ```
273
     *
274
     * For a large set of models you might consider using [[ActiveQuery::each()]] to keep memory usage within limits.
275
     *
276
     * @param array $attributes attribute values (name-value pairs) to be saved into the table
277
     * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
278
     * Please refer to [[Query::where()]] on how to specify this parameter.
279
     * @param array $params the parameters (name => value) to be bound to the query.
280
     * @return int the number of rows updated
281
     */
282 52
    public static function updateAll($attributes, $condition = '', $params = [])
283
    {
284 52
        $command = static::getDb()->createCommand();
285 52
        $command->update(static::tableName(), $attributes, $condition, $params);
286
287 52
        return $command->execute();
288
    }
289
290
    /**
291
     * Updates the whole table using the provided counter changes and conditions.
292
     *
293
     * For example, to increment all customers' age by 1,
294
     *
295
     * ```php
296
     * Customer::updateAllCounters(['age' => 1]);
297
     * ```
298
     *
299
     * Note that this method will not trigger any events.
300
     *
301
     * @param array $counters the counters to be updated (attribute name => increment value).
302
     * Use negative values if you want to decrement the counters.
303
     * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
304
     * Please refer to [[Query::where()]] on how to specify this parameter.
305
     * @param array $params the parameters (name => value) to be bound to the query.
306
     * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method.
307
     * @return int the number of rows updated
308
     */
309 6
    public static function updateAllCounters($counters, $condition = '', $params = [])
310
    {
311 6
        $n = 0;
312 6
        foreach ($counters as $name => $value) {
313 6
            $counters[$name] = new Expression("[[$name]]+:bp{$n}", [":bp{$n}" => $value]);
314 6
            $n++;
315
        }
316 6
        $command = static::getDb()->createCommand();
317 6
        $command->update(static::tableName(), $counters, $condition, $params);
318
319 6
        return $command->execute();
320
    }
321
322
    /**
323
     * Deletes rows in the table using the provided conditions.
324
     *
325
     * For example, to delete all customers whose status is 3:
326
     *
327
     * ```php
328
     * Customer::deleteAll('status = 3');
329
     * ```
330
     *
331
     * > Warning: If you do not specify any condition, this method will delete **all** rows in the table.
332
     *
333
     * Note that this method will not trigger any events. If you need [[EVENT_BEFORE_DELETE]] or
334
     * [[EVENT_AFTER_DELETE]] to be triggered, you need to [[find()|find]] the models first and then
335
     * call [[delete()]] on each of them. For example an equivalent of the example above would be:
336
     *
337
     * ```php
338
     * $models = Customer::find()->where('status = 3')->all();
339
     * foreach ($models as $model) {
340
     *     $model->delete();
341
     * }
342
     * ```
343
     *
344
     * For a large set of models you might consider using [[ActiveQuery::each()]] to keep memory usage within limits.
345
     *
346
     * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL.
347
     * Please refer to [[Query::where()]] on how to specify this parameter.
348
     * @param array $params the parameters (name => value) to be bound to the query.
349
     * @return int the number of rows deleted
350
     */
351 32
    public static function deleteAll($condition = null, $params = [])
352
    {
353 32
        $command = static::getDb()->createCommand();
354 32
        $command->delete(static::tableName(), $condition, $params);
0 ignored issues
show
Bug introduced by
It seems like $condition defined by parameter $condition on line 351 can also be of type null; however, yii\db\Command::delete() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
355
356 32
        return $command->execute();
357
    }
358
359
    /**
360
     * {@inheritdoc}
361
     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
362
     */
363 314
    public static function find()
364
    {
365 314
        return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
0 ignored issues
show
Deprecated Code introduced by
The method yii\base\BaseObject::className() has been deprecated with message: since 2.0.14. On PHP >=5.5, use `::class` instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
366
    }
367
368
    /**
369
     * Declares the name of the database table associated with this AR class.
370
     * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]
371
     * with prefix [[Connection::tablePrefix]]. For example if [[Connection::tablePrefix]] is `tbl_`,
372
     * `Customer` becomes `tbl_customer`, and `OrderItem` becomes `tbl_order_item`. You may override this method
373
     * if the table is not named after this convention.
374
     * @return string the table name
375
     */
376 15
    public static function tableName()
377
    {
378 15
        return '{{%' . Inflector::camel2id(StringHelper::basename(get_called_class()), '_') . '}}';
379
    }
380
381
    /**
382
     * Returns the schema information of the DB table associated with this AR class.
383
     * @return TableSchema the schema information of the DB table associated with this AR class.
384
     * @throws InvalidConfigException if the table for the AR class does not exist.
385
     */
386 450
    public static function getTableSchema()
387
    {
388 450
        $tableSchema = static::getDb()
389 450
            ->getSchema()
390 450
            ->getTableSchema(static::tableName());
391
392 450
        if ($tableSchema === null) {
393
            throw new InvalidConfigException('The table does not exist: ' . static::tableName());
394
        }
395
396 450
        return $tableSchema;
397
    }
398
399
    /**
400
     * Returns the primary key name(s) for this AR class.
401
     * The default implementation will return the primary key(s) as declared
402
     * in the DB table that is associated with this AR class.
403
     *
404
     * If the DB table does not declare any primary key, you should override
405
     * this method to return the attributes that you want to use as primary keys
406
     * for this AR class.
407
     *
408
     * Note that an array should be returned even for a table with single primary key.
409
     *
410
     * @return string[] the primary keys of the associated database table.
411
     */
412 257
    public static function primaryKey()
413
    {
414 257
        return static::getTableSchema()->primaryKey;
415
    }
416
417
    /**
418
     * Returns the list of all attribute names of the model.
419
     * The default implementation will return all column names of the table associated with this AR class.
420
     * @return array list of attribute names.
421
     */
422 420
    public function attributes()
423
    {
424 420
        return array_keys(static::getTableSchema()->columns);
425
    }
426
427
    /**
428
     * Declares which DB operations should be performed within a transaction in different scenarios.
429
     * The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]],
430
     * which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively.
431
     * By default, these methods are NOT enclosed in a DB transaction.
432
     *
433
     * In some scenarios, to ensure data consistency, you may want to enclose some or all of them
434
     * in transactions. You can do so by overriding this method and returning the operations
435
     * that need to be transactional. For example,
436
     *
437
     * ```php
438
     * return [
439
     *     'admin' => self::OP_INSERT,
440
     *     'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
441
     *     // the above is equivalent to the following:
442
     *     // 'api' => self::OP_ALL,
443
     *
444
     * ];
445
     * ```
446
     *
447
     * The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]])
448
     * should be done in a transaction; and in the "api" scenario, all the operations should be done
449
     * in a transaction.
450
     *
451
     * @return array the declarations of transactional operations. The array keys are scenarios names,
452
     * and the array values are the corresponding transaction operations.
453
     */
454 114
    public function transactions()
455
    {
456 114
        return [];
457
    }
458
459
    /**
460
     * {@inheritdoc}
461
     */
462 328
    public static function populateRecord($record, $row)
463
    {
464 328
        $columns = static::getTableSchema()->columns;
465 328
        foreach ($row as $name => $value) {
466 328
            if (isset($columns[$name])) {
467 328
                $row[$name] = $columns[$name]->phpTypecast($value);
468
            }
469
        }
470 328
        parent::populateRecord($record, $row);
471 328
    }
472
473
    /**
474
     * Inserts a row into the associated database table using the attribute values of this record.
475
     *
476
     * This method performs the following steps in order:
477
     *
478
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
479
     *    returns `false`, the rest of the steps will be skipped;
480
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
481
     *    failed, the rest of the steps will be skipped;
482
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
483
     *    the rest of the steps will be skipped;
484
     * 4. insert the record into database. If this fails, it will skip the rest of the steps;
485
     * 5. call [[afterSave()]];
486
     *
487
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
488
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_INSERT]], and [[EVENT_AFTER_INSERT]]
489
     * will be raised by the corresponding methods.
490
     *
491
     * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database.
492
     *
493
     * If the table's primary key is auto-incremental and is `null` during insertion,
494
     * it will be populated with the actual value after insertion.
495
     *
496
     * For example, to insert a customer record:
497
     *
498
     * ```php
499
     * $customer = new Customer;
500
     * $customer->name = $name;
501
     * $customer->email = $email;
502
     * $customer->insert();
503
     * ```
504
     *
505
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
506
     * before saving the record. Defaults to `true`. If the validation fails, the record
507
     * will not be saved to the database and this method will return `false`.
508
     * @param array $attributes list of attributes that need to be saved. Defaults to `null`,
509
     * meaning all attributes that are loaded from DB will be saved.
510
     * @return bool whether the attributes are valid and the record is inserted successfully.
511
     * @throws \Exception|\Throwable in case insert failed.
512
     */
513 98
    public function insert($runValidation = true, $attributes = null)
514
    {
515 98
        if ($runValidation && !$this->validate($attributes)) {
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 513 can also be of type array; however, yii\base\Model::validate() does only seem to accept array<integer,string>|string|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
516
            Yii::info('Model not inserted due to validation error.', __METHOD__);
517
            return false;
518
        }
519
520 98
        if (!$this->isTransactional(self::OP_INSERT)) {
521 98
            return $this->insertInternal($attributes);
522
        }
523
524
        $transaction = static::getDb()->beginTransaction();
525
        try {
526
            $result = $this->insertInternal($attributes);
527
            if ($result === false) {
528
                $transaction->rollBack();
529
            } else {
530
                $transaction->commit();
531
            }
532
533
            return $result;
534
        } catch (\Exception $e) {
535
            $transaction->rollBack();
536
            throw $e;
537
        } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
538
            $transaction->rollBack();
539
            throw $e;
540
        }
541
    }
542
543
    /**
544
     * Inserts an ActiveRecord into DB without considering transaction.
545
     * @param array $attributes list of attributes that need to be saved. Defaults to `null`,
546
     * meaning all attributes that are loaded from DB will be saved.
547
     * @return bool whether the record is inserted successfully.
548
     */
549 98
    protected function insertInternal($attributes = null)
550
    {
551 98
        if (!$this->beforeSave(true)) {
552
            return false;
553
        }
554 98
        $values = $this->getDirtyAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 549 can also be of type array; however, yii\db\BaseActiveRecord::getDirtyAttributes() does only seem to accept array<integer,string>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
555 98
        if (($primaryKeys = static::getDb()->schema->insert(static::tableName(), $values)) === false) {
556
            return false;
557
        }
558 98
        foreach ($primaryKeys as $name => $value) {
559 87
            $id = static::getTableSchema()->columns[$name]->phpTypecast($value);
560 87
            $this->setAttribute($name, $id);
561 87
            $values[$name] = $id;
562
        }
563
564 98
        $changedAttributes = array_fill_keys(array_keys($values), null);
565 98
        $this->setOldAttributes($values);
566 98
        $this->afterSave(true, $changedAttributes);
567
568 98
        return true;
569
    }
570
571
    /**
572
     * Saves the changes to this active record into the associated database table.
573
     *
574
     * This method performs the following steps in order:
575
     *
576
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
577
     *    returns `false`, the rest of the steps will be skipped;
578
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
579
     *    failed, the rest of the steps will be skipped;
580
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
581
     *    the rest of the steps will be skipped;
582
     * 4. save the record into database. If this fails, it will skip the rest of the steps;
583
     * 5. call [[afterSave()]];
584
     *
585
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
586
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]]
587
     * will be raised by the corresponding methods.
588
     *
589
     * Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
590
     *
591
     * For example, to update a customer record:
592
     *
593
     * ```php
594
     * $customer = Customer::findOne($id);
595
     * $customer->name = $name;
596
     * $customer->email = $email;
597
     * $customer->update();
598
     * ```
599
     *
600
     * Note that it is possible the update does not affect any row in the table.
601
     * In this case, this method will return 0. For this reason, you should use the following
602
     * code to check if update() is successful or not:
603
     *
604
     * ```php
605
     * if ($customer->update() !== false) {
606
     *     // update successful
607
     * } else {
608
     *     // update failed
609
     * }
610
     * ```
611
     *
612
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
613
     * before saving the record. Defaults to `true`. If the validation fails, the record
614
     * will not be saved to the database and this method will return `false`.
615
     * @param array $attributeNames list of attributes that need to be saved. Defaults to `null`,
616
     * meaning all attributes that are loaded from DB will be saved.
617
     * @return int|false the number of rows affected, or false if validation fails
618
     * or [[beforeSave()]] stops the updating process.
619
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
620
     * being updated is outdated.
621
     * @throws \Exception|\Throwable in case update failed.
622
     */
623 40
    public function update($runValidation = true, $attributeNames = null)
624
    {
625 40
        if ($runValidation && !$this->validate($attributeNames)) {
0 ignored issues
show
Bug introduced by
It seems like $attributeNames defined by parameter $attributeNames on line 623 can also be of type array; however, yii\base\Model::validate() does only seem to accept array<integer,string>|string|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
626
            Yii::info('Model not updated due to validation error.', __METHOD__);
627
            return false;
628
        }
629
630 40
        if (!$this->isTransactional(self::OP_UPDATE)) {
631 40
            return $this->updateInternal($attributeNames);
632
        }
633
634
        $transaction = static::getDb()->beginTransaction();
635
        try {
636
            $result = $this->updateInternal($attributeNames);
637
            if ($result === false) {
638
                $transaction->rollBack();
639
            } else {
640
                $transaction->commit();
641
            }
642
643
            return $result;
644
        } catch (\Exception $e) {
645
            $transaction->rollBack();
646
            throw $e;
647
        } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
648
            $transaction->rollBack();
649
            throw $e;
650
        }
651
    }
652
653
    /**
654
     * Deletes the table row corresponding to this active record.
655
     *
656
     * This method performs the following steps in order:
657
     *
658
     * 1. call [[beforeDelete()]]. If the method returns `false`, it will skip the
659
     *    rest of the steps;
660
     * 2. delete the record from the database;
661
     * 3. call [[afterDelete()]].
662
     *
663
     * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
664
     * will be raised by the corresponding methods.
665
     *
666
     * @return int|false the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
667
     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
668
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
669
     * being deleted is outdated.
670
     * @throws \Exception|\Throwable in case delete failed.
671
     */
672 7
    public function delete()
673
    {
674 7
        if (!$this->isTransactional(self::OP_DELETE)) {
675 7
            return $this->deleteInternal();
676
        }
677
678
        $transaction = static::getDb()->beginTransaction();
679
        try {
680
            $result = $this->deleteInternal();
681
            if ($result === false) {
682
                $transaction->rollBack();
683
            } else {
684
                $transaction->commit();
685
            }
686
687
            return $result;
688
        } catch (\Exception $e) {
689
            $transaction->rollBack();
690
            throw $e;
691
        } catch (\Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
692
            $transaction->rollBack();
693
            throw $e;
694
        }
695
    }
696
697
    /**
698
     * Deletes an ActiveRecord without considering transaction.
699
     * @return int|false the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
700
     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
701
     * @throws StaleObjectException
702
     */
703 7
    protected function deleteInternal()
704
    {
705 7
        if (!$this->beforeDelete()) {
706
            return false;
707
        }
708
709
        // we do not check the return value of deleteAll() because it's possible
710
        // the record is already deleted in the database and thus the method will return 0
711 7
        $condition = $this->getOldPrimaryKey(true);
712 7
        $lock = $this->optimisticLock();
713 7
        if ($lock !== null) {
714 1
            $condition[$lock] = $this->$lock;
715
        }
716 7
        $result = static::deleteAll($condition);
717 7
        if ($lock !== null && !$result) {
718 1
            throw new StaleObjectException('The object being deleted is outdated.');
719
        }
720 7
        $this->setOldAttributes(null);
721 7
        $this->afterDelete();
722
723 7
        return $result;
724
    }
725
726
    /**
727
     * Returns a value indicating whether the given active record is the same as the current one.
728
     * The comparison is made by comparing the table names and the primary key values of the two active records.
729
     * If one of the records [[isNewRecord|is new]] they are also considered not equal.
730
     * @param ActiveRecord $record record to compare to
731
     * @return bool whether the two active records refer to the same row in the same database table.
732
     */
733 3
    public function equals($record)
734
    {
735 3
        if ($this->isNewRecord || $record->isNewRecord) {
736 3
            return false;
737
        }
738
739 3
        return static::tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
740
    }
741
742
    /**
743
     * Returns a value indicating whether the specified operation is transactional in the current [[$scenario]].
744
     * @param int $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]].
745
     * @return bool whether the specified operation is transactional in the current [[scenario]].
746
     */
747 114
    public function isTransactional($operation)
748
    {
749 114
        $scenario = $this->getScenario();
750 114
        $transactions = $this->transactions();
751
752 114
        return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation);
753
    }
754
}
755