Passed
Pull Request — master (#282)
by Sergei
06:57 queued 04:13
created

BaseActiveRecord::deleteAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 6
ccs 1
cts 1
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
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\Arrays\ArrayableInterface;
13
use Yiisoft\Arrays\ArrayableTrait;
14
use Yiisoft\Db\Connection\ConnectionInterface;
15
use Yiisoft\Db\Exception\Exception;
16
use Yiisoft\Db\Exception\InvalidArgumentException;
17
use Yiisoft\Db\Exception\InvalidCallException;
18
use Yiisoft\Db\Exception\InvalidConfigException;
19
use Yiisoft\Db\Exception\NotSupportedException;
20
use Yiisoft\Db\Exception\StaleObjectException;
21
use Yiisoft\Db\Expression\Expression;
22
use Yiisoft\Db\Helper\DbStringHelper;
23
24
use function array_combine;
25
use function array_diff;
26
use function array_flip;
27
use function array_intersect;
28
use function array_key_exists;
29
use function array_keys;
30
use function array_search;
31
use function array_values;
32
use function count;
33
use function in_array;
34
use function is_array;
35
use function is_int;
36
use function reset;
37
38
/**
39
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
40
 *
41
 * See {@see ActiveRecord} for a concrete implementation.
42
 *
43
 * @template-implements ArrayAccess<int, mixed>
44
 * @template-implements IteratorAggregate<int>
45
 */
46
abstract class BaseActiveRecord implements ActiveRecordInterface, IteratorAggregate, ArrayAccess, ArrayableInterface
47
{
48
    use ArrayableTrait;
49
    use BaseActiveRecordTrait;
50
51
    private array $attributes = [];
52
    private array|null $oldAttributes = null;
53
    private array $related = [];
54
    /** @psalm-var string[][] */
55
    private array $relationsDependencies = [];
56
57
    public function __construct(
58
        protected ConnectionInterface $db,
59
        private ActiveRecordFactory|null $arFactory = null,
60
        private string $tableName = ''
61
    ) {
62
    }
63 719
64
    public function delete(): false|int
65 719
    {
66 719
        /**
67 719
          * We do not check the return value of deleteAll() because it's possible the record is already deleted in
68
          * the database and thus the method will return 0
69
          */
70
        $condition = $this->getOldPrimaryKey(true);
71
72
        $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...
73
74
        if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
75
            $condition[$lock] = $lock;
76
        }
77
78
        $result = $this->deleteAll($condition);
79
80
        if ($lock !== null && !$result) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
81
            throw new StaleObjectException('The object being deleted is outdated.');
82
        }
83
84
        $this->oldAttributes = null;
85
86
        return $result;
87
    }
88 1
89
    public function deleteAll(array $condition = [], array $params = []): int
90 1
    {
91
        $command = $this->db->createCommand();
92
        $command->delete($this->getTableName(), $condition, $params);
93
94
        return $command->execute();
95
    }
96
97
    public function equals(ActiveRecordInterface $record): bool
98
    {
99
        if ($this->getIsNewRecord() || $record->getIsNewRecord()) {
100
            return false;
101
        }
102
103
        return $this->getTableName() === $record->getTableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
104
    }
105
106
    /**
107
     * @return array The default implementation returns the names of the relations that have been populated into this
108
     * record.
109
     */
110
    public function extraFields(): array
111 1
    {
112
        $fields = array_keys($this->getRelatedRecords());
113 1
114
        return array_combine($fields, $fields);
115
    }
116
117
    /**
118
     * @psalm-return array<string, string|Closure>
119
     */
120
    public function fields(): array
121
    {
122
        $fields = array_keys($this->attributes);
123
124
        return array_combine($fields, $fields);
125
    }
126
127
    public function getAttribute(string $name): mixed
128
    {
129
        return $this->attributes[$name] ?? null;
130
    }
131
132
    public function getAttributes(array $names = null, array $except = []): array
133
    {
134
        $values = [];
135
136 1
        if ($names === null) {
137
            $names = $this->attributes();
138 1
        }
139
140
        if ($except !== []) {
141
            $names = array_diff($names, $except);
142
        }
143
144
        foreach ($names as $name) {
145
            $values[$name] = $this->$name;
146
        }
147
148
        return $values;
149
    }
150
151
    public function getIsNewRecord(): bool
152
    {
153
        return $this->oldAttributes === null;
154
    }
155
156
    /**
157
     * Returns the old value of the named attribute.
158
     *
159
     * If this record is the result of a query and the attribute is not loaded, `null` will be returned.
160
     *
161
     * @param string $name The attribute name.
162 41
     *
163
     * @return mixed the old attribute value. `null` if the attribute is not loaded before or does not exist.
164 41
     *
165
     * {@see hasAttribute()}
166
     */
167
    public function getOldAttribute(string $name): mixed
168
    {
169
        return $this->oldAttributes[$name] ?? null;
170
    }
171
172
    /**
173
     * Returns the attribute values that have been modified since they are loaded or saved most recently.
174
     *
175
     * The comparison of new and old values is made for identical values using `===`.
176
     *
177
     * @param array|null $names The names of the attributes whose values may be returned if they are changed recently.
178
     * If null, {@see attributes()} will be used.
179
     *
180
     * @return array The changed attribute values (name-value pairs).
181
     */
182
    public function getDirtyAttributes(array $names = null): array
