Passed
Push — master ( 8f2f39...434123 )
by y
01:50
created

Task   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 498
Duplicated Lines 0 %

Importance

Changes 28
Bugs 3 Features 3
Metric Value
eloc 110
c 28
b 3
f 3
dl 0
loc 498
rs 9.0399
wmc 42

40 Methods

Rating   Name   Duplication   Size   Complexity  
A getWebhooks() 0 4 1
A removeFromProject() 0 5 1
A newSubTask() 0 4 1
A removeDependents() 0 3 1
A removeFollower() 0 2 1
A removeDependent() 0 2 1
A removeFollowers() 0 4 1
A removeDependencies() 0 3 1
A removeDependency() 0 2 1
A newComment() 0 4 1
A removeTag() 0 4 1
A setParent() 0 4 1
A addTag() 0 4 1
A getAttachments() 0 2 1
A getStories() 0 2 1
A addFollower() 0 2 1
A addDependencies() 0 3 1
A getExternal() 0 2 1
A getProjects() 0 2 1
A __toString() 0 2 1
A getComments() 0 3 1
A getUrl() 0 2 1
A addDependents() 0 3 1
A getSections() 0 2 1
A addAttachment() 0 4 1
A getDependents() 0 2 1
A duplicate() 0 7 1
A getSubTasks() 0 2 1
A _getDir() 0 2 1
A getDependencies() 0 2 1
A addToProject() 0 6 1
A addFollowers() 0 4 1
A create() 0 4 1
A addDependency() 0 2 1
A addDependent() 0 2 1
A update() 0 4 1
A _onSave() 0 8 3
A addWebhook() 0 4 1
A _setData() 0 11 1
A addComment() 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\External;
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
use Helix\Asana\Workspace\WorkspaceTrait;
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 TYPE = 'task';
90
    const TYPE_DEFAULT = 'default_task';
91
    const TYPE_MILESTONE = 'milestone';
92
93
    const ASSIGN_INBOX = 'inbox';
94
    const ASSIGN_LATER = 'later';
95
    const ASSIGN_NEW = 'new';
96
    const ASSIGN_TODAY = 'today';
97
    const ASSIGN_UPCOMING = 'upcoming';
98
99
    const GET_INCOMPLETE = ['completed_since' => 'now'];
100
101
    protected const MAP = [
102
        'assignee' => User::class,
103
        'custom_fields' => FieldEntries::class,
104
        'external' => External::class, // not included by expanding "this". always lazy.
105
        'followers' => [User::class],
106
        'likes' => [Like::class],
107
        'memberships' => [Membership::class],
108
        'parent' => self::class,
109
        'tags' => [Tag::class],
110
        'workspace' => Workspace::class
111
    ];
112
113
    const OPT_FIELDS = [
114
        'memberships' => 'memberships.(project|section)'
115
    ];
116
117
    /**
118
     * `tasks/{gid}`
119
     *
120
     * @return string
121
     */
122
    final public function __toString (): string {
123
        return "tasks/{$this->getGid()}";
124
    }
125
126
    /**
127
     * `tasks`
128
     *
129
     * @return string
130
     */
131
    final protected function _getDir (): string {
132
        return 'tasks';
133
    }
134
135
    private function _onSave (): void {
136
        /** @var FieldEntries $fields */
137
        if ($fields = $this->data['custom_fields'] ?? null) {
138
            $fields->__unset(true);
139
        }
140
        /** @var External $external */
141
        if ($external = $this->data['external'] ?? null) {
142
            $external->diff = [];
143
        }
144
    }
145
146
    protected function _setData (array $data): void {
147
        // hearts were deprecated for likes
148
        unset($data['hearted'], $data['hearts'], $data['num_hearts']);
149
150
        // redundant. memberships are used instead
151
        unset($data['projects']);
152
153
        // time-based deadlines are a little passive-aggressive, don't you think?
154
        unset($data['due_at']);
155
156
        parent::_setData($data);
157
    }
