Passed
Pull Request — master (#203)
by Wilmer
02:51
created

BaseActiveRecord::unlinkAll()   C

Complexity

Conditions 16
Paths 41

Size

Total Lines 82
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 22.912

Importance

Changes 0
Metric Value
cc 16
eloc 53
nc 41
nop 2
dl 0
loc 82
ccs 28
cts 40
cp 0.7
crap 22.912
rs 5.5666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use ArrayAccess;
8
use Closure;
9
use IteratorAggregate;
10
use ReflectionException;
11
use Throwable;
12
use Yiisoft\Db\Connection\ConnectionInterface;
13
use Yiisoft\Db\Exception\Exception;
14
use Yiisoft\Db\Exception\InvalidArgumentException;
15
use Yiisoft\Db\Exception\InvalidCallException;
16
use Yiisoft\Db\Exception\InvalidConfigException;
17
use Yiisoft\Db\Exception\NotSupportedException;
18
use Yiisoft\Db\Exception\StaleObjectException;
19
use Yiisoft\Db\Expression\Expression;
20
21
use function array_combine;
22
use function array_flip;
23
use function array_intersect;
24
use function array_key_exists;
25
use function array_keys;
26
use function array_search;
27
use function array_values;
28
use function count;
29
use function in_array;
30
use function is_array;
31
use function is_int;
32
use function reset;
33
34
/**
35
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
36
 *
37
 * See {@see ActiveRecord} for a concrete implementation.
38
 *
39
 * @property array $dirtyAttributes The changed attribute values (name-value pairs). This property is read-only.
40
 * @property bool $isNewRecord Whether the record is new and should be inserted when calling {@see save()}.
41
 * @property array $oldAttributes The old attribute values (name-value pairs). Note that the type of this property
42
 * differs in getter and setter. See {@see getOldAttributes()} and {@see setOldAttributes()} for details.
43
 * @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is returned if the
44
 * primary key is composite. A string is returned otherwise (null will be returned if the key value is null).
45
 * This property is read-only.
46
 * @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if the primary
47
 * key is composite. A string is returned otherwise (null will be returned if the key value is null).
48
 * This property is read-only.
49
 * @property array $relatedRecords An array of related records indexed by relation names. This property is read-only.
50
 */
51
abstract class BaseActiveRecord implements ActiveRecordInterface, IteratorAggregate, ArrayAccess
52
{
53
    use BaseActiveRecordTrait;
54
55
    private array $attributes = [];
56
    private array|null $oldAttributes = null;
57
    private array $related = [];
58
    private array $relationsDependencies = [];
59
60
    public function __construct(
61
        protected ConnectionInterface $db,
62
        private ActiveRecordFactory|null $arFactory = null,
63 719
        private string|null $tableName = null
64
    ) {
65 719
        $this->tableName ??= static::tableName();
66 719
    }
67 719
68
    public function delete(): false|int
69
    {
70
        /**
71
         * We do not check the return value of deleteAll() because it's possible the record is already deleted in
72
         * the database and thus the method will return 0
73
         */
74
        $condition = $this->getOldPrimaryKey(true);
75
        $lock = $this->optimisticLock();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $lock is correct as $this->optimisticLock() targeting Yiisoft\ActiveRecord\Bas...ecord::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...
76
77
        if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
78
            $condition[$lock] = $this->$lock;
79
        }
80
81
        $result = $this->deleteAll($condition);
82
83
        if ($lock !== null && !$result) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
84
            throw new StaleObjectException('The object being deleted is outdated.');
85
        }
86
87
        $this->oldAttributes = null;
88 1
89
        return $result;
90 1
    }
91
92
    public function deleteAll(array $condition = [], array $params = []): int
93
    {
94
        $command = $this->db->createCommand();
95
        $command->delete($this->tableName, $condition, $params);
0 ignored issues
show
Bug introduced by
It seems like $this->tableName can also be of type null; however, parameter $table of Yiisoft\Db\Command\CommandInterface::delete() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

95
        $command->delete(/** @scrutinizer ignore-type */ $this->tableName, $condition, $params);
Loading history...
96
97
        return $command->execute();
98
    }
99
100
    public function equals(ActiveRecordInterface $record): bool
101
    {
102
        if ($this->getIsNewRecord() || ($record->getIsNewRecord())) {
103
            return false;
104
        }
105
106
        return $this->tableName === $record->getTableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
107
    }
108
109
    /**
110
     * @return array The default implementation returns the names of the relations that have been populated into this
111 1
     * record.
112
     */
113 1
    public function extraFields(): array
114
    {
115
        $fields = array_keys($this->getRelatedRecords());
116
117
        return array_combine($fields, $fields);
118
    }
119
120
    /**
121
     * @return array The default implementation returns the names of the columns whose values have been populated into
122
     * this record.
123
     */
124
    public function fields(): array
125
    {
126
        $fields = array_keys($this->attributes);
127
128
        return array_combine($fields, $fields);
129
    }
130
131
    public function getAttribute(string $name): mixed
132
    {
133
        return $this->attributes[$name] ?? null;
134
    }
135
136 1
    /**
137
     * Returns attribute values.
138 1
     *
139
     * @param array|null $names List of attributes whose value needs to be returned. Defaults to null, meaning all
140
     * attributes listed in {@see attributes()} will be returned. If it is an array, only the attributes in the array
141
     * will be returned.
142
     * @param array $except List of attributes whose value should NOT be returned.
143
     *
144
     * @return array Attribute values (name => value).
145
     */
