Passed
Push — master ( 2428ce...06a092 )
by y
02:04
created

Task::_onSave()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 4
nop 0
dl 0
loc 8
rs 10
c 0
b 0
f 0
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\DateTrait;
8
use Helix\Asana\Base\AbstractEntity\PostMutatorTrait;
9
use Helix\Asana\Base\AbstractEntity\SyncTrait;
10
use Helix\Asana\Base\AbstractEntity\WorkspaceTrait;
11
use Helix\Asana\Project\Section;
12
use Helix\Asana\Task\Attachment;
13
use Helix\Asana\Task\External;
14
use Helix\Asana\Task\FieldEntries;
15
use Helix\Asana\Task\Like;
16
use Helix\Asana\Task\Membership;
17
use Helix\Asana\Task\Story;
18
use Helix\Asana\Webhook\TaskWebhook;
19
20
/**
21
 * A task.
22
 *
23
 * @see https://developers.asana.com/docs/asana-tasks
24
 * @see https://developers.asana.com/docs/task
25
 *
26
 * @method null|User            getAssignee                 ()
27
 * @method string               getAssigneeStatus           ()
28
 * @method bool                 isCompleted                 ()
29
 * @method string               getCompletedAt              () RFC3339x
30
 * @method string               getCreatedAt                () RFC3339x
31
 * @method null|FieldEntries    getCustomFields             () Premium feature.
32
 * @method User[]               getFollowers                ()
33
 * @method bool                 getIsRenderedAsSeparator    ()
34
 * @method bool                 isLiked                     () Whether you like the task.
35
 * @method Like[]               getLikes                    ()
36
 * @method Membership[]         getMemberships              ()
37
 * @method string               getModifiedAt               () RFC3339x
38
 * @method string               getName                     ()
39
 * @method string               getNotes                    ()
40
 * @method int                  getNumLikes                 ()
41
 * @method int                  getNumSubtasks              ()
42
 * @method null|Task            getParent                   ()
43
 * @method string               getResourceSubtype          ()
44
 * @method Tag[]                getTags                     ()
45
 *
46
 * @method bool                 hasAssignee                 ()
47
 * @method bool                 hasCustomFields             () Premium feature.
48
 * @method bool                 hasFollowers                ()
49
 * @method bool                 hasLikes                    ()
50
 * @method bool                 hasMemberships              ()
51
 * @method bool                 hasName                     ()
52
 * @method bool                 hasNotes                    ()
53
 * @method bool                 hasParent                   ()
54
 * @method bool                 hasTags                     ()
55
 *
56
 * @method $this                setAssignee                 (?User $user)
57
 * @method $this                setAssigneeStatus           (string $status)
58
 * @method $this                setCompleted                (bool $completed)
59
 * @method $this                setIsRenderedAsSeparator    (bool $flag)
60
 * @method $this                setLiked                    (bool $liked) Like or unlike the task.
61
 * @method $this                setName                     (string $name)
62
 * @method $this                setNotes                    (string $notes)
63
 * @method $this                setResourceSubtype          (string $type) @depends create-only
64
 *
65
 * @method Attachment[]         selectAttachments           (callable $filter) `fn( Attachment $attachment): bool`
66
 * @method Task[]               selectDependencies          (callable $filter) `fn( Task $dependency ): bool`
67
 * @method Task[]               selectDependents            (callable $filter) `fn( Task $dependent ): bool`
68
 * @method User[]               selectFollowers             (callable $filter) `fn( User $user ): bool`
69
 * @method Story[]              selectComments              (callable $filter) `fn( Story $comment ): bool`
70
 * @method Like[]               selectLikes                 (callable $filter) `fn( Like $like ): bool`
71
 * @method Membership[]         selectMemberships           (callable $filter) `fn( Membership $membership ): bool`
72
 * @method Project[]            selectProjects              (callable $filter) `fn( Project $project ): bool`
73
 * @method Story[]              selectStories               (callable $filter) `fn( Story $story ): bool`
74
 * @method Task[]               selectSubTasks              (callable $filter) `fn( Task $subtask ): bool`
75
 * @method Tag[]                selectTags                  (callable $filter) `fn( Tag $tag ): bool`
76
 */
