Completed
Pull Request — master (#5)
by Michal
02:14
created

RedisProxy::type()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 16
ccs 11
cts 11
cp 1
rs 8.8571
cc 6
eloc 11
nc 5
nop 1
crap 6
1
<?php
2
3
namespace RedisProxy;
4
5
use Exception;
6
use Predis\Client;
7
use Predis\Response\Status;
8
use Redis;
9
10
/**
11
 * @method mixed config(string $command, $argument = null)
12
 * @method int dbsize() Return the number of keys in the selected database
13
 * @method boolean set(string $key, string $value) Set the string value of a key
14
 * @method array keys(string $pattern) Find all keys matching the given pattern
15
 * @method int hset(string $key, string $field, string $value) Set the string value of a hash field
16
 * @method array hkeys(string $key) Get all fields in a hash (without values)
17
 * @method array hgetall(string $key) Get all fields and values in a hash
18
 * @method int hlen(string $key) Get the number of fields in a hash
19
 * @method array smembers(string $key) Get all the members in a set
20
 * @method int scard(string $key) Get the number of members in a set
21
 * @method int llen(string $key) Get the length of a list
22
 * @method array lrange(string $key, int $start, int $stop) Get a range of elements from a list
23
 * @method boolean flushall() Remove all keys from all databases
24
 * @method boolean flushdb() Remove all keys from the current database
25
 */
26
class RedisProxy
27
{
28
    const DRIVER_REDIS = 'redis';
29
30
    const DRIVER_PREDIS = 'predis';
31
32
    const TYPE_STRING = 'string';
33
34
    const TYPE_SET = 'set';
35
36
    const TYPE_HASH = 'hash';
37
38
    const TYPE_LIST = 'list';
39
40
    const TYPE_SORTED_SET = 'sorted_set';
41
42
    private $driver;
43
44
    private $host;
45
46
    private $port;
47
48
    private $database = 0;
49
50
    private $selectedDatabase = 0;
51
52
    private $timeout;
53
54
    private $supportedDrivers = [
55
        self::DRIVER_REDIS,
56
        self::DRIVER_PREDIS,
57
    ];
58
59
    private $driversOrder = [];
60
61
    private $redisTypeMap = [
62
        Redis::REDIS_STRING => self::TYPE_STRING,
63
        Redis::REDIS_SET => self::TYPE_SET,
64
        Redis::REDIS_HASH => self::TYPE_HASH,
65
        Redis::REDIS_LIST => self::TYPE_LIST,
66
        Redis::REDIS_ZSET => self::TYPE_SORTED_SET,
67
    ];
68
69
    private $predisTypeMap = [
70
        'string' => self::TYPE_STRING,
71
        'set' => self::TYPE_SET,
72
        'hash' => self::TYPE_HASH,
73
        'list' => self::TYPE_LIST,
74
        'zset' => self::TYPE_SORTED_SET,
75
    ];
76
77 196
    public function __construct($host, $port, $database = 0, $timeout = null)
78
    {
79 196
        $this->host = $host;
80 196
        $this->port = $port;
81 196
        $this->database = $database;
82 196
        $this->timeout = $timeout;
83 196
        $this->driversOrder = $this->supportedDrivers;
84 196
    }
85
86
    /**
87
     * Set driver priorities - default is 1. redis, 2. predis
88
     * @param array $driversOrder
89
     * @return RedisProxy
90
     * @throws RedisProxyException if some driver is not supported
91
     */
92 196
    public function setDriversOrder(array $driversOrder)
93
    {
94 196
        foreach ($driversOrder as $driver) {
95 194
            if (!in_array($driver, $this->supportedDrivers)) {
96 194
                throw new RedisProxyException('Driver "' . $driver . '" is not supported');
97
            }
98
        }
99 194
        $this->driversOrder = $driversOrder;
100 194
        return $this;
101
    }
102
103 194
    private function init()
104
    {
105 194
        $this->prepareDriver();
106 192
        $this->select($this->database);
107 192
    }
108
109 194
    private function prepareDriver()
110
    {
111 194
        if ($this->driver !== null) {
112 192
            return;
113
        }
114
115 194
        foreach ($this->driversOrder as $preferredDriver) {
116 192
            if ($preferredDriver === self::DRIVER_REDIS && extension_loaded('redis')) {
117 96
                $this->driver = new Redis();
118 96
                return;
119
            }
120 96
            if ($preferredDriver === self::DRIVER_PREDIS && class_exists('Predis\Client')) {
121 96
                $this->driver = new Client();
122 96
                return;
123
            }
124
        }
125 2
        throw new RedisProxyException('No driver available');
126
    }
127
128
    /**
129
     * @return string|null
130
     */
131 194
    public function actualDriver()
132
    {
133 194
        if ($this->driver instanceof Redis) {
134 96
            return self::DRIVER_REDIS;
135
        }
136 102
        if ($this->driver instanceof Client) {
137 96
            return self::DRIVER_PREDIS;
138
        }
139 10
        return null;
140
    }
141
142 192
    private function connect($host, $port, $timeout = null)
143
    {
144 192
        return $this->driver->connect($host, $port, $timeout);
145
    }
146
147 192
    private function isConnected()
148
    {
149 192
        return $this->driver->isConnected();
150
    }
151
152 194
    public function __call($name, $arguments)
153
    {
154 194
        $this->init();
155 192
        $name = strtolower($name);
156
        try {
157 192
            $result = call_user_func_array([$this->driver, $name], $arguments);
158 4
        } catch (Exception $e) {
159 4
            throw new RedisProxyException("Error for command '$name', use getPrevious() for more info", 1484162284, $e);
160
        }
161 192
        return $this->transformResult($result);
162
    }
163
164
    /**
165
     * @param int $database
166
     * @return boolean true on success
167
     * @throws RedisProxyException on failure
168
     */
169 192
    public function select($database)
170
    {
171 192
        $this->prepareDriver();
172 192
        if (!$this->isConnected()) {
173 192
            $this->connect($this->host, $this->port, $this->timeout);
174
        }
175 192
        if ($database == $this->selectedDatabase) {
176 192
            return true;
177
        }
178
        try {
179 12
            $result = $this->driver->select($database);
180 2
        } catch (Exception $e) {
181 2
            throw new RedisProxyException('Invalid DB index');
182
        }
183 10
        $result = $this->transformResult($result);
184 10
        if ($result === false) {
185 2
            throw new RedisProxyException('Invalid DB index');
186
        }
187 8
        $this->database = $database;
188 8
        $this->selectedDatabase = $database;
189 8
        return $result;
190
    }
191
192
    /**
193
     * @param string $key
194
     * @return string|null
195
     */
196 4
    public function type($key)
197
    {
198 4
        $this->init();
199 4
        $result = $this->driver->type($key);
200 4
        if ($this->actualDriver() === self::DRIVER_REDIS) {
201 2
            if (isset($this->redisTypeMap[$result])) {
202 2
                return $this->redisTypeMap[$result];
203
            }
204 2
        } elseif ($this->actualDriver() === self::DRIVER_PREDIS && $result instanceof Status) {
205 2
            $result = $result->getPayload();
206 2
            if (isset($this->predisTypeMap[$result])) {
207 2
                return $this->predisTypeMap[$result];
208
            }
209
        }
210 4
        return null;
211
    }
212
213
    /**
214
     * @param string|null $section
215
     * @return array
216
     */
217 8
    public function info($section = null)
218
    {
219 8
        $this->init();
220 8
        $section = $section ? strtolower($section) : $section;
221 8
        $result = $section === null ? $this->driver->info() : $this->driver->info($section);
222
223 8
        $databases = $section === null || $section === 'keyspace' ? $this->config('get', 'databases')['databases'] : null;
224 8
        $groupedResult = InfoHelper::createInfoArray($this, $result, $databases);
225 8
        if ($section === null) {
226 4
            return $groupedResult;
227
        }
228 8
        if (isset($groupedResult[$section])) {
229 4
            return $groupedResult[$section];
230
        }
231 4
        throw new RedisProxyException('Info section "' . $section . '" doesn\'t exist');
232
    }
233
234
    /**
235
     * @param string $key
236
     * @return string|null null if hash field is not set
237
     */
238 40
    public function get($key)
239
    {
240 40
        $this->init();
241 40
        $result = $this->driver->get($key);
242 40
        return $this->convertFalseToNull($result);
243
    }
244
245
    /**
246
     * Delete a key(s)
247
     * @param array $keys
248
     * @return int number of deleted keys
249
     */
250 28
    public function del(...$keys)
251
    {
252 28
        $this->prepareArguments('del', ...$keys);
253 20
        $this->init();
254 20
        return $this->driver->del(...$keys);
255
    }
256
257
    /**
258
     * Delete a key(s)
259
     * @param array $keys
260
     * @return int number of deleted keys
261
     */
262 12
    public function delete(...$keys)
263
    {
264 12
        return $this->del(...$keys);
265
    }
266
267
    /**
268
     * Increment the integer value of a key by one
269
     * @param string $key
270
     * @return integer
271
     */
272 4
    public function incr($key)
273
    {
274 4
        $this->init();
275 4
        return $this->driver->incr($key);
276
    }
277
278
    /**
279
     * Increment the integer value of a key by the given amount
280
     * @param string $key
281
     * @param integer $increment
282
     * @return integer
283
     */
284 4
    public function incrby($key, $increment = 1)
285
    {
286 4
        $this->init();
287 4
        return $this->driver->incrby($key, (int)$increment);
288
    }
289
290
    /**
291
     * Increment the float value of a key by the given amount
292
     * @param string $key
293
     * @param float $increment
294
     * @return float
295
     */
296 8
    public function incrbyfloat($key, $increment = 1)
297
    {
298 8
        $this->init();
299 8
        return $this->driver->incrbyfloat($key, $increment);
300
    }
301
302
    /**
303
     * Decrement the integer value of a key by one
304
     * @param string $key
305
     * @return integer
306
     */
307 4
    public function decr($key)
308
    {
309 4
        $this->init();
310 4
        return $this->driver->decr($key);
311
    }
312
313
    /**
314
     * Decrement the integer value of a key by the given number
315
     * @param string $key
316
     * @param integer $decrement
317
     * @return integer
318
     */
319 4
    public function decrby($key, $decrement = 1)
320
    {
321 4
        $this->init();
322 4
        return $this->driver->decrby($key, (int)$decrement);
323
    }
324
325
    /**
326
     * Decrement the float value of a key by the given amount
327
     * @param string $key
328
     * @param float $decrement
329
     * @return float
330
     */
331 4
    public function decrbyfloat($key, $decrement = 1)
332
    {
333 4
        return $this->incrbyfloat($key, (-1) * $decrement);
334
    }
335
336
    /**
337
     * Set multiple values to multiple keys
338
     * @param array $dictionary
339
     * @return boolean true on success
340
     * @throws RedisProxyException if number of arguments is wrong
341
     */
342 12 View Code Duplication
    public function mset(...$dictionary)
343
    {
344 12
        $this->init();
345 12
        if (is_array($dictionary[0])) {
346 8
            $result = $this->driver->mset(...$dictionary);
347 8
            return $this->transformResult($result);
348
        }
349 8
        $dictionary = $this->prepareKeyValue($dictionary, 'mset');
350 4
        $result = $this->driver->mset($dictionary);
351 4
        return $this->transformResult($result);
352
    }
353
354
    /**
355
     * Multi get
356
     * @param array $keys
357
     * @return array Returns the values for all specified keys. For every key that does not hold a string value or does not exist, null is returned
358
     */
359 4
    public function mget(...$keys)
360
    {
361 4
        $keys = array_unique($this->prepareArguments('mget', ...$keys));
362 4
        $this->init();
363 4
        $values = [];
364 4
        foreach ($this->driver->mget($keys) as $value) {
365 4
            $values[] = $this->convertFalseToNull($value);
366
        }
367 4
        return array_combine($keys, $values);
368
    }
369
370
    /**
371
     * Incrementally iterate the keys space
372
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
373
     * @param string $pattern pattern for keys, use * as wild card
374
     * @param int $count
375
     * @return array|boolean|null list of found keys, returns null if $iterator is 0 or '0'
376
     */
377 4 View Code Duplication
    public function scan(&$iterator, $pattern = null, $count = null)
378
    {
379 4
        if ((string)$iterator === '0') {
380 4
            return null;
381
        }
382 4
        $this->init();
383 4
        if ($this->actualDriver() === self::DRIVER_PREDIS) {
384 2
            $returned = $this->driver->scan($iterator, ['match' => $pattern, 'count' => $count]);
385 2
            $iterator = $returned[0];
386 2
            return $returned[1];
387
        }
388 2
        return $this->driver->scan($iterator, $pattern, $count);
389
    }
390
391
    /**
392
     * Get the value of a hash field
393
     * @param string $key
394
     * @param string $field
395
     * @return string|null null if hash field is not set
396
     */
397 16
    public function hget($key, $field)
398
    {
399 16
        $this->init();
400 16
        $result = $this->driver->hget($key, $field);
401 16
        return $this->convertFalseToNull($result);
402
    }
403
404
    /**
405
     * Delete one or more hash fields, returns number of deleted fields
406
     * @param array $key
407
     * @param array $fields
408
     * @return int
409
     */
410 8
    public function hdel($key, ...$fields)
411
    {
412 8
        $fields = $this->prepareArguments('hdel', ...$fields);
413 8
        $this->init();
414 8
        return $this->driver->hdel($key, ...$fields);
415
    }
416
417
    /**
418
     * Increment the integer value of hash field by given number
419
     * @param string $key
420
     * @param string $field
421
     * @param int $increment
422
     * @return int
423
     */
424 4
    public function hincrby($key, $field, $increment = 1)
425
    {
426 4
        $this->init();
427 4
        return $this->driver->hincrby($key, $field, (int)$increment);
428
    }
429
430
    /**
431
     * Increment the float value of hash field by given amount
432
     * @param string $key
433
     * @param string $field
434
     * @param float $increment
435
     * @return float
436
     */
437 4
    public function hincrbyfloat($key, $field, $increment = 1)
438
    {
439 4
        $this->init();
440 4
        return $this->driver->hincrbyfloat($key, $field, $increment);
441
    }
442
443
    /**
444
     * Set multiple values to multiple hash fields
445
     * @param string $key
446
     * @param array $dictionary
447
     * @return boolean true on success
448
     * @throws RedisProxyException if number of arguments is wrong
449
     */
450 12 View Code Duplication
    public function hmset($key, ...$dictionary)
451
    {
452 12
        $this->init();
453 12
        if (is_array($dictionary[0])) {
454 8
            $result = $this->driver->hmset($key, ...$dictionary);
455 8
            return $this->transformResult($result);
456
        }
457 8
        $dictionary = $this->prepareKeyValue($dictionary, 'hmset');
458 4
        $result = $this->driver->hmset($key, $dictionary);
459 4
        return $this->transformResult($result);
460
    }
461
462
    /**
463
     * Multi hash get
464
     * @param string $key
465
     * @param array $fields
466
     * @return array Returns the values for all specified fields. For every field that does not hold a string value or does not exist, null is returned
467
     */
468 4
    public function hmget($key, ...$fields)
469
    {
470 4
        $fields = array_unique($this->prepareArguments('hmget', ...$fields));
471 4
        $this->init();
472 4
        $values = [];
473 4
        foreach ($this->driver->hmget($key, $fields) as $value) {
474 4
            $values[] = $this->convertFalseToNull($value);
475
        }
476 4
        return array_combine($fields, $values);
477
    }
478
479
    /**
480
     * Incrementally iterate hash fields and associated values
481
     * @param string $key
482
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
483
     * @param string $pattern pattern for fields, use * as wild card
484
     * @param int $count
485
     * @return array|boolean|null list of found fields with associated values, returns null if $iterator is 0 or '0'
486
     */
487 4 View Code Duplication
    public function hscan($key, &$iterator, $pattern = null, $count = null)
488
    {
489 4
        if ((string)$iterator === '0') {
490 4
            return null;
491
        }
492 4
        $this->init();
493 4
        if ($this->actualDriver() === self::DRIVER_PREDIS) {
494 2
            $returned = $this->driver->hscan($key, $iterator, ['match' => $pattern, 'count' => $count]);
495 2
            $iterator = $returned[0];
496 2
            return $returned[1];
497
        }
498 2
        return $this->driver->hscan($key, $iterator, $pattern, $count);
499
    }
500
501
    /**
502
     * Add one or more members to a set
503
     * @param string $key
504
     * @param array $members
505
     * @return int number of new members added to set
506
     */
507 16
    public function sadd($key, ...$members)
508
    {
509 16
        $members = $this->prepareArguments('sadd', ...$members);
510 16
        $this->init();
511 16
        return $this->driver->sadd($key, ...$members);
512
    }
513
514
    /**
515
     * Remove and return one or multiple random members from a set
516
     * @param string $key
517
     * @param int $count number of members
518
     * @return mixed string if $count is null or 1 and $key exists, array if $count > 1 and $key exists, null if $key doesn't exist
519
     */
520 4
    public function spop($key, $count = 1)
521
    {
522 4
        $this->init();
523 4
        if ($count == 1 || $count === null) {
524 4
            $result = $this->driver->spop($key);
525 4
            return $this->convertFalseToNull($result);
526
        }
527
528 4
        $members = [];
529 4
        for ($i = 0; $i < $count; ++$i) {
530 4
            $member = $this->driver->spop($key);
531 4
            if (!$member) {
532 4
                break;
533
            }
534 4
            $members[] = $member;
535
        }
536 4
        return empty($members) ? null : $members;
537
    }
538
539
    /**
540
     * Incrementally iterate Set elements
541
     * @param string $key
542
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
543
     * @param string $pattern pattern for member's values, use * as wild card
544
     * @param int $count
545
     * @return array|boolean|null list of found members, returns null if $iterator is 0 or '0'
546
     */
547 4 View Code Duplication
    public function sscan($key, &$iterator, $pattern = null, $count = null)
548
    {
549 4
        if ((string)$iterator === '0') {
550 4
            return null;
551
        }
552 4
        $this->init();
553 4
        if ($this->actualDriver() === self::DRIVER_PREDIS) {
554 2
            $returned = $this->driver->sscan($key, $iterator, ['match' => $pattern, 'count' => $count]);
555 2
            $iterator = $returned[0];
556 2
            return $returned[1];
557
        }
558 2
        return $this->driver->sscan($key, $iterator, $pattern, $count);
559
    }
560
561
    /**
562
     * Prepend one or multiple values to a list
563
     * @param string $key
564
     * @param array $elements
565
     * @return int the length of the list after the push operations
566
     */
567 28
    public function lpush($key, ...$elements)
568
    {
569 28
        $elements = $this->prepareArguments('lpush', ...$elements);
570 28
        $this->init();
571 28
        return $this->driver->lpush($key, ...$elements);
572
    }
573
574
    /**
575
     * Append one or multiple values to a list
576
     * @param string $key
577
     * @param array $elements
578
     * @return int the length of the list after the push operations
579
     */
580 12
    public function rpush($key, ...$elements)
581
    {
582 12
        $elements = $this->prepareArguments('rpush', ...$elements);
583 12
        $this->init();
584 12
        return $this->driver->rpush($key, ...$elements);
585
    }
586
587
    /**
588
     * Remove and get the first element in a list
589
     * @param string $key
590
     * @return string|null
591
     */
592 4
    public function lpop($key)
593
    {
594 4
        $this->init();
595 4
        $result = $this->driver->lpop($key);
596 4
        return $this->convertFalseToNull($result);
597
    }
598
599
    /**
600
     * Remove and get the last element in a list
601
     * @param string $key
602
     * @return string|null
603
     */
604 4
    public function rpop($key)
605
    {
606 4
        $this->init();
607 4
        $result = $this->driver->rpop($key);
608 4
        return $this->convertFalseToNull($result);
609
    }
610
611
    /**
612
     * Get an element from a list by its index
613
     * @param string $key
614
     * @param int $index zero-based, so 0 means the first element, 1 the second element and so on. -1 means the last element, -2 means the penultimate and so forth
615
     * @return string|null
616
     */
617 12
    public function lindex($key, $index)
618
    {
619 12
        $this->init();
620 12
        $result = $this->driver->lindex($key, $index);
621 12
        return $this->convertFalseToNull($result);
622
    }
623
624
    /**
625
     * Returns null instead of false for Redis driver
626
     * @param mixed $result
627
     * @return mixed
628
     */
629 80
    private function convertFalseToNull($result)
630
    {
631 80
        return $this->actualDriver() === self::DRIVER_REDIS && $result === false ? null : $result;
632
    }
633
634
    /**
635
     * Transforms Predis result Payload to boolean
636
     * @param mixed $result
637
     * @return mixed
638
     */
639 192
    private function transformResult($result)
640
    {
641 192
        if ($this->actualDriver() === self::DRIVER_PREDIS && $result instanceof Status) {
642 96
            $result = $result->getPayload() === 'OK';
643
        }
644 192
        return $result;
645
    }
646
647
    /**
648
     * Create array from input array - odd keys are used as keys, even keys are used as values
649
     * @param array $dictionary
650
     * @param string $command
651
     * @return array
652
     * @throws RedisProxyException if number of keys is not the same as number of values
653
     */
654 16
    private function prepareKeyValue(array $dictionary, $command)
655
    {
656
        $keys = array_values(array_filter($dictionary, function ($key) {
657 16
            return $key % 2 == 0;
658 16
        }, ARRAY_FILTER_USE_KEY));
659 16
        $values = array_values(array_filter($dictionary, function ($key) {
660 16
            return $key % 2 == 1;
661 16
        }, ARRAY_FILTER_USE_KEY));
662
663 16
        if (count($keys) != count($values)) {
664 8
            throw new RedisProxyException("Wrong number of arguments for $command command");
665
        }
666 8
        return array_combine($keys, $values);
667
    }
668
669 84
    private function prepareArguments($command, ...$params)
670
    {
671 84
        if (!isset($params[0])) {
672 8
            throw new RedisProxyException("Wrong number of arguments for $command command");
673
        }
674 76
        if (is_array($params[0])) {
675 32
            $params = $params[0];
676
        }
677 76
        return $params;
678
    }
679
}
680