Completed
Push — master ( 53fee4...e7f13f )
by Michal
02:28
created

RedisProxy::actualDriver()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
cc 3
eloc 6
nc 3
nop 0
crap 3
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 integer 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 integer 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 integer 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 integer scard(string $key) Get the number of members in a set
21
 * @method boolean flushall() Remove all keys from all databases
22
 * @method boolean flushdb() Remove all keys from the current database
23
 */
24
class RedisProxy
25
{
26
    const DRIVER_REDIS = 'redis';
27
28
    const DRIVER_PREDIS = 'predis';
29
30
    private $driver;
31
32
    private $host;
33
34
    private $port;
35
36
    private $database = 0;
37
38
    private $selectedDatabase = 0;
39
40
    private $timeout;
41
42
    private $supportedDrivers = [
43
        self::DRIVER_REDIS,
44
        self::DRIVER_PREDIS,
45
    ];
46
47
    private $driversOrder = [];
48
49 140
    public function __construct($host, $port, $database = 0, $timeout = null)
50
    {
51 140
        $this->host = $host;
52 140
        $this->port = $port;
53 140
        $this->database = $database;
54 140
        $this->timeout = $timeout;
55 140
        $this->driversOrder = $this->supportedDrivers;
56 140
    }
57
58
    /**
59
     * Set driver priorities - default is 1. redis, 2. predis
60
     * @param array $driversOrder
61
     * @return RedisProxy
62
     * @throws RedisProxyException if some driver is not supported
63
     */
64 140
    public function setDriversOrder(array $driversOrder)
65
    {
66 140
        foreach ($driversOrder as $driver) {
67 138
            if (!in_array($driver, $this->supportedDrivers)) {
68 70
                throw new RedisProxyException('Driver "' . $driver . '" is not supported');
69
            }
70 69
        }
71 138
        $this->driversOrder = $driversOrder;
72 138
        return $this;
73
    }
74
75 138
    private function init()
76
    {
77 138
        $this->prepareDriver();
78 136
        $this->select($this->database);
79 136
    }
80
81 138
    private function prepareDriver()
82
    {
83 138
        if ($this->driver !== null) {
84 136
            return;
85
        }
86
87 138
        foreach ($this->driversOrder as $preferredDriver) {
88 136
            if ($preferredDriver === self::DRIVER_REDIS && extension_loaded('redis')) {
89 68
                $this->driver = new Redis();
90 68
                return;
91
            }
92 68
            if ($preferredDriver === self::DRIVER_PREDIS && class_exists('Predis\Client')) {
93 68
                $this->driver = new Client();
94 68
                return;
95
            }
96 1
        }
97 2
        throw new RedisProxyException('No driver available');
98
    }
99
100
    /**
101
     * @return string|null
102
     */
103 138
    public function actualDriver()
104
    {
105 138
        if ($this->driver instanceof Redis) {
106 68
            return self::DRIVER_REDIS;
107
        }
108 74
        if ($this->driver instanceof Client) {
109 68
            return self::DRIVER_PREDIS;
110
        }
111 10
        return null;
112
    }
113
114 136
    private function connect($host, $port, $timeout = null)
115
    {
116 136
        return $this->driver->connect($host, $port, $timeout);
117
    }
118
119 136
    private function isConnected()
120
    {
121 136
        return $this->driver->isConnected();
122
    }
123
124 138
    public function __call($name, $arguments)
125
    {
126 138
        $this->init();
127 136
        $name = strtolower($name);
128
        try {
129 136
            $result = call_user_func_array([$this->driver, $name], $arguments);
130 70
        } catch (Exception $e) {
131 4
            throw new RedisProxyException("Error for command '$name', use getPrevious() for more info", 1484162284, $e);
132
        }
133 136
        return $this->transformResult($result);
134
    }
135
136
    /**
137
     * @param integer $database
138
     * @return boolean true on success
139
     * @throws RedisProxyException on failure
140
     */
141 136
    public function select($database)
142
    {
143 136
        $this->prepareDriver();
144 136
        if (!$this->isConnected()) {
145 136
            $this->connect($this->host, $this->port, $this->timeout);
146 68
        }
147 136
        if ($database == $this->selectedDatabase) {
148 136
            return true;
149
        }
150
        try {
151 12
            $result = $this->driver->select($database);
152 7
        } catch (Exception $e) {
153 2
            throw new RedisProxyException('Invalid DB index');
154
        }
155 10
        $result = $this->transformResult($result);
156 10
        if ($result === false) {
157 2
            throw new RedisProxyException('Invalid DB index');
158
        }
159 8
        $this->database = $database;
160 8
        $this->selectedDatabase = $database;
161 8
        return $result;
162
    }
163
164
    /**
165
     * @param string|null $section
166
     * @return array
167
     */
168 8
    public function info($section = null)
169
    {
170 8
        $this->init();
171 8
        $section = $section ? strtolower($section) : $section;
172 8
        $result = $section === null ? $this->driver->info() : $this->driver->info($section);
173
174 8
        $databases = $section === null || $section === 'keyspace' ? $this->config('get', 'databases')['databases'] : null;
175 8
        $groupedResult = InfoHelper::createInfoArray($this, $result, $databases);
176 8
        if ($section === null) {
177 4
            return $groupedResult;
178
        }
179 8
        if (isset($groupedResult[$section])) {
180 4
            return $groupedResult[$section];
181
        }
182 4
        throw new RedisProxyException('Info section "' . $section . '" doesn\'t exist');
183
    }
184
185
    /**
186
     * @param string $key
187
     * @return string|null null if hash field is not set
188
     */
189 16
    public function get($key)
190
    {
191 16
        $this->init();
192 16
        $result = $this->driver->get($key);
193 16
        return $this->convertFalseToNull($result);
194
    }
195
196
    /**
197
     * Delete a key(s)
198
     * @param array $keys
199
     * @return integer number of deleted keys
200
     */
201 28
    public function del(...$keys)
202
    {
203 28
        $this->prepareArguments('del', ...$keys);
204 20
        $this->init();
205 20
        return $this->driver->del(...$keys);
206
    }
207
208
    /**
209
     * Delete a key(s)
210
     * @param array $keys
211
     * @return integer number of deleted keys
212
     */
213 12
    public function delete(...$keys)
214
    {
215 12
        return $this->del(...$keys);
216
    }
217
218
    /**
219
     * Set multiple values to multiple keys
220
     * @param array $dictionary
221
     * @return boolean true on success
222
     * @throws RedisProxyException if number of arguments is wrong
223
     */
224 12 View Code Duplication
    public function mset(...$dictionary)
225
    {
226 12
        $this->init();
227 12
        if (is_array($dictionary[0])) {
228 8
            $result = $this->driver->mset(...$dictionary);
229 8
            return $this->transformResult($result);
230
        }
231 8
        $dictionary = $this->prepareKeyValue($dictionary, 'mset');
232 4
        $result = $this->driver->mset($dictionary);
233 4
        return $this->transformResult($result);
234
    }
235
236
    /**
237
     * Multi get
238
     * @param array $keys
239
     * @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
240
     */
241 4
    public function mget(...$keys)
242
    {
243 4
        $keys = array_unique($this->prepareArguments('mget', ...$keys));
244 4
        $this->init();
245 4
        $values = [];
246 4
        foreach ($this->driver->mget($keys) as $value) {
247 4
            $values[] = $this->convertFalseToNull($value);
248 2
        }
249 4
        return array_combine($keys, $values);
250
    }
251
252
    /**
253
     * Incrementally iterate the keys space
254
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
255
     * @param string $pattern pattern for keys, use * as wild card
256
     * @param integer $count
257
     * @return array|boolean|null list of found keys, returns null if $iterator is 0 or '0'
258
     */
259 4 View Code Duplication
    public function scan(&$iterator, $pattern = null, $count = null)
260
    {
261 4
        if ((string)$iterator === '0') {
262 4
            return null;
263
        }
264 4
        $this->init();
265 4
        if ($this->actualDriver() === self::DRIVER_PREDIS) {
266 2
            $returned = $this->driver->scan($iterator, ['match' => $pattern, 'count' => $count]);
267 2
            $iterator = $returned[0];
268 2
            return $returned[1];
269
        }
270 2
        return $this->driver->scan($iterator, $pattern, $count);
271
    }
272
273
    /**
274
     * Get the value of a hash field
275
     * @param string $key
276
     * @param string $field
277
     * @return string|null null if hash field is not set
278
     */
279 16
    public function hget($key, $field)
280
    {
281 16
        $this->init();
282 16
        $result = $this->driver->hget($key, $field);
283 16
        return $this->convertFalseToNull($result);
284
    }
285
286
    /**
287
     * Delete one or more hash fields, returns number of deleted fields
288
     * @param array $key
289
     * @param array $fields
290
     * @return integer
291
     */
292 8
    public function hdel($key, ...$fields)
293
    {
294 8
        $fields = $this->prepareArguments('hdel', ...$fields);
295 8
        $this->init();
296 8
        return $this->driver->hdel($key, ...$fields);
297
    }
298
299
    /**
300
     * Increment the integer value of hash field by given number
301
     * @param string $key
302
     * @param string $field
303
     * @param integer $increment
304
     * @return integer
305
     */
306 4
    public function hincrby($key, $field, $increment = 1)
307
    {
308 4
        $this->init();
309 4
        return $this->driver->hincrby($key, $field, (int)$increment);
310
    }
311
312
    /**
313
     * Increment the float value of hash field by given amount
314
     * @param string $key
315
     * @param string $field
316
     * @param float $increment
317
     * @return float
318
     */
319 4
    public function hincrbyfloat($key, $field, $increment = 1)
320
    {
321 4
        $this->init();
322 4
        return $this->driver->hincrbyfloat($key, $field, $increment);
323
    }
324
325
    /**
326
     * Set multiple values to multiple hash fields
327
     * @param string $key
328
     * @param array $dictionary
329
     * @return boolean true on success
330
     * @throws RedisProxyException if number of arguments is wrong
331
     */
332 12 View Code Duplication
    public function hmset($key, ...$dictionary)
333
    {
334 12
        $this->init();
335 12
        if (is_array($dictionary[0])) {
336 8
            $result = $this->driver->hmset($key, ...$dictionary);
337 8
            return $this->transformResult($result);
338
        }
339 8
        $dictionary = $this->prepareKeyValue($dictionary, 'hmset');
340 4
        $result = $this->driver->hmset($key, $dictionary);
341 4
        return $this->transformResult($result);
342
    }
343
344
    /**
345
     * Multi hash get
346
     * @param string $key
347
     * @param array $fields
348
     * @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
349
     */
350 4
    public function hmget($key, ...$fields)
351
    {
352 4
        $fields = array_unique($this->prepareArguments('hmget', ...$fields));
353 4
        $this->init();
354 4
        $values = [];
355 4
        foreach ($this->driver->hmget($key, $fields) as $value) {
356 4
            $values[] = $this->convertFalseToNull($value);
357 2
        }
358 4
        return array_combine($fields, $values);
359
    }
360
361
    /**
362
     * Incrementally iterate hash fields and associated values
363
     * @param string $key
364
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
365
     * @param string $pattern pattern for fields, use * as wild card
366
     * @param integer $count
367
     * @return array|boolean|null list of found fields with associated values, returns null if $iterator is 0 or '0'
368
     */
369 4 View Code Duplication
    public function hscan($key, &$iterator, $pattern = null, $count = null)
370
    {
371 4
        if ((string)$iterator === '0') {
372 4
            return null;
373
        }
374 4
        $this->init();
375 4
        if ($this->actualDriver() === self::DRIVER_PREDIS) {
376 2
            $returned = $this->driver->hscan($key, $iterator, ['match' => $pattern, 'count' => $count]);
377 2
            $iterator = $returned[0];
378 2
            return $returned[1];
379
        }
380 2
        return $this->driver->hscan($key, $iterator, $pattern, $count);
381
    }
382
383
    /**
384
     * Add one or more members to a set
385
     * @param string $key
386
     * @param array $members
387
     * @return integer number of new members added to set
388
     */
389 12
    public function sadd($key, ...$members)
390
    {
391 12
        $members = $this->prepareArguments('sadd', ...$members);
392 12
        $this->init();
393 12
        return $this->driver->sadd($key, ...$members);
394
    }
395
396
    /**
397
     * Remove and return one or multiple random members from a set
398
     * @param string $key
399
     * @param integer $count number of members
400
     * @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
401
     */
402 4
    public function spop($key, $count = 1)
403
    {
404 4
        $this->init();
405 4
        if ($count == 1 || $count === null) {
406 4
            $result = $this->driver->spop($key);
407 4
            return $this->convertFalseToNull($result);
408
        }
409
410 4
        $members = [];
411 4
        for ($i = 0; $i < $count; ++$i) {
412 4
            $member = $this->driver->spop($key);
413 4
            if (!$member) {
414 4
                break;
415
            }
416 4
            $members[] = $member;
417 2
        }
418 4
        return empty($members) ? null : $members;
419
    }
420
421
    /**
422
     * Incrementally iterate Set elements
423
     * @param string $key
424
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
425
     * @param string $pattern pattern for member's values, use * as wild card
426
     * @param integer $count
427
     * @return array|boolean|null list of found members, returns null if $iterator is 0 or '0'
428
     */
429 4 View Code Duplication
    public function sscan($key, &$iterator, $pattern = null, $count = null)
430
    {
431 4
        if ((string)$iterator === '0') {
432 4
            return null;
433
        }
434 4
        $this->init();
435 4
        if ($this->actualDriver() === self::DRIVER_PREDIS) {
436 2
            $returned = $this->driver->sscan($key, $iterator, ['match' => $pattern, 'count' => $count]);
437 2
            $iterator = $returned[0];
438 2
            return $returned[1];
439
        }
440 2
        return $this->driver->sscan($key, $iterator, $pattern, $count);
441
    }
442
443 44
    private function convertFalseToNull($result)
444
    {
445 44
        return $this->actualDriver() === self::DRIVER_REDIS && $result === false ? null : $result;
446
    }
447
448 136
    private function transformResult($result)
449
    {
450 136
        if ($this->actualDriver() === self::DRIVER_PREDIS && $result instanceof Status) {
451 68
            $result = $result->getPayload() === 'OK';
452 34
        }
453 136
        return $result;
454
    }
455
456
    /**
457
     * Create array from input array - odd keys are used as keys, even keys are used as values
458
     * @param array $dictionary
459
     * @param string $command
460
     * @return array
461
     * @throws RedisProxyException if number of keys is not the same as number of values
462
     */
463 16
    private function prepareKeyValue(array $dictionary, $command)
464
    {
465
        $keys = array_values(array_filter($dictionary, function ($key) {
466 16
            return $key % 2 == 0;
467 16
        }, ARRAY_FILTER_USE_KEY));
468 16
        $values = array_values(array_filter($dictionary, function ($key) {
469 16
            return $key % 2 == 1;
470 16
        }, ARRAY_FILTER_USE_KEY));
471
472 16
        if (count($keys) != count($values)) {
473 8
            throw new RedisProxyException("Wrong number of arguments for $command command");
474
        }
475 8
        return array_combine($keys, $values);
476
    }
477
478 52
    private function prepareArguments($command, ...$params)
479
    {
480 52
        if (!isset($params[0])) {
481 8
            throw new RedisProxyException("Wrong number of arguments for $command command");
482
        }
483 44
        if (is_array($params[0])) {
484 24
            $params = $params[0];
485 12
        }
486 44
        return $params;
487
    }
488
}
489