Completed
Push — master ( 4e528e...b698b3 )
by ARCANEDEV
04:17
created

Discussion::userUnreadMessages()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 12
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
1
<?php namespace Arcanedev\LaravelMessenger\Models;
2
3
use Arcanedev\LaravelMessenger\Bases\Model;
4
use Arcanedev\LaravelMessenger\Contracts\Discussion as DiscussionContract;
5
use Arcanedev\LaravelMessenger\Contracts\Message as MessageContract;
6
use Arcanedev\LaravelMessenger\Contracts\Participant as ParticipantContract;
7
use Carbon\Carbon;
8
use Illuminate\Database\Eloquent\Builder;
9
use Illuminate\Database\Eloquent\SoftDeletes;
10
11
/**
12
 * Class     Discussion
13
 *
14
 * @package  Arcanedev\LaravelMessenger\Models
15
 * @author   ARCANEDEV <[email protected]>
16
 *
17
 * @property  int             id
18
 * @property  string          subject
19
 * @property  \Carbon\Carbon  created_at
20
 * @property  \Carbon\Carbon  updated_at
21
 * @property  \Carbon\Carbon  deleted_at
22
 *
23
 * @property  \Illuminate\Database\Eloquent\Model         creator
24
 * @property  \Illuminate\Database\Eloquent\Collection    messages
25
 * @property  \Illuminate\Database\Eloquent\Collection    participants
26
 * @property  \Arcanedev\LaravelMessenger\Models\Message  latest_message
27
 *
28
 * @method static \Illuminate\Database\Eloquent\Builder  subject(string $subject, bool $strict)
29
 * @method static \Illuminate\Database\Eloquent\Builder  between(array $usersIds)
30
 * @method static \Illuminate\Database\Eloquent\Builder  forUser(int $userId)
31
 * @method static \Illuminate\Database\Eloquent\Builder  forUserWithNewMessages(int $userId)
32
 */