146
    public function getAttributes(array $names = null, array $except = []): array
147
    {
148
        $values = [];
149
150
        if ($names === null) {
151
            $names = $this->attributes();
152
        }
153
154
        foreach ($names as $name) {
155
            $values[$name] = $this->$name;
156
        }
157
158
        foreach ($except as $name) {
159
            unset($values[$name]);
160
        }
161
162 41
        return $values;
163
    }
164 41
165
    public function getIsNewRecord(): bool
166
    {
167
        return $this->oldAttributes === null;
168
    }
169
170
    /**
171
     * Returns the old value of the named attribute.
172
     *
173
     * If this record is the result of a query and the attribute is not loaded, `null` will be returned.
174
     *
175
     * @param string $name The attribute name.
176
     *
177
     * @return mixed the old attribute value. `null` if the attribute is not loaded before or does not exist.
178
     *
179
     * {@see hasAttribute()}
180
     */
181
    public function getOldAttribute(string $name): mixed
182
    {
183
        return $this->oldAttributes[$name] ?? null;
184
    }
185
186
    /**
187
     * Returns the attribute values that have been modified since they are loaded or saved most recently.
188
     *
189
     * The comparison of new and old values is made for identical values using `===`.
190
     *
191
     * @param array|null $names The names of the attributes whose values may be returned if they are changed recently.
192
     * If null, {@see attributes()} will be used.
193
     *
194
     * @return array The changed attribute values (name-value pairs).
195
     */
196
    public function getDirtyAttributes(array $names = null): array
197
    {
198 116
        if ($names === null) {
199
            $names = $this->attributes();
200 116
        }
201
202
        $names = array_flip($names);
203
        $attributes = [];
204
205
        if ($this->oldAttributes === null) {
206
            foreach ($this->attributes as $name => $value) {
207
                if (isset($names[$name])) {
208
                    $attributes[$name] = $value;
209
                }
210
            }
211
        } else {
212
            foreach ($this->attributes as $name => $value) {
213
                if (
214
                    isset($names[$name])
215
                    && (!array_key_exists($name, $this->oldAttributes) || $value !== $this->oldAttributes[$name])
216
                ) {
217
                    $attributes[$name] = $value;
218
                }
219
            }
220
        }
221
222
        return $attributes;
223
    }
224
225
    /**
226
     * Returns the old attribute values.
227
     *
228
     * @return array The old attribute values (name-value pairs).
229
     */
230
    public function getOldAttributes(): array
231
    {
232
        return $this->oldAttributes ?? [];
233
    }
234 202
235
    public function getOldPrimaryKey(bool $asArray = false): mixed
236 202
    {
237
        $keys = $this->primaryKey();
238
239
        if (empty($keys)) {
240
            throw new Exception(
241
                static::class . ' does not have a primary key. You should either define a primary key for '
242
                . 'the corresponding table or override the primaryKey() method.'
243
            );
244
        }
245
246
        if (!$asArray && count($keys) === 1) {
247
            return $this->oldAttributes[$keys[0]] ?? null;
248
        }
249
250
        $values = [];
251 248
252
        foreach ($keys as $name) {
253 248
            $values[$name] = $this->oldAttributes[$name] ?? null;
254
        }
255
256
        return $values;
257
    }
258
259
    public function getPrimaryKey(bool $asArray = false): mixed
260
    {
261
        $keys = $this->primaryKey();
262
263
        if (!$asArray && count($keys) === 1) {
264
            return $this->attributes[$keys[0]] ?? null;
265
        }
266
267
        $values = [];
268
269 162
        foreach ($keys as $name) {
270
            $values[$name] = $this->attributes[$name] ?? null;
271 162
        }
272 19
273
        return $values;
274
    }
275 162
276 162
    /**
277
     * Returns all populated related records.
278
     *
279
     * @return array An array of related records indexed by relation names.
280
     *
281
     * {@see getRelation()}
282
     */
283
    public function getRelatedRecords(): array
284
    {
285
        return $this->related;
286
    }
287
288 103
    public function hasAttribute($name): bool
289
    {
290 103
        return isset($this->attributes[$name]) || in_array($name, $this->attributes(), true);
291
    }
292
293
    /**
294
     * Declares a `has-many` relation.
295
     *
296
     * The declaration is returned in terms of a relational {@see ActiveQuery} instance  through which the related
297
     * record can be queried and retrieved back.
298
     *
299
     * A `has-many` relation means that there are multiple related records matching the criteria set by this relation,
300 14
     * e.g., a customer has many orders.
301
     *
302 14
     * For example, to declare the `orders` relation for `Customer` class, we can write the following code in the
303
     * `Customer` class:
304
     *
305
     * ```php
306
     * public function getOrders()
307
     * {
308
     *     return $this->hasMany(Order::className(), ['customer_id' => 'id']);
309
     * }
310
     * ```
311
     *
312 386
     * Note that in the above, the 'customer_id' key in the `$link` parameter refers to an attribute name in the related
313
     * class `Order`, while the 'id' value refers to an attribute name in the current AR class.
314 386
     *
315
     * Call methods declared in {@see ActiveQuery} to further customize the relation.
316
     *
317
     * @param string $class The class name of the related record
318
     * @param array $link The primary-foreign key constraint. The keys of the array refer to the attributes of the
319
     * record associated with the `$class` model, while the values of the array refer to the corresponding attributes in
320
     * **this** AR class.
321
     *
322
     * @return ActiveQueryInterface The relational query object.
323
     */
