Redis   F
last analyzed

Complexity

Total Complexity 72

Size/Duplication

Total Lines 615
Duplicated Lines 18.54 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 72
lcom 1
cbo 2
dl 114
loc 615
rs 2.625
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
A get() 0 28 3
B getMulti() 0 43 7
A set() 31 31 2
B setMulti() 5 46 7
A delete() 0 4 1
A deleteMulti() 0 22 3
A add() 0 32 3
C replace() 14 80 9
A cas() 14 43 4
A increment() 0 14 5
A decrement() 12 12 3
A touch() 11 11 2
A flush() 0 4 1
A getCollection() 0 38 5
A ttl() 13 13 3
B doIncrement() 14 66 9
A getVersion() 0 9 2
A supportsOptionsArray() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Redis 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Redis, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace MatthiasMullie\Scrapbook\Adapters;
4
5
use MatthiasMullie\Scrapbook\Adapters\Collections\Redis as Collection;
6
use MatthiasMullie\Scrapbook\Exception\InvalidCollection;
7
use MatthiasMullie\Scrapbook\KeyValueStore;
8
9
/**
10
 * Redis adapter. Basically just a wrapper over \Redis, but in an exchangeable
11
 * (KeyValueStore) interface.
12
 *
13
 * @author Matthias Mullie <[email protected]>
14
 * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved
15
 * @license LICENSE MIT
16
 */
