ApiClient   B
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 408
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 34.84%

Importance

Changes 0
Metric Value
wmc 40
lcom 1
cbo 15
dl 0
loc 408
ccs 46
cts 132
cp 0.3484
rs 7.6556
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 2
A setToken() 0 4 1
A getMessageBuilder() 0 4 1
A getAuthedUser() 0 6 1
A getTeam() 0 6 1
A getChannelGroupOrDMByID() 0 12 3
A getChannels() 0 10 2
A getChannelById() 0 8 1
A getChannelByName() 0 12 3
A getGroups() 0 10 2
A getGroupById() 0 8 1
A getGroupByName() 0 12 3
A getDMs() 0 10 2
A getDMById() 0 12 3
A getDMByUser() 0 4 1
A getDMByUserId() 0 8 1
A getUsers() 0 11 2
A getUserById() 0 8 1
A getUserByName() 0 12 3
A send() 0 9 1
A postMessage() 0 14 2
B apiCall() 0 39 3

How to fix   Complexity   

Complex Class

Complex classes like ApiClient 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 ApiClient, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Slack;
3
4
use GuzzleHttp;
5
use Psr\Http\Message\ResponseInterface;
6
use React\EventLoop\LoopInterface;
7
use React\Promise\Deferred;
8
use Slack\Message\Message;
9
use Slack\Message\MessageBuilder;
10
11
/**
12
 * A client for connecting to the Slack Web API and calling remote API methods.
13
 */
