Passed
Push — master ( f968a7...f474ea )
by y
07:21
created

Task   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 582
Duplicated Lines 0 %

Importance

Changes 27
Bugs 3 Features 2
Metric Value
eloc 120
c 27
b 3
f 2
dl 0
loc 582
rs 6.4799
wmc 54

51 Methods

Rating   Name   Duplication   Size   Complexity  
A selectSubTasks() 0 2 1
A setParent() 0 4 1
A setRenderedAsSeparator() 0 2 1
A addTag() 0 4 1
A getAttachments() 0 2 1
A getWebhooks() 0 4 1
A selectComments() 0 2 1
A getStories() 0 2 1
A removeFromProject() 0 5 1
A addFollower() 0 2 1
A newSubTask() 0 4 1
A removeDependents() 0 3 1
A addDependencies() 0 3 1
A removeFollower() 0 2 1
A getExternal() 0 2 1
A removeDependent() 0 2 1
A removeFollowers() 0 4 1
A removeDependencies() 0 3 1
A setDueOn() 0 5 2
A getProjects() 0 2 1
A __toString() 0 2 1
A getComments() 0 3 1
A getUrl() 0 2 1
A getSections() 0 2 1
A addDependents() 0 3 1
A addAttachment() 0 4 1
A removeDependency() 0 2 1
A getDependents() 0 2 1
A duplicate() 0 7 1
A newComment() 0 4 1
A selectStories() 0 2 1
A _getDir() 0 2 1
A getSubTasks() 0 2 1
A getDependencies() 0 2 1
A removeTag() 0 4 1
A addToProject() 0 6 1
A selectAttachments() 0 2 1
A addFollowers() 0 4 1
A create() 0 4 1
A selectDependents() 0 2 1
A addDependency() 0 2 1
A getEvents() 0 2 1
A selectDependencies() 0 2 1
A isRenderedAsSeparator() 0 2 1
A selectProjects() 0 2 1
A addDependent() 0 2 1
A update() 0 4 1
A _onSave() 0 7 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 DateTimeInterface;
6
use Helix\Asana\Base\AbstractEntity;
7
use Helix\Asana\Base\AbstractEntity\CrudTrait;
8
use Helix\Asana\Project\Section;
9
use Helix\Asana\Task\Attachment;
10
use Helix\Asana\Task\External;
11
use Helix\Asana\Task\FieldEntries;
12
use Helix\Asana\Task\Like;
13
use Helix\Asana\Task\Membership;
14
use Helix\Asana\Task\Story;
15
use Helix\Asana\Webhook\TaskWebhook;
16
use Helix\Asana\Workspace\WorkspaceTrait;
17
18
/**
19
 * A task.
20
 *
21
 * @see https://developers.asana.com/docs/asana-tasks
22
 * @see https://developers.asana.com/docs/task
23
 *
24
 * @method null|User        getAssignee         ()
25
 * @method bool             hasAssignee         ()
26
 * @method $this            setAssignee         (null|User $user)
27
 * @method string           getAssigneeStatus   ()
28
 * @method $this            setAssigneeStatus   (string $status)
29
 * @method bool             isCompleted         ()
30
 * @method $this            setCompleted        (bool $completed)
31
 * @method string           getCompletedAt      ()
32
 * @method string           getCreatedAt        ()
33
 * @method null|FieldEntries getCustomFields    () Premium only.
34
 * @method bool             hasCustomFields     () Premium only.
35
 * @method string           getDueOn            ()
36
 * @method bool             hasDueOn            ()
37
 * @method User[]           getFollowers        ()
38
 * @method bool             hasFollowers        ()
39
 * @method User[]           selectFollowers     (callable $filter) `fn( User $user ): bool`
40
 * @method bool             isLiked             () Whether you like the task.
41
 * @method $this            setLiked            (bool $liked) Like or unlike the task.
42
 * @method Like[]           getLikes            ()
43
 * @method bool             hasLikes            ()
44
 * @method Like[]           selectLikes         (callable $filter) `fn( Like $like ): bool`
45
 * @method Membership[]     getMemberships      ()
46
 * @method bool             hasMemberships      ()
47
 * @method Membership[]     selectMemberships   (callable $filter) `fn( Membership $membership ): bool`
48
 * @method string           getModifiedAt       ()
49
 * @method string           getName             ()
50
 * @method bool             hasName             ()
51
 * @method $this            setName             (string $name)
52
 * @method string           getNotes            ()
53
 * @method bool             hasNotes            ()
54
 * @method $this            setNotes            (string $notes)
55
 * @method int              getNumLikes         ()
56
 * @method int              getNumSubtasks      ()
57
 * @method null|Task        getParent           ()
58
 * @method bool             hasParent           ()
59
 * @method string           getResourceSubtype  ()
60
 * @method $this            setResourceSubtype  (string $type)
61
 * @method string           getStartOn          ()
62
 * @method string           hasStartOn          ()
63
 * @method $this            setStartOn          (string $date)
64
 * @method Tag[]            getTags             ()
65
 * @method bool             hasTags             ()
66
 * @method Tag[]            selectTags          (callable $filter) `fn( Tag $tag ): bool`
67
 */
