Passed
Push — master ( dd4d64...2428ce )
by y
02:12
created

Api::call()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 35
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

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