Passed
Push — master ( 91edba...d01bf5 )
by Wilmer
02:58
created

ActiveRecord::updateAll()   C

Complexity

Conditions 12
Paths 58

Size

Total Lines 68
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 12.0025

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 39
nc 58
nop 3
dl 0
loc 68
ccs 37
cts 38
cp 0.9737
crap 12.0025
rs 6.9666
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord\Redis;
6
7
use JsonException;
8
use Yiisoft\ActiveRecord\BaseActiveRecord;
9
use Yiisoft\Db\Exception\InvalidArgumentException;
10
use Yiisoft\Db\Exception\InvalidConfigException;
11
use Yiisoft\Strings\Inflector;
12
use Yiisoft\Strings\StringHelper;
13
14
use function count;
15
use function ctype_alnum;
16
use function end;
17
use function implode;
18
use function is_array;
19
use function is_numeric;
20
use function is_string;
21
use function json_encode;
22
use function ksort;
23
use function md5;
24
use function reset;
25
26
/**
27
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
28
 *
29
 * This class implements the ActiveRecord pattern for the [redis](http://redis.io/) key-value store.
30
 *
31
 * For defining a record a subclass should at least implement the {@see attributes()} method to define attributes. A
32
 * primary key can be defined via {@see primaryKey()} which defaults to `id` if not specified.
33
 *
34
 * The following is an example model called `Customer`:
35
 *
36
 * ```php
37
 * class Customer extends \Yiisoft\Db\Redis\ActiveRecord
38
 * {
39
 *     public function attributes()
40
 *     {
41
 *         return ['id', 'name', 'address', 'registration_date'];
42
 *     }
43
 * }
44
 * ```
45
 */