158
159
    /**
160
     * Uploads a file attachment.
161
     *
162
     * @depends after-create
163
     * @param string $file
164
     * @return Attachment
165
     */
166
    public function addAttachment (string $file) {
167
        /** @var Attachment $attachment */
168
        $attachment = $this->api->factory($this, Attachment::class, ['parent' => $this]);
169
        return $attachment->upload($file);
170
    }
171
172
    /**
173
     * Posts a new comment.
174
     *
175
     * @depends after-create
176
     * @param string $text
177
     * @return Story
178
     */
179
    public function addComment (string $text) {
180
        return $this->newComment()->setText($text)->create();
181
    }
182
183
    /**
184
     * Premium feature.
185
     *
186
     * @depends after-create
187
     * @param Task[] $tasks
188
     * @return $this
189
     */
190
    public function addDependencies (array $tasks) {
191
        $this->api->post("{$this}/addDependencies", ['dependents' => array_column($tasks, 'gid')]);
192
        return $this;
193
    }
194
195
    /**
196
     * Premium feature.
197
     *
198
     * @depends after-create
199
     * @param Task $task
200
     * @return $this
201
     */
202
    public function addDependency (Task $task) {
203
        return $this->addDependencies([$task]);
204
    }
205
206
    /**
207
     * Premium feature.
208
     *
209
     * @depends after-create
210
     * @param Task $task
211
     * @return $this
212
     */
213
    public function addDependent (Task $task) {
214
        return $this->addDependents([$task]);
215
    }
216
217
    /**
218
     * Premium feature.
219
     *
220
     * @depends after-create
221
     * @param Task[] $tasks
222
     * @return $this
223
     */
224
    public function addDependents (array $tasks) {
225
        $this->api->post("{$this}/addDependents", ['dependents' => array_column($tasks, 'gid')]);
226
        return $this;
227
    }
228
229
    /**
230
     * Adds a follower.
231
     *
232
     * @param User $user
233
     * @return $this
234
     */
235
    public function addFollower (User $user) {
236
        return $this->addFollowers([$user]);
237
    }
238
239
    /**
240
     * Adds followers.
241
     *
242
     * @see https://developers.asana.com/docs/add-followers-to-a-task
243
     *
244
     * @param User[] $users
245
     * @return $this
246
     */
247
    public function addFollowers (array $users) {
248
        return $this->_addWithPost("{$this}/addFollowers", [
249
            'followers' => array_column($users, 'gid')
250
        ], 'followers', $users);
251
    }
252
253
    /**
254
     * Adds a tag.
255
     *
256
     * @see https://developers.asana.com/docs/add-a-tag-to-a-task
257
     *
258
     * @param Tag $tag
259
     * @return $this
260
     */
261
    public function addTag (Tag $tag) {
262
        return $this->_addWithPost("{$this}/addTag", [
263
            'tag' => $tag->getGid()
264
        ], 'tags', [$tag]);
265
    }
266
267
    /**
268
     * Adds the task to a project.
269
     *
270
     * @see https://developers.asana.com/docs/add-a-project-to-a-task
271
     *
272
     * @see Project::getDefaultSection()
273
     *
274
     * @param Section $section
275
     * @return $this
276
     */
277
    public function addToProject (Section $section) {
278
        /** @var Membership $membership */
279
        $membership = $this->api->factory($this, Membership::class)
280
            ->_set('project', $section->getProject())
281
            ->_set('section', $section);
282
        return $this->_addWithPost("{$this}/addProject", $membership->toArray(), 'memberships', [$membership]);
283
    }
284
285
    /**
286
     * Creates a webhook.
287
     *
288
     * @depends after-create
289
     * @param string $target
290
     * @return TaskWebhook
291
     */
