Passed
Push — master ( e46353...1a00b5 )
by y
01:38
created

Task::getUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
rs 10
c 1
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\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\External;
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
 * @method null|User    getAssignee         ()
26
 * @method bool         hasAssignee         ()
27
 * @method $this        setAssignee         (null|User $user)
28
 * @method string       getAssigneeStatus   ()
29
 * @method $this        setAssigneeStatus   (string $status)
30
 * @method bool         isCompleted         ()
31
 * @method $this        setCompleted        (bool $completed)
32
 * @method string       getCompletedAt      ()
33
 * @method string       getCreatedAt        ()
34
 * @method null|CustomValues getCustomFields() Premium only.
35
 * @method bool         hasCustomFields     () Premium only.
36
 * @method string       getDueOn            ()
37
 * @method $this        setDueOn            (string $date)
38
 * @method User[]       getFollowers        ()
39
 * @method bool         hasFollowers        ()
40
 * @method string       getHtmlNotes        ()
41
 * @method $this        setHtmlNotes        (string $notes)
42
 * @method bool         isLiked             ()
43
 * @method $this        setLiked            (bool $liked)
44
 * @method Like[]       getLikes            ()
45
 * @method Membership[] getMemberships      ()
46
 * @method bool         hasMemberships      ()
47
 * @method string       getModifiedAt       ()
48
 * @method string       getName             ()
49
 * @method $this        setName             (string $name)
50
 * @method string       getNotes            ()
51
 * @method $this        setNotes            (string $notes)
52
 * @method int          getNumLikes         ()
53
 * @method int          getNumSubtasks      ()
54
 * @method null|Task    getParent           ()
55
 * @method bool         hasParent           ()
56
 * @method string       getResourceSubtype  ()
57
 * @method $this        setResourceSubtype  (string $type)
58
 * @method string       getStartOn          ()
59
 * @method $this        setStartOn          (string $date)
60
 * @method Tag[]        getTags             ()
61
 * @method bool         hasTags             ()
62
 */
