Passed
Push — master ( d01bf5...5a7f24 )
by Alexander
14:14
created

BaseActiveRecord::getAttributeHint()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
eloc 21
c 0
b 0
f 0
dl 0
loc 34
rs 8.4444
ccs 0
cts 20
cp 0
cc 8
nc 5
nop 1
crap 72
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\ActiveRecord\Redis\ActiveQuery as RedisActiveQuery;
13
use Yiisoft\Db\Connection\ConnectionInterface;
14
use Yiisoft\Db\Exception\Exception;
15
use Yiisoft\Db\Exception\InvalidArgumentException;
16
use Yiisoft\Db\Exception\InvalidCallException;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Exception\NotSupportedException;
19
use Yiisoft\Db\Exception\StaleObjectException;
20
21
use function array_combine;
22
use function array_flip;
23
use function array_intersect;
24
use function array_key_exists;
25
use function array_keys;
26
use function array_search;
27
use function array_values;
28
use function count;
29
use function get_class;
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
abstract class BaseActiveRecord implements ActiveRecordInterface, IteratorAggregate, ArrayAccess
53
{
54
    use BaseActiveRecordTrait;
55
56
    private array $attributes = [];
57
    private ?array $oldAttributes = null;
58
    private array $related = [];
59
    private array $relationsDependencies = [];
60
    protected ConnectionInterface $db;
61
62
    public function __construct(ConnectionInterface $db)
63
    {
64
        $this->db = $db;
65
    }
66
67
    /**
68
     * Updates the whole table using the provided attribute values and conditions.
69
     *
70
     * For example, to change the status to be 1 for all customers whose status is 2:
71
     *
72 198
     * ```php
73
     * $customer = new Customer($db);
74 198
     * $customer->updateAll(['status' => 1], 'status = 2');
75
     * ```
76
     *
77
     * @param array $attributes attribute values (name-value pairs) to be saved into the table.
78
     * @param array|string $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
79
     * Please refer to {@see Query::where()} on how to specify this parameter.
80
     * @param array $params
81
     *
82
     * @throws NotSupportedException if not overridden.
83
     *
84 5
     * @return int the number of rows updated.
85
     */
86 5
    public function updateAll(array $attributes, $condition = '', array $params = []): int
87
    {
88
        throw new NotSupportedException(__METHOD__ . ' is not supported.');
89
    }
90
91
    /**
92
     * Updates the whole table using the provided counter changes and conditions.
93
     *
94
     * For example, to increment all customers' age by 1,
95
     *
96
     * ```php
97
     * Customer::updateAllCounters(['age' => 1]);
98
     * ```
99
     *
100 37
     * @param array $counters the counters to be updated (attribute name => increment value). Use negative values if you
101
     * want to decrement the counters.
102 37
     * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
103
     * Please refer to {@see Query::where()} on how to specify this parameter.
104 37
     *
105 23
     * @throws NotSupportedException if not override.
106
     *
107
     * @return int the number of rows updated.
108 37
     */
109
    public function updateAllCounters(array $counters, $condition = ''): int
0 ignored issues
show
Unused Code introduced by
The parameter $condition is not used and could be removed. ( Ignorable by Annotation )

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

109
    public function updateAllCounters(array $counters, /** @scrutinizer ignore-unused */ $condition = ''): int

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
110 25
    {
111
        throw new NotSupportedException(__METHOD__ . ' is not supported.');
112 25
    }
113
114
    /**
115
     * Deletes rows in the table using the provided conditions.
116 25
     *
117
     * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
118
     *
119
     * For example, to delete all customers whose status is 3:
120
     *
121
     * ```php
122 37
     * $customer = new Customer($this->db);
123
     * $customer::deleteAll('status = 3');
124
     * ```
125
     *
126
     * @param array|null $condition the conditions that will be put in the WHERE part of the DELETE SQL. Please refer to
127
     * {@see Query::where()} on how to specify this parameter.
128
     *
129
     * @return int the number of rows deleted.
130
     *
131
     * @throws NotSupportedException if not overridden.
132
     */
133
    public function deleteAll(?array $condition = null): int
134
    {
135
        throw new NotSupportedException(__METHOD__ . ' is not supported.');
136
    }
137
138
    /**
139
     * Returns the name of the column that stores the lock version for implementing optimistic locking.
140
     *
141
     * Optimistic locking allows multiple users to access the same record for edits and avoids potential conflicts. In
142
     * case when a user attempts to save the record upon some staled data (because another user has modified the data),
143
     * a {@see StaleObjectException} exception will be thrown, and the update or deletion is skipped.
144
     *
145
     * Optimistic locking is only supported by {@see update()} and {@see delete()}.
146
     *
147
     * To use Optimistic locking:
148
     *
149
     * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
150
     *    Override this method to return the name of this column.
151
     * 2. In the Web form that collects the user input, add a hidden field that stores the lock version of the recording
152
     *    being updated.
153
     * 3. In the controller action that does the data updating, try to catch the {@see StaleObjectException} and
154
     *    implement necessary business logic (e.g. merging the changes, prompting stated data) to resolve the conflict.
155
     *
156
     * @return string|null the column name that stores the lock version of a table row. If `null` is returned (default
157
     * implemented), optimistic locking will not be supported.
158
     */
159
    public function optimisticLock(): ?string
160
    {
161
        return null;
162
    }
163
164
    /**
165
     * Declares a `has-one` relation.
166
     *
167
     * The declaration is returned in terms of a relational {@see ActiveQuery} instance through which the related record
168
     * can be queried and retrieved back.
169
     *
170
     * A `has-one` relation means that there is at most one related record matching the criteria set by this relation,
171
     * e.g., a customer has one country.
172
     *
173
     * For example, to declare the `country` relation for `Customer` class, we can write the following code in the
174
     * `Customer` class:
175
     *
176
     * ```php
177
     * public function getCountry()
178
     * {
179
     *     return $this->hasOne(Country::className(), ['id' => 'country_id']);
180
     * }
181
     * ```
182
     *
183
     * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name in the related class
184
     * `Country`, while the 'country_id' value refers to an attribute name in the current AR class.
185
     *
186
     * Call methods declared in {@see ActiveQuery} to further customize the relation.
187
     *
188
     * @param array|string $class the class name of the related record.
189
     * @param array $link the primary-foreign key constraint. The keys of the array refer to the attributes of the
190
     * record associated with the `$class` model, while the values of the array refer to the corresponding attributes in
191
     * **this** AR class.
192
     *
193
     * @return ActiveQueryInterface the relational query object.
194
     */
195
    public function hasOne($class, array $link): ActiveQueryInterface
196
    {
197
        return $this->createRelationQuery($class, $link, false);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type array; however, parameter $arClass of Yiisoft\ActiveRecord\Bas...::createRelationQuery() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

197
        return $this->createRelationQuery(/** @scrutinizer ignore-type */ $class, $link, false);
Loading history...
198
    }
199
200
    /**
201
     * Declares a `has-many` relation.
202
     *
203
     * The declaration is returned in terms of a relational {@see ActiveQuery} instance  through which the related
204
     * record can be queried and retrieved back.
205
     *
206
     * A `has-many` relation means that there are multiple related records matching the criteria set by this relation,
207
     * e.g., a customer has many orders.
208
     *
209
     * For example, to declare the `orders` relation for `Customer` class, we can write the following code in the
210
     * `Customer` class:
211
     *
212
     * ```php
213
     * public function getOrders()
214
     * {
215 41
     *     return $this->hasMany(Order::className(), ['customer_id' => 'id']);
216
     * }
217 41
     * ```
218
     *
219
     * Note that in the above, the 'customer_id' key in the `$link` parameter refers to an attribute name in the related
220
     * class `Order`, while the 'id' value refers to an attribute name in the current AR class.
221
     *
222
     * Call methods declared in {@see ActiveQuery} to further customize the relation.
223
     *
224
     * @param array|string $class the class name of the related record
225
     * @param array $link the primary-foreign key constraint. The keys of the array refer to the attributes of the
226
     * record associated with the `$class` model, while the values of the array refer to the corresponding attributes in
227
     * **this** AR class.
228
     *
229
     * @return ActiveQueryInterface the relational query object.
230
     */
231
    public function hasMany($class, array $link): ActiveQueryInterface
232
    {
233
        return $this->createRelationQuery($class, $link, true);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type array; however, parameter $arClass of Yiisoft\ActiveRecord\Bas...::createRelationQuery() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

233
        return $this->createRelationQuery(/** @scrutinizer ignore-type */ $class, $link, true);
Loading history...
234
    }
235
236
    /**
237
     * Creates a query instance for `has-one` or `has-many` relation.
238
     *
239
     * @param string $arClass the class name of the related record.
240
     * @param array $link the primary-foreign key constraint.
241
     * @param bool $multiple whether this query represents a relation to more than one record.
242
     *
243
     * @return ActiveQueryInterface the relational query object.
244
245
     * {@see hasOne()}
246
     * {@see hasMany()}
247
     */
248
    protected function createRelationQuery(string $arClass, array $link, bool $multiple): ActiveQueryInterface
249
    {
250
        if ($this->db->getDriverName() === 'redis') {
251 83
            $aqClassInstance = new RedisActiveQuery($arClass, $this->db);
252
        } else {
253 83
            $aqClassInstance = new ActiveQuery($arClass, $this->db);
254
        }
255
256
        return $aqClassInstance->primaryModel($this)->link($link)->multiple($multiple);
257
    }
258
259
    /**
260
     * Populates the named relation with the related records.
261
     *
262
     * Note that this method does not check if the relation exists or not.
263
     *
264
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method
265
     * (case-sensitive).
266
     * @param ActiveRecordInterface|array|null $records the related records to be populated into the relation.
267
     *
268
     * @return void
269
     *
270
     * {@see getRelation()}
271
     */
272
    public function populateRelation(string $name, $records): void
273
    {
274
        foreach ($this->relationsDependencies as &$relationNames) {
275
            unset($relationNames[$name]);
276
        }
277
278
        $this->related[$name] = $records;
279
    }
280
281
    /**
282
     * Check whether the named relation has been populated with records.
283
     *
284
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method
285
     * (case-sensitive).
286
     *
287 197
     * @return bool whether relation has been populated with records.
288
     *
289 197
     * {@see getRelation()}
290
     */
291
    public function isRelationPopulated(string $name): bool
292
    {
293
        return array_key_exists($name, $this->related);
294
    }
295
296
    /**
297
     * Returns all populated related records.
298
     *
299
     * @return array an array of related records indexed by relation names.
300
     *
301
     * {@see getRelation()}
302
     */
303
    public function getRelatedRecords(): array
304 222
    {
305
        return $this->related;
306
    }
307 222
308
    /**
309 222
     * Returns a value indicating whether the model has an attribute with the specified name.
310
     *
311
     * @param string|int $name the name or position of the attribute.
312
     *
313
     * @return bool whether the model has an attribute with the specified name.
314
     */
315
    public function hasAttribute($name): bool
316
    {
317
        return isset($this->attributes[$name]) || in_array($name, $this->attributes(), true);
318
    }
319
320
    /**
321
     * Returns the named attribute value.
322
     *
323
     * If this record is the result of a query and the attribute is not loaded, `null` will be returned.
324
     *
325 136
     * @param string $name the attribute name.
326
     *
327 136
     * @return mixed the attribute value. `null` if the attribute is not set or does not exist.
328 19
     *
329
     * {@see hasAttribute()}
330
     */
331 136
    public function getAttribute(string $name)
332 136
    {
333
        return $this->attributes[$name] ?? null;
334
    }
335
336
    /**
337
     * Sets the named attribute value.
338
     *
339
     * @param string $name the attribute name.
340
     * @param mixed $value the attribute value.
341
     *
342
     * @throws InvalidArgumentException if the named attribute does not exist.
343
     *
344 75
     * @return void
345
     *
346 75
     * {@see hasAttribute()}
347
     */
348
    public function setAttribute(string $name, $value): void
349
    {
350
        if ($this->hasAttribute($name)) {
351
            if (
352
                !empty($this->relationsDependencies[$name])
353
                && (!array_key_exists($name, $this->attributes) || $this->attributes[$name] !== $value)
354
            ) {
355
                $this->resetDependentRelations($name);
356 10
            }
357
            $this->attributes[$name] = $value;
358 10
        } else {
359
            throw new InvalidArgumentException(get_class($this) . ' has no attribute named "' . $name . '".');
360
        }
361
    }
362
363
    /**
364
     * Returns the old attribute values.
365
     *
366
     * @return array the old attribute values (name-value pairs).
367
     */
368 333
    public function getOldAttributes(): array
369
    {
370 333
        return $this->oldAttributes ?? [];
371
    }
372
373
    /**
374
     * Sets the old attribute values.
375
     *
376
     * All existing old attribute values will be discarded.
377
     *
378
     * @param array|null $values old attribute values to be set. If set to `null` this record is considered to be
379
     * {@see isNewRecord|new}.
380
     */
381
    public function setOldAttributes(?array $values): void
382
    {
383
        $this->oldAttributes = $values;
384 68
    }
385
386 68
    /**
387
     * Returns the old value of the named attribute.
388
     *
389
     * If this record is the result of a query and the attribute is not loaded, `null` will be returned.
390
     *
391
     * @param string $name the attribute name
392
     *
393
     * @return mixed the old attribute value. `null` if the attribute is not loaded before or does not exist.
394
     *
395
     * {@see hasAttribute()}
396
     */
397
    public function getOldAttribute(string $name)
398
    {
399
        return $this->oldAttributes[$name] ?? null;
400
    }
401 130
402
    /**
403 130
     * Sets the old value of the named attribute.
404
     *
405 130
     * @param string $name the attribute name.
406 130
     * @param mixed $value the old attribute value.
407
     *
408 10
     * @throws InvalidArgumentException if the named attribute does not exist.
409
     *
410 130
     * {@see hasAttribute()}
411
     */
412
    public function setOldAttribute(string $name, $value): void
413
    {
414 130
        if (isset($this->oldAttributes[$name]) || $this->hasAttribute($name)) {
415
            $this->oldAttributes[$name] = $value;
416
        } else {
417
            throw new InvalidArgumentException(get_class($this) . ' has no attribute named "' . $name . '".');
418
        }
419
    }
420
421
    /**
422
     * Marks an attribute dirty.
423
     *
424
     * This method may be called to force updating a record when calling {@see update()}, even if there is no change
425
     * being made to the record.
426
     *
427
     * @param string $name the attribute name.
428
     */
429
    public function markAttributeDirty(string $name): void
430
    {
431
        unset($this->oldAttributes[$name]);
432
    }
433
434 137
    /**
435
     * Returns a value indicating whether the named attribute has been changed.
436 137
     *
437 137
     * @param string $name the name of the attribute.
438
     * @param bool $identical whether the comparison of new and old value is made for identical values using `===`,
439
     * defaults to `true`. Otherwise `==` is used for comparison.
440
     *
441
     * @return bool whether the attribute has been changed.
442
     */
443
    public function isAttributeChanged(string $name, bool $identical = true): bool
444
    {
445
        if (isset($this->attributes[$name], $this->oldAttributes[$name])) {
446
            if ($identical) {
447
                return $this->attributes[$name] !== $this->oldAttributes[$name];
448
            }
449
450
            return $this->attributes[$name] !== $this->oldAttributes[$name];
451
        }
452
453
        return isset($this->attributes[$name]) || isset($this->oldAttributes[$name]);
454
    }
455
456
    /**
457
     * Returns the attribute values that have been modified since they are loaded or saved most recently.
458
     *
459
     * The comparison of new and old values is made for identical values using `===`.
460
     *
461
     * @param array|null $names the names of the attributes whose values may be returned if they are changed recently.
462
     * If null, {@see attributes()} will be used.
463
     *
464
     * @return array the changed attribute values (name-value pairs).
465
     */
466
    public function getDirtyAttributes(?array $names = null): array
467
    {
468
        if ($names === null) {
469
            $names = $this->attributes();
470
        }
471
472
        $names = array_flip($names);
473
        $attributes = [];
474
475
        if ($this->oldAttributes === null) {
476
            foreach ($this->attributes as $name => $value) {
477
                if (isset($names[$name])) {
478
                    $attributes[$name] = $value;
479
                }
480
            }
481
        } else {
482 6
            foreach ($this->attributes as $name => $value) {
483
                if (
484 6
                    isset($names[$name])
485 6
                    && (!array_key_exists($name, $this->oldAttributes) || $value !== $this->oldAttributes[$name])
486
                ) {
487
                    $attributes[$name] = $value;
488
                }
489
            }
490
        }
491
492
        return $attributes;
493
    }
494
495
    /**
496
     * Saves the current record.
497
     *
498
     * This method will call {@see insert()} when {@see isNewRecord} is `true`, or {@see update()} when
499
     * {@see isNewRecord} is `false`.
500
     *
501
     * For example, to save a customer record:
502
     *
503
     * ```php
504
     * $customer = new Customer($db);
505
     * $customer->name = $name;
506
     * $customer->email = $email;
507
     * $customer->save();
508
     * ```
509
     *
510
     * @param array|null $attributeNames list of attribute names that need to be saved. Defaults to null, meaning all
511
     * attributes that are loaded from DB will be saved.
512
     *
513
     * @throws Exception|StaleObjectException
514
     *
515
     * @return bool whether the saving succeeded (i.e. no validation errors occurred).
516
     */
517
    public function save(?array $attributeNames = null): bool
518
    {
519 149
        if ($this->getIsNewRecord()) {
520
            return $this->insert($attributeNames);
521 149
        }
522 145
523
        return $this->update($attributeNames) !== false;
524
    }
525 149
526 149
    /**
527
     * Saves the changes to this active record into the associated database table.
528 149
     *
529 133
     * Only the {@see dirtyAttributes|changed attribute values} will be saved into database.
530 129
     *
531 129
     * For example, to update a customer record:
532
     *
533
     * ```php
534
     * $customer = new Customer($db);
535 45
     * $customer->name = $name;
536
     * $customer->email = $email;
537 45
     * $customer->update();
538 45
     * ```
539
     *
540 45
     * Note that it is possible the update does not affect any row in the table. In this case, this method will return
541
     * 0. For this reason, you should use the following code to check if update() is successful or not:
542
     *
543
     * ```php
544
     * if ($customer->update() !== false) {
545 149
     *     // update successful
546
     * } else {
547
     *     // update failed
548
     * }
549
     * ```
550
     *
551
     * @param array|null $attributeNames list of attribute names that need to be saved. Defaults to null, meaning all
552
     * attributes that are loaded from DB will be saved.
553
     *
554
     * @return int|false the number of rows affected, or `false` if validation fails or {@see beforeSave()} stops the
555
     * updating process.
556
     * @throws NotSupportedException|Exception in case update failed.
557
     * @throws StaleObjectException if {@see href='psi_element://optimisticLock'>|optimistic locking} is enabled and the
558
     * data being updated is outdated.
559
     */
560
    public function update(?array $attributeNames = null)
561
    {
562
        return $this->updateInternal($attributeNames);
563
    }
564
565
    /**
566
     * Updates the specified attributes.
567
     *
568
     * This method is a shortcut to {@see update()} when data validation is not needed and only a small set attributes
569
     * need to be updated.
570
     *
571 141
     * You may specify the attributes to be updated as name list or name-value pairs. If the latter, the corresponding
572
     * attribute values will be modified accordingly.
573 141
     *
574 125
     * The method will then save the specified attributes into database.
575
     *
576
     * Note that this method will **not** perform data validation and will **not** trigger events.
577 30
     *
578
     * @param array $attributes the attributes (names or name-value pairs) to be updated.
579
     *
580
     * @throws Exception|NotSupportedException
581
     *
582
     * @return int the number of rows affected.
583
     */
584
    public function updateAttributes(array $attributes): int
585
    {
586
        $attrs = [];
587
588
        foreach ($attributes as $name => $value) {
589
            if (is_int($name)) {
590
                $attrs[] = $value;
591
            } else {
592
                $this->$name = $value;
593
                $attrs[] = $name;
594
            }
595
        }
596
597
        $values = $this->getDirtyAttributes($attrs);
598
599
        if (empty($values) || $this->getIsNewRecord()) {
600
            return 0;
601
        }
602
603
        $rows = $this->updateAll($values, $this->getOldPrimaryKey(true));
604
605
        foreach ($values as $name => $value) {
606
            $this->oldAttributes[$name] = $this->attributes[$name];
607
        }
608
609
        return $rows;
610
    }
611
612
    /**
613
     * {@see update()}
614
     *
615
     * @param array|null $attributes attributes to update.
616
     *
617
     * @throws Exception|NotSupportedException|StaleObjectException
618
     *
619
     * @return int the number of rows affected.
620
     */
621
    protected function updateInternal(?array $attributes = null): int
622
    {
623
        $values = $this->getDirtyAttributes($attributes);
624
625
        if (empty($values)) {
626
            return 0;
627
        }
628
629 6
        $condition = $this->getOldPrimaryKey(true);
630
        $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...
631 6
632
        if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
633
            $values[$lock] = $this->$lock + 1;
634
            $condition[$lock] = $this->$lock;
635
        }
636
637
        /**
638
         * We do not check the return value of updateAll() because it's possible that the UPDATE statement doesn't
639
         * change anything and thus returns 0.
640
         */
641
        $rows = $this->updateAll($values, $condition);
642
643
        if ($lock !== null && !$rows) {
644
            throw new StaleObjectException('The object being updated is outdated.');
645
        }
646
647
        if (isset($values[$lock])) {
648
            $this->$lock = $values[$lock];
649
        }
650
651
        $changedAttributes = [];
652
653
        foreach ($values as $name => $value) {
654 5
            $changedAttributes[$name] = $this->oldAttributes[$name] ?? null;
655
            $this->oldAttributes[$name] = $value;
656 5
        }
657
658 5
        return $rows;
659 5
    }
