Passed
Push — develop ( 4f7383...a600bc )
by nguereza
04:23
created

RedisClient::__call()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 11
rs 10
1
<?php
2
3
/**
4
 * Platine Framework
5
 *
6
 * Platine Framework is a lightweight, high-performance, simple and elegant
7
 * PHP Web framework
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Framework
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file RedisClient.php
34
 *
35
 *  The lightweight Redis client class
36
 *
37
 *  @package    Platine\Framework\Helper
38
 *  @author Platine Developers team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   https://www.platine-php.com
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Framework\Helper;
49
50
use RuntimeException;
51
52
/**
53
 * @class RedisClient
54
 * @package Platine\Framework\Helper
55
 *
56
 * @method string get(string $key)
57
 * @method string del(string $key)
58
 * @method string ttl(string $key)
59
 * @method string exists(string $key)
60
 * @method string persist(string $key)
61
 * @method string incr(string $key)
62
 * @method string decr(string $key)
63
 * @method string expire(string $key, int $seconds)
64
 * @method string incrby(string $key, int $increment)
65
 * @method string decrby(string $key, int $decrement)
66
 * @method string set(string $key, string $value)
67
 */
68
class RedisClient
69
{
70
    /**
71
     * The connection socket
72
     * line length not limited
73
     * @var resource|false
74
     */
75
    protected $socket = false;
76
77
78
    /**
79
     * Create new instance
80
     * @param string $host the Redis server address
81
     * @param int $port the Redis server port
82
     */
83
    public function __construct(
84
        protected string $host = 'localhost',
85
        protected int $port = 6379,
86
    ) {
87
    }
88
89
    /**
90
     * Dynamic method call based on Redis commands
91
     * @see https://redis.io/docs/latest/commands/
92
     * @param string $method
93
     * @param array<mixed> $args
94
     * @return mixed
95
     */
96
    public function __call(string $method, array $args): mixed
97
    {
98
        array_unshift($args, $method);
99
        $cmd = sprintf('*%d', count($args)) . "\r\n";
100
        foreach ($args as $item) {
101
            $cmd .= '$' . strlen($item) . "\r\n" . $item . "\r\n";
102
        }
103
104
        fwrite($this->getSocket(), $cmd);
105
106
        return $this->getResponse();
107
    }
108
109
    /**
110
     * Return the response from socket
111
     * @return array<string>|string|string
112
     * @throws RuntimeException
113
     */
114
    protected function getResponse(): array|string|null
115
    {
116
        $line = fgets($this->getSocket());
117
        if ($line === false) {
118
            throw new RuntimeException('Can not read from redis socket');
119
        }
120
        [$type, $result] = [$line[0], substr($line, 1, strlen($line) - 3)];
121
        if ($type === '-') {
122
            // Error
123
            throw new RuntimeException(sprintf('Redis socket return error: %s', $result));
124
        }
125
126
        if ($type === '$') { // bulk reply
127
            if ($result === '-1') {
128
                $result = null;
129
            } else {
130
                /** @var int<1, max> $length */
131
                $length = (int) $result + 2;
132
                $line = fread($this->getSocket(), $length);
133
                if ($line === false) {
134
                    throw new RuntimeException('Can not read from redis socket for bulk reply');
135
                }
136
                $result = substr($line, 0, strlen($line) - 2);
137
            }
138
        } elseif ($type === '*') { // multi-bulk reply
139
            $count = (int) $result;
140
            for ($i = 0 , $result = []; $i < $count; $i++) {
141
                $result[] = $this->getResponse();
142
            }
143
        }
144
145
        return $result;
146
    }
147
148
    /**
149
     * Return the socket instance
150
     * @return resource
151
     * @throws RuntimeException
152
     */
153
    protected function getSocket()
154
    {
155
        if (is_resource($this->socket)) {
156
            return $this->socket;
157
        }
158
        $errorCode = null;
159
        $errorMessage = null;
160
        $socket = stream_socket_client(
161
            sprintf('%s:%d', $this->host, $this->port),
162
            $errorCode,
163
            $errorMessage
164
        );
165
        if ($socket === false) {
166
            throw new RuntimeException(sprintf(
167
                'Can not connect to Redis [%d] -> [%s]',
168
                $errorCode,
169
                $errorMessage
170
            ));
171
        }
172
173
        return $this->socket = $socket;
174
    }
175
}
176