33
class Discussion extends Model implements DiscussionContract
34
{
35
    /* ------------------------------------------------------------------------------------------------
36
     |  Traits
37
     | ------------------------------------------------------------------------------------------------
38
     */
39
    use SoftDeletes;
40
41
    /* ------------------------------------------------------------------------------------------------
42
     |  Properties
43
     | ------------------------------------------------------------------------------------------------
44
     */
45
    /**
46
     * The attributes that can be set with Mass Assignment.
47
     *
48
     * @var array
49
     */
50
    protected $fillable = ['subject'];
51
52
    /**
53
     * The attributes that should be mutated to dates.
54
     *
55
     * @var array
56
     */
57
    protected $dates = ['deleted_at'];
58
59
    /* ------------------------------------------------------------------------------------------------
60
     |  Constructor
61
     | ------------------------------------------------------------------------------------------------
62
     */
63
    /**
64
     * Create a new Eloquent model instance.
65
     *
66
     * @param  array  $attributes
67
     */
68
    public function __construct(array $attributes = [])
69
    {
70
        $this->setTable(
71
            $this->getTableFromConfig('discussions', 'discussions')
72
        );
73
74
        parent::__construct($attributes);
75
    }
76
77
    /* ------------------------------------------------------------------------------------------------
78
     |  Relationships
79
     | ------------------------------------------------------------------------------------------------
80
     */
81
    /**
82
     * Participants relationship.
83
     *
84
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
85
     */
86
    public function participants()
87
    {
88
        return $this->hasMany(
89
            $this->getModelFromConfig('participants', Participant::class)
90
        );
91
    }
92
93
    /**
94
     * Messages relationship.
95
     *
96
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
97
     */
98
    public function messages()
99
    {
100
        return $this->hasMany(
101
            $this->getModelFromConfig('messages', Participant::class)
102
        );
103
    }
104
105
    /**
106
     * Get the user that created the first message.
107
     *
108
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
109
     */
110
    public function creator()
111
    {
112
        return $this->messages()->oldest()->first()->user();
113
    }
114
115
    /* ------------------------------------------------------------------------------------------------
116
     |  Scopes
117
     | ------------------------------------------------------------------------------------------------
118
     */
119
    /**
120
     * Scope discussions that the user is associated with.
121
     *
122
     * @param  \Illuminate\Database\Eloquent\Builder  $query
123
     * @param  int                                    $userId
124
     *
125
     * @return \Illuminate\Database\Eloquent\Builder
126
     */
127
    public function scopeForUser(Builder $query, $userId)
128
    {
129
        $participants = $this->getParticipantsTable();
130
131
        return $query->join($participants, function ($join) use ($participants, $userId) {
132
            /** @var \Illuminate\Database\Query\JoinClause $join */
133
            $join->on($this->getQualifiedKeyName(), '=', "{$participants}.discussion_id")
134
                 ->where("{$participants}.user_id", '=', $userId)
135
                 ->whereNull("{$participants}.deleted_at");
136
        });
137
    }
138
139
    /**
140
     * Scope discussions with new messages that the user is associated with.
141
     *
142
     * @param  \Illuminate\Database\Eloquent\Builder  $query
143
     * @param  int                                    $userId
144
     *
145
     * @return \Illuminate\Database\Eloquent\Builder
146
     */
147
    public function scopeForUserWithNewMessages(Builder $query, $userId)
148
    {
149
        $participants = $this->getParticipantsTable();
150
        $discussions  = $this->getTable();
151
        $prefix       = $this->getConnection()->getTablePrefix();
152
153
        return $this->scopeForUser($query, $userId)
154
            ->where(function (Builder $query) use ($participants, $discussions, $prefix) {
155
                $expression = $this->getConnection()->raw("{$prefix}{$participants}.last_read");
156
157
                $query->where("{$discussions}.updated_at", '>', $expression)
158
                      ->orWhereNull("{$participants}.last_read");
159
            });
160
    }
161
162
    /**
163
     * Scope discussions between given user ids.
164
     *
165
     * @param  \Illuminate\Database\Eloquent\Builder  $query
166
     * @param  array                                  $userIds
167
     *
168
     * @return \Illuminate\Database\Eloquent\Builder
169
     */
170
    public function scopeBetween(Builder $query, array $userIds)
171
    {
172
        $participants = $this->getParticipantsTable();
173
174
        return $query->whereHas($participants, function (Builder $query) use ($userIds) {
175
            $query->whereIn('user_id', $userIds)
176
                ->groupBy('discussion_id')
177
                ->havingRaw('COUNT(discussion_id)=' . count($userIds));
178
        });
179
    }
180
181
    /**
182
     * Get the participants table name.
183
     *
184
     * @return string
185
     */
186
    protected function getParticipantsTable()
187
    {
188
        return $this->getTableFromConfig('participants', 'participants');
189
    }
190
191
    /**
192
     * Scope the query by the subject.
193
     *
194
     * @param  \Illuminate\Database\Eloquent\Builder  $query
195
     * @param  string                                 $subject
196
     * @param  bool                                   $strict
197
     *
198
     * @return \Illuminate\Database\Eloquent\Builder
199
     */
200
    public function scopeSubject(Builder $query, $subject, $strict = false)
201
    {
202
        $subject = $strict ? $subject : "%{$subject}%";
203
204
        return $query->where('subject', 'like', $subject);
205
    }
206
207
    /* ------------------------------------------------------------------------------------------------
208
     |  Getters & Setters
209
     | ------------------------------------------------------------------------------------------------
210
     */
211
    /**
212
     * Get the latest_message attribute.
213
     *
214
     * @return \Arcanedev\LaravelMessenger\Models\Message
215
     */
216
    public function getLatestMessageAttribute()
217
    {
218
        return $this->messages->sortByDesc('created_at')->first();
219
    }
220
221
    /* ------------------------------------------------------------------------------------------------
222
     |  Main Functions
223
     | ------------------------------------------------------------------------------------------------
224
     */
225
    /**
226
     * Returns all of the latest discussions by `updated_at` date.
227
     *
228
     * @return \Illuminate\Database\Eloquent\Collection
229
     */
230
    public static function getLatest()
231
    {
232
        return self::latest('updated_at')->get();
233
    }
234
235
    /**
236
     * Returns all discussions by subject.
237
     *
238
     * @param  string  $subject
239
     * @param  bool    $strict
240
     *
241
     * @return \Illuminate\Database\Eloquent\Collection
242
     */
243
    public static function getBySubject($subject, $strict = false)
244
    {
245
        return self::subject($subject, $strict)->get();
246
    }
247
248
    /**
249
     * Returns an array of user ids that are associated with the discussion.
250
     *
251
     * @param  int|null  $userId
252
     *
253
     * @return array
254
     */
255
    public function participantsUserIds($userId = null)
256
    {
257
        $usersIds = $this->participants()
258
            ->withTrashed()
259
            ->pluck('user_id')
260
            ->toArray();
261
262
        if ($userId && ! in_array($userId, $usersIds)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userId of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
263
            $usersIds[] = $userId;
264
        }
265
266
        return $usersIds;
267
    }
268
269
    /**
270
     * Add a user to discussion as a participant.
271
     *
272
     * @param  int   $userId
273
     *
274
     * @return \Arcanedev\LaravelMessenger\Models\Participant
275
     */
276
    public function addParticipant($userId)
277
    {
278
        /** @var \Arcanedev\LaravelMessenger\Models\Participant $participant */
279
        $participant = $this->participants()->firstOrCreate([
280
            'user_id'       => $userId,
281
            'discussion_id' => $this->id,
282
        ]);
283
284
        return $participant;
285
    }
286
287
    /**
288
     * Add users to discussion as participants.
289
     *
290
     * @param  array  $userIds
291
     *
292
     * @return \Illuminate\Database\Eloquent\Collection
293
     */
294
    public function addParticipants(array $userIds)
295
    {
296
        foreach ($userIds as $userId) {
297
            $this->addParticipant($userId);
298
        }
299
300
        return $this->participants;
301
    }
302
303
    /**
304
     * Remove a participant from discussion.
305
     *
306
     * @param  int   $userId
307
     * @param  bool  $reload
308
     *
309
     * @return int
310
     */
311
    public function removeParticipant($userId, $reload = true)
312
    {
313
        $deleted = $this->participants()
314
            ->where('discussion_id', $this->id)
315
            ->where('user_id', $userId)
316
            ->delete();
317
318
        if ($reload) $this->load(['participants']);
319
320
        return $deleted;
321
    }
322
323
    /**
324
     * Remove participants from discussion.
325
     *
326
     * @param  array  $userIds
327
     * @param  bool   $reload
328
     *
329
     * @return int
330
     */
331
    public function removeParticipants(array $userIds, $reload = true)
332
    {
333
        $deleted = $this->participants()
334
            ->whereIn('user_id', $userIds)
335
            ->where('discussion_id', $this->id)
336
            ->delete();
337
338
        if ($reload) $this->load(['participants']);
339
340
        return $deleted;
341
    }
342
343
    /**
344
     * Mark a discussion as read for a user.
345
     *
346
     * @param  int  $userId
347
     *
348
     * @return bool|int
349
     */
350
    public function markAsRead($userId)
351
    {
352
        if ($participant = $this->getParticipantByUserId($userId)) {
353
            return $participant->update([
354
                'last_read' => Carbon::now()
355
            ]);
356
        }
357
358
        return false;
359
    }
360
361
    /**
362
     * See if the current thread is unread by the user.
363
     *
364
     * @param  int  $userId
365
     *
366
     * @return bool
367
     */
368
    public function isUnread($userId)
369
    {
370
        return ($participant = $this->getParticipantByUserId($userId))
371
            ? $participant->last_read < $this->updated_at
372
            : false;
373
    }
374
375
    /**
376
     * Finds the participant record from a user id.
377
     *
378
     * @param  int  $userId
379
     *
380
     * @return \Arcanedev\LaravelMessenger\Models\Participant
381
     */
382
    public function getParticipantByUserId($userId)
383
    {
384
        return $this->participants()
385
            ->where('user_id', $userId)
386
            ->first();
387
    }
388
389
    /**
390
     * Get the trashed participants.
391
     *
392
     * @return \Illuminate\Database\Eloquent\Collection
393
     */
394
    public function getTrashedParticipants()
395
    {
396
        return $this->participants()
397
            ->onlyTrashed()
398
            ->get();
399
    }
400
401
    /**
402
     * Restores all participants within a discussion.
403
     *
404
     * @param  bool  $reload
405
     *
406
     * @return int
407
     */
408
    public function restoreAllParticipants($reload = true)
409
    {
410
        $participants = $this->getTrashedParticipants();
411
        $restored     = $participants->filter(function (ParticipantContract $participant) {
412
            return $participant->restore();
0 ignored issues
show
Bug introduced by
The method restore() does not seem to exist on object<Arcanedev\Laravel...\Contracts\Participant>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
413
        })->count();
414
415
        if ($reload) $this->load(['participants']);
416
417
        return $restored;
418
    }
419
420
    /**
421
     * Generates a participant information as a string.
422
     *
423
     * @param  int|null       $ignoredUserId
424
     * @param  \Closure|null  $callback
425
     * @param  string         $glue
426
     *
427
     * @return string
428
     */
429
    public function participantsString($ignoredUserId = null, $callback = null, $glue = ', ')
430
    {
431
        /** @var \Illuminate\Database\Eloquent\Collection $participants */
432
        $participants = $this->participants->load(['user']);
433
434
        if (is_null($callback)) {
435
            // By default: the participant name
436
            $callback = function (ParticipantContract $participant) {
437
                return $participant->stringInfo();
438
            };
439
        }
440
441
        return $participants->filter(function (ParticipantContract $participant) use ($ignoredUserId) {
442
            return $participant->user_id !== $ignoredUserId;
0 ignored issues
show
Bug introduced by
Accessing user_id on the interface Arcanedev\LaravelMessenger\Contracts\Participant suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
443
        })->map($callback)->implode($glue);
444
    }
445
446
    /**
447
     * Checks to see if a user is a current participant of the discussion.
448
     *
449
     * @param  int  $userId
450
     *
451
     * @return bool
452
     */
453
    public function hasParticipant($userId)
454
    {
455
        return $this->participants()
456
            ->where('user_id', '=', $userId)
457
            ->count() > 0;
458
    }
459
460
    /**
461
     * Returns array of unread messages in discussion for given user.
462
     *
463
     * @param  int  $userId
464
     *
465
     * @return \Illuminate\Support\Collection
466
     */
467
    public function userUnreadMessages($userId)
468
    {
469
        /** @var \Illuminate\Database\Eloquent\Collection $messages */
470
        $participant = $this->getParticipantByUserId($userId);
471
472
        if (is_null($participant))            return collect();
473
        if (is_null($participant->last_read)) return $this->messages;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->messages; (Illuminate\Database\Eloquent\Collection) is incompatible with the return type declared by the interface Arcanedev\LaravelMesseng...ion::userUnreadMessages of type Illuminate\Support\Collection.

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...
474
475
        return $this->messages->filter(function (MessageContract $message) use ($participant) {
476
            return $message->updated_at->gt($participant->last_read);
0 ignored issues
show
Bug introduced by
Accessing updated_at on the interface Arcanedev\LaravelMessenger\Contracts\Message suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
477
        });
478
    }
479
480
    /**
481
     * Returns count of unread messages in thread for given user.
482
     *
483
     * @param  int  $userId
484
     *
485
     * @return int
486
     */
487
    public function userUnreadMessagesCount($userId)
488
    {
489
        return $this->userUnreadMessages($userId)->count();
490
    }
491
}
492