Passed
Push — master ( d25a5e...bcdfe5 )
by y
01:48
created

Task   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 488
Duplicated Lines 0 %

Importance

Changes 19
Bugs 1 Features 2
Metric Value
eloc 119
dl 0
loc 488
rs 6.96
c 19
b 1
f 2
wmc 53

39 Methods

Rating   Name   Duplication   Size   Complexity  
A setParent() 0 9 3
A setRenderedAsSeparator() 0 2 1
A addTag() 0 9 2
A getWebhooks() 0 4 1
A getAttachments() 0 2 1
A getStories() 0 2 1
A removeFromProject() 0 11 3
A newSubTask() 0 4 1
A addFollower() 0 2 1
A removeDependents() 0 3 1
A removeFollower() 0 2 1
A addDependencies() 0 3 1
A removeFollowers() 0 6 2
A removeDependent() 0 2 1
A removeDependencies() 0 3 1
A _getMap() 0 11 1
A __toString() 0 2 1
A getComments() 0 3 1
A getUrl() 0 2 1
A addDependents() 0 3 1
A addAttachment() 0 4 1
A getDependents() 0 2 1
A removeDependency() 0 2 1
A newComment() 0 6 1
A duplicate() 0 6 1
A onUpdate() 0 3 2
A getSubTasks() 0 2 1
A _getDir() 0 2 1
A getDependencies() 0 2 1
A removeTag() 0 6 2
A addToProject() 0 25 4
A addFollowers() 0 9 2
A getEvents() 0 2 1
A addDependency() 0 2 1
A isRenderedAsSeparator() 0 2 1
A addDependent() 0 2 1
A isDiff() 0 2 3
A addWebhook() 0 4 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\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\Like;
14
use Helix\Asana\Task\Membership;
15
use Helix\Asana\Task\Story;
16
use Helix\Asana\Webhook\TaskWebhook;
17
18
/**
19
 * A task.
20
 *
21
 * @see https://developers.asana.com/docs/#asana-tasks
22
 * @see https://developers.asana.com/docs/#tocS_Task
23
 *
24
 * @method null|User    getAssignee         ()
25
 * @method $this        setAssignee         (null|User $user)
26
 * @method string       getAssigneeStatus   ()
27
 * @method $this        setAssigneeStatus   (string $status)
28
 * @method bool         isCompleted         ()
29
 * @method $this        setCompleted        (bool $completed)
30
 * @method string       getCompletedAt      ()
31
 * @method string       getCreatedAt        ()
32
 * @method null|CustomValues getCustomFields()
33
 * @method bool         hasCustomFields     ()
34
 * @method string       getDueOn            ()
35
 * @method $this        setDueOn            (string $date)
36
 * @method User[]       getFollowers        ()
37
 * @method string       getHtmlNotes        ()
38
 * @method $this        setHtmlNotes        (string $notes)
39
 * @method bool         isLiked             ()
40
 * @method $this        setLiked            (bool $liked)
41
 * @method Like[]       getLikes            ()
42
 * @method Membership[] getMemberships      ()
43
 * @method string       getModifiedAt       ()
44
 * @method string       getName             ()
45
 * @method $this        setName             (string $name)
46
 * @method string       getNotes            ()
47
 * @method $this        setNotes            (string $notes)
48
 * @method int          getNumLikes         ()
49
 * @method int          getNumSubtasks      ()
50
 * @method null|Task    getParent           ()
51
 * @method bool         hasParent           ()
52
 * @method Project[]    getProjects         ()
53
 * @method string       getResourceSubtype  ()
54
 * @method $this        setResourceSubtype  (string $type)
55
 * @method string       getStartOn          ()
56
 * @method $this        setStartOn          (string $date)
57
 * @method Tag[]        getTags             ()
58
 */
