Task   A
last analyzed

Complexity

Total Complexity 41

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Importance

Changes 14
Bugs 5 Features 1
Metric Value
eloc 111
dl 0
loc 454
rs 9.1199
c 14
b 5
f 1
wmc 41

37 Methods

Rating   Name   Duplication   Size   Complexity  
A duplicate() 0 7 1
A addDependency() 0 2 1
A getWebhooks() 0 4 1
A removeFromProject() 0 5 1
A addFollower() 0 2 1
A removeFollower() 0 2 1
A getExternal() 0 2 1
A getProjects() 0 2 1
A getSections() 0 2 1
A addAttachment() 0 4 1
A newComment() 0 4 1
A newWebhook() 0 4 1
A addToProject() 0 9 2
A create() 0 4 1
A update() 0 4 1
A _onSave() 0 8 3
A _setData() 0 11 1
A setParent() 0 5 2
A addTag() 0 5 1
A getAttachments() 0 2 1
A getStories() 0 2 1
A newSubTask() 0 4 1
A removeDependents() 0 3 1
A addDependencies() 0 3 1
A removeFollowers() 0 4 1
A removeDependent() 0 2 1
A removeDependencies() 0 3 1
A getComments() 0 3 1
A getUrl() 0 2 1
A addDependents() 0 3 1
A getDependents() 0 2 1
A removeDependency() 0 2 1
A getSubTasks() 0 2 1
A getDependencies() 0 2 1
A removeTag() 0 4 1
A addFollowers() 0 4 1
A addDependent() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Task often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Task, and based on these observations, apply Extract Interface, too.

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