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

RedisProxy::zscan()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 10
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 10
loc 10
ccs 0
cts 7
cp 0
rs 9.4285
cc 2
eloc 7
nc 2
nop 4
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 2
            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 110
                throw new RedisProxyException('Driver "' . $driver . '" is not supported');
62
            }
63
        }
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 4
        } 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
        }
126 108
        if ($database == $this->database) {
127 108
            return true;
128
        }
129
        try {
130 12
            $result = $this->driver->select($database);
131 2
        } 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
    public function mset(...$dictionary)
170
    {
171 12
        if (is_array($dictionary[0])) {
172 8
            $result = $this->driver->mset(...$dictionary);
173 8
            return $this->transformResult($result);
174
        }
175 8
        $dictionary = $this->prepareKeyValue($dictionary, 'mset');
176 4
        $result = $this->driver->mset($dictionary);
177 4
        return $this->transformResult($result);
178
    }
179
180
    /**
181
     * @param string $key
182
     * @return string|null null if hash field is not set
183
     */
184 16
    public function get($key)
185
    {
186 16
        $this->init();
187 16
        $result = $this->driver->get($key);
188 16
        return $this->convertFalseToNull($result);
189
    }
190
191
    /**
192
     * Delete a key(s)
193
     * @param array ...$keys
194
     * @return integer number of deleted keys
195
     */
196 20
    public function del(...$keys)
197
    {
198 20
        $this->init();
199 20
        return $this->driver->del(...$keys);
200
    }
201
202
    /**
203
     * Delete a key(s)
204
     * @param array ...$keys
205
     * @return integer number of deleted keys
206
     */
207 8
    public function delete(...$keys)
208
    {
209 8
        return $this->del(...$keys);
210
    }
211
212
    /**
213
     * Incrementally iterate the keys space
214
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
215
     * @param string $pattern pattern for keys, use * as wild card
216
     * @param integer $count
217
     * @return array|boolean|null list of found keys, returns null if $iterator is 0 or '0'
218
     */
219 4 View Code Duplication
    public function scan(&$iterator, $pattern = null, $count = null)
220
    {
221 4
        if ((string)$iterator === '0') {
222 4
            return null;
223
        }
224 4
        $this->init();
225 4
        if ($this->driver instanceof Client) {
226 2
            $returned = $this->driver->scan($iterator, ['match' => $pattern, 'count' => $count]);
227 2
            $iterator = $returned[0];
228 2
            return $returned[1];
229
        }
230 2
        return $this->driver->scan($iterator, $pattern, $count);
231
    }
232
233
    /**
234
     * Get the value of a hash field
235
     * @param string $key
236
     * @param string $field
237
     * @return string|null null if hash field is not set
238
     */
239 16
    public function hget($key, $field)
240
    {
241 16
        $this->init();
242 16
        $result = $this->driver->hget($key, $field);
243 16
        return $this->convertFalseToNull($result);
244
    }
245
246
    /**
247
     * Delete one or more hash fields, returns number of deleted fields
248
     * @param array $key
249
     * @param array ...$fields
250
     * @return integer
251
     */
252 8
    public function hdel($key, ...$fields)
253
    {
254 8
        if (is_array($fields[0])) {
255 4
            $fields = $fields[0];
256
        }
257 8
        return $this->driver->hdel($key, ...$fields);
258
    }
259
260
    /**
261
     * Increment the integer value of hash field by given number
262
     * @param string $key
263
     * @param string $field
264
     * @param integer $increment
265
     * @return integer
266
     */
267 4
    public function hincrby($key, $field, $increment = 1)
268
    {
269 4
        return $this->driver->hincrby($key, $field, (int)$increment);
270
    }
271
272
    /**
273
     * Increment the float value of hash field by given amount
274
     * @param string $key
275
     * @param string $field
276
     * @param float $increment
277
     * @return float
278
     */
279 4
    public function hincrbyfloat($key, $field, $increment = 1)
280
    {
281 4
        return $this->driver->hincrbyfloat($key, $field, $increment);
282
    }
283
284
    /**
285
     * Set multiple values to multiple hash fields
286
     * @param string $key
287
     * @param array $dictionary
288
     * @return boolean true on success
289
     * @throws RedisProxyException if number of arguments is wrong
290
     */
291 12
    public function hmset($key, ...$dictionary)
