Passed
Push — master ( 434123...44af68 )
by y
01:43
created

Api   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Importance

Changes 18
Bugs 1 Features 2
Metric Value
eloc 105
c 18
b 1
f 2
dl 0
loc 402
rs 9.1199
wmc 41

29 Methods

Rating   Name   Duplication   Size   Complexity  
A getCustomField() 0 2 1
A getDefaultWorkspace() 0 2 1
A getLog() 0 2 1
A getWebhookEvent() 0 2 1
A delete() 0 2 1
A getPool() 0 2 1
A put() 0 9 1
A getTag() 0 2 1
A setLog() 0 2 1
A getTaskList() 0 2 1
A getUser() 0 2 1
A getAttachment() 0 2 1
A getStory() 0 2 1
A getWorkspace() 0 2 1
A loadAll() 0 2 1
A getSection() 0 2 1
A getProject() 0 2 1
A getMe() 0 2 1
A __construct() 0 3 1
A getPortfolio() 0 2 1
A getWebhook() 0 10 2
A post() 0 9 1
A load() 0 7 2
A loadEach() 0 14 4
A getTeam() 0 2 1
A getTask() 0 2 1
B call() 0 42 7
A get() 0 6 2
A factory() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Api 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.

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

1
<?php
2
3
namespace Helix\Asana;
4
5
use Generator;
6
use Helix\Asana\Api\AsanaError;
7
use Helix\Asana\Api\Pool;
8
use Helix\Asana\Base\AbstractEntity;
9
use Helix\Asana\Base\Data;
10
use Helix\Asana\Project\Section;
11
use Helix\Asana\Task\Attachment;
12
use Helix\Asana\Task\Story;
13
use Helix\Asana\User\TaskList;
14
use Helix\Asana\Webhook\ProjectWebhook;
15
use Helix\Asana\Webhook\TaskWebhook;
16
use Psr\Log\LoggerInterface;
17
use Psr\Log\NullLogger;
18
19
/**
20
 * API access.
21
 *
22
 * @see https://app.asana.com/-/developer_console
23
 */
