Completed
Push — master ( 0cd5aa...20f312 )
by Kamil
02:43
created

Redis   D

Complexity

Total Complexity 40

Size/Duplication

Total Lines 356
Duplicated Lines 4.49 %

Coupling/Cohesion

Components 1
Dependencies 24

Test Coverage

Coverage 52.1%

Importance

Changes 0
Metric Value
wmc 40
lcom 1
cbo 24
dl 16
loc 356
ccs 62
cts 119
cp 0.521
rs 4.6136
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
A __destruct() 0 5 1
A isPaused() 0 4 2
A pause() 0 7 2
A resume() 0 7 2
A isStarted() 0 4 1
A isBusy() 0 4 1
B start() 0 32 5
B stop() 0 31 4
A end() 0 17 3
A dispatch() 0 17 2
A handleStart() 8 8 2
A handleStop() 8 8 2
B handleData() 0 27 4
B handleMessage() 0 23 5
A createClient() 0 15 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Redis often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Redis, and based on these observations, apply Extract Interface, too.

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 380.

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
242
        $promise = new Promise();
243
        $this->isBeingDisconnected = true;
244
        $this->endPromise = $promise;
245
246
        return $promise;
247
    }
248
249
    /**
250
     * Dispatch Redis request.
251
     *
252
     * @param Request $command
253
     * @return PromiseInterface
254
     */
255 138
    protected function dispatch(Request $command)
256
    {
257 138
        $request = new Deferred();
258 138
        $promise = $request->getPromise();
259
260 138
        if ($this->isBeingDisconnected)
261
        {
262
            $request->reject(new ExecutionException('Redis client connection is being stopped now.'));
263
        }
264
        else
265
        {
266 138
            $this->stream->write($this->driver->commands($command));
267 138
            $this->reqs[] = $request;
268
        }
269
270 138
        return $promise;
271
    }
272
273
    /**
274
     * @internal
275
     */
276 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...
277
    {
278 138
        if ($this->stream !== null)
279
        {
280 138
            $this->stream->on('data', [ $this, 'handleData' ]);
281 138
            $this->stream->on('close', [ $this, 'stop' ]);
282
        }
283 138
    }
284
285
    /**
286
     * @internal
287
     */
288 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...
289
    {
290 138
        if ($this->stream !== null)
291
        {
292
            $this->stream->removeListener('data', [ $this, 'handleData' ]);
293
            $this->stream->removeListener('close', [ $this, 'stop' ]);
294
        }
295 138
    }
296
297
    /**
298
     * @internal
299
     * @param SocketInterface $stream
300
     * @param string $chunk
301
     */
302 138
    public function handleData($stream, $chunk)
303
    {
304
        try
305
        {
306 138
            $models = $this->driver->parseResponse($chunk);
307
        }
308
        catch (ParserException $error)
309
        {
310
            $this->emit('error', [ $this, $error ]);
311
            $this->stop();
312
            return;
313
        }
314
315 138
        foreach ($models as $data)
316
        {
317
            try
318
            {
319 138
                $this->handleMessage($data);
320
            }
321
            catch (UnderflowException $error)
322
            {
323
                $this->emit('error', [ $this, $error ]);
324
                $this->stop();
325 138
                return;
326
            }
327
        }
328 138
    }
329
330
    /**
331
     * @internal
332
     * @param ModelInterface $message
333
     */
334 138
    protected function handleMessage(ModelInterface $message)
335
    {
336 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...
337
        {
338
            throw new UnderflowException('Unexpected reply received, no matching request found');
339
        }
340
341 138
        $request = array_shift($this->reqs);
342
343 138
        if ($message instanceof ErrorReply)
344
        {
345
            $request->reject($message);
346
        }
347
        else
348
        {
349 138
            $request->resolve($message->getValueNative());
350
        }
351
352 138
        if ($this->isBeingDisconnected && !$this->isBusy())
353
        {
354 138
            $this->stop();
355
        }
356 138
    }
357
358
    /**
359
     * Create socket client with connection to Redis database.
360
     *
361
     * @param string $endpoint
362
     * @return SocketInterface
363
     * @throws ExecutionException
364
     */
365 138
    protected function createClient($endpoint)
366
    {
367 138
        $ex = null;
368
369
        try
370
        {
371 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...
372
        }
373
        catch (Error $ex)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
374
        {}
375
        catch (Exception $ex)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
376
        {}
377
378
        throw new ExecutionException('Redis connection socket could not be created!', 0, $ex);
379
    }
380
};
381