660 1
661
    /**
662 5
     * Updates one or several counter columns for the current AR object.
663 5
     *
664
     * Note that this method differs from {@see updateAllCounters()} in that it only saves counters for the current AR
665
     * object.
666
     *
667 5
     * An example usage is as follows:
668
     *
669 5
     * ```php
670 4
     * $post = new Post($db);
671
     * $post->updateCounters(['view_count' => 1]);
672
     * ```
673 5
     *
674
     * @param array $counters the counters to be updated (attribute name => increment value), use negative values if you
675 5
     * want to decrement the counters.
676 5
     *
677
     * @throws Exception|NotSupportedException
678
     *
679 5
     * @return bool whether the saving is successful.
680
     *
681
     * {@see updateAllCounters()}
682
     */
683
    public function updateCounters(array $counters): bool
684
    {
685
        if ($this->updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) {
686
            foreach ($counters as $name => $value) {
687
                if (!isset($this->attributes[$name])) {
688
                    $this->attributes[$name] = $value;
689
                } else {
690
                    $this->attributes[$name] += $value;
691
                }
692
693 40
                $this->oldAttributes[$name] = $this->attributes[$name];
694
            }
695 40
696
            return true;
697 40
        }
698
699
        return false;
700
    }
701 40
702 40
    /**
703
     * Deletes the table row corresponding to this active record.
704 40
     *
705 4
     * This method performs the following steps in order:
706 4
     *
707
     * @throws StaleObjectException if {@see optimisticLock|optimistic locking} is enabled and the data being deleted is
708
     * outdated.
709
     * @throws Exception in case delete failed.
710
     *
711
     * @return bool|int the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
712
     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
713 40
     */
