From 8ddf93f1c461ac650885a17aa9d94b12b183ed33 Mon Sep 17 00:00:00 2001 From: Harish Kumar Date: Tue, 11 Jun 2024 15:11:58 +0530 Subject: [PATCH] feat: allow configuration for `reacts_model` and `reacts_id_column` --- config/reactions.php | 17 ++ ...18_07_10_000000_create_reactions_table.php | 7 +- src/Helper.php | 36 ++++ src/Models/Reaction.php | 13 +- src/Traits/Reactable.php | 25 +-- src/Traits/Reacts.php | 17 +- tests/Stubs/Models/Profile.php | 28 +++ tests/TestCase.php | 21 +- tests/Unit/ReactableReactionEventTest.php | 7 +- tests/Unit/ReactsProfileModelTest.php | 198 ++++++++++++++++++ 10 files changed, 328 insertions(+), 41 deletions(-) create mode 100644 src/Helper.php create mode 100644 tests/Stubs/Models/Profile.php create mode 100644 tests/Unit/ReactsProfileModelTest.php diff --git a/config/reactions.php b/config/reactions.php index beb98d9..b07da49 100644 --- a/config/reactions.php +++ b/config/reactions.php @@ -8,4 +8,21 @@ */ 'table_name' => 'reactions', + /** + * The name of the model that will be used to represent + * the model (eg. User) that is reacting to a reactable model (eg. Post). + * + * If this is set to null, the package will attempt to use + * the default user model for your application. + */ + 'reacts_model' => null, + + /* + * This is the name of the column on the "reactions" table that will be used to + * identify the "reacts_model" relationship. + * + * If this is set to null, the package will attempt to use the "user_id" column + * on the "Reaction" model. + */ + 'reacts_id_column' => null, ]; diff --git a/migrations/2018_07_10_000000_create_reactions_table.php b/migrations/2018_07_10_000000_create_reactions_table.php index 5b535a7..5b15abb 100644 --- a/migrations/2018_07_10_000000_create_reactions_table.php +++ b/migrations/2018_07_10_000000_create_reactions_table.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use Qirolab\Laravel\Reactions\Helper; /** * Class CreateLoveLikesTable. @@ -17,15 +18,17 @@ class CreateReactionsTable extends Migration public function up() { Schema::create(config('reactions.table_name', 'reactions'), function (Blueprint $table) { + $userIdColumn = Helper::resolveReactsIdColumn(); + $table->increments('id'); - $table->integer('user_id')->unsigned()->index(); + $table->integer($userIdColumn)->unsigned()->index(); $table->morphs('reactable'); $table->string('type')->nullable(); $table->timestamps(); $table->unique([ 'reactable_type', 'reactable_id', - 'user_id', + $userIdColumn, ], 'react_user_unique'); // $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); }); diff --git a/src/Helper.php b/src/Helper.php new file mode 100644 index 0000000..7d57c66 --- /dev/null +++ b/src/Helper.php @@ -0,0 +1,36 @@ +belongsTo($userModel, 'user_id'); + return $this->belongsTo($userModel, $userIdColumn); } } diff --git a/src/Traits/Reactable.php b/src/Traits/Reactable.php index 7bdfc7c..a6a0d68 100644 --- a/src/Traits/Reactable.php +++ b/src/Traits/Reactable.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Builder; use Qirolab\Laravel\Reactions\Contracts\ReactsInterface; use Qirolab\Laravel\Reactions\Exceptions\InvalidReactionUser; +use Qirolab\Laravel\Reactions\Helper; use Qirolab\Laravel\Reactions\Models\Reaction; trait Reactable @@ -26,9 +27,9 @@ public function reactions() */ public function reactionsBy() { - $userModel = $this->resolveUserModel(); + $userModel = Helper::resolveReactsModel(); - $userIds = $this->reactions->pluck('user_id'); + $userIds = $this->reactions->pluck(Helper::resolveReactsIdColumn()); return $userModel::whereKey($userIds)->get(); } @@ -136,7 +137,7 @@ public function reacted($user = null) { $user = $this->getUser($user); - return $this->reactions->where('user_id', $user->getKey())->first(); + return $this->reactions->where(Helper::resolveReactsIdColumn(), $user->getKey())->first(); } /** @@ -196,7 +197,7 @@ public function scopeWhereReactedBy(Builder $query, $userId = null, $type = null try { $user = $this->getUser($userId); } catch (InvalidReactionUser $e) { - if (! $user && ! $userId) { + if (!$user && !$userId) { throw InvalidReactionUser::notDefined(); } } @@ -204,7 +205,7 @@ public function scopeWhereReactedBy(Builder $query, $userId = null, $type = null $userId = ($user) ? $user->getKey() : $userId; return $query->whereHas('reactions', function ($innerQuery) use ($userId, $type) { - $innerQuery->where('user_id', $userId); + $innerQuery->where(Helper::resolveReactsIdColumn(), $userId); if ($type) { $innerQuery->where('type', $type); @@ -222,7 +223,7 @@ public function scopeWhereReactedBy(Builder $query, $userId = null, $type = null */ private function getUser($user = null) { - if (! $user && auth()->check()) { + if (!$user && auth()->check()) { return auth()->user(); } @@ -230,20 +231,10 @@ private function getUser($user = null) return $user; } - if (! $user) { + if (!$user) { throw InvalidReactionUser::notDefined(); } throw InvalidReactionUser::invalidReactByUser(); } - - /** - * Retrieve User's model class name. - * - * @return \Illuminate\Contracts\Auth\Authenticatable - */ - private function resolveUserModel() - { - return config('auth.providers.users.model'); - } } diff --git a/src/Traits/Reacts.php b/src/Traits/Reacts.php index 8027467..389237a 100644 --- a/src/Traits/Reacts.php +++ b/src/Traits/Reacts.php @@ -5,6 +5,7 @@ use Qirolab\Laravel\Reactions\Contracts\ReactableInterface; use Qirolab\Laravel\Reactions\Events\OnDeleteReaction; use Qirolab\Laravel\Reactions\Events\OnReaction; +use Qirolab\Laravel\Reactions\Helper; use Qirolab\Laravel\Reactions\Models\Reaction; trait Reacts @@ -19,10 +20,10 @@ trait Reacts public function reactTo(ReactableInterface $reactable, $type) { $reaction = $reactable->reactions()->where([ - 'user_id' => $this->getKey(), + Helper::resolveReactsIdColumn() => $this->getKey(), ])->first(); - if (! $reaction) { + if (!$reaction) { return $this->storeReaction($reactable, $type); } @@ -44,10 +45,10 @@ public function reactTo(ReactableInterface $reactable, $type) public function removeReactionFrom(ReactableInterface $reactable) { $reaction = $reactable->reactions()->where([ - 'user_id' => $this->getKey(), + Helper::resolveReactsIdColumn() => $this->getKey(), ])->first(); - if (! $reaction) { + if (!$reaction) { return; } @@ -64,10 +65,10 @@ public function removeReactionFrom(ReactableInterface $reactable) public function toggleReactionOn(ReactableInterface $reactable, $type) { $reaction = $reactable->reactions()->where([ - 'user_id' => $this->getKey(), + Helper::resolveReactsIdColumn() => $this->getKey(), ])->first(); - if (! $reaction) { + if (!$reaction) { return $this->storeReaction($reactable, $type); } @@ -101,7 +102,7 @@ public function ReactedOn(ReactableInterface $reactable) public function isReactedOn(ReactableInterface $reactable, $type = null) { $isReacted = $reactable->reactions()->where([ - 'user_id' => $this->getKey(), + Helper::resolveReactsIdColumn() => $this->getKey(), ]); if ($type) { @@ -123,7 +124,7 @@ public function isReactedOn(ReactableInterface $reactable, $type = null) protected function storeReaction(ReactableInterface $reactable, $type) { $reaction = $reactable->reactions()->create([ - 'user_id' => $this->getKey(), + Helper::resolveReactsIdColumn() => $this->getKey(), 'type' => $type, ]); diff --git a/tests/Stubs/Models/Profile.php b/tests/Stubs/Models/Profile.php new file mode 100644 index 0000000..69a02f3 --- /dev/null +++ b/tests/Stubs/Models/Profile.php @@ -0,0 +1,28 @@ +up(); (new \CreateUsersTable())->up(); @@ -151,4 +152,16 @@ public function createUser($attributes = [], $amount = null) $amount ); } + + public function createProfile($attributes = [], $amount = null) + { + return $this->factory( + Profile::class, + array_merge( + ['name' => $this->faker()->name], + $attributes + ), + $amount + ); + } } diff --git a/tests/Unit/ReactableReactionEventTest.php b/tests/Unit/ReactableReactionEventTest.php index ea838b4..5256034 100644 --- a/tests/Unit/ReactableReactionEventTest.php +++ b/tests/Unit/ReactableReactionEventTest.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Event; use Qirolab\Laravel\Reactions\Events\OnDeleteReaction; use Qirolab\Laravel\Reactions\Events\OnReaction; +use Qirolab\Laravel\Reactions\Helper; use Qirolab\Tests\Laravel\Reactions\TestCase; class ReactableReactionEventTest extends TestCase @@ -41,7 +42,7 @@ public function it_can_fire_model_was_reacted_event_on_toggle_reaction() public function it_can_fire_reaction_deleted_and_model_was_reacted_event_on_change_reaction() { $this->article->reactions()->create([ - 'user_id' => $this->user->getKey(), + Helper::resolveReactsIdColumn() => $this->user->getKey(), 'type' => 'like', ]); @@ -55,7 +56,7 @@ public function it_can_fire_reaction_deleted_and_model_was_reacted_event_on_chan public function it_can_fire_reaction_deleted_and_model_was_reacted_event_on_change_reaction_via_toggle() { $this->article->reactions()->create([ - 'user_id' => $this->user->getKey(), + Helper::resolveReactsIdColumn() => $this->user->getKey(), 'type' => 'like', ]); @@ -68,7 +69,7 @@ public function it_can_fire_reaction_deleted_and_model_was_reacted_event_on_chan public function it_can_fire_reaction_was_deleted_event() { $this->article->reactions()->create([ - 'user_id' => $this->user->getKey(), + Helper::resolveReactsIdColumn() => $this->user->getKey(), 'type' => 'like', ]); diff --git a/tests/Unit/ReactsProfileModelTest.php b/tests/Unit/ReactsProfileModelTest.php new file mode 100644 index 0000000..1efabbb --- /dev/null +++ b/tests/Unit/ReactsProfileModelTest.php @@ -0,0 +1,198 @@ +set('reactions.reacts_model', Profile::class); + config()->set('reactions.reacts_id_column', 'profile_id'); + + (new \CreateReactionsTable())->down(); + (new \CreateUsersTable())->down(); + (new \CreateArticlesTable())->down(); + + (new \CreateReactionsTable())->up(); + (new \CreateUsersTable())->up(); + (new \CreateArticlesTable())->up(); + } + + /** @test */ + public function it_can_react_to_reactable_model() + { + $article = $this->createArticle(); + + $user = $this->createUser(); + $profile = $this->createProfile(); + + $this->actingAs($user); + + $profile->reactTo($article, 'like'); + + $this->assertEquals(1, $article->reactions()->count()); + + $this->assertTrue($profile->is($article->reactions()->first()->reactBy)); + } + + // /** @test */ + // public function it_can_react_to_reactable_by_other_user() + // { + // $article = $this->createArticle(); + + // $user = $this->createUser(); + // $profile1 = $this->createProfile(); + // $profile2 = $this->createProfile(); + + // $this->actingAs($user); + + // $profile2->reactTo($article, 'like'); + + // $this->assertEquals(1, $article->reactions()->count()); + + // $this->assertTrue($profile2->is($article->reactions()->first()->reactBy)); + // } + + /** @test */ + public function it_can_reactions_many_reactables() + { + $profile = $this->createProfile(); + + $article1 = $this->createArticle(); + $article2 = $this->createArticle(); + + $profile->reactTo($article1, 'like'); + $profile->reactTo($article2, 'like'); + + $this->assertEquals(1, $article1->reactions()->count()); + $this->assertTrue($profile->is($article1->reactions()->first()->reactBy)); + + $this->assertEquals(1, $article2->reactions()->count()); + $this->assertTrue($profile->is($article2->reactions()->first()->reactBy)); + } + + /** @test */ + public function it_cannot_duplicate_react() + { + $profile = $this->createProfile(); + + $article = $this->createArticle(); + + $profile->reactTo($article, 'like'); + $profile->reactTo($article, 'dislike'); + + $this->assertEquals(1, $article->reactions()->count()); + } + + /** @test */ + public function it_can_remove_reaction_on_reactable() + { + $profile = $this->createProfile(); + + $article = $this->createArticle(); + + $profile->reactTo($article, 'like'); + + $profile->removeReactionFrom($article); + + $this->assertEquals(0, $article->reactions()->count()); + } + + /** @test */ + public function it_can_remove_reaction_from_reacted_reactable_model() + { + $profile1 = $this->createProfile(); + + $profile2 = $this->createProfile(); + + $article = $this->createArticle(); + + $profile2->reactTo($article, 'like'); + + $profile1->removeReactionFrom($article); + + $this->assertEquals(1, $article->reactions()->count()); + } + + /** @test */ + public function it_can_react_with_toggle() + { + $profile = $this->createProfile(); + + $article = $this->createArticle(); + + $profile->toggleReactionOn($article, 'like'); + + $this->assertEquals(1, $article->reactions()->count()); + } + + /** @test */ + public function it_can_toggle_reaction_type() + { + $profile = $this->createProfile(); + + $article = $this->createArticle(); + + $profile->toggleReactionOn($article, 'like'); + $this->assertEquals(1, $article->reactions()->count()); + $this->assertEquals('like', $article->reactions()->first()->type); + + $profile->toggleReactionOn($article, 'clap'); + $this->assertEquals(1, $article->reactions()->count()); + $this->assertEquals('clap', $article->reactions()->first()->type); + } + + /** @test */ + public function it_can_remove_reaction_with_toggle() + { + $profile = $this->createProfile(); + + $article = $this->createArticle(); + + $profile->reactTo($article, 'like'); + + $profile->toggleReactionOn($article, 'like'); + + $this->assertEquals(0, $article->reactions()->count()); + } + + /** @test */ + public function it_can_check_if_reacted_on_reactable_model() + { + $profile1 = $this->createProfile(); + + $article = $this->createArticle(); + + $profile1->reactTo($article, 'like'); + + $this->assertTrue($profile1->isReactedOn($article)); + + $profile2 = $this->createProfile(); + + $this->assertFalse($profile2->isReactedOn($article)); + } + + /** @test **/ + public function it_can_have_reacted_reaction_on_reactable_model() + { + $profile = $this->createProfile(); + + $article = $this->createArticle(); + + $profile->reactTo($article, 'like'); + + $this->assertInstanceOf(Reaction::class, $profile->reactedOn($article)); + $this->assertEquals('like', $profile->reactedOn($article)->type); + } +}