183
    {
184
        if ($names === null) {
185
            $names = $this->attributes();
186
        }
187
188
        $names = array_flip($names);
189
        $attributes = [];
190
191
        if ($this->oldAttributes === null) {
192
            /**
193
             * @var string $name
194
             * @var mixed $value
195
             */
196
            foreach ($this->attributes as $name => $value) {
197
                if (isset($names[$name])) {
198 116
                    /** @psalm-var mixed */
199
                    $attributes[$name] = $value;
200 116
                }
201
            }
202
        } else {
203
            /**
204
             * @var string $name
205
             * @var mixed $value
206
             */
207
            foreach ($this->attributes as $name => $value) {
208
                if (
209
                    isset($names[$name])
210
                    && (!array_key_exists($name, $this->oldAttributes) || $value !== $this->oldAttributes[$name])
211
                ) {
212
                    /** @psalm-var mixed */
213
                    $attributes[$name] = $value;
214
                }
215
            }
216
        }
217
218
        return $attributes;
219
    }
220
221
    public function getOldAttributes(): array
222
    {
223
        return $this->oldAttributes ?? [];
224
    }
225
226
    /**
227
     * @throws InvalidConfigException
228
     * @throws Exception
229
     */
230
    public function getOldPrimaryKey(bool $asArray = false): mixed
231
    {
232
        $keys = $this->primaryKey();
233
234 202
        if (empty($keys)) {
235
            throw new Exception(
236 202
                static::class . ' does not have a primary key. You should either define a primary key for '
237
                . 'the corresponding table or override the primaryKey() method.'
238
            );
239
        }
240
241
        if ($asArray === false && count($keys) === 1) {
242
            return $this->oldAttributes[$keys[0]] ?? null;
243
        }
244
245
        $values = [];
246
247
        foreach ($keys as $name) {
248
            $values[$name] = $this->oldAttributes[$name] ?? null;
249
        }
250
251 248
        return $values;
252
    }
253 248
254
    public function getPrimaryKey(bool $asArray = false): mixed
255
    {
256
        $keys = $this->primaryKey();
257
258
        if ($asArray === false && count($keys) === 1) {
259
            return $this->{$keys[0]};
260
        }
261
262
        $values = [];
263
264
        foreach ($keys as $name) {
265
            $values[$name] = $this->$name;
266
        }
267
268
        return $values;
269 162
    }
270
271 162
    /**
272 19
     * Returns all populated related records.
273
     *
274
     * @return array An array of related records indexed by relation names.
275 162
     *
276 162
     * {@see getRelation()}
277
     */
278
    public function getRelatedRecords(): array
279
    {
280
        return $this->related;
281
    }
282
283
    public function hasAttribute($name): bool
284
    {
285
        return isset($this->attributes[$name]) || in_array($name, $this->attributes(), true);
286
    }
287
288 103
    /**
289
     * Declares a `has-many` relation.
290 103
     *
291
     * The declaration is returned in terms of a relational {@see ActiveQuery} instance  through which the related
292
     * record can be queried and retrieved back.
293
     *
294
     * A `has-many` relation means that there are multiple related records matching the criteria set by this relation,
295
     * e.g., a customer has many orders.
296
     *
297
     * For example, to declare the `orders` relation for `Customer` class, we can write the following code in the
298
     * `Customer` class:
299
     *
300 14
     * ```php
301
     * public function getOrders()
302 14
     * {
303
     *     return $this->hasMany(Order::className(), ['customer_id' => 'id']);
304
     * }
305
     * ```
306
     *
307
     * Note that in the above, the 'customer_id' key in the `$link` parameter refers to an attribute name in the related
308
     * class `Order`, while the 'id' value refers to an attribute name in the current AR class.
309
     *
310
     * Call methods declared in {@see ActiveQuery} to further customize the relation.
311
     *
312 386
     * @param string $class The class name of the related record
313
     * @param array $link The primary-foreign key constraint. The keys of the array refer to the attributes of the
314 386
     * record associated with the `$class` model, while the values of the array refer to the corresponding attributes in
315
     * **this** AR class.
316
     *
317
     * @return ActiveQueryInterface The relational query object.
318
     */
319
    public function hasMany(string $class, array $link): ActiveQueryInterface
320
    {
321
        return $this->createRelationQuery($class, $link, true);
322
    }
323
324
    /**
325
     * Declares a `has-one` relation.
326
     *
327
     * The declaration is returned in terms of a relational {@see ActiveQuery} instance through which the related record
328 116
     * can be queried and retrieved back.
329
     *
330 116
     * A `has-one` relation means that there is at most one related record matching the criteria set by this relation,
331
     * e.g., a customer has one country.
332
     *
333
     * For example, to declare the `country` relation for `Customer` class, we can write the following code in the
334
     * `Customer` class:
335
     *
336
     * ```php
337
     * public function getCountry()
338
     * {
339
     *     return $this->hasOne(Country::className(), ['id' => 'country_id']);
340
     * }
341
     * ```
342
     *
343
     * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name in the related class
344
     * `Country`, while the 'country_id' value refers to an attribute name in the current AR class.
345 154
     *
346
     * Call methods declared in {@see ActiveQuery} to further customize the relation.
347 154
     *
348
     * @param string $class The class name of the related record.
349 150
     * @param array $link The primary-foreign key constraint. The keys of the array refer to the attributes of the
350 150
     * record associated with the `$class` model, while the values of the array refer to the corresponding attributes in
351
     * **this** AR class.
352 10
     *
353
     * @return ActiveQueryInterface The relational query object.
354 150
     */