714
    public function delete()
715 40
    {
716 4
        /**
717
         * we do not check the return value of deleteAll() because it's possible the record is already deleted in
718
         * the database and thus the method will return 0
719 40
         */
720 4
        $condition = $this->getOldPrimaryKey(true);
721
        $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...
722
723 40
        if ($lock !== null) {
0 ignored issues
show
introduced by
The condition $lock !== null is always false.
Loading history...
724
            $condition[$lock] = $this->$lock;
725 40
        }
726 40
727 40
        $result = $this->deleteAll($condition);
728
729
        if ($lock !== null && !$result) {
730 40
            throw new StaleObjectException('The object being deleted is outdated.');
731
        }
732
733
        $this->oldAttributes = null;
734
735
        return $result;
736
    }
737
738
    /**
739
     * Returns a value indicating whether the current record is new.
740
     *
741
     * @return bool whether the record is new and should be inserted when calling {@see save()}.
742
     */
743
    public function getIsNewRecord(): bool
744
    {
745
        return $this->oldAttributes === null;
746
    }
747
748
    /**
749
     * Sets the value indicating whether the record is new.
750
     *
751
     * @param bool $value whether the record is new and should be inserted when calling {@see save()}.
752
     *
753
     * @see getIsNewRecord()
754
     */
755
    public function setIsNewRecord(bool $value): void
