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

RedisProxy::spop()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

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