68
class Task extends AbstractEntity {
69
70
    use CrudTrait {
71
        create as private _create;
72
        update as private _update;
73
    }
74
    use WorkspaceTrait;
75
76
    const TYPE = 'task';
77
    const TYPE_DEFAULT = 'default_task';
78
    const TYPE_MILESTONE = 'milestone';
79
80
    const ASSIGN_INBOX = 'inbox';
81
    const ASSIGN_LATER = 'later';
82
    const ASSIGN_NEW = 'new';
83
    const ASSIGN_TODAY = 'today';
84
    const ASSIGN_UPCOMING = 'upcoming';
85
86
    const FILTER_INCOMPLETE = ['completed_since' => 'now'];
87
88
    protected const MAP = [
89
        'assignee' => User::class,
90
        'custom_fields' => FieldEntries::class,
91
        'external' => External::class, // not included by expanding "this". always lazy.
92
        'followers' => [User::class],
93
        'likes' => [Like::class],
94
        'memberships' => [Membership::class],
95
        'parent' => self::class,
96
        'tags' => [Tag::class],
97
        'workspace' => Workspace::class
98
    ];
99
100
    const OPT_FIELDS = [
101
        'memberships' => 'memberships.(project|section)'
102
    ];
103
104
    final public function __toString (): string {
105
        return "tasks/{$this->getGid()}";
106
    }
107
108
    final protected function _getDir (): string {
109
        return 'tasks';
110
    }
111
112
    protected function _onSave (): void {
113
        // use isset() to avoid has() fetch.
114
        if (isset($this->data['custom_fields'])) {
115
            $this->getCustomFields()->diff = [];
116
        }
117
        if (isset($this->data['external'])) {
118
            $this->getExternal()->diff = [];
119
        }
120
    }
121
122
    protected function _setData (array $data): void {
123
        // hearts were deprecated for likes
124
        unset($data['hearted'], $data['hearts'], $data['num_hearts']);
125
126
        // redundant. memberships are used instead
127
        unset($data['projects']);
128
129
        // time-based deadlines are a little passive-aggressive, don't you think?
130
        unset($data['due_at']);
131
132
        parent::_setData($data);
133
    }
134
135
    /**
136
     * Uploads a file attachment.
137
     *
138
     * @depends after-create
139
     * @param string $file
140
     * @return Attachment
141
     */
142
    public function addAttachment (string $file) {
143
        /** @var Attachment $attachment */
144
        $attachment = $this->api->factory($this, Attachment::class, ['parent' => $this]);
145
        return $attachment->upload($file);
146
    }
147
148
    /**
149
     * Posts a comment on an existing task and returns it.
150
     *
151
     * @param string $text
152
     * @return Story
153
     */
154
    public function addComment (string $text) {
155
        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

155
        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...
156
    }
157
158
    /**
159
     * Premium feature.
160
     *
161
     * @depends after-create
162
     * @param Task[] $tasks
163
     * @return $this
164
     */
165
    public function addDependencies (array $tasks) {
166
        $this->api->post("{$this}/addDependencies", ['dependents' => array_column($tasks, 'gid')]);
167
        return $this;
168
    }
169
170
    /**
171
     * Premium feature.
172
     *
173
     * @depends after-create
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
     * @depends after-create
185
     * @param Task $task
186
     * @return $this
187
     */
188
    public function addDependent (Task $task) {
189
        return $this->addDependents([$task]);
190
    }
191
192
    /**
193
     * Premium feature.
194
     *
195
     * @depends after-create
196
     * @param Task[] $tasks
197
     * @return $this
198
     */
199
    public function addDependents (array $tasks) {
200
        $this->api->post("{$this}/addDependents", ['dependents' => array_column($tasks, 'gid')]);
201
        return $this;
202
    }
203
204
    /**
205
     * Adds a follower.
206
     *
207
     * @param User $user
208
     * @return $this
209
     */
210
    public function addFollower (User $user) {
211
        return $this->addFollowers([$user]);
212
    }
213
214
    /**
215
     * Adds followers.
216
     *
217
     * @see https://developers.asana.com/docs/add-followers-to-a-task
218
     *
219
     * @param User[] $users
220
     * @return $this
221
     */
222
    public function addFollowers (array $users) {
223
        return $this->_addWithPost("{$this}/addFollowers", [
224
            'followers' => array_column($users, 'gid')
225
        ], 'followers', $users);
226
    }
227
228
    /**
229
     * Adds a tag.
230
     *
231
     * @see https://developers.asana.com/docs/add-a-tag-to-a-task
232
     *
233
     * @param Tag $tag
234
     * @return $this
235
     */
236
    public function addTag (Tag $tag) {
237
        return $this->_addWithPost("{$this}/addTag", [
238
            'tag' => $tag->getGid()
239
        ], 'tags', [$tag]);
240
    }
241
242
    /**
243
     * Adds the task to a project.
244
     *
245
     * @see https://developers.asana.com/docs/add-a-project-to-a-task
246
     *
247
     * @see Project::getDefaultSection()
248
     *
249
     * @param Section $section
250
     * @return $this
251
     */
252
    public function addToProject (Section $section) {
253
        /** @var Membership $membership */
254
        $membership = $this->api->factory($this, Membership::class)
255
            ->_set('project', $section->getProject())
256
            ->_set('section', $section);
257
        return $this->_addWithPost("{$this}/addProject", $membership->toArray(), 'memberships', [$membership]);
258
    }
259
260
    /**
261
     * Creates and returns a webhook.
262
     *
263
     * @depends after-create
264
     * @param string $target
265
     * @return TaskWebhook
266
     */
267
    public function addWebhook (string $target) {
268
        /** @var TaskWebhook $webhook */
269
        $webhook = $this->api->factory($this, TaskWebhook::class);
270
        return $webhook->create($this, $target);
271
    }
272
273
    /**
274
     * @return $this
275
     */
276
    public function create () {
277
        $this->_create();
278
        $this->_onSave();
279
        return $this;
280
    }
281
282
    /**
283
     * Creates and returns job to duplicate the task.
284
     *
285
     * @see https://developers.asana.com/docs/duplicate-a-task
286
     *
287
     * @depends after-create
288
     * @param string $name
289
     * @param string[] $include
290
     * @return Job
291
     */
292
    public function duplicate (string $name, array $include) {
293
        /** @var array $remote */
294
        $remote = $this->api->post("{$this}/duplicate", [
295
            'name' => $name,
296
            'include' => array_values($include)
297
        ]);
298
        return $this->api->factory($this, Job::class, $remote);
299
    }
300
301
    /**
302
     * Returns the task's files.
303
     *
304
     * @depends after-create
305
     * @return Attachment[]
306
     */
307
    public function getAttachments () {
308
        return $this->api->loadAll($this, Attachment::class, "{$this}/attachments");
309
    }
310
311
    /**
312
     * Returns the task's comments.
313
     *
314
     * @depends after-create
315
     * @return Story[]
316
     */
317
    public function getComments () {
318
        return $this->selectStories(function(Story $story) {
319
            return $story->isComment();
320
        });
321
    }
322
323
    /**
324
     * Premium feature.
325
     *
326
     * @depends after-create
327
     * @return Task[]
328
     */
329
    public function getDependencies () {
330
        return $this->api->loadAll($this, self::class, "{$this}/dependencies");
331
    }
332
333
    /**
334
     * Premium feature.
335
     *
336
     * @depends after-create
337
     * @return Task[]
338
     */
339
    public function getDependents () {
340
        return $this->api->loadAll($this, self::class, "{$this}/dependents");
341
    }
342
343
    /**
344
     * Returns events since the last sync.
345
     *
346
     * @depends after-create
347
     * @param null|string $token
348
     * @return Event[]
349
     */
350
    public function getEvents (&$token) {
351
        return $this->api->sync($this->getGid(), $token);
1 ignored issue
show
Bug introduced by
It seems like $this->getGid() can also be of type null; however, parameter $gid of Helix\Asana\Api::sync() does only seem to accept string, 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

351
        return $this->api->sync(/** @scrutinizer ignore-type */ $this->getGid(), $token);
Loading history...
352
    }
353
354
    /**
355
     * This always returns an instance, regardless of whether the task on Asana actually has external data.
356
     *
357
     * Developer's note: Asana will delete the external data object if it's emptied,
358
     * and fetching it via `GET` will then return `null`, so we coalesce.
359
     *
360
     * @return External
361
     */
362
    public function getExternal () {
363
        return $this->_get('external') ?? $this->data['external'] = $this->api->factory($this, External::class);
364
    }
365
366
    /**
367
     * @return Project[]
368
     */
369
    public function getProjects () {
370
        return array_column($this->getMemberships(), 'project');
371
    }
372
373
    /**
374
     * @return Section[]
375
     */
376
    public function getSections () {
377
        return array_column($this->getMemberships(), 'section');
378
    }
379
380
    /**
381
     * Returns the task's activity.
382
     *
383
     * @depends after-create
384
     * @return Story[]
385
     */
386
    public function getStories () {
387
        return $this->api->loadAll($this, Story::class, "{$this}/stories");
388
    }
389
390
    /**
391
     * Returns the task's subtasks.
392
     *
393
     * @depends after-create
394
     * @return Task[]
395
     */
396
    public function getSubTasks () {
397
        return $this->api->loadAll($this, self::class, "{$this}/subtasks");
398
    }
399
400
    /**
401
     * Returns the task's URL.
402
     *
403
     * @depends after-create
404
     * @return string
405
     */
406
    public function getUrl (): string {
407
        return "https://app.asana.com/0/0/{$this->getGid()}";
408
    }
409
410
    /**
411
     * Returns the task's webhooks.
412
     *
413
     * @depends after-create
414
     * @return TaskWebhook[]
415
     */
416
    public function getWebhooks () {
417
        return $this->api->loadAll($this, TaskWebhook::class, 'webhooks', [
418
            'workspace' => $this->getWorkspace()->getGid(),
419
            'resource' => $this->getGid()
420
        ]);
421
    }
422
423
    /**
424
     * @return bool
425
     */
426
    public function isRenderedAsSeparator (): bool {
427
        return $this->_is('is_rendered_as_separator');
428
    }
429
430
    /**
431
     * Instantiates and returns a new comment.
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
     * Instantiates and returns a new subtask.
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
     * @param callable $filter `fn( Attachment $attachment): bool`
557
     * @return Attachment[]
558
     */
559
    public function selectAttachments (callable $filter) {
560
        return $this->_select($this->getAttachments(), $filter);
561
    }
562
563
    /**
564
     * @param callable $filter `fn( Story $comment ): bool`
565
     * @return Story[]
566
     */
567
    public function selectComments (callable $filter) {
568
        return $this->_select($this->getComments(), $filter);
569
    }
570
571
    /**
572
     * @param callable $filter `fn( Task $dependency ): bool`
573
     * @return Task[]
574
     */
575
    public function selectDependencies (callable $filter) {
576
        return $this->_select($this->getDependencies(), $filter);
577
    }
578
579
    /**
580
     * @param callable $filter `fn( Task $dependent ): bool`
581
     * @return Task[]
582
     */
583
    public function selectDependents (callable $filter) {
584
        return $this->_select($this->getDependents(), $filter);
585
    }
586
587
    /**
588
     * @param callable $filter `fn( Project $project ): bool`
589
     * @return Project[]
590
     */
591
    public function selectProjects (callable $filter) {
592
        return $this->_select($this->getProjects(), $filter);
593
    }
594
595
    /**
596
     * @param callable $filter `fn( Story $story ): bool`
597
     * @return Story[]
598
     */
599
    public function selectStories (callable $filter) {
600
        return $this->_select($this->getStories(), $filter);
601
    }
602
603
    /**
604
     * @param callable $filter `fn( Task $subtask ): bool`
605
     * @return Task[]
606
     */
607
    public function selectSubTasks (callable $filter) {
608
        return $this->_select($this->getSubTasks(), $filter);
609
    }
610
611
    /**
612
     * @param null|string|DateTimeInterface $date
613
     * @return Task
614
     */
615
    public function setDueOn ($date) {
616
        if ($date instanceof DateTimeInterface) {
617
            $date = $date->format('Y-m-d');
618
        }
619
        return $this->_set('due_on', $date);
620
    }
621
622
    /**
623
     * Makes the task a subtask of another.
624
     *
625
     * @see https://developers.asana.com/docs/set-the-parent-of-a-task
626
     * @param null|Task $parent
627
     * @return $this
628
     */
629
    public function setParent (?self $parent) {
630
        return $this->_setWithPost("{$this}/setParent", [
631
            'parent' => $parent
632
        ], 'parent', $parent);
633
    }
634
635
    /**
636
     * @param bool $flag
637
     * @return $this
638
     */
639
    public function setRenderedAsSeparator (bool $flag) {
640
        return $this->_set('is_rendered_as_separator', $flag);
641
    }
642
643
    /**
644
     * @return $this
645
     */
646
    public function update () {
647
        $this->_update();
648
        $this->_onSave();
649
        return $this;
650
    }
651
652
}