Passed
Push — master ( 75b09b...1404ee )
by Alexander
03:17
created

BaseActiveRecord   F

Complexity

Total Complexity 172

Size/Duplication

Total Lines 1327
Duplicated Lines 0 %

Test Coverage

Coverage 89.9%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 362
dl 0
loc 1327
ccs 347
cts 386
cp 0.899
rs 2
c 2
b 0
f 0
wmc 172

47 Methods

Rating   Name   Duplication   Size   Complexity  
A createRelationQuery() 0 3 1
B updateInternal() 0 38 7
A update() 0 3 1
A hasAttribute() 0 3 2
A getAttribute() 0 3 1
A hasOne() 0 3 1
A setIsNewRecord() 0 3 2
A populateRecord() 0 15 4
A updateAttributes() 0 26 6
A getPrimaryKey() 0 15 4
A optimisticLock() 0 3 1
A hasMany() 0 3 1
A updateCounters() 0 17 4
A populateRelation() 0 7 2
A getRelatedRecords() 0 3 1
A getOldAttribute() 0 3 1
A delete() 0 22 4
A getOldAttributes() 0 3 1
A __construct() 0 4 1
A refreshInternal() 0 15 3
B getDirtyAttributes() 0 27 9
A isAttributeChanged() 0 11 4
A save() 0 7 2
A equals() 0 7 4
A getIsNewRecord() 0 3 1
A updateAll() 0 3 1
A markAttributeDirty() 0 3 1
A setOldAttribute() 0 6 3
A setOldAttributes() 0 3 1
A isRelationPopulated() 0 3 1
A setAttribute() 0 12 5
A deleteAll() 0 3 1
A refresh() 0 6 1
A getOldPrimaryKey() 0 22 5
A updateAllCounters() 0 3 1
D link() 0 85 21
C unlinkAll() 0 82 16
A getAttributes() 0 17 4
D unlink() 0 84 21
B setRelationDependencies() 0 17 7
A isPrimaryKey() 0 9 2
A setAttributes() 0 5 3
A extraFields() 0 5 1
A resetDependentRelations() 0 7 2
A fields() 0 5 1
A bindModels() 0 23 4
A instantiateQuery() 0 7 2

How to fix   Complexity   

Complex Class

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

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

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

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
    private ?ActiveRecordFactory $arFactory;
61
    protected ConnectionInterface $db;
62
63 719
    public function __construct(ConnectionInterface $db, ActiveRecordFactory $arFactory = null)
64
    {
65 719
        $this->arFactory = $arFactory;
66 719
        $this->db = $db;
67 719
    }
68
69
    /**
70
     * Updates the whole table using the provided attribute values and conditions.
71
     *
72
     * For example, to change the status to be 1 for all customers whose status is 2:
73
     *
74
     * ```php
75
     * $customer = new Customer($db);
76
     * $customer->updateAll(['status' => 1], 'status = 2');
77
     * ```
78
     *
79
     * @param array $attributes attribute values (name-value pairs) to be saved into the table.
80
     * @param array|string|null $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
81
     * Please refer to {@see Query::where()} on how to specify this parameter.
82
     * @param array $params
83
     *
84
     * @throws NotSupportedException if not overridden.
85
     *
86
     * @return int the number of rows updated.
87
     */
88 1
    public function updateAll(array $attributes, $condition = null, array $params = []): int
89
    {
90 1
        throw new NotSupportedException(__METHOD__ . ' is not supported.');
91
    }
92
93
    /**
94
     * Updates the whole table using the provided counter changes and conditions.
95
     *
96
     * For example, to increment all customers' age by 1,
97
     *
98
     * ```php
99
     * Customer::updateAllCounters(['age' => 1]);
100
     * ```
101
     *
102
     * @param array $counters the counters to be updated (attribute name => increment value). Use negative values if you
103
     * want to decrement the counters.
104
     * @param array|string $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
105
     * Please refer to {@see Query::where()} on how to specify this parameter.
106
     *
107
     * @throws NotSupportedException if not override.
108
     *
109
     * @return int the number of rows updated.
110
     */
111 1
    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

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

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

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

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