756 9
    {
757
        $this->oldAttributes = $value ? null : $this->attributes;
758 9
    }
759 9
760 9
    /**
761 4
     * Repopulates this active record with the latest data.
762
     *
763 5
     * @return bool whether the row still exists in the database. If `true`, the latest data will be populated to this
764
     * active record. Otherwise, this record will remain unchanged.
765
     */
766 9
    public function refresh(): bool
767
    {
768
        /** @var $record BaseActiveRecord */
769 9
        $record = $this->instantiateQuery()->findOne($this->getPrimaryKey(true));
770
771
        return $this->refreshInternal($record);
772
    }
773
774
    /**
775
     * Repopulates this active record with the latest data from a newly fetched instance.
776
     *
777
     * @param BaseActiveRecord|null $record the record to take attributes from.
778
     *
779
     * @return bool whether refresh was successful.
780
     *
781
     * {@see refresh()}
782
     */
783
    protected function refreshInternal(?BaseActiveRecord $record): bool
784
    {
785
        if ($record === null) {
786
            return false;
787
        }
788
789
        foreach ($this->attributes() as $name) {
790 2
            $this->attributes[$name] = $record->attributes[$name] ?? null;
791
        }
792 2
793
        $this->oldAttributes = $record->oldAttributes;
794
        $this->related = [];
795
        $this->relationsDependencies = [];
796
797
        return true;
798 2
    }