59
class Task extends AbstractEntity {
60
61
    use CrudTrait;
62
    use WorkspaceTrait;
63
64
    const TYPE = 'task';
65
    const TYPE_DEFAULT = 'default_task';
66
    const TYPE_MILESTONE = 'milestone';
67
68
    const ASSIGN_INBOX = 'inbox';
69
    const ASSIGN_LATER = 'later';
70
    const ASSIGN_NEW = 'new';
71
    const ASSIGN_TODAY = 'today';
72
    const ASSIGN_UPCOMING = 'upcoming';
73
74
    final public function __toString (): string {
75
        return "tasks/{$this->getGid()}";
76
    }
77
78
    final protected function _getDir (): string {
79
        return 'tasks';
80
    }
81
82
    protected function _getMap (): array {
83
        return [
84
            'assignee' => User::class,
85
            'custom_fields' => CustomValues::class,
86
            'followers' => [User::class],
87
            'likes' => [Like::class],
88
            'memberships' => [Membership::class],
89
            'parent' => self::class,
90
            'projects' => [Project::class],
91
            'tags' => [Tag::class],
92
            'workspace' => Workspace::class
93
        ];
94
    }
95
96
    /**
97
     * Uploads a file attachment.
98
     *
99
     * @depends after-create
100
     * @param string $file
101
     * @return Attachment
102
     */
103
    public function addAttachment (string $file) {
104
        /** @var Attachment $attachment */
105
        $attachment = $this->factory(Attachment::class, ['parent' => $this]);
106
        return $attachment->upload($file);
107
    }
108
109
    /**
110
     * Posts a comment.
111
     *
112
     * @depends after-create
113
     * @param string $text
114
     * @return Story
115
     */
116
    public function addComment (string $text) {
117
        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

117
        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...
118
    }
119
120
    /**
121
     * Premium feature.
122
     *
123
     * @depends after-create
124
     * @param Task[] $tasks
125
     * @return $this
126
     */
127
    public function addDependencies (array $tasks) {
128
        $this->api->post("{$this}/addDependencies", ['dependents' => static::_getGids($tasks)]);
129
        return $this;
130
    }
131
132
    /**
133
     * Premium feature.
134
     *
135
     * @depends after-create
136
     * @param Task $task
137
     * @return $this
138
     */
139
    public function addDependency (Task $task) {
140
        return $this->addDependencies([$task]);
141
    }
142
143
    /**
144
     * Premium feature.
145
     *
146
     * @depends after-create
147
     * @param Task $task
148
     * @return $this
149
     */
150
    public function addDependent (Task $task) {
151
        return $this->addDependents([$task]);
152
    }
153
154
    /**
155
     * Premium feature.
156
     *
157
     * @depends after-create
158
     * @param Task[] $tasks
159
     * @return $this
160
     */
161
    public function addDependents (array $tasks) {
162
        $this->api->post("{$this}/addDependents", ['dependents' => static::_getGids($tasks)]);
163
        return $this;
164
    }
165
166
    /**
167
     * Adds a follower.
168
     *
169
     * @param User $user
170
     * @return $this
171
     */
172
    public function addFollower (User $user) {
173
        return $this->addFollowers([$user]);
174
    }
175
176
    /**
177
     * Adds followers.
178
     *
179
     * @param User[] $users
180
     * @return $this
181
     */
182
    public function addFollowers (array $users) {
183
        if ($this->hasGid()) {
184
            $this->api->post("{$this}/addFollowers", ['followers' => static::_getGids($users)]);
185
            $this->_merge('followers', $users);
186
        }
187
        else {
188
            $this->_merge('followers', $users, true);
189
        }
190
        return $this;
191
    }
192
193
    /**
194
     * Adds a tag.
195
     *
196
     * @param Tag $tag
197
     * @return $this
198
     */
199
    public function addTag (Tag $tag) {
200
        if ($this->hasGid()) {
201
            $this->api->post("{$this}/addTag", ['tag' => $tag->getGid()]);
202
            $this->_merge('tags', [$tag]);
203
        }
204
        else {
205
            $this->_merge('tags', [$tag], true);
206
        }
207
        return $this;
208
    }
209
210
    /**
211
     * Adds the task to a project section.
212
     *
213
     * @param Section $section
214
     * @return $this
215
     */
216
    public function addToProject (Section $section) {
217
        $project = $section->getProject();
218
        if ($this->hasGid()) {
219
            $this->api->post("{$this}/addProject", [
220
                'project' => $project->getGid(),
221
                'section' => $section->getGid()
222
            ]);
223
            if (isset($this->data['memberships'])) {
224
                $this->data['memberships'][] = $this->factory(Membership::class)->_setData([
225
                    'project' => $project,
226
                    'section' => $section
227
                ]);
228
            }
229
        }
230
        else {
231
            if (!$this->hasWorkspace()) {
232
                $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

232
                $this->setWorkspace(/** @scrutinizer ignore-type */ $project->getWorkspace());
Loading history...
233
            }
234
            $membership = $this->factory(Membership::class);
235
            $membership->_set('project', $section->getProject());
236
            $membership->_set('section', $section);
237
            $this->data['memberships'][] = $membership;
238
            $this->diff['memberships'] = true;
239
        }
240
        return $this;
241
    }
242
243
    /**
244
     * Creates and returns a webhook.
245
     *
246
     * @depends after-create
247
     * @param string $target
248
     * @return TaskWebhook
249
     */
250
    public function addWebhook (string $target) {
251
        /** @var TaskWebhook $webhook */
252
        $webhook = $this->factory(TaskWebhook::class);
253
        return $webhook->create($this, $target);
254
    }
255
256
    /**
257
     * Creates and returns job to duplicate the task.
258
     *
259
     * @see https://developers.asana.com/docs/#duplicate-a-task
260
     *
261
     * @depends after-create
262
     * @param string $name
263
     * @param string[] $include
264
     * @return Job
265
     */
266
    public function duplicate (string $name, array $include) {
267
        $remote = $this->api->post("{$this}/duplicate", [
268
            'name' => $name,
269
            'include' => array_values($include)
270
        ]);
271
        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

271
        return $this->factory(Job::class, /** @scrutinizer ignore-type */ $remote);
Loading history...
272
    }
273
274
    /**
275
     * Returns the task's files.
276
     *
277
     * @depends after-create
278
     * @return Attachment[]
279
     */
280
    public function getAttachments () {
281
        return $this->loadAll(Attachment::class, "{$this}/attachments");
282
    }
283
284
    /**
285
     * Returns the task's comments.
286
     *
287
     * @depends after-create
288
     * @return Story[]
289
     */
290
    public function getComments () {
291
        return array_values(array_filter($this->getStories(), function(Story $story) {
292
            return $story->isComment();
293
        }));
294
    }
295
296
    /**
297
     * Premium feature.
298
     *
299
     * @depends after-create
300
     * @return Task[]
301
     */
302
    public function getDependencies () {
303
        return $this->loadAll(self::class, "{$this}/dependencies");
304
    }
305
306
    /**
307
     * Premium feature.
308
     *
309
     * @depends after-create
310
     * @return Task[]
311
     */
312
    public function getDependents () {
313
        return $this->loadAll(self::class, "{$this}/dependents");
314
    }
315
316
    /**
317
     * Returns events since the last sync.
318
     *
319
     * @depends after-create
320
     * @param null|string $token
321
     * @return TaskEvent[]|StoryEvent[]
322
     */
323
    public function getEvents (&$token) {
324
        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...
325
    }
326
327
    /**
328
     * Returns the task's activity.
329
     *
330
     * @depends after-create
331
     * @return Story[]
332
     */
333
    public function getStories () {
334
        return $this->loadAll(Story::class, "{$this}/stories");
335
    }
336
337
    /**
338
     * Returns the task's subtasks.
339
     *
340
     * @depends after-create
341
     * @return Task[]
342
     */
343
    public function getSubTasks () {
344
        return $this->loadAll(self::class, "{$this}/subtasks");
345
    }
346
347
    /**
348
     * Returns the task's URL.
349
     *
350
     * @depends after-create
351
     * @return string
352
     */
353
    public function getUrl (): string {
354
        return "https://app.asana.com/0/0/{$this->getGid()}";
355
    }
356
357
    /**
358
     * Returns the task's webhooks.
359
     *
360
     * @depends after-create
361
     * @return TaskWebhook[]
362
     */
363
    public function getWebhooks () {
364
        return $this->loadAll(TaskWebhook::class, 'webhooks', [
365
            'workspace' => $this->getWorkspace()->getGid(),
366
            'resource' => $this->getGid()
367
        ]);
368
    }
369
370
    /**
371
     * Additional `custom_fields` check.
372
     *
373
     * @return bool
374
     */
375
    public function isDiff (): bool {
376
        return parent::isDiff() or $this->hasCustomFields() && $this->getCustomFields()->isDiff();
377
    }
378
379
    /**
380
     * @return bool
381
     */
382
    public function isRenderedAsSeparator (): bool {
383
        return $this->_is('is_rendered_as_separator');
384
    }
385
386
    /**
387
     * Instantiates and returns a new comment.
388
     *
389
     * @depends after-create
390
     * @return Story
391
     */
392
    public function newComment () {
393
        /** @var Story $comment */
394
        $comment = $this->factory(Story::class, [
395
            'resource_subtype' => Story::TYPE_COMMENT_ADDED
396
        ]);
397
        return $comment->_set('task', $this);
398
    }
399
400
    /**
401
     * Instantiates and returns a new subtask.
402
     *
403
     * @depends after-create
404
     * @return Task
405
     */
406
    public function newSubTask () {
407
        /** @var Task $sub */
408
        $sub = $this->factory(self::class);
409
        return $sub->setParent($this);
410
    }
411
412
    /**
413
     * Marks `custom_fields` clean.
414
     */
415
    protected function onUpdate (): void {
416
        if ($this->hasCustomFields()) {
417
            $this->getCustomFields()->diff = [];
418
        }
419
    }
420
421
    /**
422
     * Premium feature.
423
     *
424
     * @depends after-create
425
     * @param Task[] $tasks
426
     * @return $this
427
     */
428
    public function removeDependencies (array $tasks) {
429
        $this->api->post("{$this}/removeDependencies", ['dependencies' => static::_getGids($tasks)]);
430
        return $this;
431
    }
432
433
    /**
434
     * Premium feature.
435
     *
436
     * @depends after-create
437
     * @param Task $task
438
     * @return $this
439
     */
440
    public function removeDependency (Task $task) {
441
        return $this->removeDependencies([$task]);
442
    }
443
444
    /**
445
     * Premium feature.
446
     *
447
     * @depends after-create
448
     * @param Task $task
449
     * @return $this
450
     */
451
    public function removeDependent (Task $task) {
452
        return $this->removeDependents([$task]);
453
    }
454
455
    /**
456
     * Premium feature.
457
     *
458
     * @depends after-create
459
     * @param Task[] $tasks
460
     * @return $this
461
     */
462
    public function removeDependents (array $tasks) {
463
        $this->api->post("{$this}/removeDependents", ['dependents' => static::_getGids($tasks)]);
464
        return $this;
465
    }
466
467
    /**
468
     * Removes a follower.
469
     *
470
     * @param User $user
471
     * @return $this
472
     */
473
    public function removeFollower (User $user) {
474
        return $this->removeFollowers([$user]);
475
    }
476
477
    /**
478
     * Removes followers.
479
     *
480
     * @param User[] $users
481
     * @return $this
482
     */
483
    public function removeFollowers (array $users) {
484
        if ($this->hasGid()) {
485
            $this->api->post("{$this}/removeFollowers", ['followers' => static::_getGids($users)]);
486
        }
487
        $this->_remove('followers', $users);
488
        return $this;
489
    }
490
491
    /**
492
     * Removes the task from a project.
493
     *
494
     * @param Project $project
495
     * @return $this
496
     */
497
    public function removeFromProject (Project $project) {
498
        $gid = $project->getGid();
499
        if ($this->hasGid()) {
500
            $this->api->post("{$this}/removeProject", ['project' => $gid]);
501
        }
502
        if (isset($this->data['memberships'])) {
503
            $this->data['memberships'] = array_values(array_filter($this->data['memberships'], function(Membership $membership) use ($gid) {
504
                return $membership->getProject()->getGid() !== $gid;
505
            }));
506
        }
507
        return $this;
508
    }
509
510
    /**
511
     * Removes a tag.
512
     *
513
     * @param Tag $tag
514
     * @return $this
515
     */
516
    public function removeTag (Tag $tag) {
517
        if ($this->hasGid()) {
518
            $this->api->post("{$this}/removeTag", ['tag' => $tag->getGid()]);
519
        }
520
        $this->_remove('tags', [$tag]);
521
        return $this;
522
    }
523
524
    /**
525
     * Makes the task a subtask of another.
526
     *
527
     * @param null|Task $parent
528
     * @return $this
529
     */
530
    public function setParent (?self $parent) {
531
        if ($this->hasGid()) {
532
            $this->api->post("{$this}/setParent", ['parent' => $parent ? $parent->getGid() : null]);
533
            $this->data['parent'] = $parent;
534
        }
535
        else {
536
            $this->_set('parent', $parent);
537
        }
538
        return $this;
539
    }
540
541
    /**
542
     * @param bool $flag
543
     * @return $this
544
     */
545
    public function setRenderedAsSeparator (bool $flag) {
546
        return $this->_set('is_rendered_as_separator', $flag);
547
    }
548
549
}