324
    public function hasMany(string $class, array $link): ActiveQueryInterface
325
    {
326
        return $this->createRelationQuery($class, $link, true);
327
    }
328 116
329
    /**
330 116
     * Declares a `has-one` relation.
331
     *
332
     * The declaration is returned in terms of a relational {@see ActiveQuery} instance through which the related record
333
     * can be queried and retrieved back.
334
     *
335
     * A `has-one` relation means that there is at most one related record matching the criteria set by this relation,
336
     * e.g., a customer has one country.
337
     *
338
     * For example, to declare the `country` relation for `Customer` class, we can write the following code in the
339
     * `Customer` class:
340
     *
341
     * ```php
342
     * public function getCountry()
343
     * {
344
     *     return $this->hasOne(Country::className(), ['id' => 'country_id']);
345 154
     * }
346
     * ```
347 154
     *
348
     * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name in the related class
349 150
     * `Country`, while the 'country_id' value refers to an attribute name in the current AR class.
350 150
     *
351
     * Call methods declared in {@see ActiveQuery} to further customize the relation.
352 10
     *
353
     * @param string $class The class name of the related record.
354 150
     * @param array $link The primary-foreign key constraint. The keys of the array refer to the attributes of the
355
     * record associated with the `$class` model, while the values of the array refer to the corresponding attributes in
356 4
     * **this** AR class.
357
     *
358 150
     * @return ActiveQueryInterface The relational query object.
359
     */
360
    public function hasOne(string $class, array $link): ActiveQueryInterface
361
    {
362
        return $this->createRelationQuery($class, $link, false);
363
    }
364
365 8
    public function instantiateQuery(string $arClass): ActiveQueryInterface
366
    {
367 8
        return new ActiveQuery($arClass, $this->db, $this->arFactory);
368
    }
369
370
    /**
371
     * Returns a value indicating whether the named attribute has been changed.
372
     *
373
     * @param string $name The name of the attribute.
374
     * @param bool $identical Whether the comparison of new and old value is made for identical values using `===`,
375
     * defaults to `true`. Otherwise `==` is used for comparison.
376
     *
377
     * @return bool Whether the attribute has been changed.
378 137
     */
379
    public function isAttributeChanged(string $name, bool $identical = true): bool
380 137
    {
381 137
        if (isset($this->attributes[$name], $this->oldAttributes[$name])) {
382
            if ($identical) {
383
                return $this->attributes[$name] !== $this->oldAttributes[$name];
384
            }
385
386
            return $this->attributes[$name] !== $this->oldAttributes[$name];
387
        }
388
389
        return isset($this->attributes[$name]) || isset($this->oldAttributes[$name]);
390
    }
391
392
    public function isPrimaryKey(array $keys): bool
393
    {
394 24
        $pks = $this->primaryKey();
395
396 24
        if (count($keys) === count($pks)) {
397
            return count(array_intersect($keys, $pks)) === count($pks);
398
        }
399
400
        return false;
401
    }
402
403
    public function isRelationPopulated(string $name): bool
404
    {
405
        return array_key_exists($name, $this->related);
406
    }
407
408
    public function link(string $name, ActiveRecordInterface $arClass, array $extraColumns = []): void
409 8
    {
410
        $viaClass = null;
411 8
        $viaTable = null;
412 4
        $relation = $this->getRelation($name);
413
        $via = $relation->getVia();
414 4
415
        if ($via !== null) {
416 4
            if ($this->getIsNewRecord() || $arClass->getIsNewRecord()) {
417
                throw new InvalidCallException(
418
                    'Unable to link models: the models being linked cannot be newly created.'
419
                );
420
            }
421
422
            if (is_array($via)) {
0 ignored issues
show
introduced by
The condition is_array($via) is always false.
Loading history...
423
                [$viaName, $viaRelation] = $via;
424
                $viaClass = $viaRelation->getARInstance();
425
                /** unset $viaName so that it can be reloaded to reflect the change */
426 6
                unset($this->related[$viaName]);
427
            } else {
428 6
                $viaRelation = $via;
429 6
                $from = $via->getFrom();
430
                $viaTable = reset($from);
431
            }
432
433
            $columns = [];
434
435
            foreach ($viaRelation->getLink() as $a => $b) {
436
                $columns[$a] = $this->$b;
437
            }
438
439
            foreach ($relation->getLink() as $a => $b) {
440 12
                $columns[$b] = $arClass->$a;
441
            }
442 12
443 8
            foreach ($extraColumns as $k => $v) {
444 4
                $columns[$k] = $v;
445
            }
446
447 4
            if (is_array($via)) {
448
                foreach ($columns as $column => $value) {
449
                    $viaClass->$column = $value;
450 4
                }
451
452
                $viaClass->insert();
453
            } else {
454
                $this->db->createCommand()->insert($viaTable, $columns)->execute();
455
            }
456
        } else {
457
            $p1 = $arClass->isPrimaryKey(array_keys($relation->getLink()));
458
            $p2 = $this->isPrimaryKey(array_values($relation->getLink()));
459
460
            if ($p1 && $p2) {
461
                if ($this->getIsNewRecord() && $arClass->getIsNewRecord()) {
462
                    throw new InvalidCallException('Unable to link models: at most one model can be newly created.');
463 149
                }
464
465 149
                if ($this->getIsNewRecord()) {
466 145
                    $this->bindModels(array_flip($relation->getLink()), $this, $arClass);
467
                } else {
468
                    $this->bindModels($relation->getLink(), $arClass, $this);
469 149
                }
470 149
            } elseif ($p1) {
471
                $this->bindModels(array_flip($relation->getLink()), $this, $arClass);
472 149
            } elseif ($p2) {
473 133
                $this->bindModels($relation->getLink(), $arClass, $this);
474 129
            } else {
475 129
                throw new InvalidCallException(
476
                    'Unable to link models: the link defining the relation does not involve any primary key.'
477
                );
478
            }
479 45
        }
480
481 45
        /** update lazily loaded related objects */
482 45
        if (!$relation->getMultiple()) {
483
            $this->related[$name] = $arClass;
484 45
        } elseif (isset($this->related[$name])) {
485
            if ($relation->getIndexBy() !== null) {
486
                if ($relation->getIndexBy() instanceof Closure) {
487
                    $index = $relation->indexBy($arClass::class);
488
                } else {
489 149
                    $index = $arClass->{$relation->getIndexBy()};
490
                }
491
                $this->related[$name][$index] = $arClass;
492
            } else {
493
                $this->related[$name][] = $arClass;
494
            }
495
        }
496
    }
