diff --git a/app/Helpers/HasAuthor.php b/app/Helpers/HasAuthor.php index c5b0bbab6..ef88c8b95 100644 --- a/app/Helpers/HasAuthor.php +++ b/app/Helpers/HasAuthor.php @@ -15,6 +15,8 @@ public function author(): User public function authoredBy(User $author) { $this->authorRelation()->associate($author); + + $this->unsetRelation('authorRelation'); } public function authorRelation(): BelongsTo diff --git a/app/Helpers/HasLikes.php b/app/Helpers/HasLikes.php index 04154653a..1ac544b85 100644 --- a/app/Helpers/HasLikes.php +++ b/app/Helpers/HasLikes.php @@ -8,35 +8,50 @@ trait HasLikes { + /** + * @return \Illuminate\Database\Eloquent\Collection + */ + public function likes() + { + return $this->likesRelation; + } + protected static function bootHasLikes() { static::deleting(function ($model) { - $model->likes()->delete(); + $model->likesRelation()->delete(); + + $model->unsetRelation('likesRelation'); }); } public function likedBy(User $user) { - $this->likes()->create(['user_id' => $user->id()]); + $this->likesRelation()->create(['user_id' => $user->id()]); + + $this->unsetRelation('likesRelation'); } public function dislikedBy(User $user) { - optional($this->likes()->where('user_id', $user->id())->first())->delete(); - } + optional($this->likesRelation()->where('user_id', $user->id())->first())->delete(); - public function likes(): MorphMany - { - return $this->morphMany(Like::class, 'likeable'); + $this->unsetRelation('likesRelation'); } - public function isLikedBy(User $user): bool + /** + * It's important to name the relationship the same as the method because otherwise + * eager loading of the polymorphic relationship will fail on queued jobs. + * + * @see https://github.com/laravelio/laravel.io/issues/350 + */ + public function likesRelation(): MorphMany { - return $this->likes()->where('user_id', $user->id())->exists(); + return $this->morphMany(Like::class, 'likesRelation', 'likeable_type', 'likeable_id'); } - public function likesCount(): int + public function isLikedBy(User $user): bool { - return $this->likes()->count(); + return $this->likesRelation()->where('user_id', $user->id())->exists(); } } diff --git a/app/Helpers/HasTags.php b/app/Helpers/HasTags.php index 9c3ba2d8a..fb2455384 100644 --- a/app/Helpers/HasTags.php +++ b/app/Helpers/HasTags.php @@ -22,20 +22,19 @@ public function syncTags(array $tags) { $this->save(); $this->tagsRelation()->sync($tags); + + $this->unsetRelation('tagsRelation'); } public function removeTags() { $this->tagsRelation()->detach(); + + $this->unsetRelation('tagsRelation'); } public function tagsRelation(): MorphToMany { return $this->morphToMany(Tag::class, 'taggable')->withTimestamps(); } - - public function hasTags(): bool - { - return $this->tagsRelation()->count() > 0; - } } diff --git a/app/Helpers/ReceivesReplies.php b/app/Helpers/ReceivesReplies.php index dcf7b4213..c69e708c7 100644 --- a/app/Helpers/ReceivesReplies.php +++ b/app/Helpers/ReceivesReplies.php @@ -30,6 +30,8 @@ public function deleteReplies() foreach ($this->repliesRelation()->get() as $reply) { $reply->delete(); } + + $this->unsetRelation('repliesRelation'); } /** @@ -53,9 +55,4 @@ public function isConversationOld(): bool return $this->createdAt()->lt($sixMonthsAgo); } - - public function repliesCount(): int - { - return $this->repliesRelation()->count(); - } } diff --git a/app/Models/Article.php b/app/Models/Article.php index d06649eed..173434ee1 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -50,6 +50,15 @@ final class Article extends Model 'shared_at', ]; + /** + * {@inheritdoc} + */ + protected $with = [ + 'authorRelation', + 'likesRelation', + 'tagsRelation', + ]; + public function id(): int { return $this->id; @@ -242,17 +251,17 @@ public function scopeRecent(Builder $query): Builder public function scopePopular(Builder $query): Builder { - return $query->withCount('likes') - ->orderBy('likes_count', 'desc') + return $query->withCount('likesRelation') + ->orderBy('likes_relation_count', 'desc') ->orderBy('submitted_at', 'desc'); } public function scopeTrending(Builder $query): Builder { - return $query->withCount(['likes' => function ($query) { + return $query->withCount(['likesRelation' => function ($query) { $query->where('created_at', '>=', now()->subWeek()); }]) - ->orderBy('likes_count', 'desc') + ->orderBy('likes_relation_count', 'desc') ->orderBy('submitted_at', 'desc'); } diff --git a/app/Models/Reply.php b/app/Models/Reply.php index df143dbb4..a447175c7 100644 --- a/app/Models/Reply.php +++ b/app/Models/Reply.php @@ -37,7 +37,7 @@ final class Reply extends Model * {@inheritdoc} */ protected $with = [ - 'likes', + 'likesRelation', ]; public function id(): int diff --git a/app/Models/Thread.php b/app/Models/Thread.php index 67bacfcaa..9796fa2ce 100644 --- a/app/Models/Thread.php +++ b/app/Models/Thread.php @@ -59,6 +59,16 @@ final class Thread extends Model implements ReplyAble, SubscriptionAble, Feedabl 'subject', ]; + /** + * {@inheritdoc} + */ + protected $with = [ + 'authorRelation', + 'likesRelation', + 'repliesRelation', + 'tagsRelation', + ]; + public function id(): int { return $this->id; @@ -184,6 +194,7 @@ public static function feedQuery(): Builder { return static::with([ 'solutionReplyRelation', + 'likesRelation', 'repliesRelation', 'repliesRelation.authorRelation', 'tagsRelation', diff --git a/app/Models/User.php b/app/Models/User.php index bfcb155fb..1820d0b9b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -214,20 +214,6 @@ public function series(): HasMany return $this->hasMany(Series::class, 'author_id'); } - /** - * @todo Make this work with Eloquent instead of a collection - */ - public function countSolutions(): int - { - return $this->replies()->filter(function (Reply $reply) { - if ($reply->replyAble() instanceof Thread) { - return $reply->replyAble()->isSolutionReply($reply); - } - - return false; - })->count(); - } - public static function findByUsername(string $username): self { return static::where('username', $username)->firstOrFail(); @@ -251,6 +237,20 @@ public function delete() parent::delete(); } + /** + * @todo Make this work with Eloquent instead of a collection + */ + public function countSolutions(): int + { + return $this->replies()->filter(function (Reply $reply) { + if ($reply->replyAble() instanceof Thread) { + return $reply->replyAble()->isSolutionReply($reply); + } + + return false; + })->count(); + } + public function scopeMostSolutions(Builder $query) { return $query->withCount(['replyAble as most_solutions' => function ($query) { diff --git a/database/migrations/2021_03_10_161050_add_index_to_replyable_type.php b/database/migrations/2021_03_10_161050_add_index_to_replyable_type.php new file mode 100644 index 000000000..51be699dc --- /dev/null +++ b/database/migrations/2021_03_10_161050_add_index_to_replyable_type.php @@ -0,0 +1,15 @@ +index('replyable_type'); + }); + } +} diff --git a/database/schema/mysql-schema.dump b/database/schema/mysql-schema.dump index 5caa97abd..643058465 100644 --- a/database/schema/mysql-schema.dump +++ b/database/schema/mysql-schema.dump @@ -145,6 +145,7 @@ CREATE TABLE `replies` ( PRIMARY KEY (`id`), KEY `replies_author_id_index` (`author_id`), KEY `replies_replyable_id_index` (`replyable_id`), + KEY `replies_replyable_type_index` (`replyable_type`), CONSTRAINT `replies_author_id_foreign` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -323,3 +324,4 @@ INSERT INTO `migrations` VALUES (55,'2020_07_16_185353_add_twitter_columns',1); INSERT INTO `migrations` VALUES (56,'2020_10_01_093001_add_email_verified_at_column_to_users',1); INSERT INTO `migrations` VALUES (57,'2020_11_03_205735_add_uuid_to_failed_jobs_table',1); INSERT INTO `migrations` VALUES (58,'2020_11_22_194212_create_schedule_monitor_tables',1); +INSERT INTO `migrations` VALUES (59,'2021_03_10_161050_add_index_to_replyable_type',2); diff --git a/resources/views/forum/_thread.blade.php b/resources/views/forum/_thread.blade.php index 78289e535..e254dd122 100644 --- a/resources/views/forum/_thread.blade.php +++ b/resources/views/forum/_thread.blade.php @@ -8,9 +8,9 @@
- @if ($thread->hasTags()) + @if (count($tags = $thread->tags()))
- @foreach ($thread->tags() as $tag) + @foreach ($tags as $tag) {{ $tag->name() }} @@ -35,6 +35,7 @@
+

@@ -68,7 +69,7 @@ class="rounded-full p-1 bg-lio-100 text-lio-500"

@@ -76,7 +77,7 @@ class="rounded-full p-1 bg-lio-100 text-lio-500"
- {{ $thread->repliesCount() }} + {{ count($thread->replies()) }} Replies
diff --git a/resources/views/forum/threads/show.blade.php b/resources/views/forum/threads/show.blade.php index c8e23ce10..230bd7394 100644 --- a/resources/views/forum/threads/show.blade.php +++ b/resources/views/forum/threads/show.blade.php @@ -11,6 +11,7 @@
+
@@ -32,10 +33,12 @@ class="text-lio-700 mr-2">
@include('forum.threads.info.tags')
+ @include('forum.threads._view_solution')
+
+
@@ -52,7 +56,6 @@ class="forum-content" @foreach ($thread->replies() as $reply)
- @if ($thread->isSolutionReply($reply))
Solution @@ -68,23 +71,22 @@ class="forum-content"
{{ $reply->author()->name() }} - replied - {{ $reply->createdAt()->diffForHumans() }} + replied {{ $reply->createdAt()->diffForHumans() }}
-
+
@can(App\Policies\ReplyPolicy::UPDATE, $reply) Edit + Delete @endcan @can(App\Policies\ThreadPolicy::UPDATE, $thread) - @if ($thread->isSolutionReply($reply)) Unmark As Solution @@ -108,11 +110,11 @@ class="forum-content" 'body' => '

Confirm to mark this reply as the solution for "'.e($thread->subject()).'".

', ]) @endif - @endcan
+
+
@@ -143,16 +146,13 @@ class="forum-content" @else
-
@csrf @formGroup('body') - @include('_partials._editor', [ - 'content' => old('body') - ]) + @include('_partials._editor', ['content' => old('body')]) @error('body') @endFormGroup @@ -168,7 +168,6 @@ class="forum-content"
- @endif @else @@ -187,38 +186,39 @@ class="forum-content" @endif @endcan +
diff --git a/resources/views/livewire/like-thread.blade.php b/resources/views/livewire/like-thread.blade.php index 19a6e7e8d..69158f4fe 100644 --- a/resources/views/livewire/like-thread.blade.php +++ b/resources/views/livewire/like-thread.blade.php @@ -2,12 +2,12 @@ @if (Auth::guest())
👍 - {{ $this->thread->likesCount() }} + {{ count($this->thread->likes()) }}
@else @endif diff --git a/resources/views/livewire/show-articles.blade.php b/resources/views/livewire/show-articles.blade.php index 8df08897e..b4fca23aa 100644 --- a/resources/views/livewire/show-articles.blade.php +++ b/resources/views/livewire/show-articles.blade.php @@ -4,7 +4,7 @@ Loading... - @foreach($articles as $article) + @foreach ($articles as $article)
@foreach ($article->tags() as $tag) @@ -56,16 +56,16 @@
+
👏 - {{ $article->likesCount() }} + {{ count($article->likes()) }}
@endforeach {{ $articles->links() }} -
@@ -73,9 +73,11 @@ + + diff --git a/resources/views/users/_latest_replies.blade.php b/resources/views/users/_latest_replies.blade.php index 22e87fe25..34cb48bf4 100644 --- a/resources/views/users/_latest_replies.blade.php +++ b/resources/views/users/_latest_replies.blade.php @@ -16,7 +16,7 @@
- {{ $reply->likesCount() }} + {{ count($reply->likes()) }}
diff --git a/resources/views/users/_latest_threads.blade.php b/resources/views/users/_latest_threads.blade.php index b2e86d0ee..b2e8a718b 100644 --- a/resources/views/users/_latest_threads.blade.php +++ b/resources/views/users/_latest_threads.blade.php @@ -21,7 +21,7 @@ - {{ $thread->likesCount() }} + {{ count($thread->likes()) }} diff --git a/resources/views/users/_metrics.blade.php b/resources/views/users/_metrics.blade.php index c5f066f64..b12eb13fe 100644 --- a/resources/views/users/_metrics.blade.php +++ b/resources/views/users/_metrics.blade.php @@ -1,19 +1,19 @@
-

{{ $user->countThreads() }} {{ Str::plural('thread', $user->countThreads()) }}

+

{{ $countedThreads = $user->countThreads() }} {{ Str::plural('thread', $countedThreads) }}

-

{{ $user->countReplies() }} {{ Str::plural('reply', $user->countReplies()) }}

+

{{ $countedReplies = $user->countReplies() }} {{ Str::plural('reply', $countedReplies) }}

-

{{ $user->countSolutions() }} {{ Str::plural('solution', $user->countSolutions()) }}

+

{{ $countedSolutions = $user->countSolutions() }} {{ Str::plural('solution', $countedSolutions) }}

-

{{ $user->countArticles() }} {{ Str::plural('article', $user->countArticles()) }}

+

{{ $countedArticles = $user->countArticles() }} {{ Str::plural('article', $countedArticles) }}

diff --git a/resources/views/users/articles.blade.php b/resources/views/users/articles.blade.php index 08f8ce58e..2b9c26bb0 100644 --- a/resources/views/users/articles.blade.php +++ b/resources/views/users/articles.blade.php @@ -106,7 +106,7 @@
👏 - {{ $article->likesCount() }} + {{ count($article->likes()) }}
diff --git a/tests/Feature/ForumTest.php b/tests/Feature/ForumTest.php index ef800f0c0..5901648e8 100644 --- a/tests/Feature/ForumTest.php +++ b/tests/Feature/ForumTest.php @@ -176,6 +176,7 @@ public function users_cannot_edit_a_thread_with_a_subject_that_is_too_long() public function a_user_can_toggle_a_like_on_a_thread() { $this->login(); + $thread = Thread::factory()->create(); Livewire::test(LikeThread::class, ['thread' => $thread]) @@ -201,6 +202,7 @@ public function a_logged_out_user_cannot_toggle_a_like_on_a_thread() public function a_user_can_toggle_a_like_on_a_reply() { $this->login(); + $reply = Reply::factory()->create(); Livewire::test(LikeReply::class, ['reply' => $reply]) @@ -242,9 +244,7 @@ public function user_can_see_standalone_links_in_thread() 'slug' => 'the-first-thread', 'body'=> 'https://github.com/laravelio/laravel.io check this cool project', ]); - Reply::factory()->create([ - 'replyable_id' => $thread->id(), - ]); + Reply::factory()->create(['replyable_id' => $thread->id()]); $this->visit("/forum/{$thread->slug()}") ->see('<a href=\"https:\/\/github.com\/laravelio\/laravel.io\" rel=\"nofollow\" target=\"_blank\">https:\/\/github.com\/laravelio\/laravel.io<\/a>'); diff --git a/tests/Integration/Models/ArticleTest.php b/tests/Integration/Models/ArticleTest.php index 5a715718d..b8992024c 100644 --- a/tests/Integration/Models/ArticleTest.php +++ b/tests/Integration/Models/ArticleTest.php @@ -43,7 +43,8 @@ public function we_can_get_trending_articles() $articles[0]->likedBy($users[1]); // Update the like timestamp outside of the trending window. - $articles[0]->likes()->update(['created_at' => now()->subWeeks(2)]); + $articles[0]->likesRelation()->update(['created_at' => now()->subWeeks(2)]); + $articles[0]->unsetRelation('likesRelation'); // Like the remaining articles once, but inside the trending window. $articles[1]->likedBy($users[0]);