Passed
Pull Request — master (#246)
by
unknown
02:39
created

BaseActiveRecord::createRelationQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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