497
498
    /**
499
     * Marks an attribute dirty.
500
     *
501
     * This method may be called to force updating a record when calling {@see update()}, even if there is no change
502
     * being made to the record.
503
     *
504
     * @param string $name The attribute name.
505
     */
506
    public function markAttributeDirty(string $name): void
507
    {
508
        unset($this->oldAttributes[$name]);
509
    }
510
511
    /**
512
     * Returns the name of the column that stores the lock version for implementing optimistic locking.
513
     *
514 141
     * Optimistic locking allows multiple users to access the same record for edits and avoids potential conflicts. In
515
     * case when a user attempts to save the record upon some staled data (because another user has modified the data),
516 141
     * a {@see StaleObjectException} exception will be thrown, and the update or deletion is skipped.
517 125
     *
518
     * Optimistic locking is only supported by {@see update()} and {@see delete()}.
519
     *
520 30
     * To use Optimistic locking:
521
     *
522
     * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
523
     *    Override this method to return the name of this column.
524
     * 2. In the Web form that collects the user input, add a hidden field that stores the lock version of the recording
525
     *    being updated.
526
     * 3. In the controller action that does the data updating, try to catch the {@see StaleObjectException} and
527
     *    implement necessary business logic (e.g. merging the changes, prompting stated data) to resolve the conflict.
528
     *
529
     * @return string|null The column name that stores the lock version of a table row. If `null` is returned (default
530
     * implemented), optimistic locking will not be supported.
531
     */
532
    public function optimisticLock(): string|null
533
    {
534
        return null;
535
    }
536
537
    /**
538
     * Populates an active record object using a row of data from the database/storage.
539
     *
540
     * This is an internal method meant to be called to create active record objects after fetching data from the
541
     * database. It is mainly used by {@see ActiveQuery} to populate the query results into active records.
542
     *
543
     * @param array|object $row Attribute values (name => value).
544
     */
545
    public function populateRecord(array|object $row): void
546
    {
547
        $columns = array_flip($this->attributes());
548
549
        foreach ($row as $name => $value) {
550
            if (isset($columns[$name])) {
551
                $this->attributes[$name] = $value;
552
            } elseif ($this->canSetProperty($name)) {
553
                $this->$name = $value;
554
            }
555
        }
556
557
        $this->oldAttributes = $this->attributes;
558 6
        $this->related = [];
559
        $this->relationsDependencies = [];
560 6
    }
561
562
    public function populateRelation(string $name, array|ActiveRecordInterface|null $records): void
563
    {
564
        foreach ($this->relationsDependencies as &$relationNames) {
565
            unset($relationNames[$name]);
566
        }
567
568
        $this->related[$name] = $records;
569
    }
570
571
    /**
572
     * Repopulates this active record with the latest data.
573
     *
574
     * @return bool Whether the row still exists in the database. If `true`, the latest data will be populated to this
575
     * active record. Otherwise, this record will remain unchanged.
576
     */
577
    public function refresh(): bool
578
    {
579
        $record = $this->instantiateQuery(static::class)->findOne($this->getPrimaryKey(true));
580
581
        return $this->refreshInternal($record);
582 5
    }
583
584 5
    /**
585
     * Saves the current record.
586 5
     *
587 5
     * This method will call {@see insert()} when {@see isNewRecord} is `true`, or {@see update()} when
588 1
     * {@see isNewRecord} is `false`.
589
     *
590 5
     * For example, to save a customer record:
591 5
     *
592
     * ```php
593
     * $customer = new Customer($db);
594
     * $customer->name = $name;
595 5
     * $customer->email = $email;
596
     * $customer->save();
597 5
     * ```
598 4
     *
599
     * @param array|null $attributeNames List of attribute names that need to be saved. Defaults to null, meaning all
600
     * attributes that are loaded from DB will be saved.
601 5
     *
602
     * @throws Exception|StaleObjectException
603 5
     *
604 5
     * @return bool Whether the saving succeeded (i.e. no validation errors occurred).
605
     */
606
    public function save(array $attributeNames = null): bool