292
    public function addWebhook (string $target) {
293
        /** @var TaskWebhook $webhook */
294
        $webhook = $this->api->factory($this, TaskWebhook::class);
295
        return $webhook->create($this, $target);
296
    }
297
298
    /**
299
     * @return $this
300
     */
301
    public function create () {
302
        $this->_create();
303
        $this->_onSave();
304
        return $this;
305
    }
306
307
    /**
308
     * Duplicates the task.
309
     *
310
     * @see https://developers.asana.com/docs/duplicate-a-task
311
     *
312
     * @depends after-create
313
     * @param string $name
314
     * @param string[] $include
315
     * @return Job
316
     */
317
    public function duplicate (string $name, array $include) {
318
        /** @var array $remote */
319
        $remote = $this->api->post("{$this}/duplicate", [
320
            'name' => $name,
321
            'include' => array_values($include)
322
        ]);
323
        return $this->api->factory($this, Job::class, $remote);
324
    }
325
326
    /**
327
     * Attached files.
328
     *
329
     * @depends after-create
330
     * @return Attachment[]
331
     */
332
    public function getAttachments () {
333
        return $this->api->loadAll($this, Attachment::class, "{$this}/attachments");
334
    }
335
336
    /**
337
     * @depends after-create
338
     * @return Story[]
339
     */
340
    public function getComments () {
341
        return $this->selectStories(function(Story $story) {
342
            return $story->isComment();
343
        });
344
    }
345
346
    /**
347
     * Premium feature.
348
     *
349
     * @depends after-create
350
     * @return Task[]
351
     */
352
    public function getDependencies () {
353
        return $this->api->loadAll($this, self::class, "{$this}/dependencies");
354
    }
355
356
    /**
357
     * Premium feature.
358
     *
359
     * @depends after-create
360
     * @return Task[]
361
     */
362
    public function getDependents () {
363
        return $this->api->loadAll($this, self::class, "{$this}/dependents");
364
    }
365
366
    /**
367
     * A proxy to the task's "external data".
368
     *
369
     * > :info:
370
     * > This always returns an instance, regardless of whether the task on Asana actually has external data.
371
     * >
372
     * > Asana will delete the external data object if it's emptied,
373
     * > and fetching it via `GET` will then return `null`, so we coalesce.
374
     *
375
     * @return External
376
     */
377
    public function getExternal () {
378
        return $this->_get('external') ?? $this->data['external'] = $this->api->factory($this, External::class);
379
    }
380
381
    /**
382
     * @return Project[]
383
     */
384
    public function getProjects () {
385
        return array_column($this->getMemberships(), 'project');
386
    }
387
388
    /**
389
     * @return Section[]
390
     */
391
    public function getSections () {
392
        return array_column($this->getMemberships(), 'section');
393
    }
394
395
    /**
396
     * @depends after-create
397
     * @return Story[]
398
     */
399
    public function getStories () {
400
        return $this->api->loadAll($this, Story::class, "{$this}/stories");
401
    }
402
403
    /**
404
     * @depends after-create
405
     * @return Task[]
406
     */
407
    public function getSubTasks () {
408
        return $this->api->loadAll($this, self::class, "{$this}/subtasks");
409
    }
410
411
    /**
412
     * @depends after-create
413
     * @return string
414
     */
415
    public function getUrl (): string {
416
        return "https://app.asana.com/0/0/{$this->getGid()}";
417
    }
418
419
    /**
420
     * @depends after-create
421
     * @return TaskWebhook[]
422
     */
423
    public function getWebhooks () {
424
        return $this->api->loadAll($this, TaskWebhook::class, 'webhooks', [
425
            'workspace' => $this->getWorkspace()->getGid(),
426
            'resource' => $this->getGid()
427
        ]);
428
    }
429
430
    /**
431
     * Factory.
432
     *
433
     * @depends after-create
434
     * @return Story
435
     */
