SendMessages::getProjectId()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
/*
4
 * This file is part of the Tinyissue package.
5
 *
6
 * (c) Mohamed Alsharaf <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Tinyissue\Model\Project\Issue;
13
14
use Illuminate\Database\Eloquent\Collection;
15
use Tinyissue\Model\Message\Queue;
16
use Tinyissue\Model\Project;
17
use Tinyissue\Model\Project\Issue as ProjectIssue;
18
use Tinyissue\Model\Tag;
19
use Tinyissue\Model\User;
20
use Tinyissue\Services\SendMessagesAbstract;
21
22
/**
23
 * SendMessages is a class to manage & process of sending messages about issue changes.
24
 *
25
 * @author Mohamed Alsharaf <[email protected]>
26
 */
27
class SendMessages extends SendMessagesAbstract
28
{
29
    protected $template = 'update_issue';
30
31
    /**
32
     * Collection of tags.
33
     *
34
     * @var Collection
35
     */
36
    protected $tags;
37
38
    /**
39
     * Returns an instance of Issue.
40
     *
41
     * @return ProjectIssue
42
     */
43 4
    protected function getIssue()
44
    {
45 4
        if (null === $this->issue) {
46 4
            $this->issue = $this->getModel();
47
        }
48
49 4
        return $this->issue;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->issue; (Tinyissue\Model\Project\...Tinyissue\Model\Project) is incompatible with the return type declared by the abstract method Tinyissue\Services\SendMessagesAbstract::getIssue of type Tinyissue\Model\Project\Issue.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
50
    }
51
52
    /**
53
     * Returns an instance of Project.
54
     *
55
     * @return Project
56
     */
57 4
    protected function getProject()
58
    {
59 4
        return $this->getIssue()->project;
60
    }
61
62
    /**
63
     * Returns the project id.
64
     *
65
     * @return int
66
     */
67 4
    protected function getProjectId()
68
    {
69 4
        return $this->getIssue()->project_id;
70
    }
71
72
    /**
73
     * Returns message data belongs to adding an issue.
74
     *
75
     * @param Queue $queue
76
     *
77
     * @return array
78
     */
79 4
    protected function getMessageDataForAddIssue(Queue $queue)
80
    {
81 4
        $messageData                    = ['changes' => []];
82 4
        $messageData['changeByHeading'] = $queue->changeBy->fullname . ' created a new issue';
83 4
        if ($this->issue->assigned) {
84 2
            $messageData['changes']['assignee'] = $this->issue->assigned->fullname;
85
        }
86
87 4
        $tags = $this->issue->tags()->with('parent')->get();
88 4
        foreach ($tags as $tag) {
89 2
            $tagArray                                   = $tag->toShortArray();
90 2
            $tagArray['now']                            = $tagArray['name'];
91 2
            $messageData['changes'][$tag->parent->name] = $tagArray;
92
        }
93
94 4
        if ($this->issue->time_quote && !$this->issue->isQuoteLocked()) {
95
            $messageData['changes']['time_quote'] = \Html::duration($this->issue->time_quote);
96
        }
97
98 4
        return $messageData;
99
    }
100
101
    /**
102
     * Returns message data belongs to updating an issue.
103
     *
104
     * @param Queue $queue
105
     *
106
     * @return array
107
     */
108 1
    protected function getMessageDataForUpdateIssue(Queue $queue)
109
    {
110 1
        $messageData                    = [];
111 1
        $messageData['changeByHeading'] = $queue->changeBy->fullname . ' updated an issue';
112 1
        $whiteListFields                = ['change_by', 'title', 'body', 'assignee', 'status', 'type', 'resolution', 'time_quote'];
113
114 1
        foreach ($queue->payload['dirty'] as $field => $value) {
115
            // Skip fields that not part of the white list or quote is locked
116 1
            if (!in_array($field, $whiteListFields) || ($field == 'time_quote' && $this->issue->isQuoteLocked())) {
117 1
                continue;
118
            }
119
120
            // Format quote to readable time
121 1
            $value                          = $field === 'time_quote' ? \Html::duration($value) : $value;
122 1
            $value                          = $field === 'body' ? \Html::format($value) : $value;
123 1
            $messageData['changes'][$field] = [
124 1
                'now' => $value,
125 1
                'was' => $queue->getDataFromPayload('origin.' . $field),
126
            ];
127
        }
128
129 1
        return $messageData;
130
    }
131
132
    /**
133
     * Returns message data belongs to reopening an issue.
134
     *
135
     * @param Queue $queue
136
     *
137
     * @return array
138
     */
139 1
    protected function getMessageDataForReopenIssue(Queue $queue)
140
    {
141 1
        $messageData                    = [];
142 1
        $messageData['changeByHeading'] = $queue->changeBy->fullname . ' reopened an issue';
143 1
        $statusNow                      = $this->issue
144 1
            ->tags()->with('parent')->get()->where('parent.name', Tag::GROUP_STATUS)->last();
145 1
        $messageData['changes']['status'] = [
146 1
            'was' => trans('tinyissue.closed'),
147 1
            'now' => ($statusNow ? $statusNow->fullname : ''),
148 1
            'id'  => ($statusNow ? $statusNow->id : ''),
149
        ];
150
151 1
        return $messageData;
152
    }
153
154
    /**
155
     * Returns message data belongs to closing an issue.
156
     *
157
     * @param Queue $queue
158
     *
159
     * @return array
160
     */
161 2
    protected function getMessageDataForCloseIssue(Queue $queue)
162
    {
163 2
        $messageData                    = [];
164 2
        $messageData['changeByHeading'] = $queue->changeBy->fullname . ' closed an issue';
165 2
        $statusWas                      = $this->issue
166 2
            ->tags()->with('parent')->get()->where('parent.name', Tag::GROUP_STATUS)->last();
167 2
        $messageData['changes']['status'] = [
168 2
            'was' => ($statusWas ? $statusWas->fullname : ''),
169 2
            'now' => trans('tinyissue.closed'),
170
        ];
171
172 2
        return $messageData;
173
    }
174
175
    /**
176
     * Returns message data belongs to assigning an issue to a user.
177
     *
178
     * @param Queue $queue
179
     * @param array $extraData
180
     *
181
     * @return array
182
     */
183 1
    protected function getMessageDataForAssignIssue(Queue $queue, array $extraData)
184
    {
185 1
        $messageData = [];
186 1
        if (!array_key_exists('now', $extraData)) {
187 1
            $assignTo  = $this->getUserById($queue->getDataFromPayload('dirty.assigned_to'));
188 1
            $extraData = ['now' => $assignTo->fullname];
189
        }
190
191 1
        $messageData['changes']['assignee'] = $extraData;
192 1
        $messageData['changeByHeading']     = $queue->changeBy->fullname . ' assigned an issue to ' . $extraData['now'];
193
194 1
        return $messageData;
195
    }
196
197
    /**
198
     * Returns message data belongs to changing an issue tags.
199
     *
200
     * @param Queue $queue
201
     *
202
     * @return array
203
     */
204 3
    protected function getMessageDataForChangeTagIssue(Queue $queue)
205
    {
206 3
        $messageData                    = [];
207 3
        $messageData['changeByHeading'] = $queue->changeBy->fullname . ' changed an issue tag';
208
209 3
        foreach ($queue->payload['added'] as $tag) {
210 3
            $group                          = strtolower($tag['group']);
211 3
            $messageData['changes'][$group] = [
212 3
                'now'           => $tag['name'],
213 3
                'id'            => $tag['id'],
214 3
                'message_limit' => $tag['message_limit'],
215
            ];
216
        }
217
218 3
        foreach ($queue->payload['removed'] as $tag) {
219 2
            $group                                 = strtolower($tag['group']);
220 2
            $messageData['changes'][$group]['was'] = $tag['name'];
221
        }
222
223 3
        return $messageData;
224
    }
225
226
    /**
227
     * Check that the issue is load.
228
     *
229
     * @return bool
230
     */
231 4
    protected function validateData()
232
    {
233
        // if issue closed and last issue not closed, then something is wrong skip
234 4
        if (!$this->issue->isOpen() && $this->latestMessage->event !== Queue::CLOSE_ISSUE) {
235
            return false;
236
        }
237
238 4
        return true;
239
    }
240
241
    /**
242
     * Populate assigned relation in the current issue.
243
     *
244
     * @return void
245
     */
246 4
    protected function populateData()
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
247
    {
248 4
        $this->issue->setRelation('assigned', $this->getUserById($this->issue->assigned_to));
249 4
        $creator = $this->getUserById($this->issue->created_by);
250 4
        if ($creator) {
251 4
            $this->issue->setRelation('user', $creator);
252
        }
253 4
        $this->loadIssueCreatorToProjectUsers();
254 4
    }
255
256
    /**
257
     * Check if the latest message is about closing or reopening an issue.
258
     *
259
     * @return bool
260
     */
261 4
    public function isStatusMessage()
262
    {
263 4
        return ((!$this->issue->isOpen() && $this->latestMessage->event === Queue::CLOSE_ISSUE)
264 4
            || ($this->issue->isOpen() && $this->latestMessage->event === Queue::REOPEN_ISSUE));
265
    }
266
267
    /**
268
     * Process assign to user message. Send direct message to new and previous users and full subscribers.
269
     *
270
     * @return void
271
     */
272 4
    protected function processDirectMessages()
273
    {
274
        // Stop if issue is closed
275 4
        if (!$this->getIssue()->isOpen()) {
276 2
            return;
277
        }
278
279
        // Fetch all of the assign issue changes
280 4
        $assignMessages = $this->allMessages->where('event', Queue::ASSIGN_ISSUE);
281
282
        // Skip if no changes
283 4
        if ($assignMessages->isEmpty()) {
284 4
            return;
285
        }
286
287
        // Fetch the latest assignee
288
        /** @var Queue $assignMessage */
289 1
        $assignMessage = $assignMessages->first();
290
291
        // Fetch the user details of the new assignee & previous assignee if this isn't new issue
292 1
        $assigns        = [];
293 1
        $assigns['new'] = (int) $assignMessage->getDataFromPayload('dirty.assigned_to');
294 1
        if (!$this->addMessage) {
295 1
            $previousAssignMessage = $assignMessages->last();
296 1
            $assigns['old']        = (int) $previousAssignMessage->getDataFromPayload('origin.assigned_to');
297
        }
298
299
        // Fetch users objects for old and new assignee
300
        /** @var \Illuminate\Database\Eloquent\Collection $assignObjects */
301 1
        $assignObjects = (new User())->whereIn('id', $assigns)->get();
0 ignored issues
show
Documentation Bug introduced by
The method whereIn does not exist on object<Tinyissue\Model\User>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
302
303
        // If for what ever reason the user does not exists, skip this change
304
        // or if there is only one user and is not matching the new assignee.
305
        // then skip this message
306 1
        if ($assignObjects->count() === 0
307 1
            || ($assignObjects->count() === 1 && (int) $assignObjects->first()->id !== $assigns['new'])
308
        ) {
309
            return;
310
        }
311
312
        // Get the object of the new assignee
313 1
        $assignTo = $assignObjects->where('id', $assigns['new'], false)->first();
314 1
        $users    = collect([$this->createProjectUserObject($assignTo)]);
315
316
        // Exclude the user from any other message for this issue
317 1
        $this->addToExcludeUsers($assignTo);
318
319
        // Data about new and previous assignee
320
        $extraMessageData = [
321 1
            'now' => $assignTo->fullname,
322
        ];
323
324
        // Make sure that the previous assignee was not the same as the new user
325 1
        if (array_key_exists('old', $assigns) && $assigns['old'] > 0 && $assigns['new'] !== $assigns['old']) {
326
            $previousAssign = $assignObjects->where('id', $assigns['old'], false)->first();
327
            if ($previousAssign) {
328
                $extraMessageData['was'] = $previousAssign->fullname;
329
                $users->push($this->createProjectUserObject($previousAssign));
330
            }
331
        }
332
333
        // Get message data needed for the message & send
334 1
        $messageData = $this->getMessageData($assignMessage, $extraMessageData);
335 1
        $this->sendMessages($users, $messageData);
336 1
    }
337
338
    /**
339
     * Create user project object for a user.
340
     *
341
     * @param User $user
342
     *
343
     * @return Project\User
344
     */
345 1
    protected function createProjectUserObject(User $user)
346
    {
347 1
        $userProject = new Project\User([
348 1
            'user_id'    => $user->id,
349 1
            'project_id' => $this->getProjectId(),
350
        ]);
351 1
        $userProject->setRelation('user', $user);
352 1
        $userProject->setRelation('project', $this->getProject());
353
354 1
        return $userProject;
355
    }
356
357
    /**
358
     * Get collection of tags or one by ID.
359
     *
360
     * @param int $tagId
361
     *
362
     * @return Tag
363
     */
364 3
    protected function getTag($tagId)
365
    {
366 3
        if (null === $this->tags) {
367 3
            $this->tags = collect([]);
368
        }
369
370
        // Load & extract tag by ID
371 3
        $tag = $this->tags->where('id', $tagId, false)->first();
372 3
        if (!$tag) {
373 3
            $tag = (new Tag())->find($tagId);
0 ignored issues
show
Documentation Bug introduced by
The method find does not exist on object<Tinyissue\Model\Tag>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
374 3
            $this->tags->push($tag);
375
        }
376
377 3
        return $tag;
378
    }
379
380
    /**
381
     * Whether or not the user wants to receive the message.
382
     *
383
     * @param Project\User $user
384
     * @param array        $data
385
     *
386
     * @return bool
387
     */
388 4
    protected function wantToReceiveMessage(Project\User $user, array $data)
389
    {
390 4
        $status = parent::wantToReceiveMessage($user, $data);
391
392 4
        if (!$status) {
393 4
            return false;
394
        }
395
396
        // Search for tag changes and verify if the user can receive messages
397 4
        foreach ($data['changes'] as $label => $change) {
398
            // Skip other changes that are not related to tag
399 4
            if (!in_array($label, Tag::getCoreGroups())) {
400 4
                continue;
401
            }
402
403
            // If the change was only remove a tag
404 3
            if (!array_key_exists('now', $change) && array_key_exists('was', $change)) {
405
                return true;
406
            }
407
408
            // If tag id not found
409 3
            if (!array_key_exists('id', $change)) {
410 2
                return true;
411
            }
412
413
            // Fetch tag details
414 3
            $tag = $this->getTag($change['id']);
415
416
            // Check if user allowed to receive the message
417 3
            if (!$tag->allowMessagesToUser($user->user)) {
418 3
                return false;
419
            }
420
        }
421
422 4
        return true;
423
    }
424
}
425