799 2
800
    /**
801 2
     * Returns a value indicating whether the given active record is the same as the current one.
802
     *
803
     * The comparison is made by comparing the table names and the primary key values of the two active records. If one
804
     * of the records {@see isNewRecord|is new} they are also considered not equal.
805 2
     *
806
     * @param ActiveRecordInterface $record record to compare to.
807 2
     *
808
     * @return bool whether the two active records refer to the same row in the same database table.
809
     */
810
    public function equals(ActiveRecordInterface $record): bool
811 2
    {
812
        if ($this->getIsNewRecord() || $record->getIsNewRecord()) {
813 2
            return false;
814
        }
815
816
        return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey();
817
    }
818
819
    /**
820
     * Returns the primary key value(s).
821 149
     *
822
     * @param bool $asArray whether to return the primary key value as an array. If `true`, the return value will be an
823 149
     * array with column names as keys and column values as values. Note that for composite primary keys, an array will
824
     * always be returned regardless of this parameter value.
825
     * @property mixed The primary key value. An array (column name => column value) is returned if the primary key is
826
     * composite. A string is returned otherwise (null will be returned if the key value is null).
827
     *
828
     * @return mixed the primary key value. An array (column name => column value) is returned if the primary key is
829
     * composite or `$asArray` is `true`. A string is returned otherwise (null will be returned if the key value is
830
     * null).
831
     */
832
    public function getPrimaryKey(bool $asArray = false)
833
    {
834
        $keys = $this->primaryKey();
835
836
        if (!$asArray && count($keys) === 1) {
837
            return $this->attributes[$keys[0]] ?? null;
838
        }
839
840
        $values = [];
841
842
        foreach ($keys as $name) {
843
            $values[$name] = $this->attributes[$name] ?? null;
844
        }
845
846 4
        return $values;
847
    }
848
849 4
    /**
850
     * Returns the old primary key value(s).
851 4
     *
852
     * This refers to the primary key value that is populated into the record after executing a find method
853
     * (e.g. findOne()).
854
     *
855
     * The value remains unchanged even if the primary key attribute is manually assigned with a different value.
856
     *
857
     * @param bool $asArray whether to return the primary key value as an array. If `true`, the return value will be an
858
     * array with column name as key and column value as value. If this is `false` (default), a scalar value will be
859
     * returned for non-composite primary key.
860
     * @property mixed The old primary key value. An array (column name => column value) is returned if the primary key
861
     * is composite. A string is returned otherwise (null will be returned if the key value is null).
862
     *
863 32
     * @throws Exception if the AR model does not have a primary key.
864
     *
865 32
     * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
866 5
     * is composite or `$asArray` is `true`. A string is returned otherwise (null will be returned if the key value is
867
     * null).
868
     */
869 32
    public function getOldPrimaryKey(bool $asArray = false)
870 32
    {
871
        $keys = $this->primaryKey();
872
873 32
        if (empty($keys)) {
874 32
            throw new Exception(
875 32
                get_class($this) . ' does not have a primary key. You should either define a primary key for '
876
                . 'the corresponding table or override the primaryKey() method.'
877 32
            );
878
        }
879
880
        if (!$asArray && count($keys) === 1) {
881
            return $this->oldAttributes[$keys[0]] ?? null;
882
        }
883
884
        $values = [];
885
886
        foreach ($keys as $name) {
887
            $values[$name] = $this->oldAttributes[$name] ?? null;
888
        }
889
890 1
        return $values;
891
    }
892 1
893 1
    /**
894
     * Populates an active record object using a row of data from the database/storage.
895
     *
896 1
     * This is an internal method meant to be called to create active record objects after fetching data from the
897
     * database. It is mainly used by {@see ActiveQuery} to populate the query results into active records.
898
     *
899
     * @param ActiveRecordInterface|array $record the record to be populated. In most cases this will be an instance
900
     * created by {@see instantiate()} beforehand.
901
     * @param array|object $row attribute values (name => value).
902
     */
903
    public function populateRecord($record, $row): void
904
    {
905
        $columns = array_flip($record->attributes());
906
907
        foreach ($row as $name => $value) {
908
            if (isset($columns[$name])) {
909
                $record->attributes[$name] = $value;
0 ignored issues
show
Bug introduced by
Accessing attributes on the interface Yiisoft\ActiveRecord\ActiveRecordInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
910
            } elseif ($record->canSetProperty($name)) {
0 ignored issues
show
Bug introduced by
The method canSetProperty() does not exist on Yiisoft\ActiveRecord\ActiveRecordInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\ActiveRecord\ActiveRecordInterface. ( Ignorable by Annotation )

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

910
            } elseif ($record->/** @scrutinizer ignore-call */ canSetProperty($name)) {
Loading history...
911
                $record->$name = $value;
912 53
            }
913
        }
914 53
915
        $record->oldAttributes = $record->attributes;
0 ignored issues
show
Bug introduced by
Accessing oldAttributes on the interface Yiisoft\ActiveRecord\ActiveRecordInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
916 53
        $record->related = [];
0 ignored issues
show
Bug introduced by
Accessing related on the interface Yiisoft\ActiveRecord\ActiveRecordInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
917 21
        $record->relationsDependencies = [];
0 ignored issues
show
Bug introduced by
Accessing relationsDependencies on the interface Yiisoft\ActiveRecord\ActiveRecordInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
918
    }
919
920 32
    /**
921
     * Creates an active record instance.
922 32
     *
923 32
     * This method is called together with {@see populateRecord()} by {@see ActiveQuery}.
924
     *
925
     * It is not meant to be used for creating new records directly.
926 32
     *
927
     * @param ActiveRecordInterface|array $row the row data to be populated.
928
     *
929
     * You may override this method if the instance being created depends on the row data to be populated into the
930
     * record.
931
     *
932
     * For example, by creating a record based on the value of a column, you may implement the so-called single-table
933
     * inheritance mapping.
934
     *
935
     * @return ActiveRecordInterface the newly created active record
936
     */