355
    public function hasOne(string $class, array $link): ActiveQueryInterface
356 4
    {
357
        return $this->createRelationQuery($class, $link, false);
358 150
    }
359
360
    public function instantiateQuery(string $arClass): ActiveQueryInterface
361
    {
362
        return new ActiveQuery($arClass, $this->db, $this->arFactory);
363
    }
364
365 8
    /**
366
     * Returns a value indicating whether the named attribute has been changed.
367 8
     *
368
     * @param string $name The name of the attribute.
369
     * @param bool $identical Whether the comparison of new and old value is made for identical values using `===`,
370
     * defaults to `true`. Otherwise `==` is used for comparison.
371
     *
372
     * @return bool Whether the attribute has been changed.
373
     */
374
    public function isAttributeChanged(string $name, bool $identical = true): bool
375
    {
376
        if (isset($this->attributes[$name], $this->oldAttributes[$name])) {
377
            if ($identical) {
378 137
                return $this->attributes[$name] !== $this->oldAttributes[$name];
379
            }
380 137
381 137
            return $this->attributes[$name] !== $this->oldAttributes[$name];
382
        }
383
384
        return isset($this->attributes[$name]) || isset($this->oldAttributes[$name]);
385
    }
386
387
    public function isPrimaryKey(array $keys): bool
388
    {
389
        $pks = $this->primaryKey();
390
391
        if (count($keys) === count($pks)) {
392
            return count(array_intersect($keys, $pks)) === count($pks);
393
        }
394 24
395
        return false;
396 24
    }
397
398
    public function isRelationPopulated(string $name): bool
399
    {
400
        return array_key_exists($name, $this->related);
401
    }
402
403
    public function link(string $name, ActiveRecordInterface $arClass, array $extraColumns = []): void
404
    {
405
        $viaClass = null;
406
        $viaTable = null;
407
        $relation = $this->getRelation($name);
408
        $via = $relation?->getVia();
409 8
410
        if ($via !== null) {
411 8
            if ($this->getIsNewRecord() || $arClass->getIsNewRecord()) {
412 4
                throw new InvalidCallException(
413
                    'Unable to link models: the models being linked cannot be newly created.'
414 4
                );
415
            }
416 4
417
            if (is_array($via)) {
418
                [$viaName, $viaRelation] = $via;
419
                /** @psalm-var ActiveQueryInterface $viaRelation */
420
                $viaClass = $viaRelation->getARInstance();
421
                // unset $viaName so that it can be reloaded to reflect the change.
422
                /** @psalm-var string $viaName */
423
                unset($this->related[$viaName]);
424
            }
425
426 6
            if ($via instanceof ActiveQueryInterface) {
427
                $viaRelation = $via;
428 6
                $from = $via->getFrom();
429 6
                /** @psalm-var string $viaTable */
430
                $viaTable = reset($from);
431
            }
432
433
            $columns = [];
434
435
            /** @psalm-var ActiveQueryInterface|null $viaRelation */
436
            $viaLink = $viaRelation?->getLink() ?? [];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaRelation does not seem to be defined for all execution paths leading up to this point.
Loading history...
437
438
            /**
439
             * @psalm-var string $a
440 12
             * @psalm-var string $b
441
             */
442 12
            foreach ($viaLink as $a => $b) {
443 8
                /** @psalm-var mixed */
444 4
                $columns[$a] = $this->$b;
445
            }
446
447 4
            $link = $relation?->getLink() ?? [];
448
449
            /**
450 4
             * @psalm-var string $a
451
             * @psalm-var string $b
452
             */
453
            foreach ($link as $a => $b) {
454
                /** @psalm-var mixed */
455
                $columns[$b] = $arClass->$a;
456
            }
457
458
            /**
459
             * @psalm-var string $k
460
             * @psalm-var mixed $v
461
             */
462
            foreach ($extraColumns as $k => $v) {
463 149
                /** @psalm-var mixed */
464
                $columns[$k] = $v;
465 149
            }
466 145
467
            if ($viaClass instanceof ActiveRecordInterface && is_array($via)) {
468
                /**
469 149
                 * @psalm-var string $column
470 149
                 * @psalm-var mixed $value
471
                 */
472 149
                foreach ($columns as $column => $value) {
473 133
                    $viaClass->$column = $value;
474 129
                }
475 129
476
                $viaClass->insert();
477
            } elseif (is_string($viaTable)) {
478
                $this->db->createCommand()->insert($viaTable, $columns)->execute();
479 45
            }
480
        } elseif ($relation instanceof ActiveQueryInterface) {
0 ignored issues
show
introduced by
$relation is always a sub-type of Yiisoft\ActiveRecord\ActiveQueryInterface.
Loading history...
481 45
            $link = $relation->getLink();
482 45
            $p1 = $arClass->isPrimaryKey(array_keys($link));
483
            $p2 = $this->isPrimaryKey(array_values($link));
484 45
485
            if ($p1 && $p2) {
486
                if ($this->getIsNewRecord() && $arClass->getIsNewRecord()) {
487
                    throw new InvalidCallException('Unable to link models: at most one model can be newly created.');
488
                }
489 149
490
                if ($this->getIsNewRecord()) {
491
                    $this->bindModels(array_flip($link), $this, $arClass);
492
                } else {
493
                    $this->bindModels($link, $arClass, $this);
494
                }
495
            } elseif ($p1) {
496
                $this->bindModels(array_flip($link), $this, $arClass);
497
            } elseif ($p2) {
498
                $this->bindModels($link, $arClass, $this);
499
            } else {
500
                throw new InvalidCallException(
501
                    'Unable to link models: the link defining the relation does not involve any primary key.'
502
                );
503
            }
504
        }
505
506
        // update lazily loaded related objects
507
        if ($relation instanceof ActiveRecordInterface && !$relation->getMultiple()) {
508
            $this->related[$name] = $arClass;
509
        } elseif (isset($this->related[$name])) {
510
            $indexBy = $relation?->getIndexBy();
511
            if ($indexBy !== null) {
512
                if ($indexBy instanceof Closure) {
513
                    $index = $relation?->indexBy($arClass::class);
514 141
                } else {
515
                    $index = $arClass->{$indexBy};
516 141
                }
517 125
518
                if ($index !== null) {
519
                    $this->related[$name][$index] = $arClass;
520 30
                }
521
            } else {
522
                $this->related[$name][] = $arClass;
523
            }
524
        }
525
    }
