Completed
Pull Request — master (#13)
by ARCANEDEV
03:34
created

Discussion::getParticipationsTable()   A

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 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php namespace Arcanedev\LaravelMessenger\Models;
2
3
use Arcanedev\LaravelMessenger\Contracts\Discussion as DiscussionContract;
4
use Arcanedev\LaravelMessenger\Contracts\Message as MessageContract;
5
use Arcanedev\LaravelMessenger\Contracts\Participation as ParticipationContract;
6
use Carbon\Carbon;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Model as EloquentModel;
9
use Illuminate\Database\Eloquent\SoftDeletes;
10
use Illuminate\Database\Query\JoinClause;
11
use Illuminate\Support\Collection;
12
13
/**
14
 * Class     Discussion
15
 *
16
 * @package  Arcanedev\LaravelMessenger\Models
17
 * @author   ARCANEDEV <[email protected]>
18
 *
19
 * @property  int             id
20
 * @property  string          subject
21
 * @property  \Carbon\Carbon  created_at
22
 * @property  \Carbon\Carbon  updated_at
23
 * @property  \Carbon\Carbon  deleted_at
24
 *
25
 * @property  \Illuminate\Database\Eloquent\Model         creator
26
 * @property  \Illuminate\Database\Eloquent\Collection    messages
27
 * @property  \Illuminate\Database\Eloquent\Collection    participations
28
 * @property  \Arcanedev\LaravelMessenger\Models\Message  latest_message
29
 *
30
 * @method static \Illuminate\Database\Eloquent\Builder|static  subject(string $subject, bool $strict)
31
 * @method static \Illuminate\Database\Eloquent\Builder|static  between(\Illuminate\Support\Collection|array $participables)
32
 * @method static \Illuminate\Database\Eloquent\Builder|static  forUser(\Illuminate\Database\Eloquent\Model $participable)
33
 * @method static \Illuminate\Database\Eloquent\Builder|static  withParticipations()
34
 * @method static \Illuminate\Database\Eloquent\Builder|static  forUserWithNewMessages(\Illuminate\Database\Eloquent\Model $participable)
35
 */
36
class Discussion extends Model implements DiscussionContract
37
{
38
    /* -----------------------------------------------------------------
39
     |  Traits
40
     | -----------------------------------------------------------------
41
     */
42
43
    use SoftDeletes;
44
45
    /* -----------------------------------------------------------------
46
     |  Properties
47
     | -----------------------------------------------------------------
48
     */
49
50
    /**
51
     * The attributes that can be set with Mass Assignment.
52
     *
53
     * @var array
54
     */
55
    protected $fillable = ['subject'];
56
57
    /**
58
     * The attributes that should be mutated to dates.
59
     *
60
     * @var array
61
     */
62
    protected $dates = ['deleted_at'];
63
64
    /**
65
     * The attributes that should be cast to native types.
66
     *
67
     * @var array
68
     */
69
    protected $casts = [
70
        'id' => 'integer',
71
    ];
72
73
    /* -----------------------------------------------------------------
74
     |  Constructor
75
     | -----------------------------------------------------------------
76
     */
77
78
    /**
79
     * Create a new Eloquent model instance.
80
     *
81
     * @param  array  $attributes
82
     */
83 120
    public function __construct(array $attributes = [])
84
    {
85 120
        $this->setTable(
86 120
            $this->getTableFromConfig('discussions', 'discussions')
87
        );
88
89 120
        parent::__construct($attributes);
90 120
    }
91
92
    /* -----------------------------------------------------------------
93
     |  Relationships
94
     | -----------------------------------------------------------------
95
     */
96
97
    /**
98
     * Participants relationship.
99
     *
100
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
101
     */
102 84
    public function participations()
103
    {
104 84
        return $this->hasMany(
105 84
            $this->getModelFromConfig('participations', Participation::class)
106
        );
107
    }
108
109
    /**
110
     * Messages relationship.
111
     *
112
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
113
     */
114 33
    public function messages()
115
    {
116 33
        return $this->hasMany(
117 33
            $this->getModelFromConfig('messages', Participation::class)
118
        );
119
    }
120
121
    /**
122
     * Get the participable that created the first message.
123
     *
124
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
125
     */
126 3
    public function creator()
127
    {
128 3
        return $this->messages()->oldest()->first()->author();
129
    }
130
131
    /* -----------------------------------------------------------------
132
     |  Scopes
133
     | -----------------------------------------------------------------
134
     */
135
136
    /**
137
     * Scope discussions that the participable is associated with.
138
     *
139
     * @param  \Illuminate\Database\Eloquent\Builder  $query
140
     * @param  \Illuminate\Database\Eloquent\Model    $participable
141
     *
142
     * @return \Illuminate\Database\Eloquent\Builder
143
     */
144 6
    public function scopeForUser(Builder $query, EloquentModel $participable)
145
    {
146 6
        $table = $this->getParticipationsTable();
147
148 6
        return $query->join($table, function (JoinClause $join) use ($table, $participable) {
149 6
            $join->on($this->getQualifiedKeyName(), '=', "{$table}.discussion_id")
150 6
                 ->where("{$table}.participable_type", '=', $participable->getMorphClass())
151 6
                 ->where("{$table}.participable_id", '=', $participable->getKey())
152 6
                 ->whereNull("{$table}.deleted_at");
153 6
        });
154
    }
155
156
    /**
157
     * Scope discussions to load participations relationship.
158
     *
159
     * @param  \Illuminate\Database\Eloquent\Builder  $query
160
     *
161
     * @return \Illuminate\Database\Eloquent\Builder
162
     */
163 3
    public function scopeWithParticipations(Builder $query)
164
    {
165 3
        return $query->with(['participations']);
166
    }
167
168
    /**
169
     * Scope discussions with new messages that the participable is associated with.
170
     *
171
     * @param  \Illuminate\Database\Eloquent\Builder  $query
172
     * @param  \Illuminate\Database\Eloquent\Model    $participable
173
     *
174
     * @return \Illuminate\Database\Eloquent\Builder
175
     */
176 3
    public function scopeForUserWithNewMessages(Builder $query, EloquentModel $participable)
177
    {
178 3
        $prefix       = $this->getConnection()->getTablePrefix();
179 3
        $participations = $this->getParticipationsTable();
180 3
        $discussions  = $this->getTable();
181
182 3
        return $this->scopeForUser($query, $participable)
183 3
                    ->where(function (Builder $query) use ($participations, $discussions, $prefix) {
184 3
                        $expression = $this->getConnection()->raw("{$prefix}{$participations}.last_read");
185 3
                        $query->where("{$discussions}.updated_at", '>', $expression)
186 3
                              ->orWhereNull("{$participations}.last_read");
187 3
                    });
188
    }
189
190
    /**
191
     * Scope discussions between given participables.
192
     *
193
     * @param  \Illuminate\Database\Eloquent\Builder  $query
194
     * @param  \Illuminate\Support\Collection|array   $participables
195
     *
196
     * @return \Illuminate\Database\Eloquent\Builder
197
     */
198
    public function scopeBetween(Builder $query, $participables)
199
    {
200 3
        return $query->whereHas($this->getParticipationsTable(), function (Builder $query) use ($participables) {
0 ignored issues
show
Bug Compatibility introduced by
The expression $query->whereHas($this->...t($participables)); }); of type Illuminate\Database\Eloq...ns\QueriesRelationships adds the type Illuminate\Database\Eloq...ns\QueriesRelationships to the return on line 200 which is incompatible with the return type declared by the interface Arcanedev\LaravelMesseng...iscussion::scopeBetween of type Illuminate\Database\Eloquent\Builder.
Loading history...
201 3
            $index = 0;
202
203 3
            foreach ($participables as $participable) {
204
                /** @var  \Illuminate\Database\Eloquent\Model  $participable */
205
                $clause = [
206 3
                    ['participable_type', '=', $participable->getMorphClass()],
207 3
                    ['participable_id', '=', $participable->getKey()],
208
                ];
209
210 3
                $query->where($clause, null, null, $index === 0 ? 'and' : 'or');
211
212 3
                $index++;
213
            }
214
215 3
            $query->groupBy('discussion_id')
216 3
                  ->havingRaw('COUNT(discussion_id)='.count($participables));
217 3
        });
218
    }