607 5
    {
608
        if ($this->getIsNewRecord()) {
609
            return $this->insert($attributeNames);
610
        }
611
612
        return $this->update($attributeNames) !== false;
613
    }
614
615
    public function setAttribute(string $name, mixed $value): void
616
    {
617
        if ($this->hasAttribute($name)) {
618
            if (
619 40
                !empty($this->relationsDependencies[$name])
620
                && (!array_key_exists($name, $this->attributes) || $this->attributes[$name] !== $value)
621 40
            ) {
622
                $this->resetDependentRelations($name);
623 40
            }
624 4
            $this->attributes[$name] = $value;
625
        } else {
626
            throw new InvalidArgumentException(static::class . ' has no attribute named "' . $name . '".');
627 40
        }
628 40
    }
629
630 40
    /**
631 4
     * Sets the attribute values in a massive way.
632 4
     *
633
     * @param array $values Attribute values (name => value) to be assigned to the model.
634
     *
635
     * {@see attributes()}
636
     */
637
    public function setAttributes(array $values): void
638
    {
639 40
        foreach ($values as $name => $value) {
640
            if (in_array($name, $this->attributes(), true)) {
641 40
                $this->$name = $value;
642 4
            }
643
        }
644
    }
645 40
646 4
    /**
647
     * Sets the value indicating whether the record is new.
648
     *
649 40
     * @param bool $value whether the record is new and should be inserted when calling {@see save()}.
650
     *
651 40
     * @see getIsNewRecord()
652 40
     */
653 40
    public function setIsNewRecord(bool $value): void
654
    {
655
        $this->oldAttributes = $value ? null : $this->attributes;
656 40
    }
657
658
    /**
659
     * Sets the old value of the named attribute.
660
     *
661
     * @param string $name The attribute name.
662
     * @param mixed $value The old attribute value.
663
     *
664
     * @throws InvalidArgumentException If the named attribute does not exist.
665
     *
666
     * {@see hasAttribute()}
667
     */
668
    public function setOldAttribute(string $name, mixed $value): void
669
    {
670
        if (isset($this->oldAttributes[$name]) || $this->hasAttribute($name)) {
671
            $this->oldAttributes[$name] = $value;
672
        } else {
673
            throw new InvalidArgumentException(static::class . ' has no attribute named "' . $name . '".');
674
        }
675
    }
676
677
    /**
678
     * Sets the old attribute values.
679
     *
680
     * All existing old attribute values will be discarded.
681 9
     *
682
     * @param array|null $values Old attribute values to be set. If set to `null` this record is considered to be
683 9
     * {@see isNewRecord|new}.
684 9
     */
685 9
    public function setOldAttributes(array $values = null): void
686 4
    {
687
        $this->oldAttributes = $values;
688 5
    }
689
690
    public function update(array $attributeNames = null): false|int
691 9
    {
692
        return $this->updateInternal($attributeNames);
693
    }
694 9
695
    public function updateAll(array $attributes, array|string $condition = [], array $params = []): int
696
    {
697
        $command = $this->db->createCommand();
698
699
        $command->update($this->tableName, $attributes, $condition, $params);
0 ignored issues
show
Bug introduced by
It seems like $this->tableName can also be of type null; however, parameter $table of Yiisoft\Db\Command\CommandInterface::update() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

699
        $command->update(/** @scrutinizer ignore-type */ $this->tableName, $attributes, $condition, $params);
Loading history...
700
701
        return $command->execute();
702
    }
703
704
    /**
705
     * Updates the specified attributes.
706
     *
707
     * This method is a shortcut to {@see update()} when data validation is not needed and only a small set attributes
708
     * need to be updated.
709
     *
710
     * You may specify the attributes to be updated as name list or name-value pairs. If the latter, the corresponding
711
     * attribute values will be modified accordingly.
712 2
     *
713
     * The method will then save the specified attributes into database.
714
     *
715
     * Note that this method will **not** perform data validation and will **not** trigger events.
716
     *
717
     * @param array $attributes The attributes (names or name-value pairs) to be updated.
718 2
     *
719 2
     * @throws Exception
720
     * @throws NotSupportedException
721 2
     *
722
     * @return int The number of rows affected.
723
     */
724
    public function updateAttributes(array $attributes): int
725 2
    {
726
        $attrs = [];
727 2
728
        foreach ($attributes as $name => $value) {
729
            if (is_int($name)) {
730
                $attrs[] = $value;
731 2
            } else {
732
                $this->$name = $value;
733 2
                $attrs[] = $name;
734
            }
735
        }
736
737
        $values = $this->getDirtyAttributes($attrs);
738
739
        if (empty($values) || $this->getIsNewRecord()) {
740
            return 0;
741 153
        }
742
743 153
        $rows = $this->updateAll($values, $this->getOldPrimaryKey(true));
744
745
        foreach ($values as $name => $value) {
746
            $this->oldAttributes[$name] = $this->attributes[$name];
747
        }
748
749
        return $rows;
750
    }
