ActiveRecord   F
last analyzed

Complexity

Total Complexity 75

Size/Duplication

Total Lines 722
Duplicated Lines 0 %

Test Coverage

Coverage 76.95%

Importance

Changes 0
Metric Value
eloc 178
dl 0
loc 722
ccs 147
cts 191
cp 0.7695
rs 2.4
c 0
b 0
f 0
wmc 75

25 Methods

Rating   Name   Duplication   Size   Complexity  
A loadDefaultValues() 0 13 6
A deleteAll() 0 6 1
A refresh() 0 14 2
A isTransactional() 0 6 2
A findBySql() 0 6 1
A insertInternal() 0 20 4
A filterValidColumnNames() 0 19 3
A getTableSchema() 0 11 2
A populateRecord() 0 9 3
A filterCondition() 0 14 5
B insert() 0 27 7
A find() 0 3 1
B update() 0 27 7
A attributes() 0 3 1
A deleteInternal() 0 21 5
A updateAll() 0 6 1
A delete() 0 22 5
A getDb() 0 3 1
A filterValidAliases() 0 9 1
A tableName() 0 3 1
A updateAllCounters() 0 11 2
A transactions() 0 3 1
B findByCondition() 0 23 8
A equals() 0 7 4
A primaryKey() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ActiveRecord often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ActiveRecord, and based on these observations, apply Extract Interface, too.

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 4
    public function loadDefaultValues($skipIfSet = true)
