SendMessagesAbstract::process()   B
last analyzed

Complexity

Conditions 6
Paths 5

Size

Total Lines 36
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.0061

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 36
ccs 17
cts 18
cp 0.9444
rs 8.439
cc 6
eloc 17
nc 5
nop 2
crap 6.0061
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\Services;
13
14
use Illuminate\Mail\Message as MailMessage;
15
use Illuminate\Support\Collection;
16
use Illuminate\Contracts\Mail\Mailer;
17
use Tinyissue\Http\Requests\FormRequest\Note;
18
use Tinyissue\Model\Message;
19
use Tinyissue\Model\Project;
20
use Tinyissue\Model\Project\Issue;
21
use Tinyissue\Model\Role;
22
use Tinyissue\Model\User;
23
24
/**
25
 * SendMessagesAbstract is an abstract class with for objects that requires sending messages.
26
 *
27
 * @author Mohamed Alsharaf <[email protected]>
28
 */
29
abstract class SendMessagesAbstract
30
{
31
    /**
32
     * Instance of issue that this message belong to.
33
     *
34
     * @var Issue
35
     */
36
    protected $issue;
37
38
    /**
39
     * Instance of project that this message belong to.
40
     *
41
     * @var Issue
42
     */
43
    protected $project;
44
45
    /**
46
     * The latest message queued.
47
     *
48
     * @var Message\Queue
49
     */
50
    protected $latestMessage;
51
52
    /**
53
     * Collection of all of the queued messages.
54
     *
55
     * @var Collection
56
     */
57
    protected $allMessages;
58
59
    /**
60
     * Instance of a queued message that is for adding a record (ie. adding issue).
61
     *
62
     * @var Message\Queue
63
     */
64
    protected $addMessage;
65
    /**
66
     * Collection of users that must not receive messages.
67
     *
68
     * @var Collection
69
     */
70
    protected $excludeUsers;
71
    /**
72
     * Collection of all of the project users that should receive messages.
73
     *
74
     * @var Collection
75
     */
76
    protected $projectUsers;
77
    /**
78
     * Collection of full subscribers that will always receive messages.
79
     *
80
     * @var Collection
81
     */
82
    protected $fullSubscribers;
83
    /**
84
     * Name of message template.
85
     *
86
     * @var string
87
     */
88
    protected $template;
89
90
    /**
91
     * @var Mailer
92
     */
93
    protected $mailer;
94
95
    /**
96
     * Collection of all messages.
97
     *
98
     * @var Collection
99
     */
100
    protected $messages;
101
102
    /**
103
     * Set instance of Mailer.
104
     *
105
     * @param Mailer $mailer
106
     *
107
     * @return $this
108
     */
109 5
    public function setMailer(Mailer $mailer)
110
    {
111 5
        $this->mailer = $mailer;
112
113 5
        return $this;
114
    }
115
116
    /**
117
     * The main method to process the massages queue and send them.
118
     *
119
     * @param Message\Queue $latestMessage
120
     * @param Collection    $changes
121
     *
122
     * @return void
123
     */
124 5
    public function process(Message\Queue $latestMessage, Collection $changes)
125
    {
126 5
        $this->setup($latestMessage, $changes);
127
128
        // Is model deleted? or in valid
129 5
        if (!$this->getModel() || !$this->validateData()) {
130 2
            return;
131
        }
132
133 5
        $this->processDirectMessages();
134
135
        // Skip if no users found
136 5
        if ($this->getProjectUsers()->isEmpty()) {
137
            return;
138
        }
139
140 5
        $this->populateData();
141
142
        // Send the latest message if it is about status (ie. closed issue)
143 5
        if ($this->isStatusMessage()) {
144 2
            return $this->sendMessageToAll($this->latestMessage);
145
        }
146
147
        // Get message data for all of the messages combined & for add message queue if exists
148 5
        $addMessageData = [];
149 5
        if ($this->addMessage) {
150 5
            $addMessageData = $this->getMessageData($this->addMessage);
151
        }
152 5
        $everythingMessageData = $this->getCombineMessageData($this->allMessages);
153
154
        // Send messages to project users
155 5
        $this->sendMessages($this->getProjectUsers(), [
156 5
            'addMessage' => $addMessageData,
157 5
            'everything' => $everythingMessageData,
158
        ]);
159 5
    }
160
161
    /**
162
     * Setup properties needed for the process.
163
     *
164
     * @param Message\Queue $latestMessage
165
     * @param Collection    $allMessages
166
     *
167
     * @return void
168
     */
169 5
    protected function setup(Message\Queue $latestMessage, Collection $allMessages)
170
    {
171
        // Set queue messages
172 5
        $this->latestMessage = $latestMessage;
173 5
        $this->allMessages   = $allMessages;
174
175
        // Exclude the user who made the change from receiving messages
176 5
        $this->addToExcludeUsers($this->latestMessage->changeBy);
177
178
        // Extract add model message if exists
179 5
        if ($this->getModel()) {
180 5
            $addMessageIdentifier = Message\Queue::getAddEventNameFromModel($this->getModel());
181 5
            $this->addMessage     = $this->allMessages->where('event', $addMessageIdentifier)->first();
182
        }
183
184
        // Make sure to load issue
185 5
        $this->getIssue();
186 5
    }
187
188
    /**
189
     * Whether or not we have all the needed properties.
190
     *
191
     * @return bool
192
     */
193
    abstract protected function validateData();
194
195
    /**
196
     * Process any messages queue that is to send messages to specific users.
197
     * For example, assign issue to user to message the user about the issue.
198
     *
199
     * @return void
200
     */
201 3
    protected function processDirectMessages()
202
    {
203 3
    }
204
205
    /**
206
     * Populate any data or properties.
207
     *
208
     * @return void
209
     */
210 1
    protected function populateData()
211
    {
212 1
    }
213
214
    /**
215
     * Whether or not the latest message is about status change such as closed issue.
216
     *
217
     * @return bool
218
     */
219
    abstract public function isStatusMessage();
220
221
    /**
222
     * Returns the message subject.
223
     *
224
     * @return string
225
     */
226 4
    protected function getSubject()
227
    {
228 4
        return '#' . $this->issue->id . ' / ' . $this->issue->title;
229
    }
230
231
    /**
232
     * Returns an array of data needed for the message.
233
     *
234
     * @param Message\Queue $queue
235
     * @param array         $extraData
236
     *
237
     * @return array
238
     */
239 5
    protected function getMessageData(Message\Queue $queue, array $extraData = [])
240
    {
241
        // Generic info for all messages emails
242 5
        $messageData                         = [];
243 5
        $messageData['issue']                = $this->getIssue();
244 5
        $messageData['project']              = $this->getProject();
245 5
        $messageData['changes']              = [];
246 5
        $messageData['changes']['change_by'] = [
247 5
            'now' => $queue->changeBy->fullname,
248
        ];
249 5
        if ($this->getIssue()) {
250 4
            $messageData['changes']['change_by']['url'] = $this->getIssue()->to();
251
        } else {
252 1
            $messageData['changes']['change_by']['url'] = $this->getProject()->to();
253
        }
254 5
        $messageData['changeByImage']   = $queue->changeBy->image;
0 ignored issues
show
Documentation introduced by
The property image does not exist on object<Tinyissue\Model\User>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
255 5
        $messageData['changeByHeading'] = $this->getMessageHeading($queue);
256 5
        $messageData['event']           = $queue->event;
257
258
        // Info specific to a message type
259 5
        $method = 'getMessageDataFor' . ucfirst(camel_case($queue->event));
260 5
        if (method_exists($this, $method)) {
261 5
            $messageData = array_replace_recursive($messageData, $this->{$method}($queue, $extraData));
262
        }
263
264 5
        return $messageData;
265
    }
266
267
    /**
268
     * Loop through all of the messages and combine its message data.
269
     *
270
     * @param Collection $changes
271
     *
272
     * @return array
273
     */
274 5
    protected function getCombineMessageData(Collection $changes)
275
    {
276 5
        $everything = [];
277
        $changes->reverse()->each(function (Message\Queue $queue) use (&$everything) {
278 5
            if (!$everything) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $everything of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
279 5
                $everything = $this->getMessageData($queue);
280
            } else {
281 5
                $messageData = $this->getMessageData($queue);
282 5
                $everything['changes'] = array_merge($everything['changes'], $messageData['changes']);
283
            }
284 5
        });
285 5
        $latestMessage                 = $changes->first();
286 5
        $everything['changeByHeading'] = $this->getMessageHeading($latestMessage, $changes);
287 5
        $everything['event']           = $latestMessage->event;
288 5
        $messageData                   = $this->getMessageData($latestMessage);
289 5
        $everything                    = array_replace_recursive($everything, $messageData);
290
291 5
        return $everything;
292
    }