526
527
    /**
528
     * Marks an attribute dirty.
529
     *
530
     * This method may be called to force updating a record when calling {@see update()}, even if there is no change
531
     * being made to the record.
532
     *
533
     * @param string $name The attribute name.
534
     */
535
    public function markAttributeDirty(string $name): void
536
    {
537
        if ($this->oldAttributes !== null && $name !== '') {
538
            unset($this->oldAttributes[$name]);
539
        }
540
    }
541
542
    /**
543
     * Returns the name of the column that stores the lock version for implementing optimistic locking.
544
     *
545
     * Optimistic locking allows multiple users to access the same record for edits and avoids potential conflicts. In
546
     * case when a user attempts to save the record upon some staled data (because another user has modified the data),
547
     * a {@see StaleObjectException} exception will be thrown, and the update or deletion is skipped.
548
     *
549
     * Optimistic locking is only supported by {@see update()} and {@see delete()}.
550
     *
551
     * To use Optimistic locking:
552
     *
553
     * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
554
     *    Override this method to return the name of this column.
555
     * 2. In the Web form that collects the user input, add a hidden field that stores the lock version of the recording
556
     *    being updated.
557
     * 3. In the controller action that does the data updating, try to catch the {@see StaleObjectException} and
558 6
     *    implement necessary business logic (e.g. merging the changes, prompting stated data) to resolve the conflict.
559
     *
560 6
     * @return string|null The column name that stores the lock version of a table row. If `null` is returned (default
561
     * implemented), optimistic locking will not be supported.
562
     */
563
    public function optimisticLock(): string|null
564
    {
565
        return null;
566
    }
567
568
    /**
569
     * Populates an active record object using a row of data from the database/storage.
570
     *
571
     * This is an internal method meant to be called to create active record objects after fetching data from the
572
     * database. It is mainly used by {@see ActiveQuery} to populate the query results into active records.
573
     *
574
     * @param array|object $row Attribute values (name => value).
575
     */
576
    public function populateRecord(array|object $row): void
577
    {
578
        $columns = array_flip($this->attributes());
579
580
        /**
581
         * @psalm-var string $name
582 5
         * @psalm-var mixed $value
583
         */
584 5
        foreach ($row as $name => $value) {
585
            if (isset($columns[$name])) {
586 5
                $this->attributes[$name] = $value;
587 5
            } elseif ($this->canSetProperty($name)) {
588 1
                $this->$name = $value;
589
            }
590 5
        }
591 5
592
        $this->oldAttributes = $this->attributes;
593
        $this->related = [];
594
        $this->relationsDependencies = [];
595 5
    }
596
597 5
    public function populateRelation(string $name, array|ActiveRecordInterface|null $records): void
598 4
    {
599
        foreach ($this->relationsDependencies as &$relationNames) {
600
            unset($relationNames[$name]);
601 5
        }
602
603 5
        $this->related[$name] = $records;
604 5
    }
605
606
    /**
607 5
     * Repopulates this active record with the latest data.
608
     *
609
     * @return bool Whether the row still exists in the database. If `true`, the latest data will be populated to this
610
     * active record. Otherwise, this record will remain unchanged.
611
     */
612
    public function refresh(): bool
613
    {
614
        $record = $this->instantiateQuery(static::class)->findOne($this->getPrimaryKey(true));
615
616
        return $this->refreshInternal($record);
617
    }
618
619 40
    /**
620
     * Saves the current record.
621 40
     *
622
     * This method will call {@see insert()} when {@see isNewRecord} is `true`, or {@see update()} when
623 40
     * {@see isNewRecord} is `false`.
624 4
     *
625
     * For example, to save a customer record:
626
     *
627 40
     * ```php
628 40
     * $customer = new Customer($db);
629
     * $customer->name = $name;
630 40
     * $customer->email = $email;
631 4
     * $customer->save();
632 4
     * ```
633
     *
634
     * @param array|null $attributeNames List of attribute names that need to be saved. Defaults to null, meaning all
635
     * attributes that are loaded from DB will be saved.
636
     *
637
     * @throws Exception|StaleObjectException
638
     *
639 40
     * @return bool Whether the saving succeeded (i.e. no validation errors occurred).
640
     */
641 40
    public function save(array $attributeNames = null): bool
