Completed
Push — master ( 640372...fe97e8 )
by Michal
02:02
created

RedisProxy::prepareDriver()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7

Importance

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