293
294
    /**
295
     * Return text to be used for the message heading.
296
     *
297
     * @param Message\Queue   $queue
298
     * @param Collection|null $changes
299
     *
300
     * @return string
301
     */
302 5
    protected function getMessageHeading(Message\Queue $queue, Collection $changes = null)
303
    {
304 5
        $heading = $queue->changeBy->fullname . ' ';
305
306
        // If other users have made changes too
307 5
        if (!is_null($changes) && $changes->unique('change_by_id')->count() > 1) {
308 1
            $heading .= '& others ';
309
        }
310
311 5
        return $heading;
312
    }
313
314
    /**
315
     * Returns collection of all users in a project that should receive the messages.
316
     *
317
     * @return Collection
318
     */
319 5
    protected function getProjectUsers()
320
    {
321 5
        if (null === $this->projectUsers) {
322 5
            $this->projectUsers = (new Project\User())
323 5
                ->with('message', 'user', 'user.role')
324 5
                ->whereNotIn('user_id', $this->getExcludeUsers()->lists('id'))
325 5
                ->where('project_id', '=', $this->getProjectId())
326 5
                ->get();
327
        }
328
329 5
        return $this->projectUsers;
330
    }
331
332
    /**
333
     * Returns the model that is belong to the queue message.
334
     *
335
     * @return Issue|Issue\Comment|Note
336
     */