642 4
    {
643
        if ($this->getIsNewRecord()) {
644
            return $this->insert($attributeNames);
645 40
        }
646 4
647
        $this->update($attributeNames);
648
649 40
        return true;
650
    }
651 40
652 40
    public function setAttribute(string $name, mixed $value): void
653 40
    {
654
        if ($this->hasAttribute($name)) {
655
            if (
656 40
                !empty($this->relationsDependencies[$name])
657
                && (!array_key_exists($name, $this->attributes) || $this->attributes[$name] !== $value)
658
            ) {
659
                $this->resetDependentRelations($name);
660
            }
661
            $this->attributes[$name] = $value;
662
        } else {
663
            throw new InvalidArgumentException(static::class . ' has no attribute named "' . $name . '".');
664
        }
665
    }
666
667
    /**
668
     * Sets the attribute values in a massive way.
669
     *
670
     * @param array $values Attribute values (name => value) to be assigned to the model.
671
     *
672
     * {@see attributes()}
673
     */
674
    public function setAttributes(array $values): void
675
    {
676
        /** @psalm-var mixed $value */
677
        foreach ($values as $name => $value) {
678
            if (in_array($name, $this->attributes(), true)) {
679
                $this->$name = $value;
680
            }
681 9
        }
682
    }
683 9
684 9
    /**
685 9
     * Sets the value indicating whether the record is new.
686 4
     *
687
     * @param bool $value whether the record is new and should be inserted when calling {@see save()}.
688 5
     *
689
     * @see getIsNewRecord()
690
     */
691 9
    public function setIsNewRecord(bool $value): void
692
    {
693
        $this->oldAttributes = $value ? null : $this->attributes;
694 9
    }
695
696
    /**
697
     * Sets the old value of the named attribute.
698
     *
699
     * @param string $name The attribute name.
700
     * @param mixed $value The old attribute value.
701
     *
702
     * @throws InvalidArgumentException If the named attribute does not exist.
703
     *
704
     * {@see hasAttribute()}
705
     */
706
    public function setOldAttribute(string $name, mixed $value): void
707
    {
708
        if (isset($this->oldAttributes[$name]) || $this->hasAttribute($name)) {
709
            $this->oldAttributes[$name] = $value;
710
        } else {
711
            throw new InvalidArgumentException(static::class . ' has no attribute named "' . $name . '".');
712 2
        }
713
    }
714
715
    /**
716
     * Sets the old attribute values.
717
     *
718 2
     * All existing old attribute values will be discarded.
719 2
     *
720
     * @param array|null $values Old attribute values to be set. If set to `null` this record is considered to be
721 2
     * {@see isNewRecord|new}.
722
     */
723
    public function setOldAttributes(array $values = null): void
724
    {
725 2
        $this->oldAttributes = $values;
726
    }
727 2
728
    public function update(array $attributeNames = null): int
729
    {
730
        return $this->updateInternal($attributeNames);
731 2
    }
732
733 2
    public function updateAll(array $attributes, array|string $condition = [], array $params = []): int
734
    {
735
        $command = $this->db->createCommand();
736
737
        $command->update($this->getTableName(), $attributes, $condition, $params);
738
739
        return $command->execute();
740
    }
741 153
742
    public function updateAttributes(array $attributes): int
743 153
    {
744
        $attrs = [];
745
746
        foreach ($attributes as $name => $value) {
747
            if (is_int($name)) {
748
                $attrs[] = $value;
749
            } else {
750
                $this->$name = $value;
751
                $attrs[] = $name;
752
            }
753
        }
754
755
        $values = $this->getDirtyAttributes($attrs);
756
757
        if (empty($values) || $this->getIsNewRecord()) {
758
            return 0;
759
        }
760
761
        $rows = $this->updateAll($values, $this->getOldPrimaryKey(true));
762
763
        foreach ($values as $name => $value) {
764 4
            $this->oldAttributes[$name] = $value;
765
        }
766
767 4
        return $rows;
768
    }
769 4
770
    /**
771
     * Updates the whole table using the provided counter changes and conditions.
772
     *
773
     * For example, to increment all customers' age by 1,
774
     *
775
     * ```php
776
     * $customer = new Customer($db);
777
     * $customer->updateAllCounters(['age' => 1]);
778
     * ```
779
     *
780
     * Note that this method will not trigger any events.
781 32
     *
782
     * @param array $counters The counters to be updated (attribute name => increment value).
783 32
     * Use negative values if you want to decrement the counters.
784 5
     * @param array|string $condition The conditions that will be put in the WHERE part of the UPDATE SQL. Please refer
785
     * to {@see Query::where()} on how to specify this parameter.
786
     * @param array $params The parameters (name => value) to be bound to the query.
787 32
     *
788 32
     * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method.
789
     *
790
     * @throws Exception
791 32
     * @throws InvalidConfigException
792 32
     * @throws Throwable
793 32
     *
794
     * @return int The number of rows updated.
795 32
     */
796
    public function updateAllCounters(array $counters, array|string $condition = '', array $params = []): int
797
    {
798
        $n = 0;
799
800
        /** @psalm-var array<string, int> $counters */
801
        foreach ($counters as $name => $value) {
802
            $counters[$name] = new Expression("[[$name]]+:bp{$n}", [":bp{$n}" => $value]);
803
            $n++;
804
        }
805
806
        $command = $this->db->createCommand();
807
        $command->update($this->getTableName(), $counters, $condition, $params);
808 2
809
        return $command->execute();
810 2
    }
