Passed
Push — master ( 2c0ae5...dd4d64 )
by y
01:41
created

Api::getOrganizationExport()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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