63
class Task extends AbstractEntity {
64
65
    use CrudTrait {
66
        create as private _create;
67
        update as private _update;
68
    }
69
70
    use WorkspaceTrait;
71
72
    const TYPE = 'task';
73
    const TYPE_DEFAULT = 'default_task';
74
    const TYPE_MILESTONE = 'milestone';
75
76
    const ASSIGN_INBOX = 'inbox';
77
    const ASSIGN_LATER = 'later';
78
    const ASSIGN_NEW = 'new';
79
    const ASSIGN_TODAY = 'today';
80
    const ASSIGN_UPCOMING = 'upcoming';
81
82
    protected $optFields = [
83
        'memberships' => 'memberships.(project|section)'
84
    ];
85
86
    final public function __toString (): string {
87
        return "tasks/{$this->getGid()}";
88
    }
89
90
    /**
91
     * Clears the diffs for `custom_fields` and `external` sub-objects after saving.
92
     */
93
    protected function _clearSubDiffs (): void {
94
        // use isset() to avoid has() fetch.
95
        if (isset($this->data['custom_fields'])) {
96
            $this->getCustomFields()->diff = [];
97
        }
98
        if (isset($this->data['external'])) {
99
            $this->getExternal()->diff = [];
100
        }
101
    }
102
103
    final protected function _getDir (): string {
104
        return 'tasks';
105
    }
106
107
    protected function _getMap (): array {
108
        return [
109
            'assignee' => User::class,
110
            'custom_fields' => CustomValues::class,
111
            'external' => External::class, // not included by expanding "this". always lazy.
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
121
    /**
122
     * Uploads a file attachment.
123
     *
124
     * @depends after-create
125
     * @param string $file
126
     * @return Attachment
127
     */
128
    public function addAttachment (string $file) {
129
        /** @var Attachment $attachment */
130
        $attachment = $this->factory(Attachment::class, ['parent' => $this]);
131
        return $attachment->upload($file);
132
    }
133
134
    /**
135
     * Posts a comment on an existing task and returns it.
136
     *
137
     * @param string $text
138
     * @return Story
139
     */
140
    public function addComment (string $text) {
141
        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

141
        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...
142
    }
143
144
    /**
145
     * Premium feature.
146
     *
147
     * @depends after-create
148
     * @param Task[] $tasks
149
     * @return $this
150
     */
151
    public function addDependencies (array $tasks) {
152
        $this->api->post("{$this}/addDependencies", ['dependents' => static::_getGids($tasks)]);
153
        return $this;
154
    }
155
156
    /**
157
     * Premium feature.
158
     *
159
     * @depends after-create
160
     * @param Task $task
161
     * @return $this
162
     */
163
    public function addDependency (Task $task) {
164
        return $this->addDependencies([$task]);
165
    }
166
167
    /**
168
     * Premium feature.
169
     *
170
     * @depends after-create
171
     * @param Task $task
172
     * @return $this
173
     */
174
    public function addDependent (Task $task) {
175
        return $this->addDependents([$task]);
176
    }
177
178
    /**
179
     * Premium feature.
180
     *
181
     * @depends after-create
182
     * @param Task[] $tasks
183
     * @return $this
184
     */
185
    public function addDependents (array $tasks) {
186
        $this->api->post("{$this}/addDependents", ['dependents' => static::_getGids($tasks)]);
187
        return $this;
188
    }
189
190
    /**
191
     * Adds a follower.
192
     *
193
     * @param User $user
194
     * @return $this
195
     */
196
    public function addFollower (User $user) {
197
        return $this->addFollowers([$user]);
198
    }
199
200
    /**
201
     * Adds followers.
202
     *
203
     * @param User[] $users
204
     * @return $this
205
     */
206
    public function addFollowers (array $users) {
207
        if ($this->hasGid()) {
208
            $this->api->post("{$this}/addFollowers", ['followers' => static::_getGids($users)]);
209
            $this->_merge('followers', $users);
210
        }
211
        else {
212
            $this->_merge('followers', $users, true);
213
        }
214
        return $this;
215
    }
216
217
    /**
218
     * Adds a tag.
219
     *
220
     * @param Tag $tag
221
     * @return $this
222
     */
223
    public function addTag (Tag $tag) {
224
        if ($this->hasGid()) {
225
            $this->api->post("{$this}/addTag", ['tag' => $tag->getGid()]);
226
            $this->_merge('tags', [$tag]);
227
        }
228
        else {
229
            $this->_merge('tags', [$tag], true);
230
        }
231
        return $this;
232
    }
233
234
    /**
235
     * Adds the task to a project section.
236
     *
237
     * @param Section $section
238
     * @return $this
239
     */
240
    public function addToProject (Section $section) {
241
        $project = $section->getProject();
242
        if ($this->hasGid()) {
243
            $this->api->post("{$this}/addProject", [
244
                'project' => $project->getGid(),
245
                'section' => $section->getGid()
246
            ]);
247
            if (isset($this->data['memberships'])) {
248
                $this->data['memberships'][] = $this->factory(Membership::class)->_setData([
249
                    'project' => $project,
250
                    'section' => $section
251
                ]);
252
            }
253
        }
254
        else {
255
            if (!$this->hasWorkspace()) {
256
                $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

256
                $this->setWorkspace(/** @scrutinizer ignore-type */ $project->getWorkspace());
Loading history...
257
            }
258
            $membership = $this->factory(Membership::class);
259
            $membership->_set('project', $section->getProject());
260
            $membership->_set('section', $section);
261
            $this->data['memberships'][] = $membership;
262
            $this->diff['memberships'] = true;
263
        }
264
        return $this;
265
    }
266
267
    /**
268
     * Creates and returns a webhook.
269
     *
270
     * @depends after-create
271
     * @param string $target
272
     * @return TaskWebhook
273
     */
274
    public function addWebhook (string $target) {
275
        /** @var TaskWebhook $webhook */
276
        $webhook = $this->factory(TaskWebhook::class);
277
        return $webhook->create($this, $target);
278
    }
279
280
    /**
281
     * @return $this
282
     */
283
    public function create () {
284
        $this->_create();
285
        $this->_clearSubDiffs();
286
        return $this;
287
    }
288
289
    /**
290
     * Creates and returns job to duplicate the task.
291
     *
292
     * @see https://developers.asana.com/docs/#duplicate-a-task
293
     *
294
     * @depends after-create
295
     * @param string $name
296
     * @param string[] $include
297
     * @return Job
298
     */
299
    public function duplicate (string $name, array $include) {
300
        $remote = $this->api->post("{$this}/duplicate", [
301
            'name' => $name,
302
            'include' => array_values($include)
303
        ]);
304
        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

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