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

RedisProxy::rpop()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
crap 1
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 172
    public function __construct($host, $port, $database = 0, $timeout = null)
78
    {
79 172
        $this->host = $host;
80 172
        $this->port = $port;
81 172
        $this->database = $database;
82 172
        $this->timeout = $timeout;
83 172
        $this->driversOrder = $this->supportedDrivers;
84 172
    }
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 172
    public function setDriversOrder(array $driversOrder)
93
    {
94 172
        foreach ($driversOrder as $driver) {
95 170
            if (!in_array($driver, $this->supportedDrivers)) {
96 86
                throw new RedisProxyException('Driver "' . $driver . '" is not supported');
97
            }
98 85
        }
99 170
        $this->driversOrder = $driversOrder;
100 170
        return $this;
101
    }
102
103 170
    private function init()
104
    {
105 170
        $this->prepareDriver();
106 168
        $this->select($this->database);
107 168
    }
108
109 170
    private function prepareDriver()
110
    {
111 170
        if ($this->driver !== null) {
112 168
            return;
113
        }
114
115 170
        foreach ($this->driversOrder as $preferredDriver) {
116 168
            if ($preferredDriver === self::DRIVER_REDIS && extension_loaded('redis')) {
117 84
                $this->driver = new Redis();
118 84
                return;
119
            }
120 84
            if ($preferredDriver === self::DRIVER_PREDIS && class_exists('Predis\Client')) {
121 84
                $this->driver = new Client();
122 84
                return;
123
            }
124 1
        }
125 2
        throw new RedisProxyException('No driver available');
126
    }
127
128
    /**
129
     * @return string|null
130
     */
131 170
    public function actualDriver()
132
    {
133 170
        if ($this->driver instanceof Redis) {
134 84
            return self::DRIVER_REDIS;
135
        }
136 90
        if ($this->driver instanceof Client) {
137 84
            return self::DRIVER_PREDIS;
138
        }
139 10
        return null;
140
    }
141
142 168
    private function connect($host, $port, $timeout = null)
143
    {
144 168
        return $this->driver->connect($host, $port, $timeout);
145
    }
146
147 168
    private function isConnected()
148
    {
149 168
        return $this->driver->isConnected();
150
    }
151
152 170
    public function __call($name, $arguments)
153
    {
154 170
        $this->init();
155 168
        $name = strtolower($name);
156
        try {
157 168
            $result = call_user_func_array([$this->driver, $name], $arguments);
158 86
        } catch (Exception $e) {
159 4
            throw new RedisProxyException("Error for command '$name', use getPrevious() for more info", 1484162284, $e);
160
        }
161 168
        return $this->transformResult($result);
162
    }
163
164
    /**
165
     * @param int $database
166
     * @return boolean true on success
167
     * @throws RedisProxyException on failure
168
     */
169 168
    public function select($database)
170
    {
171 168
        $this->prepareDriver();
172 168
        if (!$this->isConnected()) {
173 168
            $this->connect($this->host, $this->port, $this->timeout);
174 84
        }
175 168
        if ($database == $this->selectedDatabase) {
176 168
            return true;
177
        }
178
        try {
179 12
            $result = $this->driver->select($database);
180 7
        } 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 3
        } 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 1
        }
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 16
    public function get($key)