46
class ActiveRecord extends BaseActiveRecord
47
{
48
    /**
49
     * @return ActiveQuery the newly created {@see ActiveQuery} instance.
50
     */
51 49
    public static function find(): ActiveQuery
52
    {
53 49
        return new ActiveQuery(static::class);
54
    }
55
56
    /**
57
     * Returns the primary key name(s) for this AR class.
58
     *
59
     * This method should be overridden by child classes to define the primary key. Note that an array should be
60
     * returned even when it is a single primary key.
61
     *
62
     * @return array the primary keys of this record.
63
     */
64 68
    public static function primaryKey(): array
65
    {
66 68
        return ['id'];
67
    }
68
69
    /**
70
     * Returns the list of all attribute names of the model.
71
     *
72
     * This method must be overridden by child classes to define available attributes.
73
     *
74
     * @throws InvalidConfigException
75
     *
76
     * @return array list of attribute names.
77
     */
78
    public function attributes(): array
79
    {
80
        throw new InvalidConfigException(
81
            'The attributes() method of redis ActiveRecord has to be implemented by child classes.'
82
        );
83
    }
84
85
    /**
86
     * Declares prefix of the key that represents the keys that store this records in redis.
87
     *
88
     * By default this method returns the class name as the table name by calling {@see Inflector->pascalCaseToId()}.
89
     * For example, 'Customer' becomes 'customer', and 'OrderItem' becomes 'order_item'. You may override this method
90
     * if you want different key naming.
91
     *
92
     * @return string the prefix to apply to all AR keys
93
     */
94 74
    public static function keyPrefix(): string
95
    {
96 74
        return (new Inflector())->pascalCaseToId(StringHelper::basename(static::class), '_');
97
    }
98
99
    /**
100
     * Inserts the record into the database using the attribute values of this record.
101
     *
102
     * Usage example:
103
     *
104
     * ```php
105
     * $customer = new Customer();
106
     * $customer->setAttribute('name', $name);
107
     * $customer->setAttribute('email', $email);
108
     * $customer->insert();
109
     * ```
110
     *
111
     * @param array|null $attributes list of attributes that need to be saved. Defaults to `null`, meaning all attributes
112
     * that are loaded from DB will be saved.
113
     *
114
     * @throws JsonException|InvalidArgumentException
115
     *
116
     * @return bool whether the attributes are valid and the record is inserted successfully.
117
     */
118 68
    public function insert(?array $attributes = null): bool
119
    {
120 68
        $db = static::getConnection();
121
122 68
        $values = $this->getDirtyAttributes($attributes);
123
124 68
        $pk = [];
125
126 68
        foreach ($this->primaryKey() as $key) {
127 68
            $pk[$key] = $values[$key] = $this->getAttribute($key);
128 68
            if ($pk[$key] === null) {
129
                /** use auto increment if pk is null */
130 65
                $pk[$key] = $values[$key] = $db->executeCommand(
0 ignored issues
show
Bug introduced by
The method executeCommand() does not exist on Yiisoft\Db\Connection\ConnectionInterface. It seems like you code against a sub-type of Yiisoft\Db\Connection\ConnectionInterface such as Yiisoft\Db\Redis\Connection. ( Ignorable by Annotation )

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

130
                $pk[$key] = $values[$key] = $db->/** @scrutinizer ignore-call */ executeCommand(
Loading history...
131 65
                    'INCR',
132 65
                    [static::keyPrefix() . ':s:' . $key]
133
                );
134
135 65
                $this->setAttribute($key, $values[$key]);
136 21
            } elseif (is_numeric($pk[$key])) {
137
                /** if pk is numeric update auto increment value */
138 21
                $currentPk = $db->executeCommand('GET', [static::keyPrefix() . ':s:' . $key]);
139
140 21
                if ($pk[$key] > $currentPk) {
141 21
                    $db->executeCommand('SET', [static::keyPrefix() . ':s:' . $key, $pk[$key]]);
142
                }
143
            }
144
        }
145
146
        /** save pk in a `findAll()` pool */
147 68
        $pk = static::buildKey($pk);
148
149 68
        $db->executeCommand('RPUSH', [static::keyPrefix(), $pk]);
150
151 68
        $key = static::keyPrefix() . ':a:' . $pk;
152
153
        /** save attributes */
154 68
        $setArgs = [$key];
155
156 68
        foreach ($values as $attribute => $value) {
157
            /** only insert attributes that are not null */
158 68
            if ($value !== null) {
159 68
                if (is_bool($value)) {
160 1
                    $value = (int) $value;
161
                }
162 68
                $setArgs[] = $attribute;
163 68
                $setArgs[] = $value;
164
            }
165
        }
166
167 68
        if (count($setArgs) > 1) {
168 68
            $db->executeCommand('HMSET', $setArgs);
169
        }
170
171 68
        $this->setOldAttributes($values);
172
173 68
        return true;
174
    }
175
176
    /**
177
     * Updates the whole table using the provided attribute values and conditions.
178
     *
179
     * For example, to change the status to be 1 for all customers whose status is 2:
180
     *
181
     * ```php
182
     * Customer::updateAll(['status' => 1], ['id' => 2]);
183
     * ```
184
     *
185
     * @param array $attributes attribute values (name-value pairs) to be saved into the table.
186
     * @param array|string $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
187
     * Please refer to {@see ActiveQuery::where()} on how to specify this parameter.
188
     * @param array $params
189
     *
190
     * @throws JsonException
191
     *
192
     * @return int the number of rows updated.
193
     */
194 9
    public static function updateAll(array $attributes, $condition = [], array $params = []): int
195
    {
196 9
        $db = static::getConnection();
197
198 9
        if (empty($attributes)) {
199
            return 0;
200
        }
201
202 9
        $n = 0;
203
204 9
        foreach (self::fetchPks($condition) as $pk) {
0 ignored issues
show
Bug introduced by
It seems like $condition can also be of type string; however, parameter $condition of Yiisoft\ActiveRecord\Red...ctiveRecord::fetchPks() does only seem to accept array|null, 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

204
        foreach (self::fetchPks(/** @scrutinizer ignore-type */ $condition) as $pk) {
Loading history...
205 9
            $newPk = $pk;
206 9
            $pk = static::buildKey($pk);
207 9
            $key = static::keyPrefix() . ':a:' . $pk;
208
209
            /** save attributes */
210 9
            $delArgs = [$key];
211 9
            $setArgs = [$key];
212
213 9
            foreach ($attributes as $attribute => $value) {
214 9
                if (isset($newPk[$attribute])) {
215 2
                    $newPk[$attribute] = $value;
216
                }
217
218 9
                if ($value !== null) {
219 7
                    if (is_bool($value)) {
220 1
                        $value = (int) $value;
221
                    }
222 7
                    $setArgs[] = $attribute;
223 7
                    $setArgs[] = $value;
224
                } else {
225 5
                    $delArgs[] = $attribute;
226
                }
227
            }
228
229 9
            $newPk = static::buildKey($newPk);
230 9
            $newKey = static::keyPrefix() . ':a:' . $newPk;
231
232
            /** rename index if pk changed */
233 9
            if ($newPk !== $pk) {
234 2
                $db->executeCommand('MULTI');
235
236 2
                if (count($setArgs) > 1) {
237 1
                    $db->executeCommand('HMSET', $setArgs);
238
                }
239
240 2
                if (count($delArgs) > 1) {
241 1
                    $db->executeCommand('HDEL', $delArgs);
242
                }
243
244 2
                $db->executeCommand('LINSERT', [static::keyPrefix(), 'AFTER', $pk, $newPk]);
245 2
                $db->executeCommand('LREM', [static::keyPrefix(), 0, $pk]);
246 2
                $db->executeCommand('RENAME', [$key, $newKey]);
247 2
                $db->executeCommand('EXEC');
248
            } else {
249 8
                if (count($setArgs) > 1) {
250 6
                    $db->executeCommand('HMSET', $setArgs);
251
                }
252
253 8
                if (count($delArgs) > 1) {
254 5
                    $db->executeCommand('HDEL', $delArgs);
255
                }
256
            }
257
258 9
            $n++;
259
        }
260
261 9
        return $n;
262
    }
263
264
    /**
265
     * Updates the whole table using the provided counter changes and conditions.
266
     *
267
     * For example, to increment all customers' age by 1,
268
     *
269
     * ```php
270
     * Customer::updateAllCounters(['age' => 1]);
271
     * ```
272
     *
273
     * @param array $counters the counters to be updated (attribute name => increment value). Use negative values if you
274
     * want to decrement the counters.
275
     * @param array|string $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
276
     * @param array $params
277
     *
278
     * @throws JsonException
279
     *
280
     * @return int the number of rows updated.
281
     *
282
     * Please refer to {@see ActiveQuery::where()} on how to specify this parameter.
283
     */
284 1
    public static function updateAllCounters(array $counters, $condition = '', array $params = []): int
285
    {
286 1
        if (empty($counters)) {
287
            return 0;
288
        }
289
290 1
        $n = 0;
291
292 1
        foreach (self::fetchPks($condition) as $pk) {
0 ignored issues
show
Bug introduced by
It seems like $condition can also be of type string; however, parameter $condition of Yiisoft\ActiveRecord\Red...ctiveRecord::fetchPks() does only seem to accept array|null, 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

292
        foreach (self::fetchPks(/** @scrutinizer ignore-type */ $condition) as $pk) {
Loading history...
293 1
            $key = static::keyPrefix() . ':a:' . static::buildKey($pk);
294 1
            foreach ($counters as $attribute => $value) {
295 1
                static::getConnection()->executeCommand('HINCRBY', [$key, $attribute, $value]);
296
            }
297 1
            $n++;
298
        }
299
300 1
        return $n;
301
    }
302
303
    /**
304
     * Deletes rows in the table using the provided conditions.
305
     *
306
     * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
307
     *
308
     * For example, to delete all customers whose status is 3:
309
     *
310
     * ```php
311
     * Customer::deleteAll(['status' => 3]);
312
     * ```
313
     *
314
     * @param array|null $condition the conditions that will be put in the WHERE part of the DELETE SQL.
315
     * @param array $params
316
     *
317
     * @throws JsonException
318
     *
319
     * @return int the number of rows deleted.
320
     *
321
     * Please refer to {@see ActiveQuery::where()} on how to specify this parameter.
322
     */
323 4
    public static function deleteAll(?array $condition = null, array $params = []): int
324
    {
325 4
        $db = static::getConnection();
326
327 4
        $pks = self::fetchPks($condition);
328
329 4
        if (empty($pks)) {
330 1
            return 0;
331
        }
332
333 4
        $attributeKeys = [];
334
335 4
        $db->executeCommand('MULTI');
336
337 4
        foreach ($pks as $pk) {
338 4
            $pk = static::buildKey($pk);
339 4
            $db->executeCommand('LREM', [static::keyPrefix(), 0, $pk]);
340 4
            $attributeKeys[] = static::keyPrefix() . ':a:' . $pk;
341
        }
342
343 4
        $db->executeCommand('DEL', $attributeKeys);
344
345 4
        $result = $db->executeCommand('EXEC');
346
347 4
        return (int) end($result);
348
    }
349
350 12
    private static function fetchPks(?array $condition = []): array
351
    {
352 12
        $query = static::find();
353
354 12
        $query->where($condition);
355
356
        /** limit fetched columns to pk */
357 12
        $records = $query->asArray()->all();
358
359 12
        $primaryKey = static::primaryKey();
360
361 12
        $pks = [];
362
363 12
        foreach ($records as $record) {
364 12
            $pk = [];
365
366 12
            foreach ($primaryKey as $key) {
367 12
                $pk[$key] = $record[$key];
368
            }
369
370 12
            $pks[] = $pk;
371
        }
372
373 12
        return $pks;
374
    }
375
376
    /**
377
     * Builds a normalized key from a given primary key value.
378
     *
379
     * @param mixed $key the key to be normalized.
380
     *
381
     * @throws JsonException
382
     *
383
     * @return string|int the generated key.
384
     */
385 68
    public static function buildKey($key)
386
    {
387 68
        if (is_numeric($key)) {
388 66
            return $key;
389
        }
390
391 68
        if (is_string($key)) {
392 7
            return ctype_alnum($key) && StringHelper::byteLength($key) <= 32 ? $key : md5($key);
393
        }
394
395 68
        if (is_array($key)) {
396 68
            if (count($key) === 1) {
397 66
                return self::buildKey(reset($key));
398
            }
399
400
            /** ensure order is always the same */
401 19
            ksort($key);
402
403 19
            $isNumeric = true;
404
405 19
            foreach ($key as $value) {
406 19
                if (!is_numeric($value)) {
407 19
                    $isNumeric = false;
408
                }
409
            }
410
411 19
            if ($isNumeric) {
412 19
                return implode('-', $key);
413
            }
414
        }
415
416 19
        return md5(json_encode($key, JSON_THROW_ON_ERROR | JSON_NUMERIC_CHECK));
417
    }
418
}
419