937
    public function instantiate($row): ActiveRecordInterface
0 ignored issues
show
Unused Code introduced by
The parameter $row is not used and could be removed. ( Ignorable by Annotation )

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

937
    public function instantiate(/** @scrutinizer ignore-unused */ $row): ActiveRecordInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
938
    {
939
        return new static($this->db);
940
    }
941
942
    public function instantiateQuery(): ActiveQueryInterface
943
    {
944
        if ($this->db->getDriverName() === 'redis') {
945
            return new RedisActiveQuery(static::class, $this->db);
946
        }
947
948
        return new ActiveQuery(static::class, $this->db);
949 59
    }
950
951 59
    /**
952
     * Establishes the relationship between two models.
953 59
     *
954
     * The relationship is established by setting the foreign key value(s) in one model to be the corresponding primary
955
     * key value(s) in the other model.
956
     *
957
     * The model with the foreign key will be saved into database without performing validation.
958
     *
959
     * If the relationship involves a junction table, a new row will be inserted into the junction table which contains
960 59
     * the primary key values from both models.
961
     *
962
     * Note that this method requires that the primary key value is not null.
963
     *
964 59
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via
965
     * `getOrders()` method.
966 59
     * @param ActiveRecordInterface $arClass the model to be linked with the current one.
967 59
     * @param array $extraColumns additional column values to be saved into the junction table. This parameter is only
968
     * meaningful for a relationship involving a junction table (i.e., a relation set with
969
     * {@see ActiveRelationTrait::via()} or {@see ActiveQuery::viaTable()}).
970 59
     *
971
     * @throws Exception|InvalidArgumentException|InvalidCallException if the method is unable to link two models.
972
     * @throws InvalidConfigException|ReflectionException|Throwable
973
     */
974
    public function link(string $name, ActiveRecordInterface $arClass, array $extraColumns = []): void
975
    {
976
        $relation = $this->getRelation($name);
977
978
        if ($relation->getVia() !== null) {
979
            if ($this->getIsNewRecord() || $arClass->getIsNewRecord()) {
980
                throw new InvalidCallException(
981
                    'Unable to link models: the models being linked cannot be newly created.'
982
                );
983
            }
984
985
            if (is_array($relation->getVia())) {
986 381
                /** @var $viaRelation ActiveQuery */
987
                [$viaName, $viaRelation] = $relation->getVia();
988 381
                $viaClass = $viaRelation->getARInstance();
989
                /** unset $viaName so that it can be reloaded to reflect the change */
990 381
                unset($this->related[$viaName]);
991 381
            } else {
992 381
                $viaRelation = $relation->getVia();
993 8
                $from = $relation->getVia()->getFrom();
994 8
                $viaTable = reset($from);
995
            }
996
997
            $columns = [];
998 381
999 381
            foreach ($viaRelation->getLink() as $a => $b) {
1000 381
                $columns[$a] = $this->$b;
1001 381
            }
1002
1003
            foreach ($relation->getLink() as $a => $b) {
1004
                $columns[$b] = $arClass->$a;
1005
            }
1006
1007
            foreach ($extraColumns as $k => $v) {
1008
                $columns[$k] = $v;
1009
            }
1010
1011
            if (is_array($relation->getVia())) {
1012
                foreach ($columns as $column => $value) {
1013
                    $viaClass->$column = $value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaClass does not seem to be defined for all execution paths leading up to this point.
Loading history...
1014
                }
1015
1016
                $viaClass->insert();
1017
            } else {
1018 373
                /** @var $viaTable string */
1019
                $this->db->createCommand()->insert($viaTable, $columns)->execute();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaTable does not seem to be defined for all execution paths leading up to this point.
Loading history...
1020 373
            }
1021
        } else {
1022
            $p1 = $arClass->isPrimaryKey(array_keys($relation->getLink()));
1023
            $p2 = $this->isPrimaryKey(array_values($relation->getLink()));
1024
1025
            if ($p1 && $p2) {
1026
                if ($this->getIsNewRecord() && $arClass->getIsNewRecord()) {
1027
                    throw new InvalidCallException('Unable to link models: at most one model can be newly created.');
1028
                }
1029
1030
                if ($this->getIsNewRecord()) {
1031
                    $this->bindModels(array_flip($relation->getLink()), $this, $arClass);
1032
                } else {
1033
                    $this->bindModels($relation->getLink(), $arClass, $this);
1034
                }
1035
            } elseif ($p1) {
1036
                $this->bindModels(array_flip($relation->getLink()), $this, $arClass);
1037
            } elseif ($p2) {
1038
                $this->bindModels($relation->getLink(), $arClass, $this);
1039
            } else {
1040
                throw new InvalidCallException(
1041
                    'Unable to link models: the link defining the relation does not involve any primary key.'
1042
                );
1043
            }
1044
        }
1045
1046
        /** update lazily loaded related objects */
1047
        if (!$relation->getMultiple()) {
1048
            $this->related[$name] = $arClass;
1049
        } elseif (isset($this->related[$name])) {
1050
            if ($relation->getIndexBy() !== null) {
0 ignored issues
show
introduced by
The condition $relation->getIndexBy() !== null is always true.
Loading history...
1051 9
                if ($relation->getIndexBy() instanceof Closure) {
1052
                    $index = $relation->indexBy($arClass);
0 ignored issues
show
Bug introduced by
$arClass of type Yiisoft\ActiveRecord\ActiveRecordInterface is incompatible with the type callable|string expected by parameter $column of Yiisoft\Db\Query\Query::indexBy(). ( Ignorable by Annotation )

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

1052
                    $index = $relation->indexBy(/** @scrutinizer ignore-type */ $arClass);
Loading history...
1053 9
                } else {
1054
                    $index = $arClass->{$relation->getIndexBy()};
1055 9
                }
1056 5
                $this->related[$name][$index] = $arClass;
1057
            } else {
1058
                $this->related[$name][] = $arClass;
1059
            }
1060
        }
1061
    }
1062 5
1063
    /**
1064 5
     * Destroys the relationship between two models.
1065 5
     *
1066
     * The model with the foreign key of the relationship will be deleted if `$delete` is `true`. Otherwise, the
1067 5
     * foreign key will be set `null` and the model will be saved without validation.
1068
     *
1069
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via
1070
     * `getOrders()` method.
1071
     * @param ActiveRecordInterface $arClass the model to be unlinked from the current one. You have to make sure that
1072
     * the active record is really related with the current model as this method does not check this.
1073
     * @param bool $delete whether to delete the model that contains the foreign key. If `false`, the active records
1074 5
     * foreign key will be set `null` and saved. If `true`, the model containing the foreign key will be deleted.
1075
     *
1076 5
     * @throws Exception|ReflectionException|StaleObjectException|Throwable|InvalidCallException if the models cannot be
1077 5
     * unlinked.
1078
     */