239
    {
240 16
        $this->init();
241 16
        $result = $this->driver->get($key);
242 16
        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
     * Set multiple values to multiple keys
269
     * @param array $dictionary
270
     * @return boolean true on success
271
     * @throws RedisProxyException if number of arguments is wrong
272
     */
273 12 View Code Duplication
    public function mset(...$dictionary)
274
    {
275 12
        $this->init();
276 12
        if (is_array($dictionary[0])) {
277 8
            $result = $this->driver->mset(...$dictionary);
278 8
            return $this->transformResult($result);
279
        }
280 8
        $dictionary = $this->prepareKeyValue($dictionary, 'mset');
281 4
        $result = $this->driver->mset($dictionary);
282 4
        return $this->transformResult($result);
283
    }
284
285
    /**
286
     * Multi get
287
     * @param array $keys
288
     * @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
289
     */
290 4
    public function mget(...$keys)
291
    {
292 4
        $keys = array_unique($this->prepareArguments('mget', ...$keys));
293 4
        $this->init();
294 4
        $values = [];
295 4
        foreach ($this->driver->mget($keys) as $value) {
296 4
            $values[] = $this->convertFalseToNull($value);
297 2
        }
298 4
        return array_combine($keys, $values);
299
    }
300
301
    /**
302
     * Incrementally iterate the keys space
303
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
304
     * @param string $pattern pattern for keys, use * as wild card
305
     * @param int $count
306
     * @return array|boolean|null list of found keys, returns null if $iterator is 0 or '0'
307
     */
308 4 View Code Duplication
    public function scan(&$iterator, $pattern = null, $count = null)
309
    {
310 4
        if ((string)$iterator === '0') {
311 4
            return null;
312
        }
313 4
        $this->init();
314 4
        if ($this->actualDriver() === self::DRIVER_PREDIS) {
315 2
            $returned = $this->driver->scan($iterator, ['match' => $pattern, 'count' => $count]);
316 2
            $iterator = $returned[0];
317 2
            return $returned[1];
318
        }
319 2
        return $this->driver->scan($iterator, $pattern, $count);
320
    }
321
322
    /**
323
     * Get the value of a hash field
324
     * @param string $key
325
     * @param string $field
326
     * @return string|null null if hash field is not set
327
     */
328 16
    public function hget($key, $field)
329
    {
330 16
        $this->init();
331 16
        $result = $this->driver->hget($key, $field);
332 16
        return $this->convertFalseToNull($result);
333
    }
334
335
    /**
336
     * Delete one or more hash fields, returns number of deleted fields
337
     * @param array $key
338
     * @param array $fields
339
     * @return int
340
     */
341 8
    public function hdel($key, ...$fields)
342
    {
343 8
        $fields = $this->prepareArguments('hdel', ...$fields);
344 8
        $this->init();
345 8
        return $this->driver->hdel($key, ...$fields);
346
    }
347
348
    /**
349
     * Increment the integer value of hash field by given number
350
     * @param string $key
351
     * @param string $field
352
     * @param int $increment
353
     * @return int
354
     */
355 4
    public function hincrby($key, $field, $increment = 1)
356
    {
357 4
        $this->init();
358 4
        return $this->driver->hincrby($key, $field, (int)$increment);
359
    }
360
361
    /**
362
     * Increment the float value of hash field by given amount
363
     * @param string $key
364
     * @param string $field
365
     * @param float $increment
366
     * @return float
367
     */
368 4
    public function hincrbyfloat($key, $field, $increment = 1)
369
    {
370 4
        $this->init();
371 4
        return $this->driver->hincrbyfloat($key, $field, $increment);
372
    }
373
374
    /**
375
     * Set multiple values to multiple hash fields
376
     * @param string $key
377
     * @param array $dictionary
378
     * @return boolean true on success
379
     * @throws RedisProxyException if number of arguments is wrong
380
     */
381 12 View Code Duplication
    public function hmset($key, ...$dictionary)
382
    {
383 12
        $this->init();
384 12
        if (is_array($dictionary[0])) {
385 8
            $result = $this->driver->hmset($key, ...$dictionary);
386 8
            return $this->transformResult($result);
387
        }
388 8
        $dictionary = $this->prepareKeyValue($dictionary, 'hmset');
389 4
        $result = $this->driver->hmset($key, $dictionary);
390 4
        return $this->transformResult($result);
391
    }
392
393
    /**
394
     * Multi hash get
395
     * @param string $key
396
     * @param array $fields
397
     * @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
398
     */
399 4
    public function hmget($key, ...$fields)
400
    {
401 4
        $fields = array_unique($this->prepareArguments('hmget', ...$fields));
402 4
        $this->init();
403 4
        $values = [];
404 4
        foreach ($this->driver->hmget($key, $fields) as $value) {
405 4
            $values[] = $this->convertFalseToNull($value);
406 2
        }
407 4
        return array_combine($fields, $values);
408
    }
409
410
    /**
411
     * Incrementally iterate hash fields and associated values
412
     * @param string $key
413
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
414
     * @param string $pattern pattern for fields, use * as wild card
415
     * @param int $count
416
     * @return array|boolean|null list of found fields with associated values, returns null if $iterator is 0 or '0'
417
     */
418 4 View Code Duplication
    public function hscan($key, &$iterator, $pattern = null, $count = null)
419
    {
420 4
        if ((string)$iterator === '0') {
421 4
            return null;
422
        }
423 4
        $this->init();
424 4
        if ($this->actualDriver() === self::DRIVER_PREDIS) {
425 2
            $returned = $this->driver->hscan($key, $iterator, ['match' => $pattern, 'count' => $count]);
426 2
            $iterator = $returned[0];
427 2
            return $returned[1];
428
        }
429 2
        return $this->driver->hscan($key, $iterator, $pattern, $count);
430
    }
431
432
    /**
433
     * Add one or more members to a set
434
     * @param string $key
435
     * @param array $members
436
     * @return int number of new members added to set
437
     */
438 16
    public function sadd($key, ...$members)
439
    {
440 16
        $members = $this->prepareArguments('sadd', ...$members);
441 16
        $this->init();
442 16
        return $this->driver->sadd($key, ...$members);
443
    }
444
445
    /**
446
     * Remove and return one or multiple random members from a set
447
     * @param string $key
448
     * @param int $count number of members
449
     * @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
450
     */
451 4
    public function spop($key, $count = 1)
452
    {
453 4
        $this->init();
454 4
        if ($count == 1 || $count === null) {
455 4
            $result = $this->driver->spop($key);
456 4
            return $this->convertFalseToNull($result);
457
        }
458
459 4
        $members = [];
460 4
        for ($i = 0; $i < $count; ++$i) {
461 4
            $member = $this->driver->spop($key);
462 4
            if (!$member) {
463 4
                break;
464
            }
465 4
            $members[] = $member;
466 2
        }
467 4
        return empty($members) ? null : $members;
468
    }
469
470
    /**
471
     * Incrementally iterate Set elements
472
     * @param string $key
473
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
474
     * @param string $pattern pattern for member's values, use * as wild card
475
     * @param int $count
476
     * @return array|boolean|null list of found members, returns null if $iterator is 0 or '0'
477
     */
478 4 View Code Duplication
    public function sscan($key, &$iterator, $pattern = null, $count = null)
479
    {
480 4
        if ((string)$iterator === '0') {
481 4
            return null;
482
        }
483 4
        $this->init();
484 4
        if ($this->actualDriver() === self::DRIVER_PREDIS) {
485 2
            $returned = $this->driver->sscan($key, $iterator, ['match' => $pattern, 'count' => $count]);
486 2
            $iterator = $returned[0];
487 2
            return $returned[1];
488
        }
489 2
        return $this->driver->sscan($key, $iterator, $pattern, $count);
490
    }
491
492
    /**
493
     * Prepend one or multiple values to a list
494
     * @param string $key
495
     * @param array $elements
496
     * @return int the length of the list after the push operations
497
     */
498 28
    public function lpush($key, ...$elements)
499
    {
500 28
        $elements = $this->prepareArguments('lpush', ...$elements);
501 28
        $this->init();
502 28
        return $this->driver->lpush($key, ...$elements);
503
    }
504
505
    /**
506
     * Append one or multiple values to a list
507
     * @param string $key
508
     * @param array $elements
509
     * @return int the length of the list after the push operations
510
     */
511 12
    public function rpush($key, ...$elements)
512
    {
513 12
        $elements = $this->prepareArguments('rpush', ...$elements);
514 12
        $this->init();
515 12
        return $this->driver->rpush($key, ...$elements);
516
    }
517
518
    /**
519
     * Remove and get the first element in a list
520
     * @param string $key
521
     * @return string|null
522
     */
523 4
    public function lpop($key)
524
    {
525 4
        $this->init();
526 4
        $result = $this->driver->lpop($key);
527 4
        return $this->convertFalseToNull($result);
528
    }
529
530
    /**
531
     * Remove and get the last element in a list
532
     * @param string $key
533
     * @return string|null
534
     */
535 4
    public function rpop($key)
536
    {
537 4
        $this->init();
538 4
        $result = $this->driver->rpop($key);
539 4
        return $this->convertFalseToNull($result);
540
    }
541
542
    /**
543
     * Get an element from a list by its index
544
     * @param string $key
545
     * @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
546
     * @return string|null
547
     */
548 12
    public function lindex($key, $index)
549
    {
550 12
        $this->init();
551 12
        $result = $this->driver->lindex($key, $index);
552 12
        return $this->convertFalseToNull($result);
553
    }
554
555
    /**
556
     * Returns null instead of false for Redis driver
557
     * @param mixed $result
558
     * @return mixed
559
     */
560 56
    private function convertFalseToNull($result)
561
    {
562 56
        return $this->actualDriver() === self::DRIVER_REDIS && $result === false ? null : $result;
563
    }
564
565
    /**
566
     * Transforms Predis result Payload to boolean
567
     * @param mixed $result
568
     * @return mixed
569
     */
570 168
    private function transformResult($result)
571
    {
572 168
        if ($this->actualDriver() === self::DRIVER_PREDIS && $result instanceof Status) {
573 84
            $result = $result->getPayload() === 'OK';
574 42
        }
575 168
        return $result;
576
    }
577
578
    /**
579
     * Create array from input array - odd keys are used as keys, even keys are used as values
580
     * @param array $dictionary
581
     * @param string $command
582
     * @return array
583
     * @throws RedisProxyException if number of keys is not the same as number of values
584
     */
585 16
    private function prepareKeyValue(array $dictionary, $command)
586
    {
587
        $keys = array_values(array_filter($dictionary, function ($key) {
588 16
            return $key % 2 == 0;
589 16
        }, ARRAY_FILTER_USE_KEY));
590 16
        $values = array_values(array_filter($dictionary, function ($key) {
591 16
            return $key % 2 == 1;
592 16
        }, ARRAY_FILTER_USE_KEY));
593
594 16
        if (count($keys) != count($values)) {
595 8
            throw new RedisProxyException("Wrong number of arguments for $command command");
596
        }
597 8
        return array_combine($keys, $values);
598
    }
599
600 84
    private function prepareArguments($command, ...$params)
601
    {
602 84
        if (!isset($params[0])) {
603 8
            throw new RedisProxyException("Wrong number of arguments for $command command");
604
        }
605 76
        if (is_array($params[0])) {
606 32
            $params = $params[0];
607 16
        }
608 76
        return $params;
609
    }
610
}
611