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

RedisProxy::hincrbyfloat()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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