811 1
812
    /**
813
     * Updates one or several counter columns for the current AR object.
814 1
     *
815
     * Note that this method differs from {@see updateAllCounters()} in that it only saves counters for the current AR
816
     * object.
817
     *
818
     * An example usage is as follows:
819
     *
820
     * ```php
821
     * $post = new Post($db);
822
     * $post->updateCounters(['view_count' => 1]);
823
     * ```
824
     *
825
     * @param array $counters The counters to be updated (attribute name => increment value), use negative values if you
826
     * want to decrement the counters.
827
     *
828
     * @psalm-param array<string, int> $counters
829
     *
830 53
     * @throws Exception
831
     * @throws NotSupportedException
832 53
     *
833
     * @return bool Whether the saving is successful.
834 53
     *
835 21
     * {@see updateAllCounters()}
836
     */
837
    public function updateCounters(array $counters): bool
838 32
    {
839
        if ($this->updateAllCounters($counters, $this->getOldPrimaryKey(true)) === 0) {
840 32
            return false;
841 32
        }
842
843
        foreach ($counters as $name => $value) {
844 32
            $value += $this->$name ?? 0;
845
            $this->$name = $value;
846
            $this->oldAttributes[$name] = $value;
847
        }
848
849
        return true;
850
    }
851
852
    public function unlink(string $name, ActiveRecordInterface $arClass, bool $delete = false): void
853
    {
854
        $viaClass = null;
855
        $viaTable = null;
856
        $relation = $this->getRelation($name);
857
        $viaRelation = $relation?->getVia();
858
859
        if ($viaRelation !== null) {
860
            if (is_array($viaRelation)) {
861
                [$viaName, $viaRelation] = $viaRelation;
862
                /** @psalm-var ActiveQueryInterface $viaRelation */
863
                $viaClass = $viaRelation->getARInstance();
864
                /** @psalm-var string $viaName */
865
                unset($this->related[$viaName]);
866
            }
867 60
868
            $columns = [];
869 60
            $nulls = [];
870
871 60
            if ($viaRelation instanceof ActiveQueryInterface) {
872 1
                $from = $viaRelation->getFrom();
873 1
                /** @psalm-var mixed $viaTable */
874 1
                $viaTable = reset($from);
875
876
                foreach ($viaRelation->getLink() as $a => $b) {
877
                    /** @psalm-var mixed */
878 59
                    $columns[$a] = $this->$b;
879
                }
880
881
                $link = $relation?->getLink() ?? [];
882 59
883
                foreach ($link as $a => $b) {
884 59
                    /** @psalm-var mixed */
885 59
                    $columns[$b] = $arClass->$a;
886
                }
887
888 59
                foreach (array_keys($columns) as $a) {
889
                    $nulls[$a] = null;
890
                }
891
892
                if ($viaRelation->getOn() !== null) {
893
                    $columns = ['and', $columns, $viaRelation->getOn()];
894
                }
895
            }
896
897
            if ($viaClass instanceof ActiveRecordInterface && is_array($relation?->getVia())) {
898
                if ($delete) {
899 445
                    $viaClass->deleteAll($columns);
900
                } else {
901 445
                    $viaClass->updateAll($nulls, $columns);
902
                }
903 445
            } elseif (is_string($viaTable)) {
904 445
                $command = $this->db->createCommand();
905 445
                if ($delete) {
906 8
                    $command->delete($viaTable, $columns)->execute();
907 8
                } else {
908
                    $command->update($viaTable, $nulls, $columns)->execute();
909
                }
910
            }
911 445
        } elseif ($relation instanceof ActiveQueryInterface) {
0 ignored issues
show
introduced by
$relation is always a sub-type of Yiisoft\ActiveRecord\ActiveQueryInterface.
Loading history...
912 445
            $p1 = $arClass->isPrimaryKey(array_keys($relation->getLink()));
913 445
            $p2 = $this->isPrimaryKey(array_values($relation->getLink()));
914 445
915
            if ($p2) {
916 287
                if ($delete) {
917
                    $arClass->delete();
918 287
                } else {
919 31
                    foreach ($relation->getLink() as $a => $b) {
920
                        $arClass->$a = null;
921
                    }
922 256
                    $arClass->save();
923
                }
924
            } elseif ($p1) {
925
                foreach ($relation->getLink() as $a => $b) {
926
                    /** @psalm-var mixed $values */
927
                    $values = $this->$b;
928
                    /** relation via array valued attribute */
929
                    if (is_array($values)) {
930
                        if (($key = array_search($arClass->$a, $values, false)) !== false) {
931
                            unset($values[$key]);
932
                            $this->$b = array_values($values);
933
                        }
934
                    } else {
935
                        $this->$b = null;
936
                    }
937
                }
938
                $delete ? $this->delete() : $this->save();
939
            } else {
940
                throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
941
            }
942
        }
943
944
        if ($relation instanceof ActiveQueryInterface && !$relation->getMultiple()) {
945
            unset($this->related[$name]);
946
        } elseif (isset($this->related[$name]) && is_array($this->related[$name])) {
947
            /** @psalm-var array<array-key, ActiveRecordInterface> $related */
948 9
            $related = $this->related[$name];
949
            foreach ($related as $a => $b) {
950 9
                if ($arClass->getPrimaryKey() === $b->getPrimaryKey()) {
951
                    unset($this->related[$name][$a]);
952 9
                }
953 5
            }
954
        }
955
    }
