Passed
Pull Request — master (#17)
by Michael
03:58
created

RealTimeClient   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 418
Duplicated Lines 22.01 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 0%

Importance

Changes 19
Bugs 1 Features 3
Metric Value
wmc 54
c 19
b 1
f 3
lcom 1
cbo 15
dl 92
loc 418
ccs 0
cts 217
cp 0
rs 6.6108

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
B connect() 0 63 5
A disconnect() 0 9 2
A getTeam() 0 8 2
A getChannels() 8 8 2
A getChannelById() 12 12 3
A getGroups() 8 8 2
A getGroupById() 12 12 3
A getDMs() 8 8 2
A getDMById() 12 12 3
A getUsers() 8 8 2
A getUserById() 12 12 3
B postMessage() 0 27 3
A newWebSocket() 0 11 1
D onMessage() 12 85 19

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 RealTimeClient 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 RealTimeClient, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Slack;
3
4
use Evenement\EventEmitterTrait;
5
use GuzzleHttp;
6
use Ratchet\Client\Connector;
7
use Ratchet\Client\WebSocket;
8
use React\EventLoop\LoopInterface;
9
use React\Promise;
10
use Slack\Message\Message;
11
12
/**
13
 * A client for the Slack real-time messaging API.
14
 */
15
class RealTimeClient extends ApiClient
16
{
17
    use EventEmitterTrait;
18
19
    /**
20
     * @var Connector Factory to create WebSocket connections.
21
     */
22
    protected $connector;
23
24
    /**
25
     * @var WebSocket A websocket connection to the Slack API.
26
     */
27
    protected $websocket;
28
29
    /**
30
     * @var int The ID of the last payload sent to Slack.
31
     */
32
    protected $lastMessageId = 0;
33
34
    /**
35
     * @var array An array of pending messages waiting for successful confirmation
36
     *            from Slack.
37
     */
38
    protected $pendingMessages = [];
39
40
    /**
41
     * @var bool Indicates if the client is connected.
42
     */
43
    protected $connected = false;
44
45
    /**
46
     * @var Team The team logged in to.
47
     */
48
    protected $team;
49
50
    /**
51
     * @var array A map of users.
52
     */
53
    protected $users = [];
54
55
    /**
56
     * @var array A map of channels.
57
     */
58
    protected $channels = [];
59
60
    /**
61
     * @var array A map of groups.
62
     */
63
    protected $groups = [];
64
65
    /**
66
     * @var array A map of direct message channels.
67
     */
68
    protected $dms = [];
69
70
    /**
71
     * RealTimeClient Constructor.
72
     *
73
     * @param LoopInterface $loop Event Loop.
74
     * @param GuzzleHttp\ClientInterface $httpClient Guzzle HTTP Client.
75
     * @param Connector $connector Connects to Slack RTM.
76
     */
77
    public function __construct(
78
        LoopInterface $loop,
79
        GuzzleHttp\ClientInterface $httpClient = null,
80
        Connector $connector = null
81
    ) {
82
        parent::__construct($loop, $httpClient);
83
84
        $this->connector = $connector ?: new Connector($loop);
85
    }
86
87
    /**
88
     * Connects to the real-time messaging server.
89
     *
90
     * @return \React\Promise\PromiseInterface
91
     */
92
    public function connect()
93
    {
94
        $deferred = new Promise\Deferred();
95
96
        // Request a real-time connection...
97
        $this->apiCall('rtm.start')
98
99
        // then connect to the socket...
100
        ->then(function (Payload $response) {
101
            $responseData = $response->getData();
102
            // get the team info
103
            $this->team = new Team($this, $responseData['team']);
104
105
            // Populate self user.
106
            $this->users[$responseData['self']['id']] = new User($this, $responseData['self']);
107
108
            // populate list of users
109
            foreach ($responseData['users'] as $data) {
110
                $this->users[$data['id']] = new User($this, $data);
111
            }
112
113
            // populate list of channels
114
            foreach ($responseData['channels'] as $data) {
115
                $this->channels[$data['id']] = new Channel($this, $data);
116
            }
117
118
            // populate list of groups
119
            foreach ($responseData['groups'] as $data) {
120
                $this->groups[$data['id']] = new Group($this, $data);
121
            }
122
123
            // populate list of dms
124
            foreach ($responseData['ims'] as $data) {
125
                $this->dms[$data['id']] = new DirectMessageChannel($this, $data);
126
            }
127
128
            // initiate the websocket connection
129
            return $this->newWebSocket($responseData['url']);
130
        }, function($exception) use ($deferred) {
131
            // if connection was not successful
132
            $deferred->reject(new ConnectionException(
133
                'Could not connect to Slack API: '. $exception->getMessage(),
134
                $exception->getCode()
135
            ));
136
        })
137
138
        // then wait for the connection to be ready.
139
        ->then(function (WebSocket $socket) use ($deferred) {
140
            $this->websocket = $socket;
141
142
            $this->once('hello', function () use ($deferred) {
143
                $deferred->resolve();
144
            });
145
146
            $this->once('error', function ($data) use ($deferred) {
147
                $deferred->reject(new ConnectionException(
148
                    'Could not connect to WebSocket: '.$data['error']['msg'],
149
                    $data['error']['code']));
150
            });
151
        });
152
153
        return $deferred->promise();
154
    }
155
156
    /**
157
     * Disconnects the client.
158
     */
159
    public function disconnect()
160
    {
161
        if (!$this->connected) {
162
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
163
        }
164
165
        $this->websocket->close();
166
        $this->connected = false;
167
    }
168
169
    /**
170
     * {@inheritDoc}
171
     */
172
    public function getTeam()
173
    {
174
        if (!$this->connected) {
175
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
176
        }
177
178
        return Promise\resolve($this->team);
179
    }
180
181
    /**
182
     * {@inheritDoc}
183
     */
184 View Code Duplication
    public function getChannels()
185
    {
186
        if (!$this->connected) {
187
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
188
        }
189
190
        return Promise\resolve(array_values($this->channels));
191
    }
192
193
    /**
194
     * {@inheritDoc}
195
     */
196 View Code Duplication
    public function getChannelById($id)
197
    {
198
        if (!$this->connected) {
199
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
200
        }
201
202
        if (!isset($this->channels[$id])) {
203
            return Promise\reject(new ApiException("No channel exists for ID '$id'."));
204
        }
205
206
        return Promise\resolve($this->channels[$id]);
207
    }
208
209
    /**
210
     * {@inheritDoc}
211
     */
212 View Code Duplication
    public function getGroups()
213
    {
214
        if (!$this->connected) {
215
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
216
        }
217
218
        return Promise\resolve(array_values($this->groups));
219
    }
220
221
    /**
222
     * {@inheritDoc}
223
     */
224 View Code Duplication
    public function getGroupById($id)
225
    {
226
        if (!$this->connected) {
227
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
228
        }
229
230
        if (!isset($this->groups[$id])) {
231
            return Promise\reject(new ApiException("No group exists for ID '$id'."));
232
        }
233
234
        return Promise\resolve($this->groups[$id]);
235
    }
236
237
    /**
238
     * {@inheritDoc}
239
     */
240 View Code Duplication
    public function getDMs()
241
    {
242
        if (!$this->connected) {
243
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
244
        }
245
246
        return Promise\resolve(array_values($this->dms));
247
    }
248
249
    /**
250
     * {@inheritDoc}
251
     */
252 View Code Duplication
    public function getDMById($id)
253
    {
254
        if (!$this->connected) {
255
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
256
        }
257
258
        if (!isset($this->dms[$id])) {
259
            return Promise\reject(new ApiException("No DM exists for ID '$id'."));
260
        }
261
262
        return Promise\resolve($this->dms[$id]);
263
    }
264
265
    /**
266
     * {@inheritDoc}
267
     */
268 View Code Duplication
    public function getUsers()
269
    {
270
        if (!$this->connected) {
271
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
272
        }
273
274
        return Promise\resolve(array_values($this->users));
275
    }
276
277
    /**
278
     * {@inheritDoc}
279
     */
280 View Code Duplication
    public function getUserById($id)
281
    {
282
        if (!$this->connected) {
283
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
284
        }
285
286
        if (!isset($this->users[$id])) {
287
            return Promise\reject(new ApiException("No user exists for ID '$id'."));
288
        }
289
290
        return Promise\resolve($this->users[$id]);
291
    }
292
293
    /**
294
     * {@inheritDoc}
295
     */
296
    public function postMessage(Message $message)
297
    {
298
        if (!$this->connected) {
299
            return Promise\reject(new ConnectionException('Client not connected. Did you forget to call `connect()`?'));
300
        }
301
302
        // We can't send attachments using the RTM API, so revert to the web API
303
        // to send the message
304
        if ($message->hasAttachments()) {
305
            return parent::postMessage($message);
306
        }
307
308
        $data = [
309
            'id' => ++$this->lastMessageId,
310
            'type' => 'message',
311
            'channel' => $message->data['channel'],
312
            'text' => $message->getText(),
313
        ];
314
        $this->websocket->send(json_encode($data));
315
316
        // Create a deferred object and add message to pending list so when a
317
        // success message arrives, we can de-queue it and resolve the promise.
318
        $deferred = new Promise\Deferred();
319
        $this->pendingMessages[$this->lastMessageId] = $deferred;
320
321
        return $deferred->promise();
322
    }
323
324
    /**
325
     * Creates a new WebSocket for the given URL.
326
     *
327
     * @param string $url WebSocket URL.
328
     * @return \React\Promise\PromiseInterface
329
     */
330
    private function newWebSocket($url)
331
    {
332
        return $this->connector->__invoke($url)->then(function (WebSocket $socket) {
333
            $socket->on('message', function ($data) {
334
                // parse the message and get the event name
335
                $this->onMessage(Payload::fromJson($data));
336
            });
337
338
            return $socket;
339
        });
340
    }
341
342
    /**
343
     * Handles incoming websocket messages and emits them as remote events.
344
     *
345
     * @param Payload $payload A websocket message.
346
     */
347
    private function onMessage(Payload $payload)
348
    {
349
        if (isset($payload['type'])) {
350
            switch ($payload['type']) {
351
                case 'hello':
352
                    $this->connected = true;
353
                    break;
354
355
                case 'team_rename':
356
                    $this->team->data['name'] = $payload['name'];
357
                    break;
358
359
                case 'team_domain_change':
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
360
361
                    $this->team->data['domain'] = $payload['domain'];
362
                    break;
363
364
                case 'channel_created':
365
                    $this->getChannelById($payload['channel']['id'])->then(function (Channel $channel) {
366
                        $this->channels[$channel->getId()] = $channel;
367
                    });
368
                    break;
369
370
                case 'channel_deleted':
371
                    unset($this->channels[$payload['channel']['id']]);
372
                    break;
373
374
                case 'channel_rename':
375
                    $this->channels[$payload['channel']['id']]->data['name']
376
                        = $payload['channel']['name'];
377
                    break;
378
379 View Code Duplication
                case 'channel_archive':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
380
                    $this->channels[$payload['channel']['id']]->data['is_archived'] = true;
381
                    break;
382
383 View Code Duplication
                case 'channel_unarchive':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
384
                    $this->channels[$payload['channel']['id']]->data['is_archived'] = false;
385
                    break;
386
387
                case 'group_joined':
388
                    $group = new Group($this, $payload['channel']);
389
                    $this->groups[$group->getId()] = $group;
390
                    break;
391
392
                case 'group_rename':
393
                    $this->groups[$payload['group']['id']]->data['name']
394
                        = $payload['channel']['name'];
395
                    break;
396
397 View Code Duplication
                case 'group_archive':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
398
                    $this->groups[$payload['group']['id']]->data['is_archived'] = true;
399
                    break;
400
401 View Code Duplication
                case 'group_unarchive':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
402
                    $this->groups[$payload['group']['id']]->data['is_archived'] = false;
403
                    break;
404
405
                case 'im_created':
406
                    $dm = new DirectMessageChannel($this, $payload['channel']);
407
                    $this->dms[$dm->getId()] = $dm;
408
                    break;
409
            }
410
411
            // emit an event with the attached json
412
            $this->emit($payload['type'], [$payload]);
413
        } else {
414
            // If reply_to is set, then it is a server confirmation for a previously
415
            // sent message
416
            if (isset($payload['reply_to'])) {
417
                if (isset($this->pendingMessages[$payload['reply_to']])) {
418
                    $deferred = $this->pendingMessages[$payload['reply_to']];
419
420
                    // Resolve or reject the promise that was waiting for the reply.
421
                    if (isset($payload['ok']) && $payload['ok'] === true) {
422
                        $deferred->resolve();
423
                    } else {
424
                        $deferred->reject($payload['error']);
425
                    }
426
427
                    unset($this->pendingMessages[$payload['reply_to']]);
428
                }
429
            }
430
        }
431
    }
432
}
433