436
    public function newComment () {
437
        return $this->api->factory($this, Story::class, [
438
            'resource_subtype' => Story::TYPE_COMMENT_ADDED,
439
            'target' => $this
440
        ]);
441
    }
442
443
    /**
444
     * Factory.
445
     *
446
     * @depends after-create
447
     * @return Task
448
     */
449
    public function newSubTask () {
450
        /** @var Task $sub */
451
        $sub = $this->api->factory($this, self::class);
452
        return $sub->setParent($this);
453
    }
454
455
    /**
456
     * Premium feature.
457
     *
458
     * @depends after-create
459
     * @param Task[] $tasks
460
     * @return $this
461
     */
462
    public function removeDependencies (array $tasks) {
463
        $this->api->post("{$this}/removeDependencies", ['dependencies' => array_column($tasks, 'gid')]);
464
        return $this;
465
    }
466
467
    /**
468
     * Premium feature.
469
     *
470
     * @depends after-create
471
     * @param Task $task
472
     * @return $this
473
     */
474
    public function removeDependency (Task $task) {
475
        return $this->removeDependencies([$task]);
476
    }
477
478
    /**
479
     * Premium feature.
480
     *
481
     * @depends after-create
482
     * @param Task $task
483
     * @return $this
484
     */
485
    public function removeDependent (Task $task) {
486
        return $this->removeDependents([$task]);
487
    }
488
489
    /**
490
     * Premium feature.
491
     *
492
     * @depends after-create
493
     * @param Task[] $tasks
494
     * @return $this
495
     */
496
    public function removeDependents (array $tasks) {
497
        $this->api->post("{$this}/removeDependents", ['dependents' => array_column($tasks, 'gid')]);
498
        return $this;
499
    }
500
501
    /**
502
     * Removes a follower.
503
     *
504
     * @param User $user
505
     * @return $this
506
     */
507
    public function removeFollower (User $user) {
508
        return $this->removeFollowers([$user]);
509
    }
510
511
    /**
512
     * Removes followers.
513
     *
514
     * @see https://developers.asana.com/docs/remove-followers-from-a-task
515
     *
516
     * @param User[] $users
517
     * @return $this
518
     */
519
    public function removeFollowers (array $users) {
520
        return $this->_removeWithPost("{$this}/removeFollowers", [
521
            'followers' => array_column($users, 'gid')
522
        ], 'followers', $users);
523
    }
524
525
    /**
526
     * Removes the task from a project.
527
     *
528
     * @see https://developers.asana.com/docs/remove-a-project-from-a-task
529
     *
530
     * @param Project $project
531
     * @return $this
532
     */
533
    public function removeFromProject (Project $project) {
534
        return $this->_removeWithPost("{$this}/removeProject", [
535
            'project' => $project->getGid()
536
        ], 'memberships', function(Membership $membership) use ($project) {
537
            return $membership->getProject()->getGid() !== $project->getGid();
538
        });
539
    }
540
541
    /**
542
     * Removes a tag.
543
     *
544
     * @see https://developers.asana.com/docs/remove-a-tag-from-a-task
545
     *
546
     * @param Tag $tag
547
     * @return $this
548
     */
549
    public function removeTag (Tag $tag) {
550
        return $this->_removeWithPost("{$this}/removeTag", [
551
            'tag' => $tag->getGid()
552
        ], 'tags', [$tag]);
553
    }
554
555
    /**
556
     * Makes the task a subtask of another.
557
     *
558
     * @see https://developers.asana.com/docs/set-the-parent-of-a-task
559
     * @param null|Task $parent
560
     * @return $this
561
     */
562
    public function setParent (?self $parent) {
563
        return $this->_setWithPost("{$this}/setParent", [
564
            'parent' => $parent
565
        ], 'parent', $parent);
566
    }
567
568
    /**
569
     * @return $this
570
     */
571
    public function update () {
572
        $this->_update();
573
        $this->_onSave();
574
        return $this;
575
    }
576
577
}