292
    {
293 12
        if (is_array($dictionary[0])) {
294 8
            $result = $this->driver->hmset($key, ...$dictionary);
295 8
            return $this->transformResult($result);
296
        }
297 8
        $dictionary = $this->prepareKeyValue($dictionary, 'hmset');
298 4
        $result = $this->driver->hmset($key, $dictionary);
299 4
        return $this->transformResult($result);
300
    }
301
302
    /**
303
     * Incrementally iterate hash fields and associated values
304
     * @param string $key
305
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
306
     * @param string $pattern pattern for fields, use * as wild card
307
     * @param integer $count
308
     * @return array|boolean|null list of found fields with associated values, returns null if $iterator is 0 or '0'
309
     */
310 4 View Code Duplication
    public function hscan($key, &$iterator, $pattern = null, $count = null)
311
    {
312 4
        if ((string)$iterator === '0') {
313 4
            return null;
314
        }
315 4
        $this->init();
316 4
        if ($this->driver instanceof Client) {
317 2
            $returned = $this->driver->hscan($key, $iterator, ['match' => $pattern, 'count' => $count]);
318 2
            $iterator = $returned[0];
319 2
            return $returned[1];
320
        }
321 2
        return $this->driver->hscan($key, $iterator, $pattern, $count);
322
    }
323
324
    /**
325
     * Add one or more members to a set
326
     * @param string $key
327
     * @param array ...$members
328
     * @return integer number of new members added to set
329
     */
330 12
    public function sadd($key, ...$members)
331
    {
332 12
        if (is_array($members[0])) {
333 8
            $members = $members[0];
334
        }
335 12
        return $this->driver->sadd($key, ...$members);
336
    }
337
338
    /**
339
     * Remove and return one or multiple random members from a set
340
     * @param string $key
341
     * @param integer $count number of members
342
     * @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
343
     */
344 4
    public function spop($key, $count = 1)
345
    {
346 4
        if ($count == 1 || $count === null) {
347 4
            $result = $this->driver->spop($key);
348 4
            return $this->convertFalseToNull($result);
349
        }
350
351 4
        $members = [];
352 4
        for ($i = 0; $i < $count; ++$i) {
353 4
            $member = $this->driver->spop($key);
354 4
            if (!$member) {
355 4
                break;
356
            }
357 4
            $members[] = $member;
358
        }
359 4
        return empty($members) ? null : $members;
360
    }
361
362
    /**
363
     * Incrementally iterate Set elements
364
     * @param string $key
365
     * @param mixed $iterator iterator / cursor, use $iterator = null for start scanning, when $iterator is changed to 0 or '0', scanning is finished
366
     * @param string $pattern pattern for member's values, use * as wild card
367
     * @param integer $count
368
     * @return array|boolean|null list of found members, returns null if $iterator is 0 or '0'
369
     */
370 4 View Code Duplication
    public function sscan($key, &$iterator, $pattern = null, $count = null)
371
    {
372 4
        if ((string)$iterator === '0') {
373 4
            return null;
374
        }
375 4
        $this->init();
376 4
        if ($this->driver instanceof Client) {
377 2
            $returned = $this->driver->sscan($key, $iterator, ['match' => $pattern, 'count' => $count]);
378 2
            $iterator = $returned[0];
379 2
            return $returned[1];
380
        }
381 2
        return $this->driver->sscan($key, $iterator, $pattern, $count);
382
    }
383
384 36
    private function convertFalseToNull($result)
385
    {
386 36
        return $this->driver instanceof Redis && $result === false ? null : $result;
387
    }
388
389 108
    private function transformResult($result)
390
    {
391 108
        if ($this->driver instanceof Client && $result instanceof Status) {
392 54
            $result = $result->getPayload() === 'OK';
393
        }
394 108
        return $result;
395
    }
396
397
    /**
398
     * Create array from input array - odd keys are used as keys, even keys are used as values
399
     * @param array $dictionary
400
     * @param string $command
401
     * @return array
402
     * @throws RedisProxyException if number of keys is not the same as number of values
403
     */
404 16
    private function prepareKeyValue(array $dictionary, $command)
405
    {
406
        $keys = array_values(array_filter($dictionary, function ($key) {
407 16
            return $key % 2 == 0;
408 16
        }, ARRAY_FILTER_USE_KEY));
409 16
        $values = array_values(array_filter($dictionary, function ($key) {
410 16
            return $key % 2 == 1;
411 16
        }, ARRAY_FILTER_USE_KEY));
412
413 16
        if (count($keys) != count($values)) {
414 8
            throw new RedisProxyException("Wrong number of arguments for $command");
415
        }
416 8
        return array_combine($keys, $values);
417
    }
418
}
419