Passed
Push — master ( 998eef...fb3ec1 )
by y
01:58
created

Task::getExternal()   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
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 2
rs 10
1
<?php
2
3
namespace Helix\Asana;
4
5
use Helix\Asana\Base\AbstractEntity;
6
use Helix\Asana\Base\AbstractEntity\CrudTrait;
7
use Helix\Asana\Base\AbstractEntity\WorkspaceTrait;
8
use Helix\Asana\Event\StoryEvent;
9
use Helix\Asana\Event\TaskEvent;
10
use Helix\Asana\Project\Section;
11
use Helix\Asana\Task\Attachment;
12
use Helix\Asana\Task\CustomValues;
13
use Helix\Asana\Task\External;
14
use Helix\Asana\Task\Like;
15
use Helix\Asana\Task\Membership;
16
use Helix\Asana\Task\Story;
17
use Helix\Asana\Webhook\TaskWebhook;
18
19
/**
20
 * A task.
21
 *
22
 * @see https://developers.asana.com/docs/asana-tasks
23
 * @see https://developers.asana.com/docs/task
24
 *
25
 * @method null|User    getAssignee         ()
26
 * @method bool         hasAssignee         ()
27
 * @method $this        setAssignee         (null|User $user)
28
 * @method string       getAssigneeStatus   ()
29
 * @method $this        setAssigneeStatus   (string $status)
30
 * @method bool         isCompleted         ()
31
 * @method $this        setCompleted        (bool $completed)
32
 * @method string       getCompletedAt      ()
33
 * @method string       getCreatedAt        ()
34
 * @method null|CustomValues getCustomFields() Premium only.
35
 * @method bool         hasCustomFields     () Premium only.
36
 * @method string       getDueOn            ()
37
 * @method $this        setDueOn            (string $date)
38
 * @method User[]       getFollowers        ()
39
 * @method bool         hasFollowers        ()
40
 * @method string       getHtmlNotes        ()
41
 * @method $this        setHtmlNotes        (string $notes)
42
 * @method bool         isLiked             ()
43
 * @method $this        setLiked            (bool $liked)
44
 * @method Like[]       getLikes            ()
45
 * @method Membership[] getMemberships      ()
46
 * @method bool         hasMemberships      ()
47
 * @method string       getModifiedAt       ()
48
 * @method string       getName             ()
49
 * @method $this        setName             (string $name)
50
 * @method string       getNotes            ()
51
 * @method $this        setNotes            (string $notes)
52
 * @method int          getNumLikes         ()
53
 * @method int          getNumSubtasks      ()
54
 * @method null|Task    getParent           ()
55
 * @method bool         hasParent           ()
56
 * @method string       getResourceSubtype  ()
57
 * @method $this        setResourceSubtype  (string $type)
58
 * @method string       getStartOn          ()
59
 * @method $this        setStartOn          (string $date)
60
 * @method Tag[]        getTags             ()
61
 * @method bool         hasTags             ()
62
 */