956
957
    /**
958
     * Destroys the relationship in current model.
959 5
     *
960
     * The active record with the foreign key of the relationship will be deleted if `$delete` is `true`. Otherwise, the
961 5
     * foreign key will be set `null` and the model will be saved without validation.
962 5
     *
963
     * Note that to destroy the relationship without removing records make sure your keys can be set to null.
964 5
     *
965
     * @param string $name The case sensitive name of the relationship, e.g. `orders` for a relation defined via
966
     * `getOrders()` method.
967
     * @param bool $delete Whether to delete the model that contains the foreign key.
968
     *
969
     * @throws Exception
970
     * @throws ReflectionException
971 5
     * @throws StaleObjectException
972
     * @throws Throwable
973 5
     */
974 5
    public function unlinkAll(string $name, bool $delete = false): void
975
    {
976
        $viaClass = null;
977 5
        $viaTable = null;
978 5
        $relation = $this->getRelation($name);
979
        $viaRelation = $relation?->getVia();
980
981 5
        if ($relation instanceof ActiveQueryInterface && $viaRelation !== null) {
982 5
            if (is_array($viaRelation)) {
983
                [$viaName, $viaRelation] = $viaRelation;
984
                /** @psalm-var ActiveQueryInterface $viaRelation */
985 5
                $viaClass = $viaRelation->getARInstance();
986 5
                /** @psalm-var string $viaName */
987 5
                unset($this->related[$viaName]);
988
            } else {
989
                $from = $viaRelation->getFrom();
990 5
                /** @psalm-var mixed $viaTable */
991
                $viaTable = reset($from);
992
            }
993 5
994
            $condition = [];
995
            $nulls = [];
996 9
997 9
            if ($viaRelation instanceof ActiveQueryInterface) {
998
                foreach ($viaRelation->getLink() as $a => $b) {
999 9
                    $nulls[$a] = null;
1000
                    /** @psalm-var mixed */
1001
                    $condition[$a] = $this->$b;
1002
                }
1003
1004
                if (!empty($viaRelation->getWhere())) {
1005
                    $condition = ['and', $condition, $viaRelation->getWhere()];
1006
                }
1007
1008
                if (!empty($viaRelation->getOn())) {
1009 9
                    $condition = ['and', $condition, $viaRelation->getOn()];
1010 5
                }
1011 4
            }
1012 4
1013
            if ($viaClass instanceof ActiveRecordInterface && is_array($relation->getVia())) {
1014
                if ($delete) {
1015
                    $viaClass->deleteAll($condition);
1016
                } else {
1017
                    $viaClass->updateAll($nulls, $condition);
1018
                }
1019
            } elseif (is_string($viaTable)) {
1020
                $command = $this->db->createCommand();
1021 9
                if ($delete) {
1022 5
                    $command->delete($viaTable, $condition)->execute();
1023 9
                } else {
1024 9
                    $command->update($viaTable, $nulls, $condition)->execute();
1025 4
                }
1026
            }
1027
        } elseif ($relation instanceof ActiveQueryInterface) {
1028 4
            $relatedModel = $relation->getARInstance();
1029
1030 4
            $link = $relation->getLink();
1031
            if (!$delete && count($link) === 1 && is_array($this->{$b = reset($link)})) {
1032 5
                /** relation via array valued attribute */
1033
                $this->$b = [];
1034
                $this->save();
1035 9
            } else {
1036
                $nulls = [];
1037
                $condition = [];
1038
1039
                foreach ($relation->getLink() as $a => $b) {
1040
                    $nulls[$a] = null;
1041
                    /** @psalm-var mixed */
1042
                    $condition[$a] = $this->$b;
1043
                }
1044
1045
                if (!empty($relation->getWhere())) {
1046
                    $condition = ['and', $condition, $relation->getWhere()];
1047
                }
1048
1049
                if (!empty($relation->getOn())) {
1050
                    $condition = ['and', $condition, $relation->getOn()];
1051
                }
1052
1053 5
                if ($delete) {
1054
                    $relatedModel->deleteAll($condition);
1055 5
                } else {
1056
                    $relatedModel->updateAll($nulls, $condition);
1057 5
                }
1058 5
            }
1059
        }
1060 5
1061 5
        unset($this->related[$name]);
1062 5
    }
1063
1064 4
    /**
1065 4
     * Sets relation dependencies for a property.
1066 4
     *
1067
     * @param string $name property name.
1068
     * @param ActiveQueryInterface $relation relation instance.
1069 5
     * @param string|null $viaRelationName intermediate relation.
1070 5
     */
1071 5
    private function setRelationDependencies(
1072
        string $name,
1073
        ActiveQueryInterface $relation,
1074 5
        string $viaRelationName = null
1075 5
    ): void {
1076
        $via = $relation->getVia();
1077 5
1078
        if (empty($via) && $relation->getLink()) {
1079 5
            foreach ($relation->getLink() as $attribute) {
1080 5
                $this->relationsDependencies[$attribute][$name] = $name;
1081
                if ($viaRelationName !== null) {
1082
                    $this->relationsDependencies[$attribute][] = $viaRelationName;
1083 5
                }
1084
            }
1085 5
        } elseif ($via instanceof ActiveQueryInterface) {
1086 5
            $this->setRelationDependencies($name, $via);
1087
        } elseif (is_array($via)) {
1088 5
            /**
1089
             * @psalm-var string|null $viaRelationName
1090
             * @psalm-var ActiveQueryInterface $viaQuery
1091
             */
1092 4
            [$viaRelationName, $viaQuery] = $via;
1093 4
            $this->setRelationDependencies($name, $viaQuery, $viaRelationName);
1094
        }
1095
    }
