Completed
Pull Request — master (#4)
by Michal
14:58
created

RedisProxy::actualDriver()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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