751
752
    /**
753
     * Updates the whole table using the provided counter changes and conditions.
754
     *
755
     * For example, to increment all customers' age by 1,
756
     *
757
     * ```php
758
     * $customer = new Customer($db);
759
     * $customer->updateAllCounters(['age' => 1]);
760
     * ```
761
     *
762
     * Note that this method will not trigger any events.
763
     *
764 4
     * @param array $counters The counters to be updated (attribute name => increment value).
765
     * Use negative values if you want to decrement the counters.
766
     * @param array|string $condition The conditions that will be put in the WHERE part of the UPDATE SQL. Please refer
767 4
     * to {@see Query::where()} on how to specify this parameter.
768
     * @param array $params The parameters (name => value) to be bound to the query.
769 4
     *
770
     * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method.
771
     *
772
     * @throws Exception
773
     * @throws InvalidConfigException
774
     * @throws Throwable
775
     *
776
     * @return int The number of rows updated.
777
     */
778
    public function updateAllCounters(array $counters, $condition = '', array $params = []): int
779
    {
780
        $n = 0;
781 32
782
        foreach ($counters as $name => $value) {
783 32
            $counters[$name] = new Expression("[[$name]]+:bp{$n}", [":bp{$n}" => $value]);
784 5
            $n++;
785
        }
786
787 32
        $command = $this->db->createCommand();
788 32
        $command->update($this->tableName, $counters, $condition, $params);
0 ignored issues
show
Bug introduced by
It seems like $this->tableName can also be of type null; however, parameter $table of Yiisoft\Db\Command\CommandInterface::update() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

788
        $command->update(/** @scrutinizer ignore-type */ $this->tableName, $counters, $condition, $params);
Loading history...
789
790
        return $command->execute();
791 32
    }
792 32
793 32
    /**
794
     * Updates one or several counter columns for the current AR object.
795 32
     *
796
     * Note that this method differs from {@see updateAllCounters()} in that it only saves counters for the current AR
797
     * object.
798
     *
799
     * An example usage is as follows:
800
     *
801
     * ```php
802
     * $post = new Post($db);
803
     * $post->updateCounters(['view_count' => 1]);
804
     * ```
805
     *
806
     * @param array $counters The counters to be updated (attribute name => increment value), use negative values if you
807
     * want to decrement the counters.
808 2
     *
809
     * @throws Exception
810 2
     * @throws NotSupportedException
811 1
     *
812
     * @return bool Whether the saving is successful.
813
     *
814 1
     * {@see updateAllCounters()}
815
     */
816
    public function updateCounters(array $counters): bool
817
    {
818
        if ($this->updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) {
819
            foreach ($counters as $name => $value) {
820
                if (!isset($this->attributes[$name])) {
821
                    $this->attributes[$name] = $value;
822
                } else {
823
                    $this->attributes[$name] += $value;
824
                }
825
826
                $this->oldAttributes[$name] = $this->attributes[$name];
827
            }
828
829
            return true;
830 53
        }
831
832 53
        return false;
833
    }
834 53
835 21
    public function unlink(string $name, ActiveRecordInterface $arClass, bool $delete = false): void
836
    {
837
        $viaClass = null;
838 32
        $viaTable = null;
839
        $relation = $this->getRelation($name);
840 32
841 32
        if ($relation->getVia() !== null) {
842
            if (is_array($relation->getVia())) {
0 ignored issues
show
introduced by
The condition is_array($relation->getVia()) is always false.
Loading history...
843
                [$viaName, $viaRelation] = $relation->getVia();
844 32
                $viaClass = $viaRelation->getARInstance();
845
                unset($this->related[$viaName]);
846
            } else {
847
                $viaRelation = $relation->getVia();
848
                $from = $relation->getVia()->getFrom();
849
                $viaTable = reset($from);
850
            }
851
852
            $columns = [];
853
            foreach ($viaRelation->getLink() as $a => $b) {
854
                $columns[$a] = $this->$b;
855
            }
856
857
            foreach ($relation->getLink() as $a => $b) {
858
                $columns[$b] = $arClass->$a;
859
            }
860
            $nulls = [];
861
862
            foreach (array_keys($columns) as $a) {
863
                $nulls[$a] = null;
864
            }
865
866
            if (is_array($relation->getVia())) {
0 ignored issues
show
introduced by
The condition is_array($relation->getVia()) is always false.
Loading history...
867 60
                if ($delete) {
868
                    $viaClass->deleteAll($columns);
869 60
                } else {
870
                    $viaClass->updateAll($nulls, $columns);
871 60
                }
872 1
            } else {
873 1
                $command = $this->db->createCommand();
874 1
                if ($delete) {
875
                    $command->delete($viaTable, $columns)->execute();
876
                } else {
877
                    $command->update($viaTable, $nulls, $columns)->execute();
878 59
                }
879
            }
880
        } else {
881
            $p1 = $arClass->isPrimaryKey(array_keys($relation->getLink()));
882 59
            $p2 = $this->isPrimaryKey(array_values($relation->getLink()));
883
            if ($p2) {
884 59
                if ($delete) {
885 59
                    $arClass->delete();
886
                } else {
887
                    foreach ($relation->getLink() as $a => $b) {
888 59
                        $arClass->$a = null;
889
                    }
890
                    $arClass->save();
891
                }
892
            } elseif ($p1) {
893
                foreach ($relation->getLink() as $a => $b) {
894
                    /** relation via array valued attribute */
895
                    if (is_array($this->$b)) {
896
                        if (($key = array_search($arClass->$a, $this->$b, false)) !== false) {
897
                            $values = $this->$b;
898
                            unset($values[$key]);
899 445
                            $this->$b = array_values($values);
900
                        }
901 445
                    } else {
902
                        $this->$b = null;
903 445
                    }
904 445
                }
905 445
                $delete ? $this->delete() : $this->save();
906 8
            } else {
907 8
                throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
908
            }
909
        }
910
911 445
        if (!$relation->getMultiple()) {
912 445
            unset($this->related[$name]);
913 445
        } elseif (isset($this->related[$name])) {
914 445
            /** @var ActiveRecordInterface $b */
915
            foreach ($this->related[$name] as $a => $b) {
916 287
                if ($arClass->getPrimaryKey() === $b->getPrimaryKey()) {
917
                    unset($this->related[$name][$a]);
918 287
                }
919 31
            }
920
        }
921
    }