24
class Api {
25
26
    /**
27
     * @var LoggerInterface
28
     */
29
    protected $log;
30
31
    /**
32
     * @var Pool
33
     */
34
    protected $pool;
35
36
    /**
37
     * @var string
38
     */
39
    protected $token;
40
41
    /**
42
     * @param string $token
43
     * @param null|Pool $pool
44
     */
45
    public function __construct (string $token, Pool $pool = null) {
46
        $this->token = $token;
47
        $this->pool = $pool ?? new Pool();
48
    }
49
50
    /**
51
     * cURL transport.
52
     *
53
     * @param string $method
54
     * @param string $path
55
     * @param array $curlOpts
56
     * @return null|array
57
     * @internal
58
     */
59
    public function call (string $method, string $path, array $curlOpts = []) {
60
        $log = $this->getLog();
61
        $log->log(LOG_DEBUG, "{$method} {$path}", $curlOpts);
62
        /** @var resource $ch */
63
        $ch = curl_init();
64
        curl_setopt_array($ch, [
65
            CURLOPT_CUSTOMREQUEST => $method,
66
            CURLOPT_URL => "https://app.asana.com/api/1.0/{$path}",
67
            CURLOPT_USERAGENT => 'hfw/asana',
68
            CURLOPT_FOLLOWLOCATION => false, // HTTP 201 includes Location
69
            CURLOPT_RETURNTRANSFER => true,
70
            CURLOPT_HEADER => true,
71
        ]);
72
        $curlOpts[CURLOPT_HTTPHEADER][] = "Authorization: Bearer {$this->token}";
73
        $curlOpts[CURLOPT_HTTPHEADER][] = 'Accept: application/json';
74
        $curlOpts[CURLOPT_HTTPHEADER][] = 'Expect:'; // prevent http 100
75
        curl_setopt_array($ch, $curlOpts);
76
        RETRY:
77
        $response = curl_exec($ch);
78
        $curlInfo = curl_getinfo($ch);
79
        if ($response === false) {
80
            throw new AsanaError(curl_errno($ch), curl_error($ch), $curlInfo);
81
        }
82
        /** @var string $response */
83
        [$head, $body] = explode("\r\n\r\n", $response, 2);
84
        $curlInfo['response_headers'] = $headers = array_column(array_map(function($header) {
85
            return explode(': ', $header, 2);
86
        }, array_slice(explode("\r\n", $head), 1)), 1, 0);
87
        switch ($curlInfo['http_code']) {
88
            case 200:
89
            case 201:
90
                return json_decode($body, true, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR);
91
            case 404:
92
                return null;
93
            case 429:
94
                $log->log(LOG_DEBUG, "Retry-After: {$headers['Retry-After']}");
95
                sleep($headers['Retry-After']);
96
                goto RETRY;
97
            default:
98
                $log->log(LOG_ERR, "Asana {$curlInfo['http_code']}: {$body}", $curlInfo);
99
            case 412: // normal sync error. skip log.
100
                throw new AsanaError($curlInfo['http_code'], $body, $curlInfo);
101
        }
102
    }
103
104
    /**
105
     * `HTTP DELETE`
106
     *
107
     * @param string $path
108
     */
109
    public function delete (string $path): void {
110
        $this->call('DELETE', $path);
111
    }
112
113
    /**
114
     * The central point of object creation.
115
     *
116
     * This can be overridden to return custom extensions.
117
     *
118
     * @param Api|Data $caller
119
     * @param string $class
120
     * @param array $data
121
     * @return mixed|Data|AbstractEntity
122
     */
123
    public function factory ($caller, string $class, array $data = []) {
124
        return new $class($caller, $data);
125
    }
126
127
    /**
128
     * `HTTP GET`
129
     *
130
     * @param string $path
131
     * @param array $query
132
     * @param array $opt
133
     * @return null|array
134
     */
135
    public function get (string $path, array $query = [], array $opt = []) {
136
        foreach ($opt as $name => $value) {
137
            $query["opt_{$name}"] = $value;
138
        }
139
        $response = $this->call('GET', $path . '?' . http_build_query($query));
140
        return $response['data'] ?? null;
141
    }
142
143
    /**
144
     * Loads an {@link Attachment}.
145
     *
146
     * @param string $gid
147
     * @return null|Attachment
148
     */
149
    public function getAttachment (string $gid) {
150
        return $this->load($this, Attachment::class, "attachments/{$gid}");
151
    }
152
153
    /**
154
     * Loads a {@link CustomField}.
155
     *
156
     * @param string $gid
157
     * @return null|CustomField
158
     */
159
    public function getCustomField (string $gid) {
160
        return $this->load($this, CustomField::class, "custom_fields/{$gid}");
161
    }
162
163
    /**
164
     * The API user's default workspace.
165
     *
166
     * You should only rely on this if the API user is in exactly one workspace.
167
     *
168
     * @return Workspace
169
     */
170
    public function getDefaultWorkspace () {
171
        return $this->getMe()->getDefaultWorkspace();
172
    }
173
174
    /**
175
     * @return LoggerInterface
176
     */
177
    public function getLog () {
178
        return $this->log ?? $this->log = new NullLogger();
179
    }
180
181
    /**
182
     * @return User
183
     */
184
    public function getMe () {
185
        return $this->getUser('me');
186
    }
187
188
    /**
189
     * @return Pool
190
     */
191
    public function getPool () {
192
        return $this->pool;
193
    }
194
195
    /**
196
     * Loads a {@link Portfolio}.
197
     *
198
     * @param string $gid
199
     * @return null|Portfolio
200
     */
201
    public function getPortfolio (string $gid) {
202
        return $this->load($this, Portfolio::class, "portfolios/{$gid}");
203
    }
204
205
    /**
206
     * Loads a {@link Project}.
207
     *
208
     * @param string $gid
209
     * @return null|Project
210
     */
211
    public function getProject (string $gid) {
212
        return $this->load($this, Project::class, "projects/{$gid}");
213
    }
214
215
    /**
216
     * Loads a {@link Section}.
217
     *
218
     * @param string $gid
219
     * @return null|Section
220
     */
221
    public function getSection (string $gid) {
222
        return $this->load($this, Section::class, "sections/{$gid}");
223
    }
224
225
    /**
226
     * Loads a {@link Story}.
227
     *
228
     * @param string $gid
229
     * @return null|Story
230
     */
231
    public function getStory (string $gid) {
232
        return $this->load($this, Story::class, "stories/{$gid}");
233
    }
234
235
    /**
236
     * Loads a {@link Tag}.
237
     *
238
     * @param string $gid
239
     * @return null|Tag
240
     */
241
    public function getTag (string $gid) {
242
        return $this->load($this, Tag::class, "tags/{$gid}");
243
    }
244
245
    /**
246
     * Loads a {@link Task}.
247
     *
248
     * @param string $gid
249
     * @return null|Task
250
     */
251
    public function getTask (string $gid) {
252
        return $this->load($this, Task::class, "tasks/{$gid}");
253
    }
254
255
    /**
256
     * Loads a {@link TaskList}.
257
     *
258
     * @param string $gid
259
     * @return null|TaskList
260
     */
261
    public function getTaskList (string $gid) {
262
        return $this->load($this, TaskList::class, "user_task_lists/{$gid}");
263
    }
264
265
    /**
266
     * Loads a {@link Team}.
267
     *
268
     * @param string $gid
269
     * @return null|Team
270
     */
271
    public function getTeam (string $gid) {
272
        return $this->load($this, Team::class, "teams/{$gid}");
273
    }
274
275
    /**
276
     * Loads a {@link User}.
277
     *
278
     * @param string $gid
279
     * @return null|User
280
     */
281
    public function getUser (string $gid) {
282
        return $this->load($this, User::class, "users/{$gid}");
283
    }
284
285
    /**
286
     * @param string $gid
287
     * @return ProjectWebhook|TaskWebhook
288
     */
289
    public function getWebhook (string $gid) {
290
        return $this->pool->get($gid, $this, function() use ($gid) {
291
            static $classes = [
292
                Project::TYPE => ProjectWebhook::class,
293
                Task::TYPE => TaskWebhook::class
294
            ];
295
            if ($remote = $this->get("webhooks/{$gid}", [], ['expand' => 'this'])) {
296
                return $this->factory($this, $classes[$remote['resource_type']], $remote);
297
            }
298
            return null;
299
        });
300
    }
301
302
    /**
303
     * Expands received webhook data as a full event object.
304
     *
305
     * @see https://developers.asana.com/docs/event
306
     *
307
     * @param array $data
308
     * @return Event
309
     */
310
    public function getWebhookEvent (array $data) {
311
        return $this->factory($this, Event::class, $data);
312
    }
313
314
    /**
315
     * Loads a {@link Workspace}.
316
     *
317
     * @param string $gid
318
     * @return null|Workspace
319
     */
320
    public function getWorkspace (string $gid) {
321
        return $this->load($this, Workspace::class, "workspaces/{$gid}");
322
    }
323
324
    /**
325
     * Loads the entity found at the given path + query.
326
     *
327
     * @param Api|Data $caller
328
     * @param string $class
329
     * @param string $path
330
     * @param array $query
331
     * @return null|mixed|AbstractEntity
332
     */
333
    public function load ($caller, string $class, string $path, array $query = []) {
334
        $key = rtrim($path . '?' . http_build_query($query), '?');
335
        return $this->pool->get($key, $caller, function($caller) use ($class, $path, $query) {
336
            if ($data = $this->get($path, $query, ['expand' => 'this'])) {
337
                return $this->factory($caller, $class, $data);
338
            }
339
            return null;
340
        });
341
    }
342
343
    /**
344
     * All results from {@link loadEach()}
345
     *
346
     * @param Api|Data $caller
347
     * @param string $class
348
     * @param string $path
349
     * @param array $query
350
     * @return array|AbstractEntity[]
351
     */
352
    public function loadAll ($caller, string $class, string $path, array $query = []) {
353
        return iterator_to_array($this->loadEach(...func_get_args()));
354
    }
355
356
    /**
357
     * Loads and yields each entity found at the given path + query.
358
     *
359
     * The result-set is not pooled, but individual entities are.
360
     *
361
     * @param Api|Data $caller
362
     * @param string $class
363
     * @param string $path
364
     * @param array $query `limit` can exceed `100` here.
365
     * @return Generator|AbstractEntity[]
366
     */
367
    public function loadEach ($caller, string $class, string $path, array $query = []) {
368
        $query['opt_expand'] = 'this';
369
        $remain = $query['limit'] ?? PHP_INT_MAX;
370
        do {
371
            $query['limit'] = min($remain, 100);
372
            $page = $this->call('GET', $path . '?' . http_build_query($query));
373
            foreach ($page['data'] as $each) {
374
                yield $this->pool->get($each['gid'], $caller, function($caller) use ($class, $each) {
375
                    return $this->factory($caller, $class, $each);
376
                });
377
                $remain--;
378
            }
379
            $query['offset'] = $page['next_page']['offset'] ?? null;
380
        } while ($remain and $query['offset']);
381
    }
382
383
    /**
384
     * `HTTP POST`
385
     *
386
     * @param string $path
387
     * @param array $data
388
     * @param array $opt
389
     * @return null|array
390
     */
391
    public function post (string $path, array $data = [], array $opt = []) {
392
        $response = $this->call('POST', $path, [
393
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
394
            CURLOPT_POSTFIELDS => json_encode([
395
                'options' => $opt,
396
                'data' => $data
397
            ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)
398
        ]);
399
        return $response['data'] ?? null;
400
    }
401
402
    /**
403
     * `HTTP PUT`
404
     *
405
     * @param string $path
406
     * @param array $data
407
     * @param array $opt
408
     * @return null|array
409
     */
410
    public function put (string $path, array $data = [], array $opt = []) {
411
        $response = $this->call('PUT', $path, [
412
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
413
            CURLOPT_POSTFIELDS => json_encode([
414
                'options' => $opt,
415
                'data' => $data
416
            ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)
417
        ]);
418
        return $response['data'] ?? null;
419
    }
420
421
    /**
422
     * @param LoggerInterface $log
423
     */
424
    public function setLog (LoggerInterface $log) {
425
        $this->log = $log;
426
    }
427
}