Redis::handleMessage()   B
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5.2

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 8
cts 10
cp 0.8
rs 8.5906
c 0
b 0
f 0
cc 5
eloc 10
nc 5
nop 1
crap 5.2
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 25 and the first side effect is on line 384.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
namespace Dazzle\Redis;
4
5
use Clue\Redis\Protocol\Model\ErrorReply;
6
use Clue\Redis\Protocol\Model\ModelInterface;
7
use Clue\Redis\Protocol\Parser\ParserException;
8
use Dazzle\Event\BaseEventEmitter;
9
use Dazzle\Loop\LoopAwareTrait;
10
use Dazzle\Loop\LoopInterface;
11
use Dazzle\Promise\Deferred;
12
use Dazzle\Promise\Promise;
13
use Dazzle\Promise\PromiseInterface;
14
use Dazzle\Redis\Driver\Request;
15
use Dazzle\Redis\Driver\Driver;
16
use Dazzle\Redis\Driver\DriverInterface;
17
use Dazzle\Socket\Socket;
18
use Dazzle\Socket\SocketInterface;
19
use Dazzle\Throwable\Exception\Runtime\ExecutionException;
20
use Dazzle\Throwable\Exception\Runtime\UnderflowException;
21
use Dazzle\Throwable\Exception\Runtime\WriteException;
22
use Error;
23
use Exception;
24
25
class Redis extends BaseEventEmitter implements RedisInterface
26
{
27
    use LoopAwareTrait;
28
    use Command\Compose\ApiChannelTrait;
29
    use Command\Compose\ApiClusterTrait;
30
    use Command\Compose\ApiConnTrait;
31
    use Command\Compose\ApiCoreTrait;
32
    use Command\Compose\ApiGeospatialTrait;
33
    use Command\Compose\ApiHyperLogTrait;
34
    use Command\Compose\ApiKeyValTrait;
35
    use Command\Compose\ApiListTrait;
36
    use Command\Compose\ApiSetTrait;
37
    use Command\Compose\ApiSetHashTrait;
38
    use Command\Compose\ApiSetSortedTrait;
39
    use Command\Compose\ApiTransactionTrait;
40
41
    /**
42
     * @var string
43
     */
44
    protected $endpoint;
45
46
    /**
47
     * @var SocketInterface
48
     */
49
    protected $stream;
50
51
    /**
52
     * @var DriverInterface
53
     */
54
    protected $driver;
55
56
    /**
57
     * @var bool
58
     */
59
    protected $isConnected;
60
61
    /**
62
     * @var bool
63
     */
64
    protected $isBeingDisconnected;
65
66
    /**
67
     * @var PromiseInterface|null;
68
     */
69
    protected $endPromise;
70
71
    /**
72
     * @var array
73
     */
74
    private $reqs;
75
76
    /**
77
     * @param string $endpoint
78
     * @param LoopInterface $loop
79
     */
80
    public function __construct($endpoint, LoopInterface $loop)
81
    {
82
        $this->endpoint = $endpoint;
83
        $this->loop = $loop;
84
        $this->stream = null;
85
        $this->driver = new Driver();
86
87
        $this->isConnected = false;
88
        $this->isBeingDisconnected = false;
89
        $this->endPromise = null;
90
91
        $this->reqs = [];
92
    }
93
94
    /**
95
     *
96
     */
97
    public function __destruct()
98
    {
99
        $this->stop();
100
        parent::__destruct();
101
    }
102
103
    /**
104
     * @override
105
     * @inheritDoc
106
     */
107
    public function isPaused()
108
    {
109
        return $this->stream === null ? false : $this->stream->isPaused();
110
    }
111
112
    /**
113
     * @override
114
     * @inheritDoc
115
     */
116
    public function pause()
117
    {
118
        if ($this->stream !== null)
119
        {
120
            $this->stream->pause();
121
        }
122
    }
123
124
    /**
125
     * @override
126
     * @inheritDoc
127
     */
128
    public function resume()
129
    {
130
        if ($this->stream !== null)
131
        {
132
            $this->stream->resume();
133
        }
134
    }
135
136
    /**
137
     * @override
138
     * @inheritDoc
139
     */
140 138
    public function isStarted()
141
    {
142 138
        return $this->isConnected;
143
    }
144
145
    /**
146
     * @override
147
     * @inheritDoc
148
     */
149 138
    public function isBusy()
150
    {
151 138
        return !empty($this->reqs);
152
    }
153
154
    /**
155
     * @override
156
     * @inheritDoc
157
     */
158 138
    public function start()
159
    {
160 138
        if ($this->isStarted())
161
        {
162
            return Promise::doResolve($this);
163
        }
164
165 138
        $ex = null;
166 138
        $stream = null;
167
168
        try
169
        {
170 138
            $stream = $this->createClient($this->endpoint);
171
        }
172
        catch (Error $ex)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
173
        {}
174
        catch (Exception $ex)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
175
        {}
176
177 138
        if ($ex !== null)
178
        {
179
            return Promise::doReject($ex);
180
        }
181
182 138
        $this->isConnected = true;
183 138
        $this->isBeingDisconnected = false;
184 138
        $this->stream = $stream;
185 138
        $this->handleStart();
186 138
        $this->emit('start', [ $this ]);
187
188 138
        return Promise::doResolve($this);
189
    }
190
191
    /**
192
     * @override
193
     * @inheritDoc
194
     */
195 138
    public function stop()
196
    {
197 138
        if (!$this->isStarted())
198
        {
199 138
            return Promise::doResolve($this);
200
        }
201
202 138
        $this->isBeingDisconnected = true;
203 138
        $this->isConnected = false;
204
205 138
        $this->stream->close();
206 138
        $this->stream = null;
207
208 138
        foreach ($this->reqs as $req)
209
        {
210 2
            $req->reject(new ExecutionException('Connection has been closed!'));
211
        }
212
213 138
        $this->reqs = [];
214 138
        $this->handleStop();
215 138
        $this->emit('stop', [ $this ]);
216
217 138
        if ($this->endPromise !== null)
218
        {
219
            $promise = $this->endPromise;
220
            $this->endPromise = null;
221
            $promise->resolve($this);
0 ignored issues
show
Bug introduced by
The method resolve cannot be called on $promise (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
222
        }
223
224 138
        return Promise::doResolve($this);
225
    }
226
227
    /**
228
     * @override
229
     * @inheritDoc
230
     */
231
    public function end()
232
    {
233
        if (!$this->isStarted())
234
        {
235
            return Promise::doResolve($this);
236
        }
237
        if ($this->isBeingDisconnected)
238
        {
239
            return Promise::doReject(new WriteException('Tried to double end same connection.'));
240
        }
241
        if (!$this->isBusy())
242
        {
243
            return $this->stop();
244
        }
245
246
        $promise = new Promise();
247
        $this->isBeingDisconnected = true;
248
        $this->endPromise = $promise;
249
250
        return $promise;
251
    }
252
253
    /**
254
     * Dispatch Redis request.
255
     *
256
     * @param Request $command
257
     * @return PromiseInterface
258
     */
259 138
    protected function dispatch(Request $command)
260
    {
261 138
        $request = new Deferred();
262 138
        $promise = $request->getPromise();
263
264 138
        if ($this->isBeingDisconnected)
265
        {
266
            $request->reject(new ExecutionException('Redis client connection is being stopped now.'));
267
        }
268
        else
269
        {
270 138
            $this->stream->write($this->driver->commands($command));
271 138
            $this->reqs[] = $request;
272
        }
273
274 138
        return $promise;
275
    }
276
277
    /**
278
     * @internal
279
     */
280 138 View Code Duplication
    protected function handleStart()
0 ignored issues
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...
281
    {
282 138
        if ($this->stream !== null)
283
        {
284 138
            $this->stream->on('data', [ $this, 'handleData' ]);
285 138
            $this->stream->on('close', [ $this, 'stop' ]);
286
        }
287 138
    }
288
289
    /**
290
     * @internal
291
     */
292 138 View Code Duplication
    protected function handleStop()
0 ignored issues
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...
293
    {
294 138
        if ($this->stream !== null)
295
        {
296
            $this->stream->removeListener('data', [ $this, 'handleData' ]);
297
            $this->stream->removeListener('close', [ $this, 'stop' ]);
298
        }
299 138
    }
300
301
    /**
302
     * @internal
303
     * @param SocketInterface $stream
304
     * @param string $chunk
305
     */
306 138
    public function handleData($stream, $chunk)
307
    {
308
        try
309
        {
310 138
            $models = $this->driver->parseResponse($chunk);
311
        }
312
        catch (ParserException $error)
313
        {
314
            $this->emit('error', [ $this, $error ]);
315
            $this->stop();
316
            return;
317
        }
318
319 138
        foreach ($models as $data)
320
        {
321
            try
322
            {
323 138
                $this->handleMessage($data);
324
            }
325
            catch (UnderflowException $error)
326
            {
327
                $this->emit('error', [ $this, $error ]);
328
                $this->stop();
329 138
                return;
330
            }
331
        }
332 138
    }
333
334
    /**
335
     * @internal
336
     * @param ModelInterface $message
337
     */
338 138
    protected function handleMessage(ModelInterface $message)
339
    {
340 138
        if (!$this->reqs)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->reqs 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...
341
        {
342
            throw new UnderflowException('Unexpected reply received, no matching request found');
343
        }
344
345 138
        $request = array_shift($this->reqs);
346
347 138
        if ($message instanceof ErrorReply)
348
        {
349
            $request->reject($message);
350
        }
351
        else
352
        {
353 138
            $request->resolve($message->getValueNative());
354
        }
355
356 138
        if ($this->isBeingDisconnected && !$this->isBusy())
357
        {
358 138
            $this->stop();
359
        }
360 138
    }
361
362
    /**
363
     * Create socket client with connection to Redis database.
364
     *
365
     * @param string $endpoint
366
     * @return SocketInterface
367
     * @throws ExecutionException
368
     */
369 138
    protected function createClient($endpoint)
370
    {
371 138
        $ex = null;
372
373
        try
374
        {
375 138
            return new Socket($endpoint, $this->loop);
0 ignored issues
show
Bug introduced by
It seems like $this->loop can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
376
        }
377
        catch (Error $ex)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
378
        {}
379
        catch (Exception $ex)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
380
        {}
381
382
        throw new ExecutionException('Redis connection socket could not be created!', 0, $ex);
383
    }
384
};
385