922 256
923
    /**
924
     * Destroys the relationship in current model.
925
     *
926
     * The active record with the foreign key of the relationship will be deleted if `$delete` is `true`. Otherwise, the
927
     * foreign key will be set `null` and the model will be saved without validation.
928
     *
929
     * Note that to destroy the relationship without removing records make sure your keys can be set to null.
930
     *
931
     * @param string $name The case sensitive name of the relationship, e.g. `orders` for a relation defined via
932
     * `getOrders()` method.
933
     * @param bool $delete Whether to delete the model that contains the foreign key.
934
     *
935
     * @throws Exception
936
     * @throws ReflectionException
937
     * @throws StaleObjectException
938
     * @throws Throwable
939
     */
940
    public function unlinkAll(string $name, bool $delete = false): void
941
    {
942
        $viaClass = null;
943
        $viaTable = null;
944
        $relation = $this->getRelation($name);
945
946
        if ($relation->getVia() !== null) {
947
            if (is_array($relation->getVia())) {
0 ignored issues
show
introduced by
The condition is_array($relation->getVia()) is always false.
Loading history...
948 9
                /* @var $viaRelation ActiveQuery */
949
                [$viaName, $viaRelation] = $relation->getVia();
950 9
                $viaClass = $viaRelation->getARInstance();
951
                unset($this->related[$viaName]);
952 9
            } else {
953 5
                $viaRelation = $relation->getVia();
954
                $from = $relation->getVia()->getFrom();
955
                $viaTable = reset($from);
956
            }
957
958
            $condition = [];
959 5
            $nulls = [];
960
961 5
            foreach ($viaRelation->getLink() as $a => $b) {
962 5
                $nulls[$a] = null;
963
                $condition[$a] = $this->$b;
964 5
            }
965
966
            if (!empty($viaRelation->getWhere())) {
967
                $condition = ['and', $condition, $viaRelation->getWhere()];
968
            }
969
970
            if (!empty($viaRelation->getOn())) {
971 5
                $condition = ['and', $condition, $viaRelation->getOn()];
972
            }
973 5
974 5
            if (is_array($relation->getVia())) {
0 ignored issues
show
introduced by
The condition is_array($relation->getVia()) is always false.
Loading history...
975
                if ($delete) {
976
                    $viaClass->deleteAll($condition);
977 5
                } else {
978 5
                    $viaClass->updateAll($nulls, $condition);
979
                }
980
            } else {
981 5
                $command = $this->db->createCommand();
982 5
                if ($delete) {
983
                    $command->delete($viaTable, $condition)->execute();
984
                } else {
985 5
                    $command->update($viaTable, $nulls, $condition)->execute();
986 5
                }
987 5
            }
988
        } else {
989
            $relatedModel = $relation->getARInstance();
990 5
991
            $link = $relation->getLink();
992
            if (!$delete && count($link) === 1 && is_array($this->{$b = reset($link)})) {
993 5
                /** relation via array valued attribute */
994
                $this->$b = [];
995
                $this->save();
996 9
            } else {
997 9
                $nulls = [];
998
                $condition = [];
999 9
1000
                foreach ($relation->getLink() as $a => $b) {
1001
                    $nulls[$a] = null;
1002
                    $condition[$a] = $this->$b;
1003
                }
1004
1005
                if (!empty($relation->getWhere())) {
1006
                    $condition = ['and', $condition, $relation->getWhere()];
1007
                }
1008
1009 9
                if (!empty($relation->getOn())) {
1010 5
                    $condition = ['and', $condition, $relation->getOn()];
1011 4
                }
1012 4
1013
                if ($delete) {
1014
                    $relatedModel->deleteAll($condition);
1015
                } else {
1016
                    $relatedModel->updateAll($nulls, $condition);
1017
                }
1018
            }
1019
        }
1020
1021 9
        unset($this->related[$name]);
1022 5
    }
1023 9
1024 9
    /**
1025 4
     * Sets relation dependencies for a property.
1026
     *
1027
     * @param string $name property name.
1028 4
     * @param ActiveQuery $relation relation instance.
1029
     * @param string|null $viaRelationName intermediate relation.
1030 4
     */
1031
    private function setRelationDependencies(
1032 5
        string $name,
1033
        ActiveQuery $relation,
1034
        string $viaRelationName = null
1035 9
    ): void {
1036
        $via = $relation->getVia();
1037
1038
        if (empty($via) && $relation->getLink()) {
1039
            foreach ($relation->getLink() as $attribute) {
1040
                $this->relationsDependencies[$attribute][$name] = $name;
1041
                if ($viaRelationName !== null) {
1042
                    $this->relationsDependencies[$attribute][] = $viaRelationName;
1043
                }
1044
            }
1045
        } elseif ($via instanceof ActiveQueryInterface) {
1046
            $this->setRelationDependencies($name, $via);
1047
        } elseif (is_array($via)) {
1048
            [$viaRelationName, $viaQuery] = $via;
1049
            $this->setRelationDependencies($name, $viaQuery, $viaRelationName);
1050
        }
1051
    }
