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

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