63
class Task extends AbstractEntity {
64
65
    use CrudTrait {
66
        create as private _create;
67
        update as private _update;
68
    }
69
70
    use WorkspaceTrait;
71
72
    const TYPE = 'task';
73
    const TYPE_DEFAULT = 'default_task';
74
    const TYPE_MILESTONE = 'milestone';
75
76
    const ASSIGN_INBOX = 'inbox';
77
    const ASSIGN_LATER = 'later';
78
    const ASSIGN_NEW = 'new';
79
    const ASSIGN_TODAY = 'today';
80
    const ASSIGN_UPCOMING = 'upcoming';
81
82
    public function __construct ($caller, array $data = []) {
83
        parent::__construct($caller, $data);
84
85
        // Asana has a bug where it returns empty memberships when expanding "this".
86
        // If one is found, clear the whole list so it can be reloaded lazily.
87
        if (isset($this->data['memberships'])) {
88
            foreach ($this->getMemberships() as $membership) {
89
                if (!$membership->getProject()) {
90
                    unset($this->data['memberships']);
91
                    break;
92
                }
93
            }
94
        }
95
    }
96
97
    final public function __toString (): string {
98
        return "tasks/{$this->getGid()}";
99
    }
100
101
    /**
102
     * Clears the diffs for `custom_fields` and `external` sub-objects after saving.
103
     */
104
    protected function _clearSubDiffs (): void {
105
        // use isset() to avoid has() fetch.
106
        if (isset($this->data['custom_fields'])) {
107
            $this->getCustomFields()->diff = [];
108
        }
109
        if (isset($this->data['external'])) {
110
            $this->getExternal()->diff = [];
111
        }
112
    }
113
114
    final protected function _getDir (): string {
115
        return 'tasks';
116
    }
117
118
    protected function _getMap (): array {
119
        return [
120
            'assignee' => User::class,
121
            'custom_fields' => CustomValues::class,
122
            'external' => External::class, // not included by expanding "this". always lazy.
123
            'followers' => [User::class],
124
            'likes' => [Like::class],
125
            'memberships' => [Membership::class],
126
            'parent' => self::class,
127
            'tags' => [Tag::class],
128
            'workspace' => Workspace::class
129
        ];
130
    }
131
132
    /**
133
     * Uploads a file attachment.
134
     *
135
     * @depends after-create
136
     * @param string $file
137
     * @return Attachment
138
     */
139
    public function addAttachment (string $file) {
140
        /** @var Attachment $attachment */
141
        $attachment = $this->factory(Attachment::class, ['parent' => $this]);
142
        return $attachment->upload($file);
143
    }
144
145
    /**
146
     * Posts a comment on an existing task and returns it.
147
     *
148
     * @param string $text
149
     * @return Story
150
     */
151
    public function addComment (string $text) {
152
        return $this->newComment()->setText($text)->create();
1 ignored issue
show
Unused Code introduced by
The call to Helix\Asana\Task\Story::setText() has too many arguments starting with $text. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

152
        return $this->newComment()->/** @scrutinizer ignore-call */ setText($text)->create();

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
153
    }
154
155
    /**
156
     * Premium feature.
157
     *
158
     * @depends after-create
159
     * @param Task[] $tasks
160
     * @return $this
161
     */
162
    public function addDependencies (array $tasks) {
163
        $this->api->post("{$this}/addDependencies", ['dependents' => static::_getGids($tasks)]);
164
        return $this;
165
    }
166
167
    /**
168
     * Premium feature.
169
     *
170
     * @depends after-create
171
     * @param Task $task
172
     * @return $this
173
     */
174
    public function addDependency (Task $task) {
175
        return $this->addDependencies([$task]);
176
    }
177
178
    /**
179
     * Premium feature.
180
     *
181
     * @depends after-create
182
     * @param Task $task
183
     * @return $this
184
     */
185
    public function addDependent (Task $task) {
186
        return $this->addDependents([$task]);
187
    }
188
189
    /**
190
     * Premium feature.
191
     *
192
     * @depends after-create
193
     * @param Task[] $tasks
194
     * @return $this
195
     */
196
    public function addDependents (array $tasks) {
197
        $this->api->post("{$this}/addDependents", ['dependents' => static::_getGids($tasks)]);
198
        return $this;
199
    }
200
201
    /**
202
     * Adds a follower.
203
     *
204
     * @param User $user
205
     * @return $this
206
     */
207
    public function addFollower (User $user) {
208
        return $this->addFollowers([$user]);
209
    }
210
211
    /**
212
     * Adds followers.
213
     *
214
     * @param User[] $users
215
     * @return $this
216
     */
217
    public function addFollowers (array $users) {
218
        if ($this->hasGid()) {
219
            $this->api->post("{$this}/addFollowers", ['followers' => static::_getGids($users)]);
220
            $this->_merge('followers', $users);
221
        }
222
        else {
223
            $this->_merge('followers', $users, true);
224
        }
225
        return $this;
226
    }
227
228
    /**
229
     * Adds a tag.
230
     *
231
     * @param Tag $tag
232
     * @return $this
233
     */
234
    public function addTag (Tag $tag) {
235
        if ($this->hasGid()) {
236
            $this->api->post("{$this}/addTag", ['tag' => $tag->getGid()]);
237
            $this->_merge('tags', [$tag]);
238
        }
239
        else {
240
            $this->_merge('tags', [$tag], true);
241
        }
242
        return $this;
243
    }
244
245
    /**
246
     * Adds the task to a project section.
247
     *
248
     * @param Section $section
249
     * @return $this
250
     */
251
    public function addToProject (Section $section) {
252
        $project = $section->getProject();
253
        if ($this->hasGid()) {
254
            $this->api->post("{$this}/addProject", [
255
                'project' => $project->getGid(),
256
                'section' => $section->getGid()
257
            ]);
258
            if (isset($this->data['memberships'])) {
259
                $this->data['memberships'][] = $this->factory(Membership::class)->_setData([
260
                    'project' => $project,
261
                    'section' => $section
262
                ]);
263
            }
264
        }
265
        else {
266
            if (!$this->hasWorkspace()) {
267
                $this->setWorkspace($project->getWorkspace());
1 ignored issue
show
Bug introduced by
It seems like $project->getWorkspace() can also be of type null; however, parameter $workspace of Helix\Asana\Task::setWorkspace() does only seem to accept Helix\Asana\Workspace, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

267
                $this->setWorkspace(/** @scrutinizer ignore-type */ $project->getWorkspace());
Loading history...
268
            }
269
            $membership = $this->factory(Membership::class);
270
            $membership->_set('project', $section->getProject());
271
            $membership->_set('section', $section);
272
            $this->data['memberships'][] = $membership;
273
            $this->diff['memberships'] = true;
274
        }
275
        return $this;
276
    }
277
278
    /**
279
     * Creates and returns a webhook.
280
     *
281
     * @depends after-create
282
     * @param string $target
283
     * @return TaskWebhook
284
     */
285
    public function addWebhook (string $target) {
286
        /** @var TaskWebhook $webhook */
287
        $webhook = $this->factory(TaskWebhook::class);
288
        return $webhook->create($this, $target);
289
    }
290
291
    /**
292
     * @return $this
293
     */
294
    public function create () {
295
        $this->_create();
296
        $this->_clearSubDiffs();
297
        return $this;
298
    }
299
300
    /**
301
     * Creates and returns job to duplicate the task.
302
     *
303
     * @see https://developers.asana.com/docs/#duplicate-a-task
304
     *
305
     * @depends after-create
306
     * @param string $name
307
     * @param string[] $include
308
     * @return Job
309
     */
310
    public function duplicate (string $name, array $include) {
311
        $remote = $this->api->post("{$this}/duplicate", [
312
            'name' => $name,
313
            'include' => array_values($include)
314
        ]);
315
        return $this->factory(Job::class, $remote);
1 ignored issue
show
Bug introduced by
It seems like $remote can also be of type null; however, parameter $data of Helix\Asana\Base\Data::factory() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

315
        return $this->factory(Job::class, /** @scrutinizer ignore-type */ $remote);
Loading history...
316
    }
317
318
    /**
319
     * Returns the task's files.
320
     *
321
     * @depends after-create
322
     * @return Attachment[]
323
     */
324
    public function getAttachments () {
325
        return $this->loadAll(Attachment::class, "{$this}/attachments");
326
    }
327
328
    /**
329
     * Returns the task's comments.
330
     *
331
     * @depends after-create
332
     * @return Story[]
333
     */
334
    public function getComments () {
335
        return array_values(array_filter($this->getStories(), function(Story $story) {
336
            return $story->isComment();
337
        }));
338
    }
339
340
    /**
341
     * Premium feature.
342
     *
343
     * @depends after-create
344
     * @return Task[]
345
     */
346
    public function getDependencies () {
347
        return $this->loadAll(self::class, "{$this}/dependencies");
348
    }
349
350
    /**
351
     * Premium feature.
352
     *
353
     * @depends after-create
354
     * @return Task[]
355
     */
356
    public function getDependents () {
357
        return $this->loadAll(self::class, "{$this}/dependents");
358
    }
359
360
    /**
361
     * Returns events since the last sync.
362
     *
363
     * @depends after-create
364
     * @param null|string $token
365
     * @return TaskEvent[]|StoryEvent[]
366
     */
367
    public function getEvents (&$token) {
368
        return $this->api->sync($this, $token);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->api->sync($this, $token) returns an array which contains values of type Helix\Asana\Event\ProjectEvent which are incompatible with the documented value type Helix\Asana\Event\StoryE...x\Asana\Event\TaskEvent.
Loading history...
369
    }
370
371
    /**
372
     * @return External
373
     */
374
    public function getExternal () {
375
        return $this->_get('external') ?? $this->data['external'] = $this->factory($this->_getMap()['external']);
376
    }
377
378
    /**
379
     * @return Project[]
380
     */
381
    public function getProjects () {
382
        return array_map(function(Membership $membership) {
383
            return $membership->getProject();
384
        }, $this->getMemberships());
385
    }
386
387
    /**
388
     * Returns the task's activity.
389
     *
390
     * @depends after-create
391
     * @return Story[]
392
     */
393
    public function getStories () {
394
        return $this->loadAll(Story::class, "{$this}/stories");
395
    }
396
397
    /**
398
     * Returns the task's subtasks.
399
     *
400
     * @depends after-create
401
     * @return Task[]
402
     */
403
    public function getSubTasks () {
404
        return $this->loadAll(self::class, "{$this}/subtasks");
405
    }
406
407
    /**
408
     * Returns the task's URL.
409
     *
410
     * @depends after-create
411
     * @return string
412
     */
413
    public function getUrl (): string {
414
        return "https://app.asana.com/0/0/{$this->getGid()}";
415
    }
416
417
    /**
418
     * Returns the task's webhooks.
419
     *
420
     * @depends after-create
421
     * @return TaskWebhook[]
422
     */
423
    public function getWebhooks () {
424
        return $this->loadAll(TaskWebhook::class, 'webhooks', [
425
            'workspace' => $this->getWorkspace()->getGid(),
426
            'resource' => $this->getGid()
427
        ]);
428
    }
429
430
    /**
431
     * @return bool
432
     */
433
    public function isRenderedAsSeparator (): bool {
434
        return $this->_is('is_rendered_as_separator');
435
    }
436
437
    /**
438
     * Instantiates and returns a new comment.
439
     *
440
     * @depends after-create
441
     * @return Story
442
     */
443
    public function newComment () {
444
        /** @var Story $comment */
445
        $comment = $this->factory(Story::class, [
446
            'resource_subtype' => Story::TYPE_COMMENT_ADDED
447
        ]);
448
        return $comment->_set('task', $this);
449
    }
450
451
    /**
452
     * Instantiates and returns a new subtask.
453
     *
454
     * @depends after-create
455
     * @return Task
456
     */
457
    public function newSubTask () {
458
        /** @var Task $sub */
459
        $sub = $this->factory(self::class);
460
        return $sub->setParent($this);
461
    }
462
463
    /**
464
     * Premium feature.
465
     *
466
     * @depends after-create
467
     * @param Task[] $tasks
468
     * @return $this
469
     */
470
    public function removeDependencies (array $tasks) {
471
        $this->api->post("{$this}/removeDependencies", ['dependencies' => static::_getGids($tasks)]);
472
        return $this;
473
    }
474
475
    /**
476
     * Premium feature.
477
     *
478
     * @depends after-create
479
     * @param Task $task
480
     * @return $this
481
     */
482
    public function removeDependency (Task $task) {
483
        return $this->removeDependencies([$task]);
484
    }
485
486
    /**
487
     * Premium feature.
488
     *
489
     * @depends after-create
490
     * @param Task $task
491
     * @return $this
492
     */
493
    public function removeDependent (Task $task) {
494
        return $this->removeDependents([$task]);
495
    }
496
497
    /**
498
     * Premium feature.
499
     *
500
     * @depends after-create
501
     * @param Task[] $tasks
502
     * @return $this
503
     */
504
    public function removeDependents (array $tasks) {
505
        $this->api->post("{$this}/removeDependents", ['dependents' => static::_getGids($tasks)]);
506
        return $this;
507
    }
508
509
    /**
510
     * Removes a follower.
511
     *
512
     * @param User $user
513
     * @return $this
514
     */
515
    public function removeFollower (User $user) {
516
        return $this->removeFollowers([$user]);
517
    }
518
519
    /**
520
     * Removes followers.
521
     *
522
     * @param User[] $users
523
     * @return $this
524
     */
525
    public function removeFollowers (array $users) {
526
        if ($this->hasGid()) {
527
            $this->api->post("{$this}/removeFollowers", ['followers' => static::_getGids($users)]);
528
        }
529
        $this->_remove('followers', $users);
530
        return $this;
531
    }
532
533
    /**
534
     * Removes the task from a project.
535
     *
536
     * @param Project $project
537
     * @return $this
538
     */
539
    public function removeFromProject (Project $project) {
540
        $gid = $project->getGid();
541
        if ($this->hasGid()) {
542
            $this->api->post("{$this}/removeProject", ['project' => $gid]);
543
        }
544
        if (isset($this->data['memberships'])) {
545
            $this->data['memberships'] = array_values(array_filter($this->data['memberships'], function(Membership $membership) use ($gid) {
546
                return $membership->getProject()->getGid() !== $gid;
547
            }));
548
        }
549
        return $this;
550
    }
551
552
    /**
553
     * Removes a tag.
554
     *
555
     * @param Tag $tag
556
     * @return $this
557
     */
558
    public function removeTag (Tag $tag) {
559
        if ($this->hasGid()) {
560
            $this->api->post("{$this}/removeTag", ['tag' => $tag->getGid()]);
561
        }
562
        $this->_remove('tags', [$tag]);
563
        return $this;
564
    }
565
566
    /**
567
     * Makes the task a subtask of another.
568
     *
569
     * @param null|Task $parent
570
     * @return $this
571
     */
572
    public function setParent (?self $parent) {
573
        if ($this->hasGid()) {
574
            $this->api->post("{$this}/setParent", ['parent' => $parent ? $parent->getGid() : null]);
575
            $this->data['parent'] = $parent;
576
        }
577
        else {
578
            $this->_set('parent', $parent);
579
        }
580
        return $this;
581
    }
582
583
    /**
584
     * @param bool $flag
585
     * @return $this
586
     */
587
    public function setRenderedAsSeparator (bool $flag) {
588
        return $this->_set('is_rendered_as_separator', $flag);
589
    }
590
591
    /**
592
     * @return $this
593
     */
594
    public function update () {
595
        $this->_update();
596
        $this->_clearSubDiffs();
597
        return $this;
598
    }
599
600
}