Passed
Pull Request — development (#3708)
by Martyn
15:25
created

PhpiredisSocketConnection::write()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 16
rs 10
1
<?php
2
3
/*
4
 * This file is part of the Predis package.
5
 *
6
 * (c) 2009-2020 Daniele Alessandri
7
 * (c) 2021-2023 Till Krüss
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
namespace Predis\Connection;
14
15
use Closure;
16
use InvalidArgumentException;
17
use Predis\Command\CommandInterface;
18
use Predis\NotSupportedException;
19
use Predis\Response\Error as ErrorResponse;
20
use Predis\Response\ErrorInterface as ErrorResponseInterface;
21
use Predis\Response\Status as StatusResponse;
22
23
/**
24
 * This class provides the implementation of a Predis connection that uses the
25
 * PHP socket extension for network communication and wraps the phpiredis C
26
 * extension (PHP bindings for hiredis) to parse the Redis protocol.
27
 *
28
 * This class is intended to provide an optional low-overhead alternative for
29
 * processing responses from Redis compared to the standard pure-PHP classes.
30
 * Differences in speed when dealing with short inline responses are practically
31
 * nonexistent, the actual speed boost is for big multibulk responses when this
32
 * protocol processor can parse and return responses very fast.
33
 *
34
 * For instructions on how to build and install the phpiredis extension, please
35
 * consult the repository of the project.
36
 *
37
 * The connection parameters supported by this class are:
38
 *
39
 *  - scheme: it can be either 'redis', 'tcp' or 'unix'.
40
 *  - host: hostname or IP address of the server.
41
 *  - port: TCP port of the server.
42
 *  - path: path of a UNIX domain socket when scheme is 'unix'.
43
 *  - timeout: timeout to perform the connection (default is 5 seconds).
44
 *  - read_write_timeout: timeout of read / write operations.
45
 *
46
 * @see http://github.com/nrk/phpiredis
47
 * @deprecated 2.1.2
48
 */
49
class PhpiredisSocketConnection extends AbstractConnection
50
{
51
    private $reader;
52
53
    /**
54
     * {@inheritdoc}
55
     */
56
    public function __construct(ParametersInterface $parameters)
57
    {
58
        $this->assertExtensions();
59
60
        parent::__construct($parameters);
61
62
        $this->reader = $this->createReader();
63
    }
64
65
    /**
66
     * Disconnects from the server and destroys the underlying resource and the
67
     * protocol reader resource when PHP's garbage collector kicks in.
68
     */
69
    public function __destruct()
70
    {
71
        parent::__destruct();
72
73
        phpiredis_reader_destroy($this->reader);
0 ignored issues
show
Bug introduced by
The function phpiredis_reader_destroy was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

73
        /** @scrutinizer ignore-call */ 
74
        phpiredis_reader_destroy($this->reader);
Loading history...
74
    }
75
76
    /**
77
     * Checks if the socket and phpiredis extensions are loaded in PHP.
78
     */
79
    protected function assertExtensions()
80
    {
81
        if (!extension_loaded('sockets')) {
82
            throw new NotSupportedException(
83
                'The "sockets" extension is required by this connection backend.'
84
            );
85
        }
86
87
        if (!extension_loaded('phpiredis')) {
88
            throw new NotSupportedException(
89
                'The "phpiredis" extension is required by this connection backend.'
90
            );
91
        }
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97
    protected function assertParameters(ParametersInterface $parameters)
98
    {
99
        switch ($parameters->scheme) {
100
            case 'tcp':
101
            case 'redis':
102
            case 'unix':
103
                break;
104
105
            default:
106
                throw new InvalidArgumentException("Invalid scheme: '$parameters->scheme'.");
107
        }
108
109
        if (isset($parameters->persistent)) {
110
            throw new NotSupportedException(
111
                'Persistent connections are not supported by this connection backend.'
112
            );
113
        }
114
115
        return $parameters;
116
    }
117
118
    /**
119
     * Creates a new instance of the protocol reader resource.
120
     *
121
     * @return resource
122
     */
123
    private function createReader()
124
    {
125
        $reader = phpiredis_reader_create();
0 ignored issues
show
Bug introduced by
The function phpiredis_reader_create was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

125
        $reader = /** @scrutinizer ignore-call */ phpiredis_reader_create();
Loading history...
126
127
        phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
0 ignored issues
show
Bug introduced by
The function phpiredis_reader_set_status_handler was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

127
        /** @scrutinizer ignore-call */ 
128
        phpiredis_reader_set_status_handler($reader, $this->getStatusHandler());
Loading history...
128
        phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());
0 ignored issues
show
Bug introduced by
The function phpiredis_reader_set_error_handler was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

128
        /** @scrutinizer ignore-call */ 
129
        phpiredis_reader_set_error_handler($reader, $this->getErrorHandler());
Loading history...
129
130
        return $reader;
131
    }
132
133
    /**
134
     * Returns the underlying protocol reader resource.
135
     *
136
     * @return resource
137
     */
138
    protected function getReader()
139
    {
140
        return $this->reader;
141
    }
142
143
    /**
144
     * Returns the handler used by the protocol reader for inline responses.
145
     *
146
     * @return Closure
147
     */
148
    protected function getStatusHandler()
149
    {
150
        static $statusHandler;
151
152
        if (!$statusHandler) {
153
            $statusHandler = function ($payload) {
154
                return StatusResponse::get($payload);
155
            };
156
        }
157
158
        return $statusHandler;
159
    }
160
161
    /**
162
     * Returns the handler used by the protocol reader for error responses.
163
     *
164
     * @return Closure
165
     */
166
    protected function getErrorHandler()
167
    {
168
        static $errorHandler;
169
170
        if (!$errorHandler) {
171
            $errorHandler = function ($errorMessage) {
172
                return new ErrorResponse($errorMessage);
173
            };
174
        }
175
176
        return $errorHandler;
177
    }
178
179
    /**
180
     * Helper method used to throw exceptions on socket errors.
181
     */
182
    private function emitSocketError()
183
    {
184
        $errno = socket_last_error();
185
        $errstr = socket_strerror($errno);
186
187
        $this->disconnect();
188
189
        $this->onConnectionError(trim($errstr), $errno);
190
    }
191
192
    /**
193
     * Gets the address of an host from connection parameters.
194
     *
195
     * @param ParametersInterface $parameters Parameters used to initialize the connection.
196
     *
197
     * @return string
198
     */
199
    protected static function getAddress(ParametersInterface $parameters)
200
    {
201
        if (filter_var($host = $parameters->host, FILTER_VALIDATE_IP)) {
202
            return $host;
203
        }
204
205
        if ($host === $address = gethostbyname($host)) {
206
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
207
        }
208
209
        return $address;
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215
    protected function createResource()
216
    {
217
        $parameters = $this->parameters;
218
219
        if ($parameters->scheme === 'unix') {
220
            $address = $parameters->path;
221
            $domain = AF_UNIX;
222
            $protocol = 0;
223
        } else {
224
            if (false === $address = self::getAddress($parameters)) {
0 ignored issues
show
introduced by
The condition false === $address = self::getAddress($parameters) is always false.
Loading history...
225
                $this->onConnectionError("Cannot resolve the address of '$parameters->host'.");
226
            }
227
228
            $domain = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? AF_INET6 : AF_INET;
229
            $protocol = SOL_TCP;
230
        }
231
232
        if (false === $socket = @socket_create($domain, SOCK_STREAM, $protocol)) {
233
            $this->emitSocketError();
234
        }
235
236
        $this->setSocketOptions($socket, $parameters);
0 ignored issues
show
Bug introduced by
It seems like $socket can also be of type Socket; however, parameter $socket of Predis\Connection\Phpire...ion::setSocketOptions() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

236
        $this->setSocketOptions(/** @scrutinizer ignore-type */ $socket, $parameters);
Loading history...
237
        $this->connectWithTimeout($socket, $address, $parameters);
0 ignored issues
show
Bug introduced by
It seems like $socket can also be of type Socket; however, parameter $socket of Predis\Connection\Phpire...n::connectWithTimeout() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

237
        $this->connectWithTimeout(/** @scrutinizer ignore-type */ $socket, $address, $parameters);
Loading history...
238
239
        return $socket;
240
    }
241
242
    /**
243
     * Sets options on the socket resource from the connection parameters.
244
     *
245
     * @param resource            $socket     Socket resource.
246
     * @param ParametersInterface $parameters Parameters used to initialize the connection.
247
     */
248
    private function setSocketOptions($socket, ParametersInterface $parameters)
249
    {
250
        if ($parameters->scheme !== 'unix') {
251
            if (!socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1)) {
252
                $this->emitSocketError();
253
            }
254
255
            if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
256
                $this->emitSocketError();
257
            }
258
        }
259
260
        if (isset($parameters->read_write_timeout)) {
261
            $rwtimeout = (float) $parameters->read_write_timeout;
262
            $timeoutSec = floor($rwtimeout);
263
            $timeoutUsec = ($rwtimeout - $timeoutSec) * 1000000;
264
265
            $timeout = [
266
                'sec' => $timeoutSec,
267
                'usec' => $timeoutUsec,
268
            ];
269
270
            if (!socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, $timeout)) {
271
                $this->emitSocketError();
272
            }
273
274
            if (!socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, $timeout)) {
275
                $this->emitSocketError();
276
            }
277
        }