17
class Redis implements KeyValueStore
18
{
19
    /**
20
     * @var \Redis
21
     */
22
    protected $client;
23
24
    /**
25
     * @var string|null
26
     */
27
    protected $version;
28
29
    /**
30
     * @param \Redis $client
31
     */
32
    public function __construct(\Redis $client)
33
    {
34
        $this->client = $client;
35
36
        // set a serializer if none is set already
37
        if ($this->client->getOption(\Redis::OPT_SERIALIZER) == \Redis::SERIALIZER_NONE) {
38
            $this->client->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_PHP);
39
        }
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function get($key, &$token = null)
46
    {
47
        $this->client->multi();
48
49
        $this->client->get($key);
50
        $this->client->exists($key);
51
52
        /** @var array $return */
53
        $return = $this->client->exec();
54
        if ($return === false) {
55
            return false;
56
        }
57
58
        $value = $return[0];
59
        $exists = $return[1];
60
61
        // no value = quit early, don't generate a useless token
62
        if (!$exists) {
63
            $token = null;
64
65
            return false;
66
        }
67
68
        // serializing to make sure we don't pass objects (by-reference) ;)
69
        $token = serialize($value);
70
71
        return $value;
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function getMulti(array $keys, array &$tokens = null)
78
    {
79
        $tokens = array();
80
        if (empty($keys)) {
81
            return array();
82
        }
83
84
        $this->client->multi();
85
86
        $this->client->mget($keys);
87
        foreach ($keys as $key) {
88
            $this->client->exists($key);
89
        }
90
91
        /** @var array $return */
92
        $return = $this->client->exec();
93
        if ($return === false) {
94
            return array();
95
        }
96
97
        $values = array_shift($return);
98
        $exists = $return;
99
100
        if ($values === false) {
101
            $values = array_fill_keys($keys, false);
102
        }
103
        $values = array_combine($keys, $values);
104
        $exists = array_combine($keys, $exists);
105
106
        $tokens = array();
107
        foreach ($values as $key => $value) {
108
            // filter out non-existing values
109
            if ($exists[$key] === false) {
110
                unset($values[$key]);
111
                continue;
112
            }
113
114
            // serializing to make sure we don't pass objects (by-reference) ;)
115
            $tokens[$key] = serialize($value);
116
        }
117
118
        return $values;
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 View Code Duplication
    public function set($key, $value, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
125
    {
126
        $ttl = $this->ttl($expire);
127
128
        /*
129
         * Negative ttl behavior isn't properly documented & doesn't always
130
         * appear to treat the value as non-existing. Let's play safe and just
131
         * delete it right away!
132
         */
133
        if ($ttl < 0) {
134
            $this->delete($key);
135
136
            return true;
137
        }
138
139
        /*
140
         * phpredis advises: "Calling setex() is preferred if you want a Time To
141
         * Live". It seems that setex it what set will fall back to if you pass
142
         * it a TTL anyway.
143
         * Redis advises: "Note: Since the SET command options can replace
144
         * SETNX, SETEX, PSETEX, it is possible that in future versions of Redis
145
         * these three commands will be deprecated and finally removed."
146
         * I'll just go with set() - it works and seems the desired path for the
147
         * future.
148
         *
149
         * @see https://github.com/ukko/phpredis-phpdoc/blob/master/src/Redis.php#L190
150
         * @see https://github.com/phpredis/phpredis#set
151
         * @see http://redis.io/commands/SET
152
         */
153
        return $this->client->set($key, $value, $ttl);
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159
    public function setMulti(array $items, $expire = 0)
160
    {
161
        if (empty($items)) {
162
            return array();
163
        }
164
165
        $ttl = $this->ttl($expire);
166
167
        /*
168
         * Negative ttl behavior isn't properly documented & doesn't always
169
         * appear to treat the value as non-existing. Let's play safe and just
170
         * delete it right away!
171
         */
172 View Code Duplication
        if ($ttl < 0) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
173
            $this->deleteMulti(array_keys($items));
174
175
            return array_fill_keys(array_keys($items), true);
176
        }
177
178
        if ($ttl === null) {
179
            $success = $this->client->mset($items);
180
181
            return array_fill_keys(array_keys($items), $success);
182
        }
183
184
        $this->client->multi();
185
        $this->client->mset($items);
186
187
        // Redis has no convenient multi-expire method
188
        foreach ($items as $key => $value) {
189
            $this->client->expire($key, $ttl);
190
        }
191
192
        /* @var bool[] $return */
193
        $result = (array) $this->client->exec();
194
195
        $return = array();
196
        $keys = array_keys($items);
197
        $success = array_shift($result);
198
        foreach ($result as $i => $value) {
199
            $key = $keys[$i];
200
            $return[$key] = $success && $value;
201
        }
202
203
        return $return;
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209
    public function delete($key)
210
    {
211
        return (bool) $this->client->del($key);
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217
    public function deleteMulti(array $keys)
218
    {
219
        if (empty($keys)) {
220
            return array();
221
        }
222
223
        /*
224
         * del will only return the amount of deleted entries, but we also want
225
         * to know which failed. Deletes will only fail for items that don't
226
         * exist, so we'll just ask for those and see which are missing.
227
         */
228
        $items = $this->getMulti($keys);
229
230
        $this->client->del($keys);
231
232
        $return = array();
233
        foreach ($keys as $key) {
234
            $return[$key] = array_key_exists($key, $items);
235
        }
236
237
        return $return;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243
    public function add($key, $value, $expire = 0)
244
    {
245
        $ttl = $this->ttl($expire);
246
247
        /*
248
         * Negative ttl behavior isn't properly documented & doesn't always
249
         * appear to treat the value as non-existing. Let's play safe and not
250
         * even create the value (also saving a request)
251
         */
252
        if ($ttl < 0) {
253
            return true;
254
        }
255
256
        if ($ttl === null) {
257
            return $this->client->setnx($key, $value);
258
        }
259
260
        /*
261
         * I could use Redis 2.6.12-style options array:
262
         * $this->client->set($key, $value, array('xx', 'ex' => $ttl));
263
         * However, this one should be pretty fast already, compared to the
264
         * replace-workaround below.
265
         */
266
        $this->client->multi();
267
        $this->client->setnx($key, $value);
268
        $this->client->expire($key, $ttl);
269
270
        /** @var bool[] $return */
271
        $return = (array) $this->client->exec();
272
273
        return !in_array(false, $return);
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    public function replace($key, $value, $expire = 0)
280
    {
281
        $ttl = $this->ttl($expire);
282
283
        /*
284
         * Negative ttl behavior isn't properly documented & doesn't always
285
         * appear to treat the value as non-existing. Let's play safe and just
286
         * delete it right away!
287
         */
288
        if ($ttl < 0) {
289
            return $this->delete($key);
290
        }
291
292
        /*
293
         * Redis supports passing set() an extended options array since >=2.6.12
294
         * which allows for an easy and 1-request way to replace a value.
295
         * That version already comes with Ubuntu 14.04. Ubuntu 12.04 (still
296
         * widely used and in LTS) comes with an older version, however, so I
297
         * want to support that too.
298
         * Supporting both versions comes at a cost.
299
         * I'll optimize for recent versions, which will get (in case of replace
300
         * failure) 1 additional network request (for version info). Older
301
         * versions will get 2 additional network requests: a failed replace
302
         * (because the options are unknown) & a version check.
303
         */
304
        if ($this->version === null || $this->supportsOptionsArray()) {
305
            $options = array('xx');
306
            if ($ttl > 0) {
307
                /*
308
                 * Not adding 0 TTL to options:
309
                 * * HHVM (used to) interpret(s) wrongly & throw an exception
310
                 * * it's not needed anyway, for 0...
311
                 * @see https://github.com/facebook/hhvm/pull/4833
312
                 */
313
                $options['ex'] = $ttl;
314
            }
315
316
            // either we support options array or we haven't yet checked, in
317
            // which case I'll assume a recent server is running
318
            $result = $this->client->set($key, $value, $options);
319
            if ($result !== false) {
320
                return $result;
321
            }
322
323
            if ($this->supportsOptionsArray()) {
324
                // failed execution, but not because our Redis version is too old
325
                return false;
326
            }
327
        }
328
329
        // workaround for old Redis versions
330
        $this->client->watch($key);
331
332
        $exists = $this->client->exists('key');
333 View Code Duplication
        if (!$exists) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
334
            /*
335
             * HHVM Redis only got unwatch recently
336
             * @see https://github.com/asgrim/hhvm/commit/bf5a259cece5df8a7617133c85043608d1ad5316
337
             */
338
            if (method_exists($this->client, 'unwatch')) {
339
                $this->client->unwatch();
340
            } else {
341
                // this should also kill the watch...
342
                $this->client->multi()->discard();
343
            }
344
345
            return false;
346
        }
347
348
        // since we're watching the key, this will fail should it change in the
349
        // meantime
350
        $this->client->multi();
351
352
        $this->client->set($key, $value, $ttl);
353
354
        /** @var bool[] $return */
355
        $return = (array) $this->client->exec();
356
357
        return !in_array(false, $return);
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     */
363
    public function cas($token, $key, $value, $expire = 0)
364
    {
365
        $this->client->watch($key);
366
367
        // check if the value still matches CAS token
368
        $comparison = $this->client->get($key);
369 View Code Duplication
        if (serialize($comparison) !== $token) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
370
            /*
371
             * HHVM Redis only got unwatch recently
372
             * @see https://github.com/asgrim/hhvm/commit/bf5a259cece5df8a7617133c85043608d1ad5316
373
             */
374
            if (method_exists($this->client, 'unwatch')) {
375
                $this->client->unwatch();
376
            } else {
377
                // this should also kill the watch...
378
                $this->client->multi()->discard();
379
            }
380
381
            return false;
382
        }
383
384
        $ttl = $this->ttl($expire);
385
386
        // since we're watching the key, this will fail should it change in the
387
        // meantime
388
        $this->client->multi();
389
390
        /*
391
         * Negative ttl behavior isn't properly documented & doesn't always
392
         * appear to treat the value as non-existing. Let's play safe and just
393
         * delete it right away!
394
         */
395
        if ($ttl < 0) {
396
            $this->client->del($key);
397
        } else {
398
            $this->client->set($key, $value, $ttl);
399
        }
400
401
        /** @var bool[] $return */
402
        $return = (array) $this->client->exec();
403
404
        return !in_array(false, $return);
405
    }
406
407
    /**
408
     * {@inheritdoc}
409
     */
410
    public function increment($key, $offset = 1, $initial = 0, $expire = 0)
411
    {
412
        if ($offset <= 0 || $initial < 0) {
413
            return false;
414
        }
415
416
        // INCRBY initializes (at 0) & immediately increments, whereas we
417
        // only do initialization if the value does not yet exist
418
        if ($initial + $offset === 0 && $expire === 0) {
419
            return $this->client->incrBy($key, $offset);
420
        }
421
422
        return $this->doIncrement($key, $offset, $initial, $expire);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->doIncrement($key,...et, $initial, $expire); of type boolean|integer|double adds the type double to the return on line 422 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::increment of type integer|boolean.
Loading history...
423
    }
424
425
    /**
426
     * {@inheritdoc}
427
     */
428 View Code Duplication
    public function decrement($key, $offset = 1, $initial = 0, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
429
    {
430
        if ($offset <= 0 || $initial < 0) {
431
            return false;
432
        }
433
434
        // DECRBY can't be used. Not even if we don't need an initial
435
        // value (it auto-initializes at 0) or expire. Problem is it
436
        // will decrement below 0, which is something we don't support.
437
438
        return $this->doIncrement($key, -$offset, $initial, $expire);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression $this->doIncrement($key,...et, $initial, $expire); of type boolean|integer|double adds the type double to the return on line 438 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::decrement of type integer|boolean.
Loading history...
439
    }
440
441
    /**
442
     * {@inheritdoc}
443
     */
444 View Code Duplication
    public function touch($key, $expire)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
445
    {
446
        $ttl = $this->ttl($expire);
447
448
        if ($ttl < 0) {
449
            // Redis can't set expired, so just remove in that case ;)
450
            return (bool) $this->client->del($key);
451
        }
452
453
        return $this->client->expire($key, $ttl);
454
    }
455
456
    /**
457
     * {@inheritdoc}
458
     */
459
    public function flush()
460
    {
461
        return $this->client->flushAll();
462
    }
463
464
    /**
465
     * {@inheritdoc}
466
     */
467
    public function getCollection($name)
468
    {
469
        if (!is_numeric($name)) {
470
            throw new InvalidCollection(
471
                'Redis database names must be numeric. '.serialize($name).' given.'
472
            );
473
        }
474
475
        // we can't reuse $this->client in a different object, because it'll
476
        // operate on a different database
477
        $client = new \Redis();
478
479
        if ($this->client->getPersistentID() !== null) {
0 ignored issues
show
Bug introduced by Matthias Mullie
The method getPersistentID() does not exist on Redis. Did you maybe mean persist()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
480
            $client->pconnect(
481
                $this->client->getHost(),
482
                $this->client->getPort(),
483
                $this->client->getTimeout()
484
            );
485
        } else {
486
            $client->connect(
487
                $this->client->getHost(),
488
                $this->client->getPort(),
489
                $this->client->getTimeout()
490
            );
491
        }
492
493
        $auth = $this->client->getAuth();
494
        if ($auth !== null) {
495
            $client->auth($auth);
496
        }
497
498
        $readTimeout = $this->client->getReadTimeout();
499
        if ($readTimeout) {
500
            $client->setOption(\Redis::OPT_READ_TIMEOUT, $this->client->getReadTimeout());
501
        }
502
503
        return new Collection($client, $name);
504
    }
505
506
    /**
507
     * Redis expects true TTL, not expiration timestamp.
508
     *
509
     * @param int $expire
510
     *
511
     * @return int|null TTL in seconds, or `null` for no expiration
512
     */
513 View Code Duplication
    protected function ttl($expire)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
514
    {
515
        if ($expire === 0) {
516
            return null;
517
        }
518
519
        // relative time in seconds, <30 days
520
        if ($expire > 30 * 24 * 60 * 60) {
521
            return $expire - time();
522
        }
523
524
        return $expire;
525
    }
526
527
    /**
528
     * Shared between increment/decrement: both have mostly the same logic
529
     * (decrement just increments a negative value), but need their validation
530
     * & use of non-ttl native methods split up.
531
     *
532
     * @param string $key
533
     * @param int    $offset
534
     * @param int    $initial
535
     * @param int    $expire
536
     *
537
     * @return int|bool
538
     */
539
    protected function doIncrement($key, $offset, $initial, $expire)
540
    {
541
        $ttl = $this->ttl($expire);
542
543
        $this->client->watch($key);
544
545
        $value = $this->client->get($key);
546
547
        if ($value === false) {
548
            /*
549
             * Negative ttl behavior isn't properly documented & doesn't always
550
             * appear to treat the value as non-existing. Let's play safe and not
551
             * even create the value (also saving a request)
552
             */
553
            if ($ttl < 0) {
554
                return true;
555
            }
556
557
            // value is not yet set, store initial value!
558
            $this->client->multi();
559
            $this->client->set($key, $initial, $ttl);
560
561
            /** @var bool[] $return */
562
            $return = (array) $this->client->exec();
563
564
            return !in_array(false, $return) ? $initial : false;
565
        }
566
567
        // can't increment if a non-numeric value is set
568 View Code Duplication
        if (!is_numeric($value) || $value < 0) {
0 ignored issues
show
Duplication introduced by Matthias Mullie
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
569
            /*
570
             * HHVM Redis only got unwatch recently.
571
             * @see https://github.com/asgrim/hhvm/commit/bf5a259cece5df8a7617133c85043608d1ad5316
572
             */
573
            if (method_exists($this->client, 'unwatch')) {
574
                $this->client->unwatch();
575
            } else {
576
                // this should also kill the watch...
577
                $this->client->multi()->discard();
578
            }
579
580
            return false;
581
        }
582
583
        $value += $offset;
584
        // value can never be lower than 0
585
        $value = max(0, $value);
586
587
        $this->client->multi();
588
589
        /*
590
         * Negative ttl behavior isn't properly documented & doesn't always
591
         * appear to treat the value as non-existing. Let's play safe and just
592
         * delete it right away!
593
         */
594
        if ($ttl < 0) {
595
            $this->client->del($key);
596
        } else {
597
            $this->client->set($key, $value, $ttl);
598
        }
599
600
        /** @var bool[] $return */
601
        $return = (array) $this->client->exec();
602
603
        return !in_array(false, $return) ? $value : false;
604
    }
605
606
    /**
607
     * Returns the version of the Redis server we're connecting to.
608
     *
609
     * @return string
610
     */
611
    protected function getVersion()
612
    {
613
        if ($this->version === null) {
614
            $info = $this->client->info();
615
            $this->version = $info['redis_version'];
616
        }
617
618
        return $this->version;
619
    }
620
621
    /**
622
     * Version-based check to test if passing an options array to set() is
623
     * supported.
624
     *
625
     * @return bool
626
     */
627
    protected function supportsOptionsArray()
628
    {
629
        return version_compare($this->getVersion(), '2.6.12') >= 0;
630
    }
631
}
632