219
220
    /**
221
     * Scope the query by the subject.
222
     *
223
     * @param  \Illuminate\Database\Eloquent\Builder  $query
224
     * @param  string                                 $subject
225
     * @param  bool                                   $strict
226
     *
227
     * @return \Illuminate\Database\Eloquent\Builder
228
     */
229 6
    public function scopeSubject(Builder $query, $subject, $strict = false)
230
    {
231 6
        return $query->where('subject', 'like', $strict ? $subject : "%{$subject}%");
232
    }
233
234
    /* -----------------------------------------------------------------
235
     |  Getters & Setters
236
     | -----------------------------------------------------------------
237
     */
238
239
    /**
240
     * Get the latest_message attribute.
241
     *
242
     * @return \Arcanedev\LaravelMessenger\Models\Message
243
     */
244 3
    public function getLatestMessageAttribute()
245
    {
246 3
        return $this->messages->sortByDesc('created_at')->first();
247
    }
248
249
    /**
250
     * Get the participations table name.
251
     *
252
     * @return string
253
     */
254 9
    protected function getParticipationsTable()
255
    {
256 9
        return $this->getTableFromConfig('participations', 'participations');
257
    }
258
259
    /* -----------------------------------------------------------------
260
     |  Main Methods
261
     | -----------------------------------------------------------------
262
     */
