Api::getOrganizationExport()   A
last analyzed

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