From 90aaec18e2b8a77dd0590c9f46d753794fd8bee8 Mon Sep 17 00:00:00 2001 From: JimChen Date: Sat, 13 Mar 2021 18:31:06 +0800 Subject: [PATCH 01/16] =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + composer.json | 10 +- config/vote.php | 10 + database/migrations/create_votes_table.php | 34 --- phpunit.xml | 3 +- src/CanBeVoted.php | 87 -------- src/Events/CancelVoted.php | 8 + src/Events/DownVoted.php | 8 + src/Events/Event.php | 23 ++ src/Events/UpVoted.php | 8 + src/Exceptions/UnexpectValueException.php | 10 + src/Traits/Votable.php | 93 ++++++++ src/Traits/Voter.php | 159 +++++++++++++ src/Vote.php | 248 ++++++++------------- src/VoteItems.php | 36 +++ src/VoteServiceProvider.php | 15 +- 16 files changed, 472 insertions(+), 282 deletions(-) create mode 100644 config/vote.php delete mode 100644 database/migrations/create_votes_table.php delete mode 100644 src/CanBeVoted.php create mode 100644 src/Events/CancelVoted.php create mode 100644 src/Events/DownVoted.php create mode 100644 src/Events/Event.php create mode 100644 src/Events/UpVoted.php create mode 100644 src/Exceptions/UnexpectValueException.php create mode 100644 src/Traits/Votable.php create mode 100644 src/Traits/Voter.php create mode 100644 src/VoteItems.php diff --git a/.gitignore b/.gitignore index f7f520e..5f59bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea /vendor/ composer.lock +.phpunit.result.cache + diff --git a/composer.json b/composer.json index 09a206c..ca99516 100644 --- a/composer.json +++ b/composer.json @@ -8,10 +8,14 @@ } ], "require": { - "php": ">=5.5.9" + "php": ">=7.2", + "laravel/framework": "^5.5|~6.0|~7.0|~8.0", + "symfony/polyfill-php80": "^1.22" }, "require-dev": { - "phpunit/phpunit": "~5.0" + "mockery/mockery": "^1.3", + "orchestra/testbench": "^3.5|~4.0|~5.0|~6.0", + "phpunit/phpunit": "^8.5|~9.0" }, "license": "MIT", "autoload": { @@ -31,6 +35,6 @@ ] } }, - "minimum-stability": "dev", + "minimum-stability": "stable", "prefer-stable": true } diff --git a/config/vote.php b/config/vote.php new file mode 100644 index 0000000..8bbf47d --- /dev/null +++ b/config/vote.php @@ -0,0 +1,10 @@ + 'votes', + + 'user_foreign_key' => 'user_id', + + 'vote_model' => \Jcc\LaravelVote\Vote::class, +]; \ No newline at end of file diff --git a/database/migrations/create_votes_table.php b/database/migrations/create_votes_table.php deleted file mode 100644 index aa49138..0000000 --- a/database/migrations/create_votes_table.php +++ /dev/null @@ -1,34 +0,0 @@ -unsignedInteger('user_id'); - $table->unsignedInteger('votable_id'); - $table->string('votable_type')->index(); - $table->enum('type', ['up_vote', 'down_vote'])->default('up_vote'); - - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::drop('votes'); - } -} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 5fb5529..ce90235 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,8 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" - syntaxCheck="false"> + stopOnFailure="false"> ./tests/ diff --git a/src/CanBeVoted.php b/src/CanBeVoted.php deleted file mode 100644 index 8292a3d..0000000 --- a/src/CanBeVoted.php +++ /dev/null @@ -1,87 +0,0 @@ - - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Jcc\LaravelVote; - -trait CanBeVoted -{ - /** - * Check if user is voted by given user. - * - * @param $user - * - * @return bool - */ - public function isVotedBy($user) - { - return $this->voters->contains($user); - } - - /** - * Return the total vote score - * - * @return int - */ - public function countTotalVotes() - { - $downVote = $this->countVoters('down_vote'); - $upVotes = $this->countVoters('up_vote'); - return $upVotes - $downVote; - } - - /** - * Count the number of up votes. - * - * @return int - */ - public function countUpVoters() - { - return $this->countVoters('up_vote'); - } - - /** - * Count the number of down votes. - * - * @return int - */ - public function countDownVoters() - { - return $this->countVoters('down_vote'); - } - - /** - * Count the number of voters. - * - * @param string $type - * - * @return int - */ - public function countVoters($type = null) - { - $voters = $this->voters(); - - if(!is_null($type)) $voters->wherePivot('type', $type); - - return $voters->count(); - } - - /** - * Return voters. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - public function voters() - { - $property = property_exists($this, 'vote') ? $this->vote : __CLASS__; - - return $this->morphToMany($property, 'votable', $this->vote_table ?: 'votes'); - } -} diff --git a/src/Events/CancelVoted.php b/src/Events/CancelVoted.php new file mode 100644 index 0000000..9795977 --- /dev/null +++ b/src/Events/CancelVoted.php @@ -0,0 +1,8 @@ +vote = $vote; + } +} diff --git a/src/Events/UpVoted.php b/src/Events/UpVoted.php new file mode 100644 index 0000000..4eba1ff --- /dev/null +++ b/src/Events/UpVoted.php @@ -0,0 +1,8 @@ +relationLoaded('voters')) { + return $this->voters->contains($user); + } + + return $this->voters() + ->where(\config('vote.user_foreign_key'), $user->getKey()) + ->when(is_string($type), function ($builder) use ($type) { + $builder->where('type', (string)new VoteItems($type)); + }) + ->exists(); + } + + return false; + } + + /** + * Return voters. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function voters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->belongsToMany( + config('auth.providers.users.model'), + config('vote.votes_table'), + 'votable_id', + config('vote.user_foreign_key') + ) + ->where('votable_type', $this->getMorphClass()); + } + + /** + * @param \Illuminate\Database\Eloquent\Model $user + * + * @return bool + */ + public function isUpVoteBy(Model $user) + { + return $this->isVoteBy($user, VoteItems::UP); + } + + /** + * Return up voters. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function upVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->voters()->where('type', VoteItems::UP); + } + + /** + * @param \Illuminate\Database\Eloquent\Model $user + * + * @return bool + */ + public function isDownVoteBy(Model $user) + { + return $this->isVoteBy($user, VoteItems::DOWN); + } + + /** + * Return down voters. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function downVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->voters()->where('type', VoteItems::DOWN); + } +} diff --git a/src/Traits/Voter.php b/src/Traits/Voter.php new file mode 100644 index 0000000..f5c0d6f --- /dev/null +++ b/src/Traits/Voter.php @@ -0,0 +1,159 @@ + $object->getMorphClass(), + 'votable_id' => $object->getKey(), + \config('vote.user_foreign_key') => $this->getKey(), + ]; + + /* @var \Illuminate\Database\Eloquent\Model $vote */ + $vote = \app(\config('vote.vote_model')); + + $type = (string)new VoteItems($type); + + /* @var \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder $vote */ + return tap($vote->where($attributes)->firstOr( + function () use ($vote, $attributes, $type) { + $vote->unguard(); + + if ($this->relationLoaded('votes')) { + $this->unsetRelation('votes'); + } + + return $vote->create(\array_merge($attributes, [ + 'type' => $type + ])); + } + ), function (Model $model) use ($type) { + $model->update(['type' => $type]); + }); + } + + /** + * @param \Illuminate\Database\Eloquent\Model $object + * + * @return bool + */ + public function hasVoted(Model $object, ?string $type = null): bool + { + return ($this->relationLoaded('votes') ? $this->votes : $this->votes()) + ->where('votable_id', $object->getKey()) + ->where('votable_type', $object->getMorphClass()) + ->when(\is_string($type), function ($builder) use ($type) { + $builder->where('type', (string)new VoteItems($type)); + }) + ->count() > 0; + } + + /** + * @param Model $object + * @return bool + * @throws \Exception + */ + public function cancelVote(Model $object): bool + { + /* @var \Jcc\LaravelVote\Vote $relation */ + $relation = \app(\config('vote.vote_model')) + ->where('votable_id', $object->getKey()) + ->where('votable_type', $object->getMorphClass()) + ->where(\config('vote.user_foreign_key'), $this->getKey()) + ->first(); + + if ($relation) { + if ($this->relationLoaded('votes')) { + $this->unsetRelation('votes'); + } + + return $relation->delete(); + } + + return true; + } + + /** + * @return HasMany + */ + public function votes(): HasMany + { + return $this->hasMany(\config('vote.vote_model'), \config('vote.user_foreign_key'), $this->getKeyName()); + } + + /** + * Get Query Builder for votes + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getVotedItems(string $model, ?string $type = null) + { + return \app($model)->whereHas( + 'voters', + function ($builder) use ($type) { + return $builder->where(\config('vote.user_foreign_key'), $this->getKey())->when(\is_string($type), function ($builder) use ($type) { + $builder->where('type', (string)new VoteItems($type)); + }); + } + ); + } + + public function upVote(Model $object): Vote + { + return $this->vote($object, VoteItems::UP); + } + + public function downVote(Model $object): Vote + { + return $this->vote($object, VoteItems::DOWN); + } + + public function hasUpVoted(Model $object) + { + return $this->hasVoted($object, VoteItems::UP); + } + + public function hasDownVoted(Model $object) + { + return $this->hasVoted($object, VoteItems::DOWN); + } + + public function toggleUpVote(Model $object) + { + return $this->hasUpVoted($object) ? $this->cancelVote($object) : $this->upVote($object); + } + + public function toggleDownVote(Model $object) + { + return $this->hasDownVoted($object) ? $this->cancelVote($object) : $this->downVote($object); + } + + public function getUpVotedItems(string $model) + { + return $this->getVotedItems($model, VoteItems::UP); + } + + public function getDownVotedItems(string $model) + { + return $this->getVotedItems($model, VoteItems::DOWN); + } +} diff --git a/src/Vote.php b/src/Vote.php index a154a4e..c6921f0 100644 --- a/src/Vote.php +++ b/src/Vote.php @@ -1,159 +1,99 @@ - * - * This source file is subject to the MIT license that is bundled - * with this source code in the file LICENSE. - */ - namespace Jcc\LaravelVote; -trait Vote +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Support\Facades\Auth; +use Jcc\LaravelVote\Events\CancelVoted; +use Jcc\LaravelVote\Events\DownVoted; +use Jcc\LaravelVote\Events\UpVoted; + +class Vote extends Model { - protected $voteRelation = __CLASS__; - - /** - * Up vote a item or items. - * - * @param int|array|\Illuminate\Database\Eloquent\Model $item - * - * @return boolean - */ - public function upVote($item) - { - $this->cancelVote($item); - - return $this->vote($item); - } - - /** - * Down vote a item or items. - * - * @param int|array|\Illuminate\Database\Eloquent\Model $item - * - * @return boolean - */ - public function downVote($item) - { - $this->cancelVote($item); - - return $this->vote($item, 'down_vote'); - } - - /** - * Vote a item or items. - * - * @param int|array|\Illuminate\Database\Eloquent\Model $item - * @param string $type - * @return boolean - */ - public function vote($item, $type = 'up_vote') - { - $items = array_fill_keys((array) $this->checkVoteItem($item), ['type' => $type]); - - return $this->votedItems()->sync($items, false); - } - - /** - * Cancel vote a item or items. - * - * @param int|array|\Illuminate\Database\Eloquent\Model $item - * - * @return int - */ - public function cancelVote($item) - { - $item = $this->checkVoteItem($item); - - return $this->votedItems()->detach((array)$item); - } - - /** - * Determine whether the type of 'up_vote' item exist - * - * @param $item - * - * @return boolean - */ - public function hasUpVoted($item) - { - return $this->hasVoted($item, 'up_vote'); - } - - /** - * Determine whether the type of 'down_vote' item exist - * - * @param $item - * - * @return boolean - */ - public function hasDownVoted($item) - { - return $this->hasVoted($item, 'down_vote'); - } - - /** - * Check if user has voted item. - * - * @param $item - * @param string $type - * - * @return bool - */ - public function hasVoted($item, $type = null) - { - $item = $this->checkVoteItem($item); - - $votedItems = $this->votedItems(); - - if(!is_null($type)) $votedItems->wherePivot('type', $type); - - return $votedItems->get()->contains($item); - } - - /** - * Return the user what has items. - * - * @param string $class - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - public function votedItems($class = null) - { - if (!empty($class)) { - $this->setVoteRelation($class); - } - - return $this->morphedByMany($this->voteRelation, 'votable', $this->vote_table ?: 'votes')->withTimestamps(); - } - - /** - * Determine whether $item is an instantiated object of \Illuminate\Database\Eloquent\Model - * - * @param $item - * - * @return int - */ - protected function checkVoteItem($item) - { - if ($item instanceof \Illuminate\Database\Eloquent\Model) { - $this->setVoteRelation(get_class($item)); - return $item->id; - }; - - return $item; - } - - /** - * Set the vote relation class. - * - * @param string $class - */ - protected function setVoteRelation($class) - { - return $this->voteRelation = $class; - } -} \ No newline at end of file + protected $guarded = []; + + protected $dispatchesEvents = [ + 'upVoted' => UpVoted::class, + 'downVoted' => DownVoted::class, + 'deleted' => CancelVoted::class, + ]; + + protected $observables = [ + 'upVoted', 'downVoted', + ]; + + /** + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + $this->table = \config('vote.votes_table'); + + parent::__construct($attributes); + } + + protected static function boot() + { + parent::boot(); + + self::creating(function (Vote $vote) { + $userForeignKey = \config('vote.user_foreign_key'); + $vote->{$userForeignKey} = $vote->{$userForeignKey} ?: Auth::id(); + }); + + $eventCallback = function (Vote $vote) { + if ($vote->isUp()) { + $vote->fireModelEvent('upVoted', false); + } + if ($vote->isDown()) { + $vote->fireModelEvent('downVoted', false); + } + }; + self::created($eventCallback); + self::updated($eventCallback); + } + + public function votable(): MorphTo + { + return $this->morphTo(); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(\config('auth.providers.users.model'), \config('vote.user_foreign_key')); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function voter() + { + return $this->user(); + } + + /** + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $type + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeWithType(Builder $query, string $type) + { + return $query->where('votable_type', \app($type)->getMorphClass()); + } + + public function isUp() + { + return $this->type === VoteItems::UP; + } + + public function isDown() + { + return $this->type === VoteItems::DOWN; + } +} diff --git a/src/VoteItems.php b/src/VoteItems.php new file mode 100644 index 0000000..aace629 --- /dev/null +++ b/src/VoteItems.php @@ -0,0 +1,36 @@ +value = $value; + } + + public static function getValues() + { + return [ self::UP, self::DOWN ]; + } + + public function __toString() + { + return $this->value; + } +} diff --git a/src/VoteServiceProvider.php b/src/VoteServiceProvider.php index b4ab598..af8595c 100644 --- a/src/VoteServiceProvider.php +++ b/src/VoteServiceProvider.php @@ -20,9 +20,17 @@ class VoteServiceProvider extends ServiceProvider */ public function boot() { + $this->publishes([ + \dirname(__DIR__) . '/config/vote.php' => config_path('vote.php'), + ], 'config'); + $this->publishes([ - __DIR__.'/../database/migrations/create_votes_table.php' => database_path('migrations/'.date('Y_m_d_His').'_create_votes_table.php'), + \dirname(__DIR__) . '/migrations/' => database_path('migrations'), ], 'migrations'); + + if ($this->app->runningInConsole()) { + $this->loadMigrationsFrom(\dirname(__DIR__) . '/migrations/'); + } } /** @@ -30,6 +38,9 @@ public function boot() */ public function register() { - // + $this->mergeConfigFrom( + \dirname(__DIR__) . '/config/vote.php', + 'vote' + ); } } \ No newline at end of file From 45f9bd8c75800f941ee63c22fbe4fac65bff05cf Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 02:09:05 +0800 Subject: [PATCH 02/16] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 3 +- .../2021_03_13_000000_create_votes_table.php | 31 +++ src/Events/DownVoted.php | 8 - src/Events/Event.php | 2 +- src/Events/{UpVoted.php => Voted.php} | 2 +- src/Traits/Votable.php | 16 +- src/Traits/Voter.php | 10 +- src/Vote.php | 42 ++- tests/Book.php | 13 + tests/FeatureTest.php | 251 ++++++++++++++++++ tests/Post.php | 13 + tests/TestCase.php | 42 ++- tests/User.php | 15 ++ .../2021_03_13_000000_create_books_table.php | 28 ++ .../2021_03_13_000000_create_posts_table.php | 28 ++ .../2021_03_13_000000_create_users_table.php | 28 ++ 16 files changed, 481 insertions(+), 51 deletions(-) create mode 100644 migrations/2021_03_13_000000_create_votes_table.php delete mode 100644 src/Events/DownVoted.php rename src/Events/{UpVoted.php => Voted.php} (62%) create mode 100644 tests/Book.php create mode 100644 tests/FeatureTest.php create mode 100644 tests/Post.php create mode 100644 tests/User.php create mode 100644 tests/migrations/2021_03_13_000000_create_books_table.php create mode 100644 tests/migrations/2021_03_13_000000_create_posts_table.php create mode 100644 tests/migrations/2021_03_13_000000_create_users_table.php diff --git a/composer.json b/composer.json index ca99516..bab71d2 100644 --- a/composer.json +++ b/composer.json @@ -14,8 +14,7 @@ }, "require-dev": { "mockery/mockery": "^1.3", - "orchestra/testbench": "^3.5|~4.0|~5.0|~6.0", - "phpunit/phpunit": "^8.5|~9.0" + "orchestra/testbench": "^3.5|~4.0|~5.0|~6.0" }, "license": "MIT", "autoload": { diff --git a/migrations/2021_03_13_000000_create_votes_table.php b/migrations/2021_03_13_000000_create_votes_table.php new file mode 100644 index 0000000..29444b0 --- /dev/null +++ b/migrations/2021_03_13_000000_create_votes_table.php @@ -0,0 +1,31 @@ +increments('id'); + $table->unsignedBigInteger(config('vote.user_foreign_key'))->index(); + $table->morphs('votable'); + $table->string('vote_type', 16)->default('up_vote'); // 'up_vote'/'down_vote' + $table->timestamps(); + }); + + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists(config('vote.votes_table')); + } +} diff --git a/src/Events/DownVoted.php b/src/Events/DownVoted.php deleted file mode 100644 index a586e69..0000000 --- a/src/Events/DownVoted.php +++ /dev/null @@ -1,8 +0,0 @@ -relationLoaded('voters')) { @@ -27,7 +27,7 @@ public function isVoteBy(Model $user, ?string $type = null): bool return $this->voters() ->where(\config('vote.user_foreign_key'), $user->getKey()) ->when(is_string($type), function ($builder) use ($type) { - $builder->where('type', (string)new VoteItems($type)); + $builder->where('vote_type', (string)new VoteItems($type)); }) ->exists(); } @@ -56,9 +56,9 @@ public function voters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany * * @return bool */ - public function isUpVoteBy(Model $user) + public function isUpVotedBy(Model $user) { - return $this->isVoteBy($user, VoteItems::UP); + return $this->isVotedBy($user, VoteItems::UP); } /** @@ -68,7 +68,7 @@ public function isUpVoteBy(Model $user) */ public function upVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { - return $this->voters()->where('type', VoteItems::UP); + return $this->voters()->where('vote_type', VoteItems::UP); } /** @@ -76,9 +76,9 @@ public function upVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMan * * @return bool */ - public function isDownVoteBy(Model $user) + public function isDownVotedBy(Model $user) { - return $this->isVoteBy($user, VoteItems::DOWN); + return $this->isVotedBy($user, VoteItems::DOWN); } /** @@ -88,6 +88,6 @@ public function isDownVoteBy(Model $user) */ public function downVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { - return $this->voters()->where('type', VoteItems::DOWN); + return $this->voters()->where('vote_type', VoteItems::DOWN); } } diff --git a/src/Traits/Voter.php b/src/Traits/Voter.php index f5c0d6f..15f523d 100644 --- a/src/Traits/Voter.php +++ b/src/Traits/Voter.php @@ -20,7 +20,7 @@ trait Voter * @return \Jcc\LaravelVote\Vote * @throws \Jcc\LaravelVote\Exceptions\UnexpectValueException */ - protected function vote(Model $object, string $type): Vote + public function vote(Model $object, string $type): Vote { $attributes = [ 'votable_type' => $object->getMorphClass(), @@ -43,11 +43,11 @@ function () use ($vote, $attributes, $type) { } return $vote->create(\array_merge($attributes, [ - 'type' => $type + 'vote_type' => $type ])); } ), function (Model $model) use ($type) { - $model->update(['type' => $type]); + $model->update(['vote_type' => $type]); }); } @@ -62,7 +62,7 @@ public function hasVoted(Model $object, ?string $type = null): bool ->where('votable_id', $object->getKey()) ->where('votable_type', $object->getMorphClass()) ->when(\is_string($type), function ($builder) use ($type) { - $builder->where('type', (string)new VoteItems($type)); + $builder->where('vote_type', (string)new VoteItems($type)); }) ->count() > 0; } @@ -111,7 +111,7 @@ public function getVotedItems(string $model, ?string $type = null) 'voters', function ($builder) use ($type) { return $builder->where(\config('vote.user_foreign_key'), $this->getKey())->when(\is_string($type), function ($builder) use ($type) { - $builder->where('type', (string)new VoteItems($type)); + $builder->where('vote_type', (string)new VoteItems($type)); }); } ); diff --git a/src/Vote.php b/src/Vote.php index c6921f0..a9d8fac 100644 --- a/src/Vote.php +++ b/src/Vote.php @@ -2,26 +2,22 @@ namespace Jcc\LaravelVote; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Facades\Auth; use Jcc\LaravelVote\Events\CancelVoted; -use Jcc\LaravelVote\Events\DownVoted; -use Jcc\LaravelVote\Events\UpVoted; +use Jcc\LaravelVote\Events\Voted; class Vote extends Model { protected $guarded = []; protected $dispatchesEvents = [ - 'upVoted' => UpVoted::class, - 'downVoted' => DownVoted::class, - 'deleted' => CancelVoted::class, - ]; + 'created' => Voted::class, + 'updated' => Voted::class, - protected $observables = [ - 'upVoted', 'downVoted', + 'deleted' => CancelVoted::class, ]; /** @@ -42,17 +38,6 @@ protected static function boot() $userForeignKey = \config('vote.user_foreign_key'); $vote->{$userForeignKey} = $vote->{$userForeignKey} ?: Auth::id(); }); - - $eventCallback = function (Vote $vote) { - if ($vote->isUp()) { - $vote->fireModelEvent('upVoted', false); - } - if ($vote->isDown()) { - $vote->fireModelEvent('downVoted', false); - } - }; - self::created($eventCallback); - self::updated($eventCallback); } public function votable(): MorphTo @@ -82,17 +67,28 @@ public function voter() * * @return \Illuminate\Database\Eloquent\Builder */ - public function scopeWithType(Builder $query, string $type) + public function scopeWithVotableType(Builder $query, string $type) { return $query->where('votable_type', \app($type)->getMorphClass()); } - public function isUp() + /** + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $type + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeWithVoteType(Builder $query, string $type) + { + return $query->where('vote_type', (string)new VoteItems($type)); + } + + public function isUp(): bool { return $this->type === VoteItems::UP; } - public function isDown() + public function isDown(): bool { return $this->type === VoteItems::DOWN; } diff --git a/tests/Book.php b/tests/Book.php new file mode 100644 index 0000000..6236778 --- /dev/null +++ b/tests/Book.php @@ -0,0 +1,13 @@ + User::class]); + } + + public function test_basic_features() + { + /** @var User $user */ + $user = User::create(['name' => 'jcc']); + /** @var Post $post */ + $post = Post::create(['title' => 'Hello world!']); + + $user->vote($post, VoteItems::UP); + + Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { + $vote = $event->vote; + return $vote->votable instanceof Post + && $vote->user instanceof User + && $vote->user->id === $user->id + && $vote->votable->id === $post->id; + }); + + self::assertTrue($user->hasVoted($post)); + self::assertTrue($user->hasUpVoted($post)); + self::assertFalse($user->hasDownVoted($post)); + + self::assertTrue($post->isVotedBy($user)); + self::assertTrue($post->isUpVotedBy($user)); + self::assertFalse($post->isDownVotedBy($user)); + + self::assertTrue($user->cancelVote($post)); + + $user->downVote($post); + + Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { + $vote = $event->vote; + return $vote->votable instanceof Post + && $vote->user instanceof User + && $vote->user->id === $user->id + && $vote->votable->id === $post->id; + }); + + self::assertTrue($user->hasVoted($post)); + self::assertFalse($user->hasUpVoted($post)); + self::assertTrue($user->hasDownVoted($post)); + + self::assertTrue($post->isVotedBy($user)); + self::assertFalse($post->isUpVotedBy($user)); + self::assertTrue($post->isDownVotedBy($user)); + } + + public function test_cancelVote_features() + { + $user1 = User::create(['name' => 'jcc']); + $user2 = User::create(['name' => 'allen']); + + $post = Post::create(['title' => 'Hello world!']); + + $user1->vote($post, VoteItems::DOWN); + $user2->vote($post, VoteItems::UP); + + $user1->cancelVote($post); + $user2->cancelVote($post); + + self::assertFalse($user1->hasVote($post)); + self::assertFalse($user1->hasDownVote($post)); + self::assertFalse($user1->hasUpVote($post)); + + self::assertFalse($user1->hasVote($post)); + self::assertFalse($user2->hasUpVote($post)); + self::assertFalse($user2->hasDownVote($post)); + } + + public function test_upVoted_to_downVoted_each_other_features() + { + $user1 = User::create(['name' => 'jcc']); + $user2 = User::create(['name' => 'allen']); + $post = Post::create(['title' => 'Hello world!']); + + $user1->vote($post, VoteItems::UP); + self::assertTrue($user1->hasUpVote($post)); + self::assertFalse($user1->hasDownVote($post)); + + $user1->vote($post, VoteItems::DOWN); + self::assertFalse($user1->hasUpVote($post)); + self::assertTrue($user1->hasDownVote($post)); + + $user2->vote($post, VoteItems::DOWN); + self::assertFalse($user2->hasUpVote($post)); + self::assertTrue($user2->hasDownVote($post)); + + $user2->vote($post, VoteItems::UP); + self::assertTrue($user2->hasUpVote($post)); + self::assertFalse($user2->hasDownVote($post)); + } + + public function test_aggregations() + { + $user = User::create(['name' => 'jcc']); + + $post1 = Post::create(['title' => 'Hello world!']); + $post2 = Post::create(['title' => 'Hello everyone!']); + $post3 = Post::create(['title' => 'Hello players!']); + $book1 = Book::create(['title' => 'Learn laravel.']); + $book2 = Book::create(['title' => 'Learn symfony.']); + $book3 = Book::create(['title' => 'Learn yii2.']); + + $user->vote($post1, VoteItems::UP); + $user->vote($post2, VoteItems::DOWN); + $user->downVote($post3); + + $user->vote($book1, VoteItems::UP); + $user->vote($book2, VoteItems::DOWN); + $user->downVote($book3); + + self::assertSame(6, $user->votes()->count()); + self::assertSame(4, $user->votes()->withVoteType(VoteItems::UP)->count()); + self::assertSame(2, $user->votes()->withVoteType(VoteItems::DOWN)->count()); + + self::assertSame(3, $user->votes()->withVotableType(Book::class)->count()); + self::assertSame(1, $user->votes()->withVoteType(VoteItems::DOWN)->withVotableType(Book::class)->count()); + } + + public function test_vote_same_model() + { + $user1 = User::create(['name' => 'jcc']); + $user2 = User::create(['name' => 'allen']); + $user3 = User::create(['name' => 'taylor']); + + $user1->vote($user2, VoteItems::UP); + $user3->vote($user1, VoteItems::DOWN); + + self::assertTrue($user1->hasVoted($user2)); + self::assertTrue($user2->isVotedBy($user1)); + + self::assertTrue($user1->hasUpVoted($user2)); + self::assertTrue($user2->isUpVotedBy($user1)); + + self::assertTrue($user3->hasVoted($user1)); + self::assertTrue($user1->isVotedBy($user3)); + + self::assertTrue($user3->hasDownVoted($user1)); + self::assertTrue($user1->isDownVotedBy($user3)); + } + + public function test_object_voters() + { + $user1 = User::create(['name' => 'jcc']); + $user2 = User::create(['name' => 'allen']); + $user3 = User::create(['name' => 'taylor']); + + $post = Post::create(['title' => 'Hello world!']); + + $user1->vote($post, VoteItems::UP); + $user2->vote($post, VoteItems::DOWN); + + self::assertCount(2, $post->voters); + self::assertSame('jcc', $post->voters[0]['name']); + self::assertSame('allen', $post->voters[1]['name']); + + $sqls = $this->getQueryLog(function () use ($post, $user1, $user2, $user3) { + self::assertTrue($post->isVotedBy($user1)); + self::assertTrue($post->isVotedBy($user2)); + self::assertFalse($post->isVotedBy($user3)); + }); + + self::assertEmpty($sqls->all()); + } + + public function test_object_likers_with_custom_morph_class_name() + { + $user1 = User::create(['name' => 'jcc']); + $user2 = User::create(['name' => 'allen']); + $user3 = User::create(['name' => 'taylor']); + + $post = Post::create(['title' => 'Hello world!']); + + Relation::morphMap([ + 'posts' => Post::class, + ]); + + $user1->vote($post, VoteItems::UP); + $user2->vote($post, VoteItems::DOWN); + + self::assertCount(2, $post->voters); + self::assertSame('jcc', $post->voters[0]['name']); + self::assertSame('allen', $post->voters[1]['name']); + } + + public function test_eager_loading() + { + $user = User::create(['name' => 'jcc']); + + $post1 = Post::create(['title' => 'Hello world!']); + $post2 = Post::create(['title' => 'Hello everyone!']); + $book1 = Book::create(['title' => 'Learn laravel.']); + $book2 = Book::create(['title' => 'Learn symfony.']); + + $user->vote($post1, VoteItems::UP); + $user->vote($post2, VoteItems::DOWN); + $user->vote($book1, VoteItems::UP); + $user->vote($book2, VoteItems::DOWN); + + // start recording + $sqls = $this->getQueryLog(function () use ($user) { + $user->load('votes.votable'); + }); + + self::assertSame(3, $sqls->count()); + + // from loaded relations + $sqls = $this->getQueryLog(function () use ($user, $post1) { + $user->hasVoted($post1); + }); + + self::assertEmpty($sqls->all()); + } + + /** + * @param \Closure $callback + * + * @return \Illuminate\Support\Collection + */ + protected function getQueryLog(\Closure $callback): \Illuminate\Support\Collection + { + $sqls = \collect([]); + DB::listen(function ($query) use ($sqls) { + $sqls->push(['sql' => $query->sql, 'bindings' => $query->bindings]); + }); + + $callback(); + + return $sqls; + } +} diff --git a/tests/Post.php b/tests/Post.php new file mode 100644 index 0000000..d0db79b --- /dev/null +++ b/tests/Post.php @@ -0,0 +1,13 @@ +set('database.default', 'testing'); + $app['config']->set('database.connections.testing', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + + /** + * run package database migrations. + */ + public function setUp(): void + { + parent::setUp(); + $this->loadMigrationsFrom(__DIR__.'/migrations'); + $this->loadMigrationsFrom(dirname(__DIR__).'/migrations'); + } } diff --git a/tests/User.php b/tests/User.php new file mode 100644 index 0000000..f17ac58 --- /dev/null +++ b/tests/User.php @@ -0,0 +1,15 @@ +increments('id'); + $table->string('title'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('books'); + } +} \ No newline at end of file diff --git a/tests/migrations/2021_03_13_000000_create_posts_table.php b/tests/migrations/2021_03_13_000000_create_posts_table.php new file mode 100644 index 0000000..4428bd0 --- /dev/null +++ b/tests/migrations/2021_03_13_000000_create_posts_table.php @@ -0,0 +1,28 @@ +increments('id'); + $table->string('title'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('posts'); + } +} \ No newline at end of file diff --git a/tests/migrations/2021_03_13_000000_create_users_table.php b/tests/migrations/2021_03_13_000000_create_users_table.php new file mode 100644 index 0000000..ea72a8a --- /dev/null +++ b/tests/migrations/2021_03_13_000000_create_users_table.php @@ -0,0 +1,28 @@ +increments('id'); + $table->string('name'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('users'); + } +} \ No newline at end of file From 381becd09d729dfc1926f5237b301136c60fbf77 Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 02:11:53 +0800 Subject: [PATCH 03/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=90=8D=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/FeatureTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FeatureTest.php b/tests/FeatureTest.php index a7f9737..cc526e3 100644 --- a/tests/FeatureTest.php +++ b/tests/FeatureTest.php @@ -183,7 +183,7 @@ public function test_object_voters() self::assertEmpty($sqls->all()); } - public function test_object_likers_with_custom_morph_class_name() + public function test_object_votes_with_custom_morph_class_name() { $user1 = User::create(['name' => 'jcc']); $user2 = User::create(['name' => 'allen']); From 0d6b3451921cfae9260822f1a4cff317ab78b9cd Mon Sep 17 00:00:00 2001 From: Jim Chen Date: Sun, 14 Mar 2021 02:39:51 +0800 Subject: [PATCH 04/16] Create php.yml --- .github/workflows/php.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/php.yml diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..65aa183 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,24 @@ +name: Test +on: [push, pull_request] + +jobs: + phpunit: + name: PHP-${{ matrix.php_version }}-${{ matrix.perfer }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php_version: + - 7.2 + - 7.3 + - 7.4 + perfer: + - stable + container: + image: nauxliu/php-ci-image:${{ matrix.php_version }} + steps: + - uses: actions/checkout@master + - name: Install Dependencies + run: composer install --prefer-dist --no-interaction --no-suggest --prefer-${{ matrix.perfer }} + - name: Run PHPUnit + run: ./vendor/bin/phpunit From 7aa5d1ca3c6a33f1c42f57e1c463126460deb472 Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 02:44:54 +0800 Subject: [PATCH 05/16] =?UTF-8?q?=E6=9B=B4=E6=96=B0CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 65aa183..8d1654a 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -19,6 +19,6 @@ jobs: steps: - uses: actions/checkout@master - name: Install Dependencies - run: composer install --prefer-dist --no-interaction --no-suggest --prefer-${{ matrix.perfer }} + run: composer install --prefer-dist --no-interaction --no-suggest - name: Run PHPUnit run: ./vendor/bin/phpunit From 0a1b113eff81058cb2a193c64c18e6249df8fb81 Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 02:48:54 +0800 Subject: [PATCH 06/16] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/FeatureTest.php | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/FeatureTest.php b/tests/FeatureTest.php index cc526e3..fe977f2 100644 --- a/tests/FeatureTest.php +++ b/tests/FeatureTest.php @@ -46,7 +46,7 @@ public function test_basic_features() self::assertTrue($user->cancelVote($post)); - $user->downVote($post); + $user->vote($post, VoteItems::DOWN); Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { $vote = $event->vote; @@ -78,13 +78,13 @@ public function test_cancelVote_features() $user1->cancelVote($post); $user2->cancelVote($post); - self::assertFalse($user1->hasVote($post)); - self::assertFalse($user1->hasDownVote($post)); - self::assertFalse($user1->hasUpVote($post)); + self::assertFalse($user1->hasVoted($post)); + self::assertFalse($user1->hasDownVoted($post)); + self::assertFalse($user1->hasUpVoted($post)); - self::assertFalse($user1->hasVote($post)); - self::assertFalse($user2->hasUpVote($post)); - self::assertFalse($user2->hasDownVote($post)); + self::assertFalse($user1->hasVoted($post)); + self::assertFalse($user2->hasUpVoted($post)); + self::assertFalse($user2->hasDownVoted($post)); } public function test_upVoted_to_downVoted_each_other_features() @@ -94,20 +94,20 @@ public function test_upVoted_to_downVoted_each_other_features() $post = Post::create(['title' => 'Hello world!']); $user1->vote($post, VoteItems::UP); - self::assertTrue($user1->hasUpVote($post)); - self::assertFalse($user1->hasDownVote($post)); + self::assertTrue($user1->hasUpVoted($post)); + self::assertFalse($user1->hasDownVoted($post)); $user1->vote($post, VoteItems::DOWN); - self::assertFalse($user1->hasUpVote($post)); - self::assertTrue($user1->hasDownVote($post)); + self::assertFalse($user1->hasUpVoted($post)); + self::assertTrue($user1->hasDownVoted($post)); $user2->vote($post, VoteItems::DOWN); - self::assertFalse($user2->hasUpVote($post)); - self::assertTrue($user2->hasDownVote($post)); + self::assertFalse($user2->hasUpVoted($post)); + self::assertTrue($user2->hasDownVoted($post)); $user2->vote($post, VoteItems::UP); - self::assertTrue($user2->hasUpVote($post)); - self::assertFalse($user2->hasDownVote($post)); + self::assertTrue($user2->hasUpVoted($post)); + self::assertFalse($user2->hasDownVoted($post)); } public function test_aggregations() @@ -122,12 +122,12 @@ public function test_aggregations() $book3 = Book::create(['title' => 'Learn yii2.']); $user->vote($post1, VoteItems::UP); - $user->vote($post2, VoteItems::DOWN); - $user->downVote($post3); + $user->vote($post2, VoteItems::UP); + $user->vote($post3, VoteItems::DOWN); $user->vote($book1, VoteItems::UP); - $user->vote($book2, VoteItems::DOWN); - $user->downVote($book3); + $user->vote($book2, VoteItems::UP); + $user->vote($book3, VoteItems::DOWN); self::assertSame(6, $user->votes()->count()); self::assertSame(4, $user->votes()->withVoteType(VoteItems::UP)->count()); From d0c25e7b98984371c62efaed69f4eb9d9497d0ee Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 09:46:01 +0800 Subject: [PATCH 07/16] fix style --- .github/workflows/lint.yml | 22 +++++++++ .php_cs | 48 +++++++++++++++++++ composer.json | 10 +++- src/Events/CancelVoted.php | 1 - src/Events/Voted.php | 1 - src/Exceptions/UnexpectValueException.php | 1 - src/Traits/Votable.php | 6 +-- src/Traits/Voter.php | 17 +++---- src/Vote.php | 6 +-- src/VoteItems.php | 2 +- src/VoteServiceProvider.php | 8 ++-- tests/Book.php | 2 +- tests/FeatureTest.php | 2 + tests/Post.php | 2 +- tests/TestCase.php | 10 ++-- .../2021_03_13_000000_create_books_table.php | 2 +- .../2021_03_13_000000_create_posts_table.php | 2 +- .../2021_03_13_000000_create_users_table.php | 2 +- 18 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .php_cs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..90a6f7c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Lint +on: [ push, pull_request ] + +jobs: + lint: + name: PHP-${{ matrix.php_version }}-${{ matrix.perfer }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php_version: + - 7.4 + perfer: + - stable + container: + image: nauxliu/php-ci-image:${{ matrix.php_version }} + steps: + - uses: actions/checkout@master + - name: Install Dependencies + run: composer install --prefer-dist --no-interaction --no-suggest + - name: Check Style + run: composer check-style \ No newline at end of file diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..30d36cd --- /dev/null +++ b/.php_cs @@ -0,0 +1,48 @@ +setRules([ + '@PSR2' => true, + 'binary_operator_spaces' => true, + 'blank_line_after_opening_tag' => true, + 'compact_nullable_typehint' => true, + 'declare_equal_normalize' => true, + 'lowercase_cast' => true, + 'lowercase_static_reference' => true, + 'new_with_braces' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_leading_import_slash' => true, + 'no_whitespace_in_blank_line' => true, + 'ordered_class_elements' => [ + 'order' => [ + 'use_trait', + ], + ], + 'ordered_imports' => [ + 'imports_order' => [ + 'class', + 'function', + 'const', + ], + 'sort_algorithm' => 'none', + ], + 'return_type_declaration' => true, + 'short_scalar_cast' => true, + 'single_blank_line_before_namespace' => true, + 'single_trait_insert_per_statement' => true, + 'ternary_operator_spaces' => true, + 'unary_operator_spaces' => true, + 'visibility_required' => [ + 'elements' => [ + 'const', + 'method', + 'property', + ], + ], + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->exclude('vendor') + ->in([__DIR__.'/src/', __DIR__.'/tests/']) + ) +; \ No newline at end of file diff --git a/composer.json b/composer.json index bab71d2..54655b3 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ }, "require-dev": { "mockery/mockery": "^1.3", - "orchestra/testbench": "^3.5|~4.0|~5.0|~6.0" + "orchestra/testbench": "^3.5|~4.0|~5.0|~6.0", + "friendsofphp/php-cs-fixer": "^2.18" }, "license": "MIT", "autoload": { @@ -35,5 +36,10 @@ } }, "minimum-stability": "stable", - "prefer-stable": true + "prefer-stable": true, + "scripts": { + "check-style": "php-cs-fixer fix --using-cache=no --diff --config=.php_cs --dry-run --ansi", + "fix-style": "php-cs-fixer fix --using-cache=no --config=.php_cs --ansi", + "test": "vendor/bin/phpunit --colors=always" + } } diff --git a/src/Events/CancelVoted.php b/src/Events/CancelVoted.php index 9795977..f23109e 100644 --- a/src/Events/CancelVoted.php +++ b/src/Events/CancelVoted.php @@ -4,5 +4,4 @@ class CancelVoted extends Event { - } diff --git a/src/Events/Voted.php b/src/Events/Voted.php index e512469..8d82604 100644 --- a/src/Events/Voted.php +++ b/src/Events/Voted.php @@ -4,5 +4,4 @@ class Voted extends Event { - } diff --git a/src/Exceptions/UnexpectValueException.php b/src/Exceptions/UnexpectValueException.php index c751b14..87aab9d 100644 --- a/src/Exceptions/UnexpectValueException.php +++ b/src/Exceptions/UnexpectValueException.php @@ -6,5 +6,4 @@ class UnexpectValueException extends Exception { - } diff --git a/src/Traits/Votable.php b/src/Traits/Votable.php index 56b17b1..b847800 100644 --- a/src/Traits/Votable.php +++ b/src/Traits/Votable.php @@ -60,7 +60,7 @@ public function isUpVotedBy(Model $user) { return $this->isVotedBy($user, VoteItems::UP); } - + /** * Return up voters. * @@ -68,7 +68,7 @@ public function isUpVotedBy(Model $user) */ public function upVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { - return $this->voters()->where('vote_type', VoteItems::UP); + return $this->voters()->where('vote_type', VoteItems::UP); } /** @@ -88,6 +88,6 @@ public function isDownVotedBy(Model $user) */ public function downVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { - return $this->voters()->where('vote_type', VoteItems::DOWN); + return $this->voters()->where('vote_type', VoteItems::DOWN); } } diff --git a/src/Traits/Voter.php b/src/Traits/Voter.php index 15f523d..ac252f8 100644 --- a/src/Traits/Voter.php +++ b/src/Traits/Voter.php @@ -15,7 +15,7 @@ trait Voter { /** - * @param Model $object + * @param Model $object * @param string $type * @return \Jcc\LaravelVote\Vote * @throws \Jcc\LaravelVote\Exceptions\UnexpectValueException @@ -23,8 +23,8 @@ trait Voter public function vote(Model $object, string $type): Vote { $attributes = [ - 'votable_type' => $object->getMorphClass(), - 'votable_id' => $object->getKey(), + 'votable_type' => $object->getMorphClass(), + 'votable_id' => $object->getKey(), \config('vote.user_foreign_key') => $this->getKey(), ]; @@ -43,7 +43,7 @@ function () use ($vote, $attributes, $type) { } return $vote->create(\array_merge($attributes, [ - 'vote_type' => $type + 'vote_type' => $type, ])); } ), function (Model $model) use ($type) { @@ -52,7 +52,7 @@ function () use ($vote, $attributes, $type) { } /** - * @param \Illuminate\Database\Eloquent\Model $object + * @param \Illuminate\Database\Eloquent\Model $object * * @return bool */ @@ -110,9 +110,10 @@ public function getVotedItems(string $model, ?string $type = null) return \app($model)->whereHas( 'voters', function ($builder) use ($type) { - return $builder->where(\config('vote.user_foreign_key'), $this->getKey())->when(\is_string($type), function ($builder) use ($type) { - $builder->where('vote_type', (string)new VoteItems($type)); - }); + return $builder->where(\config('vote.user_foreign_key'), $this->getKey())->when(\is_string($type), + function ($builder) use ($type) { + $builder->where('vote_type', (string)new VoteItems($type)); + }); } ); } diff --git a/src/Vote.php b/src/Vote.php index a9d8fac..8bf5e01 100644 --- a/src/Vote.php +++ b/src/Vote.php @@ -14,10 +14,10 @@ class Vote extends Model protected $guarded = []; protected $dispatchesEvents = [ - 'created' => Voted::class, - 'updated' => Voted::class, + 'created' => Voted::class, + 'updated' => Voted::class, - 'deleted' => CancelVoted::class, + 'deleted' => CancelVoted::class, ]; /** diff --git a/src/VoteItems.php b/src/VoteItems.php index aace629..2e06329 100644 --- a/src/VoteItems.php +++ b/src/VoteItems.php @@ -26,7 +26,7 @@ public function __construct(string $value) public static function getValues() { - return [ self::UP, self::DOWN ]; + return [self::UP, self::DOWN]; } public function __toString() diff --git a/src/VoteServiceProvider.php b/src/VoteServiceProvider.php index af8595c..678ef4f 100644 --- a/src/VoteServiceProvider.php +++ b/src/VoteServiceProvider.php @@ -24,9 +24,9 @@ public function boot() \dirname(__DIR__) . '/config/vote.php' => config_path('vote.php'), ], 'config'); - $this->publishes([ - \dirname(__DIR__) . '/migrations/' => database_path('migrations'), - ], 'migrations'); + $this->publishes([ + \dirname(__DIR__) . '/migrations/' => database_path('migrations'), + ], 'migrations'); if ($this->app->runningInConsole()) { $this->loadMigrationsFrom(\dirname(__DIR__) . '/migrations/'); @@ -43,4 +43,4 @@ public function register() 'vote' ); } -} \ No newline at end of file +} diff --git a/tests/Book.php b/tests/Book.php index 6236778..9493ee2 100644 --- a/tests/Book.php +++ b/tests/Book.php @@ -7,7 +7,7 @@ class Book extends Model { - use Votable; + use Votable; protected $fillable = ['title']; } diff --git a/tests/FeatureTest.php b/tests/FeatureTest.php index fe977f2..2ec322a 100644 --- a/tests/FeatureTest.php +++ b/tests/FeatureTest.php @@ -30,6 +30,7 @@ public function test_basic_features() Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { $vote = $event->vote; + return $vote->votable instanceof Post && $vote->user instanceof User && $vote->user->id === $user->id @@ -50,6 +51,7 @@ public function test_basic_features() Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { $vote = $event->vote; + return $vote->votable instanceof Post && $vote->user instanceof User && $vote->user->id === $user->id diff --git a/tests/Post.php b/tests/Post.php index d0db79b..590fe8b 100644 --- a/tests/Post.php +++ b/tests/Post.php @@ -7,7 +7,7 @@ class Post extends Model { - use Votable; + use Votable; protected $fillable = ['title']; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 270e0e7..23d444d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -15,7 +15,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase */ protected function getPackageProviders($app) { - return [ VoteServiceProvider::class ]; + return [VoteServiceProvider::class]; } /** @@ -28,9 +28,9 @@ protected function getEnvironmentSetUp($app) // Setup default database to use sqlite :memory: $app['config']->set('database.default', 'testing'); $app['config']->set('database.connections.testing', [ - 'driver' => 'sqlite', + 'driver' => 'sqlite', 'database' => ':memory:', - 'prefix' => '', + 'prefix' => '', ]); } @@ -40,7 +40,7 @@ protected function getEnvironmentSetUp($app) public function setUp(): void { parent::setUp(); - $this->loadMigrationsFrom(__DIR__.'/migrations'); - $this->loadMigrationsFrom(dirname(__DIR__).'/migrations'); + $this->loadMigrationsFrom(__DIR__ . '/migrations'); + $this->loadMigrationsFrom(dirname(__DIR__) . '/migrations'); } } diff --git a/tests/migrations/2021_03_13_000000_create_books_table.php b/tests/migrations/2021_03_13_000000_create_books_table.php index f14a586..ab863c3 100644 --- a/tests/migrations/2021_03_13_000000_create_books_table.php +++ b/tests/migrations/2021_03_13_000000_create_books_table.php @@ -25,4 +25,4 @@ public function down() { Schema::dropIfExists('books'); } -} \ No newline at end of file +} diff --git a/tests/migrations/2021_03_13_000000_create_posts_table.php b/tests/migrations/2021_03_13_000000_create_posts_table.php index 4428bd0..a1409c2 100644 --- a/tests/migrations/2021_03_13_000000_create_posts_table.php +++ b/tests/migrations/2021_03_13_000000_create_posts_table.php @@ -25,4 +25,4 @@ public function down() { Schema::dropIfExists('posts'); } -} \ No newline at end of file +} diff --git a/tests/migrations/2021_03_13_000000_create_users_table.php b/tests/migrations/2021_03_13_000000_create_users_table.php index ea72a8a..a4aa67d 100644 --- a/tests/migrations/2021_03_13_000000_create_users_table.php +++ b/tests/migrations/2021_03_13_000000_create_users_table.php @@ -25,4 +25,4 @@ public function down() { Schema::dropIfExists('users'); } -} \ No newline at end of file +} From d3f59fcee9c067d7326ace2f275c7bc9eb907d2f Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 09:49:58 +0800 Subject: [PATCH 08/16] fix style --- .php_cs | 2 +- config/vote.php | 2 +- migrations/2021_03_13_000000_create_votes_table.php | 1 - src/Traits/Voter.php | 6 ++++-- tests/Book.php | 2 +- tests/Post.php | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.php_cs b/.php_cs index 30d36cd..dd00957 100644 --- a/.php_cs +++ b/.php_cs @@ -43,6 +43,6 @@ return PhpCsFixer\Config::create() ->setFinder( PhpCsFixer\Finder::create() ->exclude('vendor') - ->in([__DIR__.'/src/', __DIR__.'/tests/']) + ->in([__DIR__.'/src/', __DIR__.'/tests/', __DIR__.'/config/', __DIR__.'/migrations/']) ) ; \ No newline at end of file diff --git a/config/vote.php b/config/vote.php index 8bbf47d..e918bde 100644 --- a/config/vote.php +++ b/config/vote.php @@ -7,4 +7,4 @@ 'user_foreign_key' => 'user_id', 'vote_model' => \Jcc\LaravelVote\Vote::class, -]; \ No newline at end of file +]; diff --git a/migrations/2021_03_13_000000_create_votes_table.php b/migrations/2021_03_13_000000_create_votes_table.php index 29444b0..2878cce 100644 --- a/migrations/2021_03_13_000000_create_votes_table.php +++ b/migrations/2021_03_13_000000_create_votes_table.php @@ -18,7 +18,6 @@ public function up() $table->string('vote_type', 16)->default('up_vote'); // 'up_vote'/'down_vote' $table->timestamps(); }); - } /** diff --git a/src/Traits/Voter.php b/src/Traits/Voter.php index ac252f8..1adcc2e 100644 --- a/src/Traits/Voter.php +++ b/src/Traits/Voter.php @@ -110,10 +110,12 @@ public function getVotedItems(string $model, ?string $type = null) return \app($model)->whereHas( 'voters', function ($builder) use ($type) { - return $builder->where(\config('vote.user_foreign_key'), $this->getKey())->when(\is_string($type), + return $builder->where(\config('vote.user_foreign_key'), $this->getKey())->when( + \is_string($type), function ($builder) use ($type) { $builder->where('vote_type', (string)new VoteItems($type)); - }); + } + ); } ); } diff --git a/tests/Book.php b/tests/Book.php index 9493ee2..e14d1d7 100644 --- a/tests/Book.php +++ b/tests/Book.php @@ -9,5 +9,5 @@ class Book extends Model { use Votable; - protected $fillable = ['title']; + protected $fillable = ['title']; } diff --git a/tests/Post.php b/tests/Post.php index 590fe8b..e3222e6 100644 --- a/tests/Post.php +++ b/tests/Post.php @@ -9,5 +9,5 @@ class Post extends Model { use Votable; - protected $fillable = ['title']; + protected $fillable = ['title']; } From 240b6a7cc6ddf98f709fb2b392a47e4d086955e7 Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 09:50:27 +0800 Subject: [PATCH 09/16] fix style --- config/vote.php | 6 +- .../2021_03_13_000000_create_votes_table.php | 40 +- src/Events/Event.php | 26 +- src/Traits/Votable.php | 142 +++--- src/Traits/Voter.php | 290 ++++++------ src/Vote.php | 162 +++---- src/VoteItems.php | 52 +-- src/VoteServiceProvider.php | 26 +- tests/FeatureTest.php | 422 +++++++++--------- tests/TestCase.php | 70 +-- tests/User.php | 6 +- .../2021_03_13_000000_create_books_table.php | 36 +- .../2021_03_13_000000_create_posts_table.php | 36 +- .../2021_03_13_000000_create_users_table.php | 36 +- 14 files changed, 675 insertions(+), 675 deletions(-) diff --git a/config/vote.php b/config/vote.php index e918bde..a37ca87 100644 --- a/config/vote.php +++ b/config/vote.php @@ -2,9 +2,9 @@ return [ - 'votes_table' => 'votes', + 'votes_table' => 'votes', - 'user_foreign_key' => 'user_id', + 'user_foreign_key' => 'user_id', - 'vote_model' => \Jcc\LaravelVote\Vote::class, + 'vote_model' => \Jcc\LaravelVote\Vote::class, ]; diff --git a/migrations/2021_03_13_000000_create_votes_table.php b/migrations/2021_03_13_000000_create_votes_table.php index 2878cce..174e8ce 100644 --- a/migrations/2021_03_13_000000_create_votes_table.php +++ b/migrations/2021_03_13_000000_create_votes_table.php @@ -6,25 +6,25 @@ class CreateVotesTable extends Migration { - /** - * Run the migrations. - */ - public function up() - { - Schema::create(config('vote.votes_table'), function (Blueprint $table) { - $table->increments('id'); - $table->unsignedBigInteger(config('vote.user_foreign_key'))->index(); - $table->morphs('votable'); - $table->string('vote_type', 16)->default('up_vote'); // 'up_vote'/'down_vote' - $table->timestamps(); - }); - } + /** + * Run the migrations. + */ + public function up() + { + Schema::create(config('vote.votes_table'), function (Blueprint $table) { + $table->increments('id'); + $table->unsignedBigInteger(config('vote.user_foreign_key'))->index(); + $table->morphs('votable'); + $table->string('vote_type', 16)->default('up_vote'); // 'up_vote'/'down_vote' + $table->timestamps(); + }); + } - /** - * Reverse the migrations. - */ - public function down() - { - Schema::dropIfExists(config('vote.votes_table')); - } + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists(config('vote.votes_table')); + } } diff --git a/src/Events/Event.php b/src/Events/Event.php index 8ac4947..a421c6e 100644 --- a/src/Events/Event.php +++ b/src/Events/Event.php @@ -6,18 +6,18 @@ class Event { - /** - * @var \Illuminate\Database\Eloquent\Model| - */ - public $vote; + /** + * @var \Illuminate\Database\Eloquent\Model| + */ + public $vote; - /** - * Event constructor. - * - * @param \Illuminate\Database\Eloquent\Model $vote - */ - public function __construct(Model $vote) - { - $this->vote = $vote; - } + /** + * Event constructor. + * + * @param \Illuminate\Database\Eloquent\Model $vote + */ + public function __construct(Model $vote) + { + $this->vote = $vote; + } } diff --git a/src/Traits/Votable.php b/src/Traits/Votable.php index b847800..32e2537 100644 --- a/src/Traits/Votable.php +++ b/src/Traits/Votable.php @@ -12,82 +12,82 @@ */ trait Votable { - /** - * @param \Illuminate\Database\Eloquent\Model $user - * - * @return bool - */ - public function isVotedBy(Model $user, ?string $type = null): bool - { - if (\is_a($user, config('auth.providers.users.model'))) { - if ($this->relationLoaded('voters')) { - return $this->voters->contains($user); - } + /** + * @param \Illuminate\Database\Eloquent\Model $user + * + * @return bool + */ + public function isVotedBy(Model $user, ?string $type = null): bool + { + if (\is_a($user, config('auth.providers.users.model'))) { + if ($this->relationLoaded('voters')) { + return $this->voters->contains($user); + } - return $this->voters() - ->where(\config('vote.user_foreign_key'), $user->getKey()) - ->when(is_string($type), function ($builder) use ($type) { - $builder->where('vote_type', (string)new VoteItems($type)); - }) - ->exists(); - } + return $this->voters() + ->where(\config('vote.user_foreign_key'), $user->getKey()) + ->when(is_string($type), function ($builder) use ($type) { + $builder->where('vote_type', (string)new VoteItems($type)); + }) + ->exists(); + } - return false; - } + return false; + } - /** - * Return voters. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - public function voters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany - { - return $this->belongsToMany( - config('auth.providers.users.model'), - config('vote.votes_table'), - 'votable_id', - config('vote.user_foreign_key') - ) - ->where('votable_type', $this->getMorphClass()); - } + /** + * Return voters. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function voters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->belongsToMany( + config('auth.providers.users.model'), + config('vote.votes_table'), + 'votable_id', + config('vote.user_foreign_key') + ) + ->where('votable_type', $this->getMorphClass()); + } - /** - * @param \Illuminate\Database\Eloquent\Model $user - * - * @return bool - */ - public function isUpVotedBy(Model $user) - { - return $this->isVotedBy($user, VoteItems::UP); - } + /** + * @param \Illuminate\Database\Eloquent\Model $user + * + * @return bool + */ + public function isUpVotedBy(Model $user) + { + return $this->isVotedBy($user, VoteItems::UP); + } - /** - * Return up voters. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - public function upVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany - { - return $this->voters()->where('vote_type', VoteItems::UP); - } + /** + * Return up voters. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function upVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->voters()->where('vote_type', VoteItems::UP); + } - /** - * @param \Illuminate\Database\Eloquent\Model $user - * - * @return bool - */ - public function isDownVotedBy(Model $user) - { - return $this->isVotedBy($user, VoteItems::DOWN); - } + /** + * @param \Illuminate\Database\Eloquent\Model $user + * + * @return bool + */ + public function isDownVotedBy(Model $user) + { + return $this->isVotedBy($user, VoteItems::DOWN); + } - /** - * Return down voters. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - public function downVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany - { - return $this->voters()->where('vote_type', VoteItems::DOWN); - } + /** + * Return down voters. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function downVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->voters()->where('vote_type', VoteItems::DOWN); + } } diff --git a/src/Traits/Voter.php b/src/Traits/Voter.php index 1adcc2e..aaec2b6 100644 --- a/src/Traits/Voter.php +++ b/src/Traits/Voter.php @@ -14,149 +14,149 @@ */ trait Voter { - /** - * @param Model $object - * @param string $type - * @return \Jcc\LaravelVote\Vote - * @throws \Jcc\LaravelVote\Exceptions\UnexpectValueException - */ - public function vote(Model $object, string $type): Vote - { - $attributes = [ - 'votable_type' => $object->getMorphClass(), - 'votable_id' => $object->getKey(), - \config('vote.user_foreign_key') => $this->getKey(), - ]; - - /* @var \Illuminate\Database\Eloquent\Model $vote */ - $vote = \app(\config('vote.vote_model')); - - $type = (string)new VoteItems($type); - - /* @var \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder $vote */ - return tap($vote->where($attributes)->firstOr( - function () use ($vote, $attributes, $type) { - $vote->unguard(); - - if ($this->relationLoaded('votes')) { - $this->unsetRelation('votes'); - } - - return $vote->create(\array_merge($attributes, [ - 'vote_type' => $type, - ])); - } - ), function (Model $model) use ($type) { - $model->update(['vote_type' => $type]); - }); - } - - /** - * @param \Illuminate\Database\Eloquent\Model $object - * - * @return bool - */ - public function hasVoted(Model $object, ?string $type = null): bool - { - return ($this->relationLoaded('votes') ? $this->votes : $this->votes()) - ->where('votable_id', $object->getKey()) - ->where('votable_type', $object->getMorphClass()) - ->when(\is_string($type), function ($builder) use ($type) { - $builder->where('vote_type', (string)new VoteItems($type)); - }) - ->count() > 0; - } - - /** - * @param Model $object - * @return bool - * @throws \Exception - */ - public function cancelVote(Model $object): bool - { - /* @var \Jcc\LaravelVote\Vote $relation */ - $relation = \app(\config('vote.vote_model')) - ->where('votable_id', $object->getKey()) - ->where('votable_type', $object->getMorphClass()) - ->where(\config('vote.user_foreign_key'), $this->getKey()) - ->first(); - - if ($relation) { - if ($this->relationLoaded('votes')) { - $this->unsetRelation('votes'); - } - - return $relation->delete(); - } - - return true; - } - - /** - * @return HasMany - */ - public function votes(): HasMany - { - return $this->hasMany(\config('vote.vote_model'), \config('vote.user_foreign_key'), $this->getKeyName()); - } - - /** - * Get Query Builder for votes - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function getVotedItems(string $model, ?string $type = null) - { - return \app($model)->whereHas( - 'voters', - function ($builder) use ($type) { - return $builder->where(\config('vote.user_foreign_key'), $this->getKey())->when( - \is_string($type), - function ($builder) use ($type) { - $builder->where('vote_type', (string)new VoteItems($type)); - } - ); - } - ); - } - - public function upVote(Model $object): Vote - { - return $this->vote($object, VoteItems::UP); - } - - public function downVote(Model $object): Vote - { - return $this->vote($object, VoteItems::DOWN); - } - - public function hasUpVoted(Model $object) - { - return $this->hasVoted($object, VoteItems::UP); - } - - public function hasDownVoted(Model $object) - { - return $this->hasVoted($object, VoteItems::DOWN); - } - - public function toggleUpVote(Model $object) - { - return $this->hasUpVoted($object) ? $this->cancelVote($object) : $this->upVote($object); - } - - public function toggleDownVote(Model $object) - { - return $this->hasDownVoted($object) ? $this->cancelVote($object) : $this->downVote($object); - } - - public function getUpVotedItems(string $model) - { - return $this->getVotedItems($model, VoteItems::UP); - } - - public function getDownVotedItems(string $model) - { - return $this->getVotedItems($model, VoteItems::DOWN); - } + /** + * @param Model $object + * @param string $type + * @return \Jcc\LaravelVote\Vote + * @throws \Jcc\LaravelVote\Exceptions\UnexpectValueException + */ + public function vote(Model $object, string $type): Vote + { + $attributes = [ + 'votable_type' => $object->getMorphClass(), + 'votable_id' => $object->getKey(), + \config('vote.user_foreign_key') => $this->getKey(), + ]; + + /* @var \Illuminate\Database\Eloquent\Model $vote */ + $vote = \app(\config('vote.vote_model')); + + $type = (string)new VoteItems($type); + + /* @var \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder $vote */ + return tap($vote->where($attributes)->firstOr( + function () use ($vote, $attributes, $type) { + $vote->unguard(); + + if ($this->relationLoaded('votes')) { + $this->unsetRelation('votes'); + } + + return $vote->create(\array_merge($attributes, [ + 'vote_type' => $type, + ])); + } + ), function (Model $model) use ($type) { + $model->update(['vote_type' => $type]); + }); + } + + /** + * @param \Illuminate\Database\Eloquent\Model $object + * + * @return bool + */ + public function hasVoted(Model $object, ?string $type = null): bool + { + return ($this->relationLoaded('votes') ? $this->votes : $this->votes()) + ->where('votable_id', $object->getKey()) + ->where('votable_type', $object->getMorphClass()) + ->when(\is_string($type), function ($builder) use ($type) { + $builder->where('vote_type', (string)new VoteItems($type)); + }) + ->count() > 0; + } + + /** + * @param Model $object + * @return bool + * @throws \Exception + */ + public function cancelVote(Model $object): bool + { + /* @var \Jcc\LaravelVote\Vote $relation */ + $relation = \app(\config('vote.vote_model')) + ->where('votable_id', $object->getKey()) + ->where('votable_type', $object->getMorphClass()) + ->where(\config('vote.user_foreign_key'), $this->getKey()) + ->first(); + + if ($relation) { + if ($this->relationLoaded('votes')) { + $this->unsetRelation('votes'); + } + + return $relation->delete(); + } + + return true; + } + + /** + * @return HasMany + */ + public function votes(): HasMany + { + return $this->hasMany(\config('vote.vote_model'), \config('vote.user_foreign_key'), $this->getKeyName()); + } + + /** + * Get Query Builder for votes + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getVotedItems(string $model, ?string $type = null) + { + return \app($model)->whereHas( + 'voters', + function ($builder) use ($type) { + return $builder->where(\config('vote.user_foreign_key'), $this->getKey())->when( + \is_string($type), + function ($builder) use ($type) { + $builder->where('vote_type', (string)new VoteItems($type)); + } + ); + } + ); + } + + public function upVote(Model $object): Vote + { + return $this->vote($object, VoteItems::UP); + } + + public function downVote(Model $object): Vote + { + return $this->vote($object, VoteItems::DOWN); + } + + public function hasUpVoted(Model $object) + { + return $this->hasVoted($object, VoteItems::UP); + } + + public function hasDownVoted(Model $object) + { + return $this->hasVoted($object, VoteItems::DOWN); + } + + public function toggleUpVote(Model $object) + { + return $this->hasUpVoted($object) ? $this->cancelVote($object) : $this->upVote($object); + } + + public function toggleDownVote(Model $object) + { + return $this->hasDownVoted($object) ? $this->cancelVote($object) : $this->downVote($object); + } + + public function getUpVotedItems(string $model) + { + return $this->getVotedItems($model, VoteItems::UP); + } + + public function getDownVotedItems(string $model) + { + return $this->getVotedItems($model, VoteItems::DOWN); + } } diff --git a/src/Vote.php b/src/Vote.php index 8bf5e01..f910992 100644 --- a/src/Vote.php +++ b/src/Vote.php @@ -11,85 +11,85 @@ class Vote extends Model { - protected $guarded = []; - - protected $dispatchesEvents = [ - 'created' => Voted::class, - 'updated' => Voted::class, - - 'deleted' => CancelVoted::class, - ]; - - /** - * @param array $attributes - */ - public function __construct(array $attributes = []) - { - $this->table = \config('vote.votes_table'); - - parent::__construct($attributes); - } - - protected static function boot() - { - parent::boot(); - - self::creating(function (Vote $vote) { - $userForeignKey = \config('vote.user_foreign_key'); - $vote->{$userForeignKey} = $vote->{$userForeignKey} ?: Auth::id(); - }); - } - - public function votable(): MorphTo - { - return $this->morphTo(); - } - - /** - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function user() - { - return $this->belongsTo(\config('auth.providers.users.model'), \config('vote.user_foreign_key')); - } - - /** - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function voter() - { - return $this->user(); - } - - /** - * @param \Illuminate\Database\Eloquent\Builder $query - * @param string $type - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeWithVotableType(Builder $query, string $type) - { - return $query->where('votable_type', \app($type)->getMorphClass()); - } - - /** - * @param \Illuminate\Database\Eloquent\Builder $query - * @param string $type - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeWithVoteType(Builder $query, string $type) - { - return $query->where('vote_type', (string)new VoteItems($type)); - } - - public function isUp(): bool - { - return $this->type === VoteItems::UP; - } - - public function isDown(): bool - { - return $this->type === VoteItems::DOWN; - } + protected $guarded = []; + + protected $dispatchesEvents = [ + 'created' => Voted::class, + 'updated' => Voted::class, + + 'deleted' => CancelVoted::class, + ]; + + /** + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + $this->table = \config('vote.votes_table'); + + parent::__construct($attributes); + } + + protected static function boot() + { + parent::boot(); + + self::creating(function (Vote $vote) { + $userForeignKey = \config('vote.user_foreign_key'); + $vote->{$userForeignKey} = $vote->{$userForeignKey} ?: Auth::id(); + }); + } + + public function votable(): MorphTo + { + return $this->morphTo(); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(\config('auth.providers.users.model'), \config('vote.user_foreign_key')); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function voter() + { + return $this->user(); + } + + /** + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $type + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeWithVotableType(Builder $query, string $type) + { + return $query->where('votable_type', \app($type)->getMorphClass()); + } + + /** + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string $type + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeWithVoteType(Builder $query, string $type) + { + return $query->where('vote_type', (string)new VoteItems($type)); + } + + public function isUp(): bool + { + return $this->type === VoteItems::UP; + } + + public function isDown(): bool + { + return $this->type === VoteItems::DOWN; + } } diff --git a/src/VoteItems.php b/src/VoteItems.php index 2e06329..f06e982 100644 --- a/src/VoteItems.php +++ b/src/VoteItems.php @@ -7,30 +7,30 @@ final class VoteItems implements Stringable { - /** - * @var string - */ - protected $value; - - public const UP = 'up_vote'; - - public const DOWN = 'down_vote'; - - public function __construct(string $value) - { - if (!in_array($value, self::getValues(), true)) { - throw new UnexpectValueException("Unexpect Value: {$value}"); - } - $this->value = $value; - } - - public static function getValues() - { - return [self::UP, self::DOWN]; - } - - public function __toString() - { - return $this->value; - } + /** + * @var string + */ + protected $value; + + public const UP = 'up_vote'; + + public const DOWN = 'down_vote'; + + public function __construct(string $value) + { + if (!in_array($value, self::getValues(), true)) { + throw new UnexpectValueException("Unexpect Value: {$value}"); + } + $this->value = $value; + } + + public static function getValues() + { + return [self::UP, self::DOWN]; + } + + public function __toString() + { + return $this->value; + } } diff --git a/src/VoteServiceProvider.php b/src/VoteServiceProvider.php index 678ef4f..fc46b63 100644 --- a/src/VoteServiceProvider.php +++ b/src/VoteServiceProvider.php @@ -20,17 +20,17 @@ class VoteServiceProvider extends ServiceProvider */ public function boot() { - $this->publishes([ - \dirname(__DIR__) . '/config/vote.php' => config_path('vote.php'), - ], 'config'); + $this->publishes([ + \dirname(__DIR__) . '/config/vote.php' => config_path('vote.php'), + ], 'config'); - $this->publishes([ - \dirname(__DIR__) . '/migrations/' => database_path('migrations'), - ], 'migrations'); + $this->publishes([ + \dirname(__DIR__) . '/migrations/' => database_path('migrations'), + ], 'migrations'); - if ($this->app->runningInConsole()) { - $this->loadMigrationsFrom(\dirname(__DIR__) . '/migrations/'); - } + if ($this->app->runningInConsole()) { + $this->loadMigrationsFrom(\dirname(__DIR__) . '/migrations/'); + } } /** @@ -38,9 +38,9 @@ public function boot() */ public function register() { - $this->mergeConfigFrom( - \dirname(__DIR__) . '/config/vote.php', - 'vote' - ); + $this->mergeConfigFrom( + \dirname(__DIR__) . '/config/vote.php', + 'vote' + ); } } diff --git a/tests/FeatureTest.php b/tests/FeatureTest.php index 2ec322a..e95c07b 100644 --- a/tests/FeatureTest.php +++ b/tests/FeatureTest.php @@ -10,244 +10,244 @@ class FeatureTest extends TestCase { - public function setUp(): void - { - parent::setUp(); + public function setUp(): void + { + parent::setUp(); - Event::fake(); + Event::fake(); - config(['auth.providers.users.model' => User::class]); - } + config(['auth.providers.users.model' => User::class]); + } - public function test_basic_features() - { - /** @var User $user */ - $user = User::create(['name' => 'jcc']); - /** @var Post $post */ - $post = Post::create(['title' => 'Hello world!']); - - $user->vote($post, VoteItems::UP); + public function test_basic_features() + { + /** @var User $user */ + $user = User::create(['name' => 'jcc']); + /** @var Post $post */ + $post = Post::create(['title' => 'Hello world!']); + + $user->vote($post, VoteItems::UP); - Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { - $vote = $event->vote; + Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { + $vote = $event->vote; - return $vote->votable instanceof Post - && $vote->user instanceof User - && $vote->user->id === $user->id - && $vote->votable->id === $post->id; - }); + return $vote->votable instanceof Post + && $vote->user instanceof User + && $vote->user->id === $user->id + && $vote->votable->id === $post->id; + }); - self::assertTrue($user->hasVoted($post)); - self::assertTrue($user->hasUpVoted($post)); - self::assertFalse($user->hasDownVoted($post)); + self::assertTrue($user->hasVoted($post)); + self::assertTrue($user->hasUpVoted($post)); + self::assertFalse($user->hasDownVoted($post)); - self::assertTrue($post->isVotedBy($user)); - self::assertTrue($post->isUpVotedBy($user)); - self::assertFalse($post->isDownVotedBy($user)); + self::assertTrue($post->isVotedBy($user)); + self::assertTrue($post->isUpVotedBy($user)); + self::assertFalse($post->isDownVotedBy($user)); - self::assertTrue($user->cancelVote($post)); - - $user->vote($post, VoteItems::DOWN); + self::assertTrue($user->cancelVote($post)); + + $user->vote($post, VoteItems::DOWN); - Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { - $vote = $event->vote; + Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { + $vote = $event->vote; - return $vote->votable instanceof Post - && $vote->user instanceof User - && $vote->user->id === $user->id - && $vote->votable->id === $post->id; - }); + return $vote->votable instanceof Post + && $vote->user instanceof User + && $vote->user->id === $user->id + && $vote->votable->id === $post->id; + }); - self::assertTrue($user->hasVoted($post)); - self::assertFalse($user->hasUpVoted($post)); - self::assertTrue($user->hasDownVoted($post)); + self::assertTrue($user->hasVoted($post)); + self::assertFalse($user->hasUpVoted($post)); + self::assertTrue($user->hasDownVoted($post)); - self::assertTrue($post->isVotedBy($user)); - self::assertFalse($post->isUpVotedBy($user)); - self::assertTrue($post->isDownVotedBy($user)); - } + self::assertTrue($post->isVotedBy($user)); + self::assertFalse($post->isUpVotedBy($user)); + self::assertTrue($post->isDownVotedBy($user)); + } - public function test_cancelVote_features() - { - $user1 = User::create(['name' => 'jcc']); - $user2 = User::create(['name' => 'allen']); + public function test_cancelVote_features() + { + $user1 = User::create(['name' => 'jcc']); + $user2 = User::create(['name' => 'allen']); - $post = Post::create(['title' => 'Hello world!']); + $post = Post::create(['title' => 'Hello world!']); - $user1->vote($post, VoteItems::DOWN); - $user2->vote($post, VoteItems::UP); + $user1->vote($post, VoteItems::DOWN); + $user2->vote($post, VoteItems::UP); - $user1->cancelVote($post); - $user2->cancelVote($post); + $user1->cancelVote($post); + $user2->cancelVote($post); - self::assertFalse($user1->hasVoted($post)); - self::assertFalse($user1->hasDownVoted($post)); - self::assertFalse($user1->hasUpVoted($post)); + self::assertFalse($user1->hasVoted($post)); + self::assertFalse($user1->hasDownVoted($post)); + self::assertFalse($user1->hasUpVoted($post)); - self::assertFalse($user1->hasVoted($post)); - self::assertFalse($user2->hasUpVoted($post)); - self::assertFalse($user2->hasDownVoted($post)); - } + self::assertFalse($user1->hasVoted($post)); + self::assertFalse($user2->hasUpVoted($post)); + self::assertFalse($user2->hasDownVoted($post)); + } - public function test_upVoted_to_downVoted_each_other_features() - { - $user1 = User::create(['name' => 'jcc']); - $user2 = User::create(['name' => 'allen']); - $post = Post::create(['title' => 'Hello world!']); + public function test_upVoted_to_downVoted_each_other_features() + { + $user1 = User::create(['name' => 'jcc']); + $user2 = User::create(['name' => 'allen']); + $post = Post::create(['title' => 'Hello world!']); - $user1->vote($post, VoteItems::UP); - self::assertTrue($user1->hasUpVoted($post)); - self::assertFalse($user1->hasDownVoted($post)); + $user1->vote($post, VoteItems::UP); + self::assertTrue($user1->hasUpVoted($post)); + self::assertFalse($user1->hasDownVoted($post)); - $user1->vote($post, VoteItems::DOWN); - self::assertFalse($user1->hasUpVoted($post)); - self::assertTrue($user1->hasDownVoted($post)); + $user1->vote($post, VoteItems::DOWN); + self::assertFalse($user1->hasUpVoted($post)); + self::assertTrue($user1->hasDownVoted($post)); - $user2->vote($post, VoteItems::DOWN); - self::assertFalse($user2->hasUpVoted($post)); - self::assertTrue($user2->hasDownVoted($post)); + $user2->vote($post, VoteItems::DOWN); + self::assertFalse($user2->hasUpVoted($post)); + self::assertTrue($user2->hasDownVoted($post)); - $user2->vote($post, VoteItems::UP); - self::assertTrue($user2->hasUpVoted($post)); - self::assertFalse($user2->hasDownVoted($post)); - } + $user2->vote($post, VoteItems::UP); + self::assertTrue($user2->hasUpVoted($post)); + self::assertFalse($user2->hasDownVoted($post)); + } - public function test_aggregations() - { - $user = User::create(['name' => 'jcc']); + public function test_aggregations() + { + $user = User::create(['name' => 'jcc']); - $post1 = Post::create(['title' => 'Hello world!']); - $post2 = Post::create(['title' => 'Hello everyone!']); - $post3 = Post::create(['title' => 'Hello players!']); - $book1 = Book::create(['title' => 'Learn laravel.']); - $book2 = Book::create(['title' => 'Learn symfony.']); - $book3 = Book::create(['title' => 'Learn yii2.']); - - $user->vote($post1, VoteItems::UP); - $user->vote($post2, VoteItems::UP); - $user->vote($post3, VoteItems::DOWN); + $post1 = Post::create(['title' => 'Hello world!']); + $post2 = Post::create(['title' => 'Hello everyone!']); + $post3 = Post::create(['title' => 'Hello players!']); + $book1 = Book::create(['title' => 'Learn laravel.']); + $book2 = Book::create(['title' => 'Learn symfony.']); + $book3 = Book::create(['title' => 'Learn yii2.']); + + $user->vote($post1, VoteItems::UP); + $user->vote($post2, VoteItems::UP); + $user->vote($post3, VoteItems::DOWN); - $user->vote($book1, VoteItems::UP); - $user->vote($book2, VoteItems::UP); - $user->vote($book3, VoteItems::DOWN); + $user->vote($book1, VoteItems::UP); + $user->vote($book2, VoteItems::UP); + $user->vote($book3, VoteItems::DOWN); - self::assertSame(6, $user->votes()->count()); - self::assertSame(4, $user->votes()->withVoteType(VoteItems::UP)->count()); - self::assertSame(2, $user->votes()->withVoteType(VoteItems::DOWN)->count()); + self::assertSame(6, $user->votes()->count()); + self::assertSame(4, $user->votes()->withVoteType(VoteItems::UP)->count()); + self::assertSame(2, $user->votes()->withVoteType(VoteItems::DOWN)->count()); - self::assertSame(3, $user->votes()->withVotableType(Book::class)->count()); - self::assertSame(1, $user->votes()->withVoteType(VoteItems::DOWN)->withVotableType(Book::class)->count()); - } - - public function test_vote_same_model() - { - $user1 = User::create(['name' => 'jcc']); - $user2 = User::create(['name' => 'allen']); - $user3 = User::create(['name' => 'taylor']); - - $user1->vote($user2, VoteItems::UP); - $user3->vote($user1, VoteItems::DOWN); - - self::assertTrue($user1->hasVoted($user2)); - self::assertTrue($user2->isVotedBy($user1)); - - self::assertTrue($user1->hasUpVoted($user2)); - self::assertTrue($user2->isUpVotedBy($user1)); - - self::assertTrue($user3->hasVoted($user1)); - self::assertTrue($user1->isVotedBy($user3)); - - self::assertTrue($user3->hasDownVoted($user1)); - self::assertTrue($user1->isDownVotedBy($user3)); - } - - public function test_object_voters() - { - $user1 = User::create(['name' => 'jcc']); - $user2 = User::create(['name' => 'allen']); - $user3 = User::create(['name' => 'taylor']); - - $post = Post::create(['title' => 'Hello world!']); - - $user1->vote($post, VoteItems::UP); - $user2->vote($post, VoteItems::DOWN); - - self::assertCount(2, $post->voters); - self::assertSame('jcc', $post->voters[0]['name']); - self::assertSame('allen', $post->voters[1]['name']); - - $sqls = $this->getQueryLog(function () use ($post, $user1, $user2, $user3) { - self::assertTrue($post->isVotedBy($user1)); - self::assertTrue($post->isVotedBy($user2)); - self::assertFalse($post->isVotedBy($user3)); - }); - - self::assertEmpty($sqls->all()); - } - - public function test_object_votes_with_custom_morph_class_name() - { - $user1 = User::create(['name' => 'jcc']); - $user2 = User::create(['name' => 'allen']); - $user3 = User::create(['name' => 'taylor']); - - $post = Post::create(['title' => 'Hello world!']); - - Relation::morphMap([ - 'posts' => Post::class, - ]); - - $user1->vote($post, VoteItems::UP); - $user2->vote($post, VoteItems::DOWN); - - self::assertCount(2, $post->voters); - self::assertSame('jcc', $post->voters[0]['name']); - self::assertSame('allen', $post->voters[1]['name']); - } - - public function test_eager_loading() - { - $user = User::create(['name' => 'jcc']); - - $post1 = Post::create(['title' => 'Hello world!']); - $post2 = Post::create(['title' => 'Hello everyone!']); - $book1 = Book::create(['title' => 'Learn laravel.']); - $book2 = Book::create(['title' => 'Learn symfony.']); - - $user->vote($post1, VoteItems::UP); - $user->vote($post2, VoteItems::DOWN); - $user->vote($book1, VoteItems::UP); - $user->vote($book2, VoteItems::DOWN); - - // start recording - $sqls = $this->getQueryLog(function () use ($user) { - $user->load('votes.votable'); - }); - - self::assertSame(3, $sqls->count()); - - // from loaded relations - $sqls = $this->getQueryLog(function () use ($user, $post1) { - $user->hasVoted($post1); - }); - - self::assertEmpty($sqls->all()); - } - - /** - * @param \Closure $callback - * - * @return \Illuminate\Support\Collection - */ - protected function getQueryLog(\Closure $callback): \Illuminate\Support\Collection - { - $sqls = \collect([]); - DB::listen(function ($query) use ($sqls) { - $sqls->push(['sql' => $query->sql, 'bindings' => $query->bindings]); - }); - - $callback(); + self::assertSame(3, $user->votes()->withVotableType(Book::class)->count()); + self::assertSame(1, $user->votes()->withVoteType(VoteItems::DOWN)->withVotableType(Book::class)->count()); + } + + public function test_vote_same_model() + { + $user1 = User::create(['name' => 'jcc']); + $user2 = User::create(['name' => 'allen']); + $user3 = User::create(['name' => 'taylor']); + + $user1->vote($user2, VoteItems::UP); + $user3->vote($user1, VoteItems::DOWN); + + self::assertTrue($user1->hasVoted($user2)); + self::assertTrue($user2->isVotedBy($user1)); + + self::assertTrue($user1->hasUpVoted($user2)); + self::assertTrue($user2->isUpVotedBy($user1)); + + self::assertTrue($user3->hasVoted($user1)); + self::assertTrue($user1->isVotedBy($user3)); + + self::assertTrue($user3->hasDownVoted($user1)); + self::assertTrue($user1->isDownVotedBy($user3)); + } + + public function test_object_voters() + { + $user1 = User::create(['name' => 'jcc']); + $user2 = User::create(['name' => 'allen']); + $user3 = User::create(['name' => 'taylor']); + + $post = Post::create(['title' => 'Hello world!']); + + $user1->vote($post, VoteItems::UP); + $user2->vote($post, VoteItems::DOWN); + + self::assertCount(2, $post->voters); + self::assertSame('jcc', $post->voters[0]['name']); + self::assertSame('allen', $post->voters[1]['name']); + + $sqls = $this->getQueryLog(function () use ($post, $user1, $user2, $user3) { + self::assertTrue($post->isVotedBy($user1)); + self::assertTrue($post->isVotedBy($user2)); + self::assertFalse($post->isVotedBy($user3)); + }); + + self::assertEmpty($sqls->all()); + } + + public function test_object_votes_with_custom_morph_class_name() + { + $user1 = User::create(['name' => 'jcc']); + $user2 = User::create(['name' => 'allen']); + $user3 = User::create(['name' => 'taylor']); + + $post = Post::create(['title' => 'Hello world!']); + + Relation::morphMap([ + 'posts' => Post::class, + ]); + + $user1->vote($post, VoteItems::UP); + $user2->vote($post, VoteItems::DOWN); + + self::assertCount(2, $post->voters); + self::assertSame('jcc', $post->voters[0]['name']); + self::assertSame('allen', $post->voters[1]['name']); + } + + public function test_eager_loading() + { + $user = User::create(['name' => 'jcc']); + + $post1 = Post::create(['title' => 'Hello world!']); + $post2 = Post::create(['title' => 'Hello everyone!']); + $book1 = Book::create(['title' => 'Learn laravel.']); + $book2 = Book::create(['title' => 'Learn symfony.']); + + $user->vote($post1, VoteItems::UP); + $user->vote($post2, VoteItems::DOWN); + $user->vote($book1, VoteItems::UP); + $user->vote($book2, VoteItems::DOWN); + + // start recording + $sqls = $this->getQueryLog(function () use ($user) { + $user->load('votes.votable'); + }); + + self::assertSame(3, $sqls->count()); + + // from loaded relations + $sqls = $this->getQueryLog(function () use ($user, $post1) { + $user->hasVoted($post1); + }); + + self::assertEmpty($sqls->all()); + } + + /** + * @param \Closure $callback + * + * @return \Illuminate\Support\Collection + */ + protected function getQueryLog(\Closure $callback): \Illuminate\Support\Collection + { + $sqls = \collect([]); + DB::listen(function ($query) use ($sqls) { + $sqls->push(['sql' => $query->sql, 'bindings' => $query->bindings]); + }); + + $callback(); - return $sqls; - } + return $sqls; + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 23d444d..373f8d0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,41 +6,41 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase { - /** - * Load package service provider. - * - * @param \Illuminate\Foundation\Application $app - * - * @return array - */ - protected function getPackageProviders($app) - { - return [VoteServiceProvider::class]; - } + /** + * Load package service provider. + * + * @param \Illuminate\Foundation\Application $app + * + * @return array + */ + protected function getPackageProviders($app) + { + return [VoteServiceProvider::class]; + } - /** - * Define environment setup. - * - * @param \Illuminate\Foundation\Application $app - */ - protected function getEnvironmentSetUp($app) - { - // Setup default database to use sqlite :memory: - $app['config']->set('database.default', 'testing'); - $app['config']->set('database.connections.testing', [ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'prefix' => '', - ]); - } + /** + * Define environment setup. + * + * @param \Illuminate\Foundation\Application $app + */ + protected function getEnvironmentSetUp($app) + { + // Setup default database to use sqlite :memory: + $app['config']->set('database.default', 'testing'); + $app['config']->set('database.connections.testing', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } - /** - * run package database migrations. - */ - public function setUp(): void - { - parent::setUp(); - $this->loadMigrationsFrom(__DIR__ . '/migrations'); - $this->loadMigrationsFrom(dirname(__DIR__) . '/migrations'); - } + /** + * run package database migrations. + */ + public function setUp(): void + { + parent::setUp(); + $this->loadMigrationsFrom(__DIR__ . '/migrations'); + $this->loadMigrationsFrom(dirname(__DIR__) . '/migrations'); + } } diff --git a/tests/User.php b/tests/User.php index f17ac58..10ecc38 100644 --- a/tests/User.php +++ b/tests/User.php @@ -8,8 +8,8 @@ class User extends Model { - use Voter; - use Votable; + use Voter; + use Votable; - protected $fillable = ['name']; + protected $fillable = ['name']; } diff --git a/tests/migrations/2021_03_13_000000_create_books_table.php b/tests/migrations/2021_03_13_000000_create_books_table.php index ab863c3..c31189d 100644 --- a/tests/migrations/2021_03_13_000000_create_books_table.php +++ b/tests/migrations/2021_03_13_000000_create_books_table.php @@ -6,23 +6,23 @@ class CreateBooksTable extends Migration { - /** - * Run the migrations. - */ - public function up() - { - Schema::create('books', function (Blueprint $table) { - $table->increments('id'); - $table->string('title'); - $table->timestamps(); - }); - } + /** + * Run the migrations. + */ + public function up() + { + Schema::create('books', function (Blueprint $table) { + $table->increments('id'); + $table->string('title'); + $table->timestamps(); + }); + } - /** - * Reverse the migrations. - */ - public function down() - { - Schema::dropIfExists('books'); - } + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('books'); + } } diff --git a/tests/migrations/2021_03_13_000000_create_posts_table.php b/tests/migrations/2021_03_13_000000_create_posts_table.php index a1409c2..56fa9b5 100644 --- a/tests/migrations/2021_03_13_000000_create_posts_table.php +++ b/tests/migrations/2021_03_13_000000_create_posts_table.php @@ -6,23 +6,23 @@ class CreatePostsTable extends Migration { - /** - * Run the migrations. - */ - public function up() - { - Schema::create('posts', function (Blueprint $table) { - $table->increments('id'); - $table->string('title'); - $table->timestamps(); - }); - } + /** + * Run the migrations. + */ + public function up() + { + Schema::create('posts', function (Blueprint $table) { + $table->increments('id'); + $table->string('title'); + $table->timestamps(); + }); + } - /** - * Reverse the migrations. - */ - public function down() - { - Schema::dropIfExists('posts'); - } + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('posts'); + } } diff --git a/tests/migrations/2021_03_13_000000_create_users_table.php b/tests/migrations/2021_03_13_000000_create_users_table.php index a4aa67d..b72e592 100644 --- a/tests/migrations/2021_03_13_000000_create_users_table.php +++ b/tests/migrations/2021_03_13_000000_create_users_table.php @@ -6,23 +6,23 @@ class CreateUsersTable extends Migration { - /** - * Run the migrations. - */ - public function up() - { - Schema::create('users', function (Blueprint $table) { - $table->increments('id'); - $table->string('name'); - $table->timestamps(); - }); - } + /** + * Run the migrations. + */ + public function up() + { + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->timestamps(); + }); + } - /** - * Reverse the migrations. - */ - public function down() - { - Schema::dropIfExists('users'); - } + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('users'); + } } From 62fdb591ea49971f46f4dc8c037ac6eac8afb52b Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 10:09:46 +0800 Subject: [PATCH 10/16] phpstan analyse --- .github/workflows/lint.yml | 12 +++++++----- .gitignore | 2 +- composer.json | 6 ++++-- src/Events/Event.php | 2 +- src/Vote.php | 10 ++++++++-- src/VoteItems.php | 8 +++++++- 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90a6f7c..de7a821 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,8 +15,10 @@ jobs: container: image: nauxliu/php-ci-image:${{ matrix.php_version }} steps: - - uses: actions/checkout@master - - name: Install Dependencies - run: composer install --prefer-dist --no-interaction --no-suggest - - name: Check Style - run: composer check-style \ No newline at end of file + - uses: actions/checkout@master + - name: Install Dependencies + run: composer install --prefer-dist --no-interaction --no-suggest + - name: Check Style + run: composer check-style + - name: PHPStan analyse + run: composer phpstan \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5f59bcb..7e0b1cc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ /vendor/ composer.lock .phpunit.result.cache - +.php_cs.cache diff --git a/composer.json b/composer.json index 54655b3..2a26716 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "require-dev": { "mockery/mockery": "^1.3", "orchestra/testbench": "^3.5|~4.0|~5.0|~6.0", - "friendsofphp/php-cs-fixer": "^2.18" + "friendsofphp/php-cs-fixer": "^2.18", + "phpstan/phpstan": "^0.12.81" }, "license": "MIT", "autoload": { @@ -40,6 +41,7 @@ "scripts": { "check-style": "php-cs-fixer fix --using-cache=no --diff --config=.php_cs --dry-run --ansi", "fix-style": "php-cs-fixer fix --using-cache=no --config=.php_cs --ansi", - "test": "vendor/bin/phpunit --colors=always" + "test": "vendor/bin/phpunit --colors=always", + "phpstan": "vendor/bin/phpstan analyse src -l 5" } } diff --git a/src/Events/Event.php b/src/Events/Event.php index a421c6e..693d04a 100644 --- a/src/Events/Event.php +++ b/src/Events/Event.php @@ -7,7 +7,7 @@ class Event { /** - * @var \Illuminate\Database\Eloquent\Model| + * @var \Illuminate\Database\Eloquent\Model */ public $vote; diff --git a/src/Vote.php b/src/Vote.php index f910992..0dc5375 100644 --- a/src/Vote.php +++ b/src/Vote.php @@ -9,6 +9,12 @@ use Jcc\LaravelVote\Events\CancelVoted; use Jcc\LaravelVote\Events\Voted; +/** + * Class Vote + * + * @property string $vote_type + * @property string $votable_type + */ class Vote extends Model { protected $guarded = []; @@ -85,11 +91,11 @@ public function scopeWithVoteType(Builder $query, string $type) public function isUp(): bool { - return $this->type === VoteItems::UP; + return $this->vote_type === VoteItems::UP; } public function isDown(): bool { - return $this->type === VoteItems::DOWN; + return $this->vote_type === VoteItems::DOWN; } } diff --git a/src/VoteItems.php b/src/VoteItems.php index f06e982..d498ad0 100644 --- a/src/VoteItems.php +++ b/src/VoteItems.php @@ -24,11 +24,17 @@ public function __construct(string $value) $this->value = $value; } - public static function getValues() + /** + * @return string[] + */ + public static function getValues(): array { return [self::UP, self::DOWN]; } + /** + * @return string + */ public function __toString() { return $this->value; From 29eb894991e13a71680b9da269e9d30808001fa6 Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 10:18:53 +0800 Subject: [PATCH 11/16] rm .DS_Store --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7e0b1cc..40086ad 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.lock .phpunit.result.cache .php_cs.cache +.DS_Store From 37047274a88d23eedd1c69fcd0b48197f2f98c75 Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 10:39:04 +0800 Subject: [PATCH 12/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 97 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 6f75b55..090a9fe 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,13 @@ -# Laravel 5 Vote System +# Laravel Vote System :tada: This package helps you to add user based vote system to your model. -> This project code is basically the same as [laravel-follow](https://github.com/overtrue/laravel-follow). - ## Installation You can install the package using Composer: ```sh -$ composer require jcc/laravel-vote -vvv +$ composer require "jcc/laravel-vote:~2.0" ``` Then add the service provider to `config/app.php`: @@ -27,24 +25,22 @@ $ php artisan vendor:publish --provider="Jcc\LaravelVote\VoteServiceProvider" -- Finally, use VoteTrait in User model: ```php -use Jcc\LaravelVote\Vote; +use Jcc\LaravelVote\Traits\Voter; class User extends Model { - use Vote; + use Voter; } ``` Or use CanBeVoted in Comment model: ```php -use Jcc\LaravelVote\CanBeVoted; +use Jcc\LaravelVote\Traits\Votable; class Comment extends Model { - use CanBeVoted; - - protected $vote = User::class; + use Votable; } ``` @@ -79,12 +75,12 @@ $user->cancelVote($comment); #### Get user has voted comment items ```php -$user->votedItems(Comment::class)->get(); +$user->getVotedItems(Comment::class)->get(); ``` #### Check if user has up or down vote -``` +```php $comment = Comment::find(1); $user->hasVoted($comment); @@ -92,7 +88,7 @@ $user->hasVoted($comment); #### Check if user has up vote -``` +```php $comment = Comment::find(1); $user->hasUpVoted($comment); @@ -100,7 +96,7 @@ $user->hasUpVoted($comment); #### Check if user has down vote -``` +```php $comment = Comment::find(1); $user->hasDownVoted($comment); @@ -111,37 +107,96 @@ $user->hasDownVoted($comment); #### Get comment voters ```php -$comment->voters(); +$comment->voters()->get(); ``` #### Count comment voters ```php -$comment->countVoters(); +$comment->voters()->count(); +``` + +#### Get comment up voters + +```php +$comment->upVoters()->get(); ``` #### Count comment up voters ```php -$comment->countUpVoters(); +$comment->upVoters()->count(); +``` + +#### Get comment down voters + +```php +$comment->downVoters()->get(); ``` #### Count comment down voters ```php -$comment->countDownVoters(); +$comment->downVoters()->count(); ``` #### Check if voted by ```php -$comment->isVotedBy(1); +$user = User::find(1); + +$comment->isVotedBy($user); +``` + +#### Check if up voted by + +```php +$user = User::find(1); + +$comment->isUpVotedBy($user); +``` + +#### Check if down voted by + +```php +$user = User::find(1); + +$comment->isDownVotedBy($user); +``` + +### N+1 issue + +To avoid the N+1 issue, you can use eager loading to reduce this operation to just 2 queries. When querying, you may specify which relationships should be eager loaded using the `with` method: + +```php +// Voter +$users = User::with('votes')->get(); + +foreach($users as $user) { + $user->hasVoted($comment); +} + +// Votable +$comments = Comment::with('voters')->get(); + +foreach($comments as $comment) { + $comment->isVotedBy($user); +} ``` +### Events + +| **Event** | **Description** | +| --- | --- | +| `Jcc\LaravelVote\Events\Voted` | Triggered when the relationship is created or updated. | +| `Jcc\LaravelVote\Events\CancelVoted` | Triggered when the relationship is deleted. | + + ## Reference -[laravel-follow](https://github.com/overtrue/laravel-follow) +- [laravel-follow](https://github.com/overtrue/laravel-follow) +- [laravel-like](https://github.com/overtrue/laravel-like) ## License -MIT +[MIT](LICENSE) From e14c843beeb27f0a7ccbcccf29b687ba885e7b23 Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 10:40:48 +0800 Subject: [PATCH 13/16] cs --- src/Traits/Votable.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Traits/Votable.php b/src/Traits/Votable.php index 32e2537..06347b8 100644 --- a/src/Traits/Votable.php +++ b/src/Traits/Votable.php @@ -19,14 +19,14 @@ trait Votable */ public function isVotedBy(Model $user, ?string $type = null): bool { - if (\is_a($user, config('auth.providers.users.model'))) { + if (\is_a($user, \config('auth.providers.users.model'))) { if ($this->relationLoaded('voters')) { return $this->voters->contains($user); } return $this->voters() ->where(\config('vote.user_foreign_key'), $user->getKey()) - ->when(is_string($type), function ($builder) use ($type) { + ->when(\is_string($type), function ($builder) use ($type) { $builder->where('vote_type', (string)new VoteItems($type)); }) ->exists(); @@ -43,10 +43,10 @@ public function isVotedBy(Model $user, ?string $type = null): bool public function voters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany { return $this->belongsToMany( - config('auth.providers.users.model'), - config('vote.votes_table'), + \config('auth.providers.users.model'), + \config('vote.votes_table'), 'votable_id', - config('vote.user_foreign_key') + \config('vote.user_foreign_key') ) ->where('votable_type', $this->getMorphClass()); } From edc7a7861bb87f1a6a6882b87ba04fba20da225b Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 11:37:09 +0800 Subject: [PATCH 14/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E6=96=AD=E8=A8=80=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/FeatureTest.php | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/tests/FeatureTest.php b/tests/FeatureTest.php index e95c07b..1d9486e 100644 --- a/tests/FeatureTest.php +++ b/tests/FeatureTest.php @@ -30,7 +30,8 @@ public function test_basic_features() Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { $vote = $event->vote; - + self::assertTrue($vote->isUp()); + self::assertFalse($vote->isDown()); return $vote->votable instanceof Post && $vote->user instanceof User && $vote->user->id === $user->id @@ -47,11 +48,13 @@ public function test_basic_features() self::assertTrue($user->cancelVote($post)); - $user->vote($post, VoteItems::DOWN); + Event::fake(); + $user->vote($post, VoteItems::DOWN); Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { $vote = $event->vote; - + self::assertFalse($vote->isUp()); + self::assertTrue($vote->isDown()); return $vote->votable instanceof Post && $vote->user instanceof User && $vote->user->id === $user->id @@ -65,6 +68,23 @@ public function test_basic_features() self::assertTrue($post->isVotedBy($user)); self::assertFalse($post->isUpVotedBy($user)); self::assertTrue($post->isDownVotedBy($user)); + + /** @var User $user */ + $user = User::create(['name' => 'jcc']); + /** @var Post $post */ + $post = Post::create(['title' => 'Hello world!']); + $user->vote($post, VoteItems::UP); + Event::fake(); + $user->vote($post, VoteItems::DOWN); + Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { + $vote = $event->vote; + self::assertFalse($vote->isUp()); + self::assertTrue($vote->isDown()); + return $vote->votable instanceof Post + && $vote->user instanceof User + && $vote->user->id === $user->id + && $vote->votable->id === $post->id; + }); } public function test_cancelVote_features() @@ -95,21 +115,24 @@ public function test_upVoted_to_downVoted_each_other_features() $user2 = User::create(['name' => 'allen']); $post = Post::create(['title' => 'Hello world!']); - $user1->vote($post, VoteItems::UP); + $upModel = $user1->vote($post, VoteItems::UP); self::assertTrue($user1->hasUpVoted($post)); self::assertFalse($user1->hasDownVoted($post)); - $user1->vote($post, VoteItems::DOWN); + $downModel = $user1->vote($post, VoteItems::DOWN); self::assertFalse($user1->hasUpVoted($post)); self::assertTrue($user1->hasDownVoted($post)); + self::assertTrue($user1->hasDownVoted($post)); + self::assertEquals($upModel->id, $downModel->id); - $user2->vote($post, VoteItems::DOWN); + $downModel = $user2->vote($post, VoteItems::DOWN); self::assertFalse($user2->hasUpVoted($post)); self::assertTrue($user2->hasDownVoted($post)); - $user2->vote($post, VoteItems::UP); + $upModel = $user2->vote($post, VoteItems::UP); self::assertTrue($user2->hasUpVoted($post)); self::assertFalse($user2->hasDownVoted($post)); + self::assertEquals($upModel->id, $downModel->id); } public function test_aggregations() From cc2bd5df74ba8c8946a6c817263ff569fb3f49ef Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 12:17:33 +0800 Subject: [PATCH 15/16] update composer.json scripts --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2a26716..2c5e14b 100644 --- a/composer.json +++ b/composer.json @@ -39,8 +39,8 @@ "minimum-stability": "stable", "prefer-stable": true, "scripts": { - "check-style": "php-cs-fixer fix --using-cache=no --diff --config=.php_cs --dry-run --ansi", - "fix-style": "php-cs-fixer fix --using-cache=no --config=.php_cs --ansi", + "check-style": "vendor/bin/php-cs-fixer fix --using-cache=no --diff --config=.php_cs --dry-run --ansi", + "fix-style": "vendor/bin/php-cs-fixer fix --using-cache=no --config=.php_cs --ansi", "test": "vendor/bin/phpunit --colors=always", "phpstan": "vendor/bin/phpstan analyse src -l 5" } From 00c2a44119c5efeaf302a532ef63622be101f7e3 Mon Sep 17 00:00:00 2001 From: JimChen Date: Sun, 14 Mar 2021 12:22:33 +0800 Subject: [PATCH 16/16] Add VoteItems TestCase --- tests/FeatureTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/FeatureTest.php b/tests/FeatureTest.php index 1d9486e..6cf730d 100644 --- a/tests/FeatureTest.php +++ b/tests/FeatureTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Jcc\LaravelVote\Events\Voted; +use Jcc\LaravelVote\Exceptions\UnexpectValueException; use Jcc\LaravelVote\VoteItems; class FeatureTest extends TestCase @@ -19,6 +20,17 @@ public function setUp(): void config(['auth.providers.users.model' => User::class]); } + public function test_voteItems() + { + self::assertEquals('up_vote', (string)(new VoteItems('up_vote'))); + self::assertEquals('down_vote', (string)(new VoteItems('down_vote'))); + + $invalidValue = 'foobar'; + $this->expectException(UnexpectValueException::class); + $this->expectDeprecationMessage("Unexpect Value: {$invalidValue}"); + new VoteItems($invalidValue); + } + public function test_basic_features() { /** @var User $user */