337 5
    protected function getModel()
338
    {
339 5
        return $this->latestMessage->model;
340
    }
341
342
    /**
343
     * Returns an instance of project issue.
344
     *
345
     * @return Issue
346
     */
347
    abstract protected function getIssue();
348
349
    /**
350
     * Returns an instance of project.
351
     *
352
     * @return Project
353
     */
354
    abstract protected function getProject();
355
356
    /**
357
     * Returns the id of a project.
358
     *
359
     * @return int
360
     */
361
    abstract protected function getProjectId();
362
363
    /**
364
     * Returns collection of all of the users that must not receive messages.
365
     *
366
     * @return Collection
367
     */
368 5
    protected function getExcludeUsers()
369
    {
370 5
        if (null === $this->excludeUsers) {
371 5
            $this->excludeUsers = collect([]);
372
        }
373
374 5
        return $this->excludeUsers;
375
    }
376
377
    /**
378
     * Exclude a user from receiving messages.
379
     *
380
     * @param User $user
381
     *
382
     * @return $this
383
     */
384 5
    protected function addToExcludeUsers(User $user)
385
    {
386 5
        $this->getExcludeUsers()->push($user);
387
388 5
        return $this;
389
    }
390
391
    /**
392
     * Find user by id. This search the project users and fallback to excluded list of users.
393
     *
394
     * @param int $userId
395
     *
396
     * @return User
397
     */
398 4
    protected function getUserById($userId)
399
    {
400 4
        $projectUser = $this->getProjectUsers()->where('user_id', $userId, false)->first();
401
402 4
        if (!$projectUser) {
403 4
            return $this->getExcludeUsers()->where('id', $userId, false)->first();
404
        }
405
406 4
        return $projectUser->user;
407
    }
408
409
    /**
410
     * Returns collection of all messages.
411
     *
412
     * @return Collection
413
     */
414 5
    protected function getMessages()
415
    {
416 5
        if (null === $this->messages) {
417 5
            $this->messages = (new Message())->orderBy('id', 'ASC')->get();
0 ignored issues
show
Documentation Bug introduced by
The method orderBy does not exist on object<Tinyissue\Model\Message>? 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...
418
        }
419
420 5
        return $this->messages;
421
    }
422
423
    /**
424
     * Send a message to a user.
425
     *
426
     * @param User  $user
427
     * @param array $data
428
     *
429
     * @return mixed
430
     */
431 5
    private function sendMessage(User $user, array $data)
432
    {
433
        // Make sure the data contains changes
434 5
        if (!array_key_exists('changes', $data) && count($data['changes']) > 1) {
435
            return;
436
        }
437
438 5
        return $this->mailer->send('email.' . $this->template, $data, function (MailMessage $message) use ($user) {
439 5
            $message->to($user->email, $user->fullname)->subject($this->getSubject());
440 5
        });
441
    }
442
443
    /**
444
     * Send a message to a collection of users, or send customised message per use logic.
445
     *
446
     * @param Collection $users
447
     * @param array      $data
448
     *
449
     * @return void
450
     */
451 5
    protected function sendMessages(Collection $users, array $data)
452
    {
453 5
        foreach ($users as $user) {
454 5
            $userMessageData = $this->getUserMessageData($user->user_id, $data);
455 5
            if (!$this->wantToReceiveMessage($user, $userMessageData)) {
456 5
                continue;
457
            }
458
459 5
            $this->sendMessage($user->user, $userMessageData);
460
        }
461 5
    }