1079
    public function unlink(string $name, ActiveRecordInterface $arClass, bool $delete = false): void
1080 5
    {
1081 5
        $relation = $this->getRelation($name);
1082
1083
        if ($relation->getVia() !== null) {
1084 5
            if (is_array($relation->getVia())) {
1085 5
                /** @var $viaRelation ActiveQuery */
1086
                [$viaName, $viaRelation] = $relation->getVia();
1087
                $viaClass = $viaRelation->getArInstance();
1088 5
                unset($this->related[$viaName]);
1089
            } else {
1090
                $viaRelation = $relation->getVia();
1091 5
                $from = $relation->getVia()->getFrom();
1092
                $viaTable = reset($from);
1093 5
            }
1094 5
1095
            $columns = [];
1096
            foreach ($viaRelation->getLink() as $a => $b) {
1097 5
                $columns[$a] = $this->$b;
1098
            }
1099
1100 5
            foreach ($relation->getLink() as $a => $b) {
1101
                $columns[$b] = $arClass->$a;
1102
            }
1103 9
            $nulls = [];
1104 9
1105
            foreach (array_keys($columns) as $a) {
1106 9
                $nulls[$a] = null;
1107
            }
1108
1109
            if (is_array($relation->getVia())) {
1110
                /** @var $viaClass ActiveRecordInterface */
1111
                if ($delete) {
1112
                    $viaClass->deleteAll($columns);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaClass does not seem to be defined for all execution paths leading up to this point.
Loading history...
1113
                } else {
1114
                    $viaClass->updateAll($nulls, $columns);
1115
                }
1116 9
            } else {
1117 5
                /** @var $viaTable string */
1118 4
                $command = $this->db->createCommand();
1119 4
                if ($delete) {
1120
                    $command->delete($viaTable, $columns)->execute();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaTable does not seem to be defined for all execution paths leading up to this point.
Loading history...
1121
                } else {
1122
                    $command->update($viaTable, $nulls, $columns)->execute();
1123
                }
1124
            }
1125
        } else {
1126
            $p1 = $arClass->isPrimaryKey(array_keys($relation->getLink()));
1127
            $p2 = $this->isPrimaryKey(array_values($relation->getLink()));
1128 9
            if ($p2) {
1129 5
                if ($delete) {
1130 9
                    $arClass->delete();
1131 9
                } else {
1132 4
                    foreach ($relation->getLink() as $a => $b) {
1133
                        $arClass->$a = null;
1134
                    }
1135 4
                    $arClass->save();
1136
                }
1137 4
            } elseif ($p1) {
1138
                foreach ($relation->getLink() as $a => $b) {
1139 5
                    /** relation via array valued attribute */
1140
                    if (is_array($this->$b)) {
1141
                        if (($key = array_search($arClass->$a, $this->$b, false)) !== false) {
1142 9
                            $values = $this->$b;
1143
                            unset($values[$key]);
1144
                            $this->$b = array_values($values);
1145
                        }
1146
                    } else {
1147
                        $this->$b = null;
1148
                    }
1149
                }
1150
                $delete ? $this->delete() : $this->save();
1151
            } else {
1152
                throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.');
1153
            }
1154
        }
1155
1156
        if (!$relation->getMultiple()) {
1157
            unset($this->related[$name]);
1158
        } elseif (isset($this->related[$name])) {
1159
            /** @var $b ActiveRecordInterface */
1160
            foreach ($this->related[$name] as $a => $b) {
1161
                if ($arClass->getPrimaryKey() === $b->getPrimaryKey()) {
1162 5
                    unset($this->related[$name][$a]);
1163
                }
1164 5
            }
1165
        }
1166 5
    }
1167 5
1168
    /**
1169 5
     * Destroys the relationship in current model.
1170 5
     *
1171 5
     * The model with the foreign key of the relationship will be deleted if `$delete` is `true`. Otherwise, the foreign
1172
     * key will be set `null` and the model will be saved without validation.
1173 4
     *
1174 4
     * Note that to destroy the relationship without removing records make sure your keys can be set to null.
1175 4
     *
1176
     * @param string $name the case sensitive name of the relationship, e.g. `orders` for a relation defined via
1177
     * `getOrders()` method.
1178 5
     * @param bool $delete whether to delete the model that contains the foreign key.
1179 5
     *
1180 5
     * @throws Exception|ReflectionException|StaleObjectException|Throwable
1181
     */
1182
    public function unlinkAll(string $name, bool $delete = false): void
1183 5
    {
1184 5
        $relation = $this->getRelation($name);
1185
1186 5
        if ($relation->getVia() !== null) {
1187
            if (is_array($relation->getVia())) {
1188 5
                /* @var $viaRelation ActiveQuery */
1189 5
                [$viaName, $viaRelation] = $relation->getVia();
1190
                $viaClass = $viaRelation->getARInstance();
1191
                unset($this->related[$viaName]);
1192 5
            } else {
1193
                $viaRelation = $relation->getVia();
1194 5
                $from = $relation->getVia()->getFrom();
1195 5
                $viaTable = reset($from);
1196
            }
1197 5
1198
            $condition = [];
1199
            $nulls = [];
1200
1201
            foreach ($viaRelation->getLink() as $a => $b) {
1202 4
                $nulls[$a] = null;
1203 4
                $condition[$a] = $this->$b;
1204
            }
1205
1206 5
            if (!empty($viaRelation->getWhere())) {
1207
                $condition = ['and', $condition, $viaRelation->getWhere()];
1208
            }
1209
1210 5
            if (!empty($viaRelation->getOn())) {
1211 5
                $condition = ['and', $condition, $viaRelation->getOn()];
1212 5
            }
1213 5
1214 5
            if (is_array($relation->getVia())) {
1215
                /** @var $viaClass ActiveRecordInterface */
1216 5
                if ($delete) {
1217 5
                    $viaClass->deleteAll($condition);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaClass does not seem to be defined for all execution paths leading up to this point.
Loading history...
1218
                } else {
1219 5
                    $viaClass->updateAll($nulls, $condition);
1220
                }
1221
            } else {
1222
                /** @var $viaTable string */
1223
                $command = $this->db->createCommand();
1224
                if ($delete) {
1225
                    $command->delete($viaTable, $condition)->execute();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $viaTable does not seem to be defined for all execution paths leading up to this point.
Loading history...
1226
                } else {
1227
                    $command->update($viaTable, $nulls, $condition)->execute();
1228
                }
1229
            }
1230
        } else {
1231
            $relatedModel = $relation->getARInstance();
1232
1233
            $link = $relation->getLink();
1234
            if (!$delete && count($link) === 1 && is_array($this->{$b = reset($link)})) {
1235
                /** relation via array valued attribute */
1236
                $this->$b = [];
1237
                $this->save();
1238
            } else {
1239 5
                $nulls = [];
1240
                $condition = [];
1241 5
1242
                foreach ($relation->getLink() as $a => $b) {
1243 5
                    $nulls[$a] = null;
1244 5
                    $condition[$a] = $this->$b;
1245 5
                }
1246
1247
                if (!empty($relation->getWhere())) {
1248
                    $condition = ['and', $condition, $relation->getWhere()];
1249 5
                }
1250
1251
                if (!empty($relation->getOn())) {
1252
                    $condition = ['and', $condition, $relation->getOn()];
1253
                }
1254
1255
                if ($delete) {
1256
                    $relatedModel->deleteAll($condition);
1257
                } else {
1258
                    $relatedModel->updateAll($nulls, $condition);
1259
                }
1260
            }
1261
        }
1262
1263
        unset($this->related[$name]);
1264
    }