263
264
    /**
265
     * Returns all of the latest discussions by `updated_at` date.
266
     *
267
     * @return \Illuminate\Database\Eloquent\Collection
268
     */
269 3
    public static function getLatest()
270
    {
271 3
        return self::query()->latest('updated_at')->get();
272
    }
273
274
    /**
275
     * Returns all discussions by subject.
276
     *
277
     * @param  string  $subject
278
     * @param  bool    $strict
279
     *
280
     * @return \Illuminate\Database\Eloquent\Collection
281
     */
282 6
    public static function getBySubject($subject, $strict = false)
283
    {
284 6
        return self::subject($subject, $strict)->get();
0 ignored issues
show
Bug introduced by
The method get does only exist in Illuminate\Database\Eloquent\Builder, but not in Arcanedev\LaravelMessenger\Models\Discussion.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
285
    }
286
287
    /**
288
     * Returns an array of participables that are associated with the discussion.
289
     *
290
     * @return \Illuminate\Database\Eloquent\Collection
291
     */
292 6
    public function getParticipables()
293
    {
294 6
        return $this->participations()
295 6
            ->withTrashed()
296 6
            ->get()
297 6
            ->transform(function (ParticipationContract $participant) {
298 6
                return $participant->participable;
0 ignored issues
show
Bug introduced by
Accessing participable on the interface Arcanedev\LaravelMessenger\Contracts\Participation 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...
299 6
            })
300 6
            ->unique(function (EloquentModel $participable) {
301 6
                return $participable->getMorphClass().'-'.$participable->getKey();
302 6
            });
303
    }
304
305
    /**
306
     * Add a participable to discussion.
307
     *
308
     * @param  \Illuminate\Database\Eloquent\Model  $participable
309
     *
310
     * @return \Arcanedev\LaravelMessenger\Models\Participation|mixed
311
     */
312 42
    public function addParticipant(EloquentModel $participable)
313
    {
314 42
        return $this->participations()->firstOrCreate([
315 42
            'participable_id'   => $participable->getKey(),
316 42
            'participable_type' => $participable->getMorphClass(),
317 42
            'discussion_id'     => $this->id,
318
        ]);
319
    }
320
321
    /**
322
     * Add many participables to discussion.
323
     *
324
     * @param  \Illuminate\Support\Collection|array  $participables
325
     *
326
     * @return \Illuminate\Database\Eloquent\Collection
327
     */
328 24
    public function addParticipants($participables)
329
    {
330 24
        foreach ($participables as $participable) {
331 24
            $this->addParticipant($participable);
332
        }
333
334 24
        return $this->participations;
335
    }
336
337
    /**
338
     * Remove a participable from discussion.
339
     *
340
     * @param  \Illuminate\Database\Eloquent\Model  $participable
341
     * @param  bool                                 $reload
342
     *
343
     * @return int
344
     */
345 6
    public function removeParticipant(EloquentModel $participable, $reload = true)
346
    {
347 6
        return $this->removeParticipants([$participable], $reload);
348
    }
349
350
    /**
351
     * Remove many participables from discussion.
352
     *
353
     * @param  \Illuminate\Support\Collection|array  $participables
354
     * @param  bool   $reload
355
     *
356
     * @return int
357
     */
358 18
    public function removeParticipants($participables, $reload = true)
359
    {
360 18
        $deleted = 0;
361
362 18
        foreach ($participables as $participable) {
363
            /** @var  \Illuminate\Database\Eloquent\Model  $participable */
364 18
            $deleted += $this->participations()
365 18
                ->where('participable_type', $participable->getMorphClass())
366 18
                ->where('participable_id', $participable->getKey())
367 18
                ->where('discussion_id', $this->id)
368 18
                ->delete();
369
        }
370
371 18
        if ($reload)
372 9
            $this->load(['participations']);
373
374 18
        return $deleted;
375
    }