1096 5
1097
    /**
1098
     * Creates a query instance for `has-one` or `has-many` relation.
1099
     *
1100 5
     * @param string $arClass The class name of the related record.
1101 5
     * @param array $link The primary-foreign key constraint.
1102 5
     * @param bool $multiple Whether this query represents a relation to more than one record.
1103 5
     *
1104 5
     * @return ActiveQueryInterface The relational query object.
1105
1106 5
     * {@see hasOne()}
1107 5
     * {@see hasMany()}
1108
     */
1109 5
    protected function createRelationQuery(string $arClass, array $link, bool $multiple): ActiveQueryInterface
1110
    {
1111
        return $this->instantiateQuery($arClass)->primaryModel($this)->link($link)->multiple($multiple);
1112
    }
1113
1114
    /**
1115
     * Repopulates this active record with the latest data from a newly fetched instance.
1116
     *
1117
     * @param ActiveRecordInterface|array|null $record The record to take attributes from.
1118
     *
1119
     * @return bool Whether refresh was successful.
1120
     *
1121
     * {@see refresh()}
1122
     */
1123
    protected function refreshInternal(array|ActiveRecordInterface $record = null): bool
1124
    {
1125
        if ($record === null || is_array($record)) {
0 ignored issues
show
introduced by
The condition is_array($record) is always true.
Loading history...
1126
            return false;
1127
        }
1128
1129
        foreach ($this->attributes() as $name) {
1130 5
            $this->attributes[$name] = $record->getAttribute($name);
1131
        }
1132 5
1133
        $this->oldAttributes = $record->getOldAttributes();
1134 5
        $this->related = [];
1135 5
        $this->relationsDependencies = [];
1136 5
1137
        return true;
1138
    }
1139
1140 5
    /**
1141
     * {@see update()}
1142
     *
1143
     * @param array|null $attributes Attributes to update.
1144
     *
1145
     * @throws Exception
1146
     * @throws NotSupportedException
1147
     * @throws StaleObjectException
1148
     *
1149
     * @return int The number of rows affected.
1150
     */
1151
    protected function updateInternal(array $attributes = null): int
1152
    {
1153
        $values = $this->getDirtyAttributes($attributes);
1154
1155
        if (empty($values)) {
1156 22
            return 0;
1157
        }
1158 22
1159
        $condition = $this->getOldPrimaryKey(true);
1160 22
        $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...
1161 8
1162
        if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
1163 4
            $lockValue = $this->$lock;
1164 4
1165 4
            $condition[$lock] = $lockValue;
1166
            $values[$lock] = ++$lockValue;
1167 4
1168 4
            $rows = $this->updateAll($values, $condition);
1169 4
1170
            if ($rows === 0) {
1171
                throw new StaleObjectException('The object being updated is outdated.');
1172 8
            }
1173 8
1174
            $this->$lock = $lockValue;
1175 8
        } else {
1176 8
            $rows = $this->updateAll($values, $condition);
1177 8
        }
1178
1179
        foreach ($values as $name => $value) {
1180 8
            $this->oldAttributes[$name] = $value;
1181
        }
1182
1183
        return $rows;
1184 8
    }
1185
1186
    private function bindModels(
1187
        array $link,
1188 8
        ActiveRecordInterface $foreignModel,
1189
        ActiveRecordInterface $primaryModel
1190 4
    ): void {
1191 4
        /** @psalm-var string[] $link */
1192
        foreach ($link as $fk => $pk) {
1193 4
            /** @psalm-var mixed $value */
1194
            $value = $primaryModel->$pk;
1195
1196
            if ($value === null) {
1197 4
                throw new InvalidCallException(
1198 4
                    'Unable to link active record: the primary key of ' . $primaryModel::class . ' is null.'
1199 4
                );
1200
            }
1201 8
1202
            /**
1203
             * relation via array valued attribute
1204
             *
1205 14
             * @psalm-suppress MixedArrayAssignment
1206
             */
1207 14
            if (is_array($foreignModel->$fk)) {
1208 14
                /** @psalm-var mixed */
1209
                $foreignModel->{$fk}[] = $value;
1210
            } else {
1211
                $foreignModel->{$fk} = $value;
1212
            }
1213 14
        }
1214 14
1215
        $foreignModel->save();
1216 14
    }
1217 14
1218 14
    /**
1219
     * Resets dependent related models checking if their links contain specific attribute.
1220
     *
1221 14
     * @param string $attribute The changed attribute name.
1222 10
     */
1223
    private function resetDependentRelations(string $attribute): void
1224
    {
1225 14
        foreach ($this->relationsDependencies[$attribute] as $relation) {
1226 4
            unset($this->related[$relation]);
1227
        }
1228
1229 14
        unset($this->relationsDependencies[$attribute]);
1230 9
    }
1231
1232 5
    public function getTableName(): string
1233
    {
1234
        if ($this->tableName === '') {
1235
            $this->tableName = '{{%' . DbStringHelper::pascalCaseToId(DbStringHelper::baseName(static::class)) . '}}';
1236
        }
1237 22
1238 22
        return $this->tableName;
1239
    }
1240
}
1241