77
class Task extends AbstractEntity {
78
79
    use CrudTrait {
80
        create as private _create;
81
        update as private _update;
82
    }
83
84
    use DateTrait;
85
    use PostMutatorTrait;
86
    use SyncTrait;
87
    use WorkspaceTrait;
88
89
    const DIR = 'tasks';
90
    const TYPE = 'task';
91
92
    const TYPE_DEFAULT = 'default_task';
93
    const TYPE_MILESTONE = 'milestone';
94
95
    const ASSIGN_INBOX = 'inbox';
96
    const ASSIGN_LATER = 'later';
97
    const ASSIGN_NEW = 'new';
98
    const ASSIGN_TODAY = 'today';
99
    const ASSIGN_UPCOMING = 'upcoming';
100
101
    const GET_INCOMPLETE = ['completed_since' => 'now'];
102
103
    protected const MAP = [
104
        'assignee' => User::class,
105
        'custom_fields' => FieldEntries::class,
106
        'external' => External::class, // not included by expanding "this". always lazy.
107
        'followers' => [User::class],
108
        'likes' => [Like::class],
109
        'memberships' => [Membership::class],
110
        'parent' => self::class,
111
        'tags' => [Tag::class],
112
        'workspace' => Workspace::class
113
    ];
114
115
    const OPT_FIELDS = [
116
        'memberships' => 'memberships.(project|section)'
117
    ];
118
119
    private function _onSave (): void {
120
        /** @var FieldEntries $fields */
121
        if ($fields = $this->data['custom_fields'] ?? null) {
122
            $fields->__unset(true);
123
        }
124
        /** @var External $external */
125
        if ($external = $this->data['external'] ?? null) {
126
            $external->diff = [];
127
        }
128
    }
129
130
    protected function _setData (array $data): void {
131
        // hearts were deprecated for likes
132
        unset($data['hearted'], $data['hearts'], $data['num_hearts']);
133
134
        // redundant. memberships are used instead
135
        unset($data['projects']);
136
137
        // time-based deadlines are a little passive-aggressive, don't you think?
138
        unset($data['due_at']);
139
140
        parent::_setData($data);
141
    }
142
143
    /**
144
     * Uploads a file attachment.
145
     *
146
     * @depends after-create
147
     * @param string $file
148
     * @return Attachment
149
     */
150
    public function addAttachment (string $file) {
151
        /** @var Attachment $attachment */
152
        $attachment = $this->api->factory($this, Attachment::class, ['parent' => $this]);
153
        return $attachment->create($file);
154
    }
155
156
    /**
157
     * Posts a new comment.
158
     *
159
     * @depends after-create
160
     * @param string $text
161
     * @return Story
162
     */
163
    public function addComment (string $text) {
164
        return $this->newComment()->setText($text)->create();
165
    }
166
167
    /**
168
     * Premium feature.
169
     *
170
     * @depends after-create
171
     * @param Task[] $tasks
172
     * @return $this
173
     */
174
    public function addDependencies (array $tasks) {
175
        $this->api->post("{$this}/addDependencies", ['dependents' => array_column($tasks, 'gid')]);
176
        return $this;
177
    }
178
179
    /**
180
     * Premium feature.
181
     *
182
     * @depends after-create
183
     * @param Task $task
184
     * @return $this
185
     */
186
    public function addDependency (Task $task) {
187
        return $this->addDependencies([$task]);
188
    }
189
190
    /**
191
     * Premium feature.
192
     *
193
     * @depends after-create
194
     * @param Task $task
195
     * @return $this
196
     */
197
    public function addDependent (Task $task) {
198
        return $this->addDependents([$task]);
199
    }
200
201
    /**
202
     * Premium feature.
203
     *
204
     * @depends after-create
205
     * @param Task[] $tasks
206
     * @return $this
207
     */
208
    public function addDependents (array $tasks) {
209
        $this->api->post("{$this}/addDependents", ['dependents' => array_column($tasks, 'gid')]);
210
        return $this;
211
    }
212
213
    /**
214
     * Adds a follower.
215
     *
216
     * @param User $user
217
     * @return $this
218
     */
219
    public function addFollower (User $user) {
220
        return $this->addFollowers([$user]);
221
    }
222
223
    /**
224
     * Adds followers.
225
     *
226
     * @see https://developers.asana.com/docs/add-followers-to-a-task
227
     *
228
     * @param User[] $users
229
     * @return $this
230
     */
231
    public function addFollowers (array $users) {
232
        return $this->_addWithPost("{$this}/addFollowers", [
233
            'followers' => array_column($users, 'gid')
234
        ], 'followers', $users);
235
    }
236
237
    /**
238
     * Adds a tag.
239
     *
240
     * @see https://developers.asana.com/docs/add-a-tag-to-a-task
241
     *
242
     * @param Tag $tag
243
     * @return $this
244
     */
245
    public function addTag (Tag $tag) {
246
        return $this->_addWithPost("{$this}/addTag", [
247
            'tag' => $tag->getGid()
248
        ], 'tags', [$tag]);
249
    }
250
251
    /**
252
     * Adds the task to a project.
253
     *
254
     * @see https://developers.asana.com/docs/add-a-project-to-a-task
255
     *
256
     * @see Project::getDefaultSection()
257
     *
258
     * @param Section $section
259
     * @return $this
260
     */
261
    public function addToProject (Section $section) {
262
        /** @var Membership $membership */
263
        $membership = $this->api->factory($this, Membership::class)
264
            ->_set('project', $section->getProject())
265
            ->_set('section', $section);
266
        return $this->_addWithPost("{$this}/addProject", $membership->toArray(), 'memberships', [$membership]);
267
    }
268
269
    /**
270
     * Creates a webhook.
271
     *
272
     * @depends after-create
273
     * @param string $target
274
     * @return TaskWebhook
275
     */
276
    public function addWebhook (string $target) {
277
        /** @var TaskWebhook $webhook */
278
        $webhook = $this->api->factory($this, TaskWebhook::class);
279
        $webhook->_set('resource', $this);
280
        $webhook->_set('target', $target);
281
        return $webhook->create();
282
    }
283
284
    /**
285
     * @return $this
286
     */
287
    public function create () {
288
        $this->_create();
289
        $this->_onSave();
290
        return $this;
291
    }
292
293
    /**
294
     * Duplicates the task.
295
     *
296
     * @see https://developers.asana.com/docs/duplicate-a-task
297
     *
298
     * @depends after-create
299
     * @param string $name
300
     * @param string[] $include
301
     * @return Job
302
     */
303
    public function duplicate (string $name, array $include) {
304
        /** @var array $remote */
305
        $remote = $this->api->post("{$this}/duplicate", [
306
            'name' => $name,
307
            'include' => array_values($include)
308
        ]);
309
        return $this->api->factory($this, Job::class, $remote);
310
    }
311
312
    /**
313
     * Attached files.
314
     *
315
     * @depends after-create
316
     * @return Attachment[]
317
     */
318
    public function getAttachments () {
319
        return $this->api->loadAll($this, Attachment::class, "{$this}/attachments");
320
    }
321
322
    /**
323
     * @depends after-create
324
     * @return Story[]
325
     */
326
    public function getComments () {
327
        return $this->selectStories(function(Story $story) {
328
            return $story->isComment();
329
        });
330
    }
331
332
    /**
333
     * Premium feature.
334
     *
335
     * @depends after-create
336
     * @return Task[]
337
     */
338
    public function getDependencies () {
339
        return $this->api->loadAll($this, self::class, "{$this}/dependencies");
340
    }
341
342
    /**
343
     * Premium feature.
344
     *
345
     * @depends after-create
346
     * @return Task[]
347
     */
348
    public function getDependents () {
349
        return $this->api->loadAll($this, self::class, "{$this}/dependents");
350
    }
351
352
    /**
353
     * A proxy to the task's "external data".
354
     *
355
     * > :info:
356
     * > This always returns an instance, regardless of whether the task on Asana actually has external data.
357
     * >
358
     * > Asana will delete the external data object if it's emptied,
359
     * > and fetching it via `GET` will then return `null`, so we coalesce.
360
     *
361
     * @return External
362
     */
363
    public function getExternal () {
364
        return $this->_get('external') ?? $this->data['external'] = $this->api->factory($this, External::class);
365
    }
366
367
    /**
368
     * @return Project[]
369
     */
370
    public function getProjects () {
371
        return array_column($this->getMemberships(), 'project');
372
    }
373
374
    /**
375
     * @return Section[]
376
     */
377
    public function getSections () {
378
        return array_column($this->getMemberships(), 'section');
379
    }
380
381
    /**
382
     * @depends after-create
383
     * @return Story[]
384
     */
385
    public function getStories () {
386
        return $this->api->loadAll($this, Story::class, "{$this}/stories");
387
    }
388
389
    /**
390
     * @depends after-create
391
     * @return Task[]
392
     */
393
    public function getSubTasks () {
394
        return $this->api->loadAll($this, self::class, "{$this}/subtasks");
395
    }
396
397
    /**
398
     * @depends after-create
399
     * @return string
400
     */
401
    public function getUrl (): string {
402
        return "https://app.asana.com/0/0/{$this->getGid()}";
403
    }
404
405
    /**
406
     * @depends after-create
407
     * @return TaskWebhook[]
408
     */
409
    public function getWebhooks () {
410
        return $this->api->loadAll($this, TaskWebhook::class, 'webhooks', [
411
            'workspace' => $this->getWorkspace()->getGid(),
412
            'resource' => $this->getGid()
413
        ]);
414
    }
415
416
    /**
417
     * Factory.
418
     *
419
     * @depends after-create
420
     * @return Story
421
     */
422
    public function newComment () {
423
        return $this->api->factory($this, Story::class, [
424
            'resource_subtype' => Story::TYPE_COMMENT_ADDED,
425
            'target' => $this
426
        ]);
427
    }
428
429
    /**
430
     * Factory.
431
     *
432
     * @depends after-create
433
     * @return Task
434
     */
435
    public function newSubTask () {
436
        /** @var Task $sub */
437
        $sub = $this->api->factory($this, self::class);
438
        return $sub->setParent($this);
439
    }
440
441
    /**
442
     * Premium feature.
443
     *
444
     * @depends after-create
445
     * @param Task[] $tasks
446
     * @return $this
447
     */
448
    public function removeDependencies (array $tasks) {
449
        $this->api->post("{$this}/removeDependencies", ['dependencies' => array_column($tasks, 'gid')]);
450
        return $this;
451
    }
452
453
    /**
454
     * Premium feature.
455
     *
456
     * @depends after-create
457
     * @param Task $task
458
     * @return $this
459
     */
460
    public function removeDependency (Task $task) {
461
        return $this->removeDependencies([$task]);
462
    }
463
464
    /**
465
     * Premium feature.
466
     *
467
     * @depends after-create
468
     * @param Task $task
469
     * @return $this
470
     */
471
    public function removeDependent (Task $task) {
472
        return $this->removeDependents([$task]);
473
    }
474
475
    /**
476
     * Premium feature.
477
     *
478
     * @depends after-create
479
     * @param Task[] $tasks
480
     * @return $this
481
     */
482
    public function removeDependents (array $tasks) {
483
        $this->api->post("{$this}/removeDependents", ['dependents' => array_column($tasks, 'gid')]);
484
        return $this;
485
    }
486
487
    /**
488
     * Removes a follower.
489
     *
490
     * @param User $user
491
     * @return $this
492
     */
493
    public function removeFollower (User $user) {
494
        return $this->removeFollowers([$user]);
495
    }
496
497
    /**
498
     * Removes followers.
499
     *
500
     * @see https://developers.asana.com/docs/remove-followers-from-a-task
501
     *
502
     * @param User[] $users
503
     * @return $this
504
     */
505
    public function removeFollowers (array $users) {
506
        return $this->_removeWithPost("{$this}/removeFollowers", [
507
            'followers' => array_column($users, 'gid')
508
        ], 'followers', $users);
509
    }
510
511
    /**
512
     * Removes the task from a project.
513
     *
514
     * @see https://developers.asana.com/docs/remove-a-project-from-a-task
515
     *
516
     * @param Project $project
517
     * @return $this
518
     */
519
    public function removeFromProject (Project $project) {
520
        return $this->_removeWithPost("{$this}/removeProject", [
521
            'project' => $project->getGid()
522
        ], 'memberships', function(Membership $membership) use ($project) {
523
            return $membership->getProject()->getGid() !== $project->getGid();
524
        });
525
    }
526
527
    /**
528
     * Removes a tag.
529
     *
530
     * @see https://developers.asana.com/docs/remove-a-tag-from-a-task
531
     *
532
     * @param Tag $tag
533
     * @return $this
534
     */
535
    public function removeTag (Tag $tag) {
536
        return $this->_removeWithPost("{$this}/removeTag", [
537
            'tag' => $tag->getGid()
538
        ], 'tags', [$tag]);
539
    }
540
541
    /**
542
     * Makes the task a subtask of another.
543
     *
544
     * @see https://developers.asana.com/docs/set-the-parent-of-a-task
545
     * @param null|Task $parent
546
     * @return $this
547
     */
548
    public function setParent (?self $parent) {
549
        return $this->_setWithPost("{$this}/setParent", [
550
            'parent' => $parent
551
        ], 'parent', $parent);
552
    }
553
554
    /**
555
     * @return $this
556
     */
557
    public function update () {
558
        $this->_update();
559
        $this->_onSave();
560
        return $this;
561
    }
562
563
}