Completed
Branch master (266485)
by Michal
02:29 queued 26s
created

RedisProxy::prepareKeyValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

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