278
    }
279
280
    /**
281
     * Opens the actual connection to the server with a timeout.
282
     *
283
     * @param resource            $socket     Socket resource.
284
     * @param string              $address    IP address (DNS-resolved from hostname)
285
     * @param ParametersInterface $parameters Parameters used to initialize the connection.
286
     *
287
     * @return void
288
     */
289
    private function connectWithTimeout($socket, $address, ParametersInterface $parameters)
290
    {
291
        socket_set_nonblock($socket);
292
293
        if (@socket_connect($socket, $address, (int) $parameters->port) === false) {
294
            $error = socket_last_error();
295
296
            if ($error != SOCKET_EINPROGRESS && $error != SOCKET_EALREADY) {
297
                $this->emitSocketError();
298
            }
299
        }
300
301
        socket_set_block($socket);
302
303
        $null = null;
304
        $selectable = [$socket];
305
306
        $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0);
307
        $timeoutSecs = floor($timeout);
308
        $timeoutUSecs = ($timeout - $timeoutSecs) * 1000000;
309
310
        $selected = socket_select($selectable, $selectable, $null, $timeoutSecs, $timeoutUSecs);
0 ignored issues
show
Bug introduced by
$timeoutSecs of type double is incompatible with the type integer expected by parameter $tv_sec of socket_select(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

310
        $selected = socket_select($selectable, $selectable, $null, /** @scrutinizer ignore-type */ $timeoutSecs, $timeoutUSecs);
Loading history...
Bug introduced by
$null of type null is incompatible with the type array expected by parameter $except of socket_select(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

310
        $selected = socket_select($selectable, $selectable, /** @scrutinizer ignore-type */ $null, $timeoutSecs, $timeoutUSecs);
Loading history...
Bug introduced by
$timeoutUSecs of type double is incompatible with the type integer expected by parameter $tv_usec of socket_select(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

310
        $selected = socket_select($selectable, $selectable, $null, $timeoutSecs, /** @scrutinizer ignore-type */ $timeoutUSecs);
Loading history...
311
312
        if ($selected === 2) {
313
            $this->onConnectionError('Connection refused.', SOCKET_ECONNREFUSED);
314
        }
315
316
        if ($selected === 0) {
317
            $this->onConnectionError('Connection timed out.', SOCKET_ETIMEDOUT);
318
        }
319
320
        if ($selected === false) {
321
            $this->emitSocketError();
322
        }
323
    }
324
325
    /**
326
     * {@inheritdoc}
327
     */
328
    public function connect()
329
    {
330
        if (parent::connect() && $this->initCommands) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->initCommands of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
331
            foreach ($this->initCommands as $command) {
332
                $response = $this->executeCommand($command);
333
334
                if ($response instanceof ErrorResponseInterface) {
335
                    $this->onConnectionError("`{$command->getId()}` failed: {$response->getMessage()}", 0);
336
                }
337
            }
338
        }
339
    }
340
341
    /**
342
     * {@inheritdoc}
343
     */
344
    public function disconnect()
345
    {
346
        if ($this->isConnected()) {
347
            phpiredis_reader_reset($this->reader);
0 ignored issues
show
Bug introduced by
The function phpiredis_reader_reset was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

347
            /** @scrutinizer ignore-call */ 
348
            phpiredis_reader_reset($this->reader);
Loading history...
348
            socket_close($this->getResource());
349
350
            parent::disconnect();
351
        }
352
    }
353
354
    /**
355
     * {@inheritdoc}
356
     */
357
    protected function write($buffer)
358
    {
359
        $socket = $this->getResource();
360
361
        while (($length = strlen($buffer)) > 0) {
362
            $written = socket_write($socket, $buffer, $length);
363
364
            if ($length === $written) {
365
                return;
366
            }
367
368
            if ($written === false) {
369
                $this->onConnectionError('Error while writing bytes to the server.');
370
            }
371
372
            $buffer = substr($buffer, $written);
373
        }
374
    }
375
376
    /**
377
     * {@inheritdoc}
378
     */
379
    public function read()
380
    {
381
        $socket = $this->getResource();
382
        $reader = $this->reader;
383
384
        while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = phpiredis_reader_get_state($reader)) {
0 ignored issues
show
Bug introduced by
The constant Predis\Connection\PHPIRE...READER_STATE_INCOMPLETE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
Bug introduced by
The function phpiredis_reader_get_state was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

384
        while (PHPIREDIS_READER_STATE_INCOMPLETE === $state = /** @scrutinizer ignore-call */ phpiredis_reader_get_state($reader)) {
Loading history...
385
            if (@socket_recv($socket, $buffer, 4096, 0) === false || $buffer === '' || $buffer === null) {
386
                $this->emitSocketError();
387
            }
388
389
            phpiredis_reader_feed($reader, $buffer);
0 ignored issues
show
Bug introduced by
The function phpiredis_reader_feed was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

389
            /** @scrutinizer ignore-call */ 
390
            phpiredis_reader_feed($reader, $buffer);
Loading history...
390
        }
391
392
        if ($state === PHPIREDIS_READER_STATE_COMPLETE) {
0 ignored issues
show
Bug introduced by
The constant Predis\Connection\PHPIREDIS_READER_STATE_COMPLETE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
393
            return phpiredis_reader_get_reply($reader);
0 ignored issues
show
Bug introduced by
The function phpiredis_reader_get_reply was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

393
            return /** @scrutinizer ignore-call */ phpiredis_reader_get_reply($reader);
Loading history...
394
        } else {
395
            $this->onProtocolError(phpiredis_reader_get_error($reader));
0 ignored issues
show
Bug introduced by
The function phpiredis_reader_get_error was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

395
            $this->onProtocolError(/** @scrutinizer ignore-call */ phpiredis_reader_get_error($reader));
Loading history...
396
397
            return;
398
        }
399
    }
400
401
    /**
402
     * {@inheritdoc}
403
     */
404
    public function writeRequest(CommandInterface $command)
405
    {
406
        $arguments = $command->getArguments();
407
        array_unshift($arguments, $command->getId());
408
409
        $this->write(phpiredis_format_command($arguments));
0 ignored issues
show
Bug introduced by
The function phpiredis_format_command was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

409
        $this->write(/** @scrutinizer ignore-call */ phpiredis_format_command($arguments));
Loading history...
410
    }
411
412
    /**
413
     * {@inheritdoc}
414
     */
415
    public function __wakeup()
416
    {
417
        $this->assertExtensions();
418
        $this->reader = $this->createReader();
419
    }
420
}
421