118
    {
119 4
        $columns = static::getTableSchema()->columns;
120 4
        foreach ($this->attributes() as $name) {
121 4
            if (isset($columns[$name])) {
122 4
                $defaultValue = $columns[$name]->defaultValue;
123 4
                if ($defaultValue !== null && (!$skipIfSet || $this->getAttribute($name) === null)) {
124 4
                    $this->setAttribute($name, $defaultValue);
125
                }
126
            }
127
        }
128
129 4
        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 6
    public static function findBySql($sql, $params = [])
162
    {
163 6
        $query = static::find();
164 6
        $query->sql = $sql;
165
166 6
        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 262
    protected static function findByCondition($condition)
178
    {
179 262
        $query = static::find();
180
181 262
        if (!ArrayHelper::isAssociative($condition) && !$condition instanceof ExpressionInterface) {
182
            // query by primary key
183 173
            $primaryKey = static::primaryKey();
184 173
            if (isset($primaryKey[0])) {
185 173
                $pk = $primaryKey[0];
186 173
                if (!empty($query->join) || !empty($query->joinWith)) {
187 3
                    $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 173
                $condition = [$pk => is_array($condition) ? array_values($condition) : $condition];
191
            } else {
192 173
                throw new InvalidConfigException('"' . get_called_class() . '" must have a primary key.');
193
            }
194 107
        } elseif (is_array($condition)) {
195 107
            $aliases = static::filterValidAliases($query);
196 107
            $condition = static::filterCondition($condition, $aliases);
197
        }
198
199 238
        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 125
    protected static function filterValidAliases(Query $query)
212
    {
213 125
        $tables = $query->getTablesUsedInFrom();
214
215 125
        $aliases = array_diff(array_keys($tables), $tables);
216
217 125
        return array_map(function ($alias) {
218 42
            return preg_replace('/{{(\w+)}}/', '$1', $alias);
219 125
        }, 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 107
    protected static function filterCondition(array $condition, array $aliases = [])
236
    {
237 107
        $result = [];
238 107
        $db = static::getDb();
239 107
        $columnNames = static::filterValidColumnNames($db, $aliases);
240
241 107
        foreach ($condition as $key => $value) {
242 107
            if (is_string($key) && !in_array($db->quoteSql($key), $columnNames, true)) {
243 24
                throw new InvalidArgumentException('Key "' . $key . '" is not a column name and can not be used as a filter');
244
            }
245 83
            $result[$key] = is_array($value) ? array_values($value) : $value;
246
        }
247
248 83
        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 107
    protected static function filterValidColumnNames($db, array $aliases)
262
    {
263 107
        $columnNames = [];
264 107
        $tableName = static::tableName();
265 107
        $quotedTableName = $db->quoteTableName($tableName);
266
267 107
        foreach (static::getTableSchema()->getColumnNames() as $columnName) {
268 107
            $columnNames[] = $columnName;
269 107
            $columnNames[] = $db->quoteColumnName($columnName);
270 107
            $columnNames[] = "$tableName.$columnName";
271 107
            $columnNames[] = $db->quoteSql("$quotedTableName.[[$columnName]]");
272 107
            foreach ($aliases as $tableAlias) {
273 33
                $columnNames[] = "$tableAlias.$columnName";
274 33
                $quotedTableAlias = $db->quoteTableName($tableAlias);
275 33
                $columnNames[] = $db->quoteSql("$quotedTableAlias.[[$columnName]]");
276
            }
277
        }
278
279 107
        return $columnNames;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285 29
    public function refresh()
286
    {
287 29
        $query = static::find();
288 29
        $tableName = key($query->getTablesUsedInFrom());
289 29
        $pk = [];
290
        // disambiguate column names in case ActiveQuery adds a JOIN
291 29
        foreach ($this->getPrimaryKey(true) as $key => $value) {
292 29
            $pk[$tableName . '.' . $key] = $value;
293
        }
294 29
        $query->where($pk);
295
296
        /* @var $record BaseActiveRecord */
297 29
        $record = $query->noCache()->one();
298 29
        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 56
    public static function updateAll($attributes, $condition = '', $params = [])
333
    {
334 56
        $command = static::getDb()->createCommand();
335 56
        $command->update(static::tableName(), $attributes, $condition, $params);
336
337 56
        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 6
    public static function updateAllCounters($counters, $condition = '', $params = [])
360
    {
361 6
        $n = 0;
362 6
        foreach ($counters as $name => $value) {
363 6
            $counters[$name] = new Expression("[[$name]]+:bp{$n}", [":bp{$n}" => $value]);
364 6
            $n++;
365
        }
366 6
        $command = static::getDb()->createCommand();
367 6
        $command->update(static::tableName(), $counters, $condition, $params);
368
369 6
        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 35
    public static function deleteAll($condition = null, $params = [])
402
    {
403 35
        $command = static::getDb()->createCommand();
404 35
        $command->delete(static::tableName(), $condition, $params);
405
406 35
        return $command->execute();
407
    }
408
409
    /**
410
     * {@inheritdoc}
411
     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
412
     */
413 381
    public static function find()
414
    {
415 381
        return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

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

415
        return Yii::createObject(/** @scrutinizer ignore-deprecated */ ActiveQuery::className(), [get_called_class()]);

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

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

Loading history...
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 16
    public static function tableName()
427
    {
428 16
        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 557
    public static function getTableSchema()
437
    {
438 557
        $tableSchema = static::getDb()
439 557
            ->getSchema()
440 557
            ->getTableSchema(static::tableName());
441
442 557
        if ($tableSchema === null) {
443
            throw new InvalidConfigException('The table does not exist: ' . static::tableName());
444
        }
445
446 557
        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 304
    public static function primaryKey()
463
    {
464 304
        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 494
    public function attributes()
473
    {
474 494
        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 126
    public function transactions()
505
    {
506 126
        return [];
507
    }
508
509
    /**
510
     * {@inheritdoc}
511
     */
512 394
    public static function populateRecord($record, $row)
513
    {
514 394
        $columns = static::getTableSchema()->columns;
515 394
        foreach ($row as $name => $value) {
516 394
            if (isset($columns[$name])) {
517 394
                $row[$name] = $columns[$name]->phpTypecast($value);
518
            }
519
        }
520 394
        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 109
    public function insert($runValidation = true, $attributes = null)
564
    {
565 109
        if ($runValidation && !$this->validate($attributes)) {
566
            Yii::info('Model not inserted due to validation error.', __METHOD__);
567
            return false;
568
        }
569
570 109
        if (!$this->isTransactional(self::OP_INSERT)) {
571 109
            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 109
    protected function insertInternal($attributes = null)
600
    {
601 109
        if (!$this->beforeSave(true)) {
602
            return false;
603
        }
604 109
        $values = $this->getDirtyAttributes($attributes);
605 109
        if (($primaryKeys = static::getDb()->schema->insert(static::tableName(), $values)) === false) {
606
            return false;
607
        }
608 109
        foreach ($primaryKeys as $name => $value) {
609 98
            $id = static::getTableSchema()->columns[$name]->phpTypecast($value);
610 98
            $this->setAttribute($name, $id);
611 98
            $values[$name] = $id;
612
        }
613
614 109
        $changedAttributes = array_fill_keys(array_keys($values), null);
615 109
        $this->setOldAttributes($values);
616 109
        $this->afterSave(true, $changedAttributes);
617
618 109
        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 41
    public function update($runValidation = true, $attributeNames = null)
674
    {
675 41
        if ($runValidation && !$this->validate($attributeNames)) {
676
            Yii::info('Model not updated due to validation error.', __METHOD__);
677
            return false;
678
        }
679
680 41
        if (!$this->isTransactional(self::OP_UPDATE)) {
681 41
            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 7
    public function delete()
723
    {
724 7
        if (!$this->isTransactional(self::OP_DELETE)) {
725 7
            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 7
    protected function deleteInternal()
754
    {
755 7
        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 7
        $condition = $this->getOldPrimaryKey(true);
762 7
        $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 7
        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 7
        $result = static::deleteAll($condition);
767 7
        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 7
        $this->setOldAttributes(null);
771 7
        $this->afterDelete();
772
773 7
        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 3
    public function equals($record)
784
    {
785 3
        if ($this->isNewRecord || $record->isNewRecord) {
786 3
            return false;
787
        }
788
789 3
        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 126
    public function isTransactional($operation)
798
    {
799 126
        $scenario = $this->getScenario();
800 126
        $transactions = $this->transactions();
801
802 126
        return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation);
803
    }
804
}
805