1265
1266
    private function bindModels(
1267
        array $link,
1268
        ActiveRecordInterface $foreignModel,
1269
        ActiveRecordInterface $primaryModel
1270 22
    ): void {
1271
        foreach ($link as $fk => $pk) {
1272 22
            $value = $primaryModel->$pk;
1273
1274 22
            if ($value === null) {
1275 8
                throw new InvalidCallException(
1276
                    'Unable to link active record: the primary key of ' . get_class($primaryModel) . ' is null.'
1277 4
                );
1278 4
            }
1279 4
1280
            /** relation via array valued attribute */
1281 4
            if (is_array($foreignModel->$fk)) {
1282 4
                $foreignModel->{$fk}[] = $value;
1283 4
            } else {
1284
                $foreignModel->{$fk} = $value;
1285
            }
1286 8
        }
1287 8
1288
        $foreignModel->save();
1289 8
    }
1290 8
1291 8
    /**
1292
     * Returns a value indicating whether the given set of attributes represents the primary key for this model.
1293
     *
1294 8
     * @param array $keys the set of attributes to check.
1295
     *
1296
     * @return bool whether the given set of attributes represents the primary key for this model.
1297
     */
1298 8
    public function isPrimaryKey(array $keys): bool
1299
    {
1300
        $pks = $this->primaryKey();
1301
1302 8
        if (count($keys) === count($pks)) {
1303
            return count(array_intersect($keys, $pks)) === count($pks);
1304 4
        }
1305 4
1306
        return false;
1307 4
    }
1308
1309
    public function fields(): array
1310
    {
1311
        $fields = array_keys($this->attributes);
1312 4
1313 4
        return array_combine($fields, $fields);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_combine($fields, $fields) could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
1314 4
    }
1315
1316 8
    public function extraFields(): array
1317
    {
1318
        $fields = array_keys($this->getRelatedRecords());
1319
1320
        return array_combine($fields, $fields);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_combine($fields, $fields) could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
1321 14
    }
1322
1323 14
    /**
1324 14
     * Resets dependent related models checking if their links contain specific attribute.
1325
     *
1326
     * @param string $attribute The changed attribute name.
1327
     */
1328
    private function resetDependentRelations(string $attribute): void
1329 14
    {
1330 14
        foreach ($this->relationsDependencies[$attribute] as $relation) {
1331
            unset($this->related[$relation]);
1332 14
        }
1333 14
1334 14
        unset($this->relationsDependencies[$attribute]);
1335
    }
1336
1337 14
    /**
1338 10
     * Sets relation dependencies for a property.
1339
     *
1340
     * @param string $name property name.
1341 14
     * @param ActiveQuery $relation relation instance.
1342 4
     * @param string|null $viaRelationName intermediate relation.
1343
     */
1344
    private function setRelationDependencies(
1345 14
        string $name,
1346 9
        ActiveQuery $relation,
1347
        ?string $viaRelationName = null
1348 5
    ): void {
1349
        if (empty($relation->getVia()) && $relation->getLink()) {
1350
            foreach ($relation->getLink() as $attribute) {
1351
                $this->relationsDependencies[$attribute][$name] = $name;
1352
                if ($viaRelationName !== null) {
1353 22
                    $this->relationsDependencies[$attribute][] = $viaRelationName;
1354 22
                }
1355
            }
1356 9
        } elseif ($relation->getVia() instanceof ActiveQueryInterface) {
1357
            $this->setRelationDependencies($name, $relation->getVia());
1358
        } elseif (is_array($relation->getVia())) {
1359
            [$viaRelationName, $viaQuery] = $relation->getVia();
1360
            $this->setRelationDependencies($name, $viaQuery, $viaRelationName);
1361 9
        }
1362 9
    }
1363
1364 9
    /**
1365
     * Returns attribute values.
1366
     *
1367
     * @param array|null $names list of attributes whose value needs to be returned. Defaults to null, meaning all
1368
     * attributes listed in {@see attributes()} will be returned. If it is an array, only the attributes in the array
1369
     * will be returned.
1370
     * @param array $except list of attributes whose value should NOT be returned.
1371 9
     *
1372
     * @return array attribute values (name => value).
1373
     */
1374 9
    public function getAttributes(?array $names = null, array $except = []): array
1375
    {
1376
        $values = [];
1377
1378 9
        if ($names === null) {
1379 9
            $names = $this->attributes();
1380
        }
1381
1382
        foreach ($names as $name) {
1383
            $values[$name] = $this->$name;
1384
        }
1385
1386
        foreach ($except as $name) {
1387
            unset($values[$name]);
1388 76
        }
1389
1390 76
        return $values;
1391
    }
1392 76
1393 73
    /**
1394
     * Sets the attribute values in a massive way.
1395
     *
1396 29
     * @param array $values attribute values (name => value) to be assigned to the model.
1397
     *
1398
     * {@see attributes()}
1399
     */
1400
    public function setAttributes(array $values): void
1401
    {
1402
        foreach ($values as $name => $value) {
1403
            if (in_array($name, $this->attributes(), true)) {
1404
                $this->$name = $value;
1405
            }
1406
        }
1407
    }
1408
}
1409