462
463
    /**
464
     * Get customised message per user logic.
465
     *
466
     * @param int   $userId
467
     * @param array $messagesData
468
     *
469
     * @return array
470
     */
471 5
    protected function getUserMessageData($userId, array $messagesData)
472
    {
473 5
        if (array_key_exists('event', $messagesData)) {
474 2
            return $messagesData;
475
        }
476
477
        // Possible message data
478 5
        $addMessageData        = $messagesData['addMessage'];
479 5
        $everythingMessageData = $messagesData['everything'];
480
481
        // Check if the user has seen the model data and made a change
482 5
        $changeMadeByUser = $this->allMessages->where('change_by_id', $userId);
483
484
        // This user has never seen this model data
485 5
        if (!$changeMadeByUser->count()) {
486 5
            if ($this->addMessage) {
487 5
                return $addMessageData;
488
            }
489
490 4
            return $everythingMessageData;
491
        }
492
493
        // This user has seen this model data
494
        // Get all of the changes that may happened later.
495
        // Combine them and send message to the user about these changes.
496 1
        $everythingMessageData = $this->getCombineMessageData(
497 1
            $this->allMessages->forget($changeMadeByUser->keys()->toArray())
498
        );
499
500 1
        return $everythingMessageData;
501
    }
502
503
    /**
504
     * Whether or not the user wants to receive the message.
505
     *
506
     * @param Project\User $user
507
     * @param array        $data
508
     *
509
     * @return bool
510
     */
511 5
    protected function wantToReceiveMessage(Project\User $user, array $data)
512
    {
513
        /** @var Message $message */
514 5
        $message = $user->message;
515 5
        if (!$message) {
516 5
            $roleName = $user->user->role->role;
517 5
            $message  = $this->getMessages()->where('name', Message::$defaultMessageToRole[$roleName])->first();
518
        }
519
520
        // No message to send,
521
        // - if we can't find message object or
522
        // - messages are disabled or
523
        // - event is inactive for the user message setting
524 5
        if (!$message || $message->isDisabled() || !$message->isActiveEvent($data['event'])) {
525 5
            return false;
526
        }
527
528
        // Wants to see all updates in project
529 5
        if ((bool) $message->in_all_issues === true) {
530 5
            return true;
531
        }
532
533 5
        if (!$this->getIssue()) {
534 1
            return false;
535
        }
536
537
        // For issue only send messages if user is assignee or creator
538 4
        $creator  = $this->getIssue()->user;
539 4
        $assignee = $this->getIssue()->assigned;
540 4
        if ($user->user_id === $creator->id || ($assignee && $user->user_id === $assignee->id)) {
541 3
            return true;
542
        }
543
544 4
        return false;
545
    }
546
547
    /**
548
     * Send a message to al users in project and full subscribes.
549
     *
550
     * @param Message\Queue $queue
551
     *
552
     * @return void
553
     */
554 2
    protected function sendMessageToAll(Message\Queue $queue)
555
    {
556 2
        $messageData = $this->getMessageData($queue);
557
558 2
        $this->sendMessages($this->getProjectUsers(), $messageData);
559 2
    }
560
561
    /**
562
     * Load the creator of an issue to the collection of project users. So we can send message to creator if needed.
563
     *
564
     * @return void
565
     */
566 4
    protected function loadIssueCreatorToProjectUsers()
567
    {
568
        // Stop if we can't get the issue
569 4
        if (!$this->getIssue()) {
570
            return;
571
        }
572
573
        // Get issue creator
574 4
        $creator = $this->getIssue()->user;
575
576
        // Stop if creator excluded from messages
577 4
        $excluded = $this->getExcludeUsers()->where('id', $creator->id, false)->first();
578 4
        if ($excluded) {
579 4
            return;
580
        }
581
582
        // Stop if the creator already part of the project users
583 4
        $existInProject = $this->getProjectUsers()->where('user_id', $creator->id, false)->first();
584 4
        if ($existInProject) {
585 4
            return;
586
        }
587
588
        // Create virtual project user object & add to collection
589
        $userProject = new Project\User([
590
            'user_id'    => $creator->id,
591
            'project_id' => $this->getProjectId(),
592
        ]);
593
        $userProject->setRelation('user', $creator);
594
        $userProject->setRelation('project', $this->getProject());
595
        $this->getProjectUsers()->push($userProject);
596
    }
597
}
598