376
377
    /**
378
     * Mark a discussion as read for a participable.
379
     *
380
     * @param  \Illuminate\Database\Eloquent\Model  $participable
381
     *
382
     * @return bool|int
383
     */
384 15
    public function markAsRead(EloquentModel $participable)
385
    {
386 15
        if ($participant = $this->getParticipationByParticipable($participable)) {
387 12
            return $participant->update([
388 12
                'last_read' => Carbon::now()
389
            ]);
390
        }
391
392 3
        return false;
393
    }
394
395
    /**
396
     * See if the current thread is unread by the participable.
397
     *
398
     * @param  \Illuminate\Database\Eloquent\Model  $participable
399
     *
400
     * @return bool
401
     */
402 6
    public function isUnread(EloquentModel $participable)
403
    {
404 6
        return ($participant = $this->getParticipationByParticipable($participable))
405 6
            ? $participant->last_read < $this->updated_at
406 6
            : false;
407
    }
408
409
    /**
410
     * Finds the participant record from a participable model.
411
     *
412
     * @param  \Illuminate\Database\Eloquent\Model  $participable
413
     *
414
     * @return \Arcanedev\LaravelMessenger\Models\Participation|mixed
415
     */
416 21
    public function getParticipationByParticipable(EloquentModel $participable)
417
    {
418 21
        return $this->participations()
419 21
            ->where('participable_type', $participable->getMorphClass())
420 21
            ->where('participable_id', $participable->getKey())
421 21
            ->first();
422
    }
423
424
    /**
425
     * Get the trashed participations.
426
     *
427
     * @return \Illuminate\Database\Eloquent\Collection
428
     */
429 6
    public function getTrashedParticipations()
430
    {
431 6
        return $this->participations()->onlyTrashed()->get();
432
    }
433
434
    /**
435
     * Restores all participations within a discussion.
436
     *
437
     * @param  bool  $reload
438
     *
439
     * @return int
440
     */
441 3
    public function restoreAllParticipations($reload = true)
442
    {
443 3
        $restored = $this->getTrashedParticipations()
444 3
            ->filter(function (ParticipationContract $participant) {
445 3
                return $participant->restore();
446 3
            })
447 3
            ->count();
448
449 3
        if ($reload)
450 3
            $this->load(['participations']);
451
452 3
        return $restored;
453
    }
454
455
    /**
456
     * Generates a participant information as a string.
457
     *
458
     * @param  \Closure|null  $callback
459
     * @param  string         $glue
460
     *
461
     * @return string
462
     */
463 6
    public function participationsString($callback = null, $glue = ', ')
464
    {
465
        /** @var \Illuminate\Database\Eloquent\Collection $participations */
466 6
        $participations = $this->participations->load(['participable']);
467
468 6
        if (is_null($callback)) {
469
            // By default: the participant name
470 3
            $callback = function (ParticipationContract $participant) {
471 3
                return $participant->stringInfo();
472 3
            };
473
        }
474
475 6
        return $participations->map($callback)->implode($glue);
476
    }
477
478
    /**
479
     * Checks to see if a participable is a current participant of the discussion.
480
     *
481
     * @param  \Illuminate\Database\Eloquent\Model  $participable
482
     *
483
     * @return bool
484
     */
485 6
    public function hasParticipation(EloquentModel $participable)
486
    {
487 6
        return $this->participations()
488 6
            ->where('participable_id', '=', $participable->getKey())
489 6
            ->where('participable_type', '=', $participable->getMorphClass())
490 6
            ->count() > 0;
491
    }
492
493
    /**
494
     * Get the unread messages in discussion for a specific participable.
495
     *
496
     * @param  \Illuminate\Database\Eloquent\Model  $participable
497
     *
498
     * @return \Illuminate\Support\Collection
499
     */
500 6
    public function getUnreadMessages(EloquentModel $participable)
501
    {
502 6
        $participation = $this->getParticipationByParticipable($participable);
503
504 6
        if (is_null($participation))
505 3
            return new Collection;
506
507 6
        return is_null($participation->last_read)
508 6
            ? $this->messages->toBase()
509 6
            : $this->messages->filter(function (MessageContract $message) use ($participation) {
510 6
                return $message->updated_at->gt($participation->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...
511 6
            })->toBase();
512
    }
513
}
514