1052
1053 5
    /**
1054
     * Creates a query instance for `has-one` or `has-many` relation.
1055 5
     *
1056
     * @param string $arClass The class name of the related record.
1057 5
     * @param array $link The primary-foreign key constraint.
1058 5
     * @param bool $multiple Whether this query represents a relation to more than one record.
1059
     *
1060 5
     * @return ActiveQueryInterface The relational query object.
1061 5
1062 5
     * {@see hasOne()}
1063
     * {@see hasMany()}
1064 4
     */
1065 4
    protected function createRelationQuery(string $arClass, array $link, bool $multiple): ActiveQueryInterface
1066 4
    {
1067
        return $this->instantiateQuery($arClass)->primaryModel($this)->link($link)->multiple($multiple);
1068
    }
1069 5
1070 5
    /**
1071 5
     * Repopulates this active record with the latest data from a newly fetched instance.
1072
     *
1073
     * @param ActiveRecord|array|null $record The record to take attributes from.
1074 5
     *
1075 5
     * @return bool Whether refresh was successful.
1076
     *
1077 5
     * {@see refresh()}
1078
     */
1079 5
    protected function refreshInternal(array|ActiveRecord $record = null): bool
1080 5
    {
1081
        if ($record === null || $record === []) {
1082
            return false;
1083 5
        }
1084
1085 5
        foreach ($this->attributes() as $name) {
1086 5
            $this->attributes[$name] = $record->attributes[$name] ?? null;
1087
        }
1088 5
1089
        $this->oldAttributes = $record->oldAttributes;
1090
        $this->related = [];
1091
        $this->relationsDependencies = [];
1092 4
1093 4
        return true;
1094
    }
1095
1096 5
    /**
1097
     * {@see update()}
1098
     *
1099
     * @param array|null $attributes Attributes to update.
1100 5
     *
1101 5
     * @throws Exception
1102 5
     * @throws NotSupportedException
1103 5
     * @throws StaleObjectException
1104 5
     *
1105
     * @return int The number of rows affected.
1106 5
     */
1107 5
    protected function updateInternal(array $attributes = null): int
1108
    {
1109 5
        $values = $this->getDirtyAttributes($attributes);
1110
1111
        if (empty($values)) {
1112
            return 0;
1113
        }
1114
1115
        $condition = $this->getOldPrimaryKey(true);
1116
        $lock = $this->optimisticLock();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $lock is correct as $this->optimisticLock() targeting Yiisoft\ActiveRecord\Bas...ecord::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...
1117
1118
        if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
1119
            $values[$lock] = $this->$lock + 1;
1120
            $condition[$lock] = $this->$lock;
1121
        }
1122
1123
        /**
1124
         * We do not check the return value of updateAll() because it's possible that the UPDATE statement doesn't
1125
         * change anything and thus returns 0.
1126
         */
1127
        $rows = $this->updateAll($values, $condition);
1128
1129
        if ($lock !== null && !$rows) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
1130 5
            throw new StaleObjectException('The object being updated is outdated.');
1131
        }
1132 5
1133
        if (isset($values[$lock])) {
1134 5
            $this->$lock = $values[$lock];
1135 5
        }
1136 5
1137
        $changedAttributes = [];
1138
1139
        foreach ($values as $name => $value) {
1140 5
            $changedAttributes[$name] = $this->oldAttributes[$name] ?? null;
1141
            $this->oldAttributes[$name] = $value;
1142
        }
1143
1144
        return $rows;
1145
    }
1146
1147
    private function bindModels(
1148
        array $link,
1149
        ActiveRecordInterface $foreignModel,
1150
        ActiveRecordInterface $primaryModel
1151
    ): void {
1152
        foreach ($link as $fk => $pk) {
1153
            $value = $primaryModel->$pk;
1154
1155
            if ($value === null) {
1156 22
                throw new InvalidCallException(
1157
                    'Unable to link active record: the primary key of ' . $primaryModel::class . ' is null.'
1158 22
                );
1159
            }
1160 22
1161 8
            /** relation via array valued attribute */
1162
            if (is_array($foreignModel->$fk)) {
1163 4
                $foreignModel->{$fk}[] = $value;
1164 4
            } else {
1165 4
                $foreignModel->{$fk} = $value;
1166
            }
1167 4
        }
1168 4
1169 4
        $foreignModel->save();
1170
    }
1171
1172 8
    /**
1173 8
     * Resets dependent related models checking if their links contain specific attribute.
1174
     *
1175 8
     * @param string $attribute The changed attribute name.
1176 8
     */
1177 8
    private function resetDependentRelations(string $attribute): void
1178
    {
1179
        foreach ($this->relationsDependencies[$attribute] as $relation) {
1180 8
            unset($this->related[$relation]);
1181
        }
1182
1183
        unset($this->relationsDependencies[$attribute]);
1184 8
    }
1185
1186
    public function getTableName(): string
1187
    {
1188 8
        if ($this->tableName === null) {
1189
            throw new InvalidConfigException('The "tableName" property must be set.');
1190 4
        }
1191 4
1192
        return $this->tableName;
1193 4
    }
1194
}
1195