14
class ApiClient
15
{
16
    /**
17
     * The base URL for API requests.
18
     */
19
    const BASE_URL = 'https://slack.com/api/';
20
21
    /**
22
     * @var string The Slack API token string.
23
     */
24
    protected $token;
25
26
    /**
27
     * @var GuzzleHttp\ClientInterface A Guzzle HTTP client.
28
     */
29
    protected $httpClient;
30
31
    /**
32
     * @var LoopInterface An event loop instance.
33
     */
34
    protected $loop;
35
36
    /**
37
     * Creates a new API client instance.
38
     *
39
     * @param GuzzleHttp\ClientInterface $httpClient A Guzzle client instance to
40
     *                                               send requests with.
41
     */
42 58
    public function __construct(LoopInterface $loop, GuzzleHttp\ClientInterface $httpClient = null)
43
    {
44 58
        $this->loop = $loop;
45 58
        $this->httpClient = $httpClient ?: new GuzzleHttp\Client();
46 58
    }
47
48
    /**
49
     * Sets the Slack API token to be used during method calls.
50
     *
51
     * @param string $token The API token string.
52
     */
53
    public function setToken($token)
54
    {
55
        $this->token = $token;
56
    }
57
58
    /**
59
     * Gets a message builder for creating a new message object.
60
     *
61
     * @return \Slack\Message\MessageBuilder
62
     */
63
    public function getMessageBuilder()
64
    {
65
        return new MessageBuilder($this);
66
    }
67
68
    /**
69
     * Gets the currently authenticated user.
70
     *
71
     * @return \React\Promise\PromiseInterface A promise for the currently authenticated user.
72
     */
73
    public function getAuthedUser()
74
    {
75
        return $this->apiCall('auth.test')->then(function (Payload $response) {
76
            return $this->getUserById($response['user_id']);
77
        });
78
    }
79
80
    /**
81
     * Gets information about the current Slack team logged in to.
82
     *
83
     * @return \React\Promise\PromiseInterface A promise for the current Slack team.
84
     */
85 1
    public function getTeam()
86
    {
87
        return $this->apiCall('team.info')->then(function (Payload $response) {
88 1
            return new Team($this, $response['team']);
89 1
        });
90
    }
91
92
    /**
93
     * Gets a channel, group, or DM channel by ID.
94
     *
95
     * @param string $id The channel ID.
96
     *
97
     * @return \React\Promise\PromiseInterface A promise for a channel interface.
98
     */
99
    public function getChannelGroupOrDMByID($id)
100
    {
101
        if ($id[0] === 'D') {
102
            return $this->getDMById($id);
103
        }
104
105
        if ($id[0] === 'G') {
106
            return $this->getGroupById($id);
107
        }
108
109
        return $this->getChannelById($id);
110
    }
111
112
    /**
113
     * Gets all channels in the team.
114
     *
115
     * @return \React\Promise\PromiseInterface
116
     */
117
    public function getChannels()
118
    {
119
        return $this->apiCall('channels.list')->then(function ($response) {
120
            $channels = [];
121
            foreach ($response['channels'] as $channel) {
122
                $channels[] = new Channel($this, $channel);
123
            }
124
            return $channels;
125
        });
126
    }
127
128
    /**
129
     * Gets a channel by its ID.
130
     *
131
     * @param string $id A channel ID.
132
     *
133
     * @return \React\Promise\PromiseInterface A promise for a channel object.
134
     */
135 2
    public function getChannelById($id)
136
    {
137 2
        return $this->apiCall('channels.info', [
138 2
            'channel' => $id,
139
        ])->then(function (Payload $response) {
140 2
            return new Channel($this, $response['channel']);
141 2
        });
142
    }
143
144
    /**
145
     * Gets a channel by its name.
146
     *
147
     * @param string $name The name of the channel.
148
     *
149
     * @return \React\Promise\PromiseInterface
150
     */
151
    public function getChannelByName($name)
152
    {
153
        return $this->getChannels()->then(function (array $channels) use ($name) {
154
            foreach ($channels as $channel) {
155
                if ($channel->getName() === $name) {
156
                    return $channel;
157
                }
158
            }
159
160
            throw new ApiException('Channel ' . $name . ' not found.');
161
        });
162
    }
163
164
    /**
165
     * Gets all groups the authenticated user is a member of.
166
     *
167
     * @return \React\Promise\PromiseInterface
168
     */
169
    public function getGroups()
170
    {
171
        return $this->apiCall('groups.list')->then(function ($response) {
172
            $groups = [];
173
            foreach ($response['groups'] as $group) {
174
                $groups[] = new Group($this, $group);
175
            }
176
            return $groups;
177
        });
178
    }
179
180
    /**
181
     * Gets a group by its ID.
182
     *
183
     * @param string $id A group ID.
184
     *
185
     * @return \React\Promise\PromiseInterface A promise for a group object.
186
     */
187
    public function getGroupById($id)
188
    {
189
        return $this->apiCall('groups.info', [
190
            'channel' => $id,
191
        ])->then(function (Payload $response) {
192
            return new Group($this, $response['group']);
193
        });
194
    }
195
196
    /**
197
     * Gets a group by its name.
198
     *
199
     * @param string $name The name of the group.
200
     *
201
     * @return \React\Promise\PromiseInterface
202
     */
203
    public function getGroupByName($name)
204
    {
205
        return $this->getGroups()->then(function (array $groups) use ($name) {
206
            foreach ($groups as $group) {
207
                if ($group->getName() === $name) {
208
                    return $group;
209
                }
210
            }
211
212
            throw new ApiException('Group ' . $name . ' not found.');
213
        });
214
    }
215
216
    /**
217
     * Gets all DMs the authenticated user has.
218
     *
219
     * @return \React\Promise\PromiseInterface
220
     */
221
    public function getDMs()
222
    {
223
        return $this->apiCall('im.list')->then(function ($response) {
224
            $dms = [];
225
            foreach ($response['ims'] as $dm) {
226
                $dms[] = new DirectMessageChannel($this, $dm);
227
            }
228
            return $dms;
229
        });
230
    }
231
232
    /**
233
     * Gets a direct message channel by its ID.
234
     *
235
     * @param string $id A DM channel ID.
236
     *
237
     * @return \React\Promise\PromiseInterface A promise for a DM object.
238
     */
239
    public function getDMById($id)
240
    {
241
        return $this->getDMs()->then(function (array $dms) use ($id) {
242
            foreach ($dms as $dm) {
243
                if ($dm->getId() === $id) {
244
                    return $dm;
245
                }
246
            }
247
248
            throw new ApiException('DM ' . $id . ' not found.');
249
        });
250
    }
251
252
    /**
253
     * Gets a direct message channel for a given user.
254
     *
255
     * @param User $user The user to get a DM for.
256
     *
257
     * @return \React\Promise\PromiseInterface A promise for a DM object.
258
     */
259
    public function getDMByUser(User $user)
260
    {
261
        return $this->getDMByUserId($user->getId());
262
    }
263
264
    /**
265
     * Gets a direct message channel by user's ID.
266
     *
267
     * @param string $id A user ID.
268
     *
269
     * @return \React\Promise\PromiseInterface A promise for a DM object.
270
     */
271
    public function getDMByUserId($id)
272
    {
273
        return $this->apiCall('im.open', [
274
            'user' => $id,
275
        ])->then(function (Payload $response) {
276
            return $this->getDMById($response['channel']['id']);
277
        });
278
    }
279
280
    /**
281
     * Gets all users in the Slack team.
282
     *
283
     * @return \React\Promise\PromiseInterface A promise for an array of users.
284
     */
285 2
    public function getUsers()
286
    {
287
        // get the user list
288
        return $this->apiCall('users.list')->then(function (Payload $response) {
289 2
            $users = [];
290 2
            foreach ($response['members'] as $member) {
291 2
                $users[] = new User($this, $member);
292 2
            }
293 2
            return $users;
294 2
        });
295
    }
296
297
    /**
298
     * Gets a user by its ID.
299
     *
300
     * @param string $id A user ID.
301
     *
302
     * @return \React\Promise\PromiseInterface A promise for a user object.
303
     */
304 4
    public function getUserById($id)
305
    {
306 4
        return $this->apiCall('users.info', [
307 4
            'user' => $id,
308
        ])->then(function (Payload $response) {
309 4
            return new User($this, $response['user']);
310 4
        });
311
    }
312
313
    /**
314
     * Gets a user by username.
315
     *
316
     * If the user could not be found, the returned promise is rejected with a
317
     * `UserNotFoundException` exception.
318
     *
319
     * @return \React\Promise\PromiseInterface A promise for a user object.
320
     */
321 1
    public function getUserByName($username)
322
    {
323
        return $this->getUsers()->then(function (array $users) use ($username) {
324 1
            foreach ($users as $user) {
325 1
                if ($user->getUsername() === $username) {
326 1
                    return $user;
327
                }
328 1
            }
329
330
            throw new UserNotFoundException("The user \"$username\" does not exist.");
331 1
        });
332
    }
333
334
    /**
335
     * Sends a regular text message to a given channel.
336
     *
337
     * @param  string                          $text    The message text.
338
     * @param  ChannelInterface                $channel The channel to send the message to.
339
     * @return \React\Promise\PromiseInterface
340
     */
341
    public function send($text, ChannelInterface $channel)
342
    {
343
        $message = $this->getMessageBuilder()
344
                        ->setText($text)
345
                        ->setChannel($channel)
346
                        ->create();
347
348
        return $this->postMessage($message);
349
    }
350
351
    /**
352
     * Posts a message.
353
     *
354
     * @param \Slack\Message\Message $message The message to post.
355
     *
356
     * @return \React\Promise\PromiseInterface
357
     */
358
    public function postMessage(Message $message)
359
    {
360
        $options = [
361
            'text' => $message->getText(),
362
            'channel' => $message->data['channel'],
363
            'as_user' => true,
364
        ];
365
366
        if ($message->hasAttachments()) {
367
            $options['attachments'] = json_encode($message->getAttachments());
368
        }
369
370
        return $this->apiCall('chat.postMessage', $options);
371
    }
372
373
    /**
374
     * Sends an API request.
375
     *
376
     * @param string $method The API method to call.
377
     * @param array  $args   An associative array of arguments to pass to the
378
     *                       method call.
379
     *
380
     * @return \React\Promise\PromiseInterface A promise for an API response.
381
     */
382 11
    public function apiCall($method, array $args = [])
383
    {
384
        // create the request url
385 11
        $requestUrl = self::BASE_URL . $method;
386
387
        // set the api token
388 11
        $args['token'] = $this->token;
389
390
        // send a post request with all arguments
391 11
        $promise = $this->httpClient->postAsync($requestUrl, [
392 11
            'form_params' => $args,
393 11
        ]);
394
395
        // Add requests to the event loop to be handled at a later date.
396
        $this->loop->futureTick(function () use ($promise) {
397 11
            $promise->wait();
398 11
        });
399
400
        // When the response has arrived, parse it and resolve. Note that our
401
        // promises aren't pretty; Guzzle promises are not compatible with React
402
        // promises, so the only Guzzle promises ever used die in here and it is
403
        // React from here on out.
404 11
        $deferred = new Deferred();
405 11
        $promise->then(function (ResponseInterface $response) use ($deferred) {
406
            // get the response as a json object
407 11
            $payload = Payload::fromJson((string) $response->getBody());
408
409
            // check if there was an error
410 11
            if (isset($payload['ok']) && $payload['ok'] === true) {
411 11
                $deferred->resolve($payload);
412 11
            } else {
413
                // make a nice-looking error message and throw an exception
414
                $niceMessage = ucfirst(str_replace('_', ' ', $payload['error']));
415
                $deferred->reject(new ApiException($niceMessage));
416
            }
417 11
        });
418
419 11
        return $deferred->promise();
420
    }
421
}
422