├── .DS_Store ├── .github └── workflows │ ├── lint.yml │ └── php.yml ├── .gitignore ├── .php_cs ├── LICENSE ├── README.md ├── composer.json ├── config └── vote.php ├── migrations └── 2021_03_13_000000_create_votes_table.php ├── phpunit.xml ├── src ├── .gitignore ├── Events │ ├── CancelVoted.php │ ├── Event.php │ └── Voted.php ├── Exceptions │ └── UnexpectValueException.php ├── Traits │ ├── Votable.php │ └── Voter.php ├── Vote.php ├── VoteItems.php └── VoteServiceProvider.php └── tests ├── Book.php ├── FeatureTest.php ├── Post.php ├── TestCase.php ├── User.php └── migrations ├── 2021_03_13_000000_create_books_table.php ├── 2021_03_13_000000_create_posts_table.php └── 2021_03_13_000000_create_users_table.php /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcc/laravel-vote/b2ff453c2e8bbb5b9620e1a75f8dcddd6e1bd330/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [ push, pull_request ] 3 | 4 | jobs: 5 | lint: 6 | name: PHP-${{ matrix.php_version }}-${{ matrix.perfer }} 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | php_version: 12 | - 7.4 13 | perfer: 14 | - stable 15 | container: 16 | image: nauxliu/php-ci-image:${{ matrix.php_version }} 17 | steps: 18 | - uses: actions/checkout@master 19 | - name: Install Dependencies 20 | run: composer install --prefer-dist --no-interaction --no-suggest 21 | - name: Check Style 22 | run: composer check-style 23 | - name: PHPStan analyse 24 | run: composer phpstan -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | phpunit: 6 | name: PHP-${{ matrix.php_version }}-${{ matrix.perfer }} 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | php_version: 12 | - 7.2 13 | - 7.3 14 | - 7.4 15 | perfer: 16 | - stable 17 | container: 18 | image: nauxliu/php-ci-image:${{ matrix.php_version }} 19 | steps: 20 | - uses: actions/checkout@master 21 | - name: Install Dependencies 22 | run: composer install --prefer-dist --no-interaction --no-suggest 23 | - name: Run PHPUnit 24 | run: ./vendor/bin/phpunit 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor/ 3 | composer.lock 4 | .phpunit.result.cache 5 | .php_cs.cache 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRules([ 5 | '@PSR2' => true, 6 | 'binary_operator_spaces' => true, 7 | 'blank_line_after_opening_tag' => true, 8 | 'compact_nullable_typehint' => true, 9 | 'declare_equal_normalize' => true, 10 | 'lowercase_cast' => true, 11 | 'lowercase_static_reference' => true, 12 | 'new_with_braces' => true, 13 | 'no_blank_lines_after_class_opening' => true, 14 | 'no_leading_import_slash' => true, 15 | 'no_whitespace_in_blank_line' => true, 16 | 'ordered_class_elements' => [ 17 | 'order' => [ 18 | 'use_trait', 19 | ], 20 | ], 21 | 'ordered_imports' => [ 22 | 'imports_order' => [ 23 | 'class', 24 | 'function', 25 | 'const', 26 | ], 27 | 'sort_algorithm' => 'none', 28 | ], 29 | 'return_type_declaration' => true, 30 | 'short_scalar_cast' => true, 31 | 'single_blank_line_before_namespace' => true, 32 | 'single_trait_insert_per_statement' => true, 33 | 'ternary_operator_spaces' => true, 34 | 'unary_operator_spaces' => true, 35 | 'visibility_required' => [ 36 | 'elements' => [ 37 | 'const', 38 | 'method', 39 | 'property', 40 | ], 41 | ], 42 | ]) 43 | ->setFinder( 44 | PhpCsFixer\Finder::create() 45 | ->exclude('vendor') 46 | ->in([__DIR__.'/src/', __DIR__.'/tests/', __DIR__.'/config/', __DIR__.'/migrations/']) 47 | ) 48 | ; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 jcc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Vote System 2 | 3 | :tada: This package helps you to add user based vote system to your model. 4 | 5 | ## Installation 6 | 7 | You can install the package using Composer: 8 | 9 | ```sh 10 | $ composer require "jcc/laravel-vote:~2.0" 11 | ``` 12 | 13 | Then add the service provider to `config/app.php`: 14 | 15 | ```php 16 | Jcc\LaravelVote\VoteServiceProvider::class 17 | ``` 18 | 19 | Publish the migrations file: 20 | 21 | ```sh 22 | $ php artisan vendor:publish --provider="Jcc\LaravelVote\VoteServiceProvider" --tag="migrations" 23 | ``` 24 | 25 | Finally, use VoteTrait in User model: 26 | 27 | ```php 28 | use Jcc\LaravelVote\Traits\Voter; 29 | 30 | class User extends Model 31 | { 32 | use Voter; 33 | } 34 | ``` 35 | 36 | Or use CanBeVoted in Comment model: 37 | 38 | ```php 39 | use Jcc\LaravelVote\Traits\Votable; 40 | 41 | class Comment extends Model 42 | { 43 | use Votable; 44 | } 45 | ``` 46 | 47 | ## Usage 48 | 49 | ### For User model 50 | 51 | #### Up vote a comment or comments 52 | 53 | ```php 54 | $comment = Comment::find(1); 55 | 56 | $user->upVote($comment); 57 | ``` 58 | 59 | ### Down vote a comment or comments 60 | 61 | ```php 62 | $comment = Comment::find(1); 63 | 64 | $user->downVote($comment); 65 | ``` 66 | 67 | #### Cancel vote a comment or comments 68 | 69 | ```php 70 | $comment = Comment::find(1); 71 | 72 | $user->cancelVote($comment); 73 | ``` 74 | 75 | #### Get user has voted comment items 76 | 77 | ```php 78 | $user->getVotedItems(Comment::class)->get(); 79 | ``` 80 | 81 | #### Check if user has up or down vote 82 | 83 | ```php 84 | $comment = Comment::find(1); 85 | 86 | $user->hasVoted($comment); 87 | ``` 88 | 89 | #### Check if user has up vote 90 | 91 | ```php 92 | $comment = Comment::find(1); 93 | 94 | $user->hasUpVoted($comment); 95 | ``` 96 | 97 | #### Check if user has down vote 98 | 99 | ```php 100 | $comment = Comment::find(1); 101 | 102 | $user->hasDownVoted($comment); 103 | ``` 104 | 105 | ### For Comment model 106 | 107 | #### Get comment voters 108 | 109 | ```php 110 | $comment->voters()->get(); 111 | ``` 112 | 113 | #### Count comment voters 114 | 115 | ```php 116 | $comment->voters()->count(); 117 | ``` 118 | 119 | #### Get comment up voters 120 | 121 | ```php 122 | $comment->upVoters()->get(); 123 | ``` 124 | 125 | #### Count comment up voters 126 | 127 | ```php 128 | $comment->upVoters()->count(); 129 | ``` 130 | 131 | #### Get comment down voters 132 | 133 | ```php 134 | $comment->downVoters()->get(); 135 | ``` 136 | 137 | #### Count comment down voters 138 | 139 | ```php 140 | $comment->downVoters()->count(); 141 | ``` 142 | 143 | #### Check if voted by 144 | 145 | ```php 146 | $user = User::find(1); 147 | 148 | $comment->isVotedBy($user); 149 | ``` 150 | 151 | #### Check if up voted by 152 | 153 | ```php 154 | $user = User::find(1); 155 | 156 | $comment->isUpVotedBy($user); 157 | ``` 158 | 159 | #### Check if down voted by 160 | 161 | ```php 162 | $user = User::find(1); 163 | 164 | $comment->isDownVotedBy($user); 165 | ``` 166 | 167 | ### N+1 issue 168 | 169 | 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: 170 | 171 | ```php 172 | // Voter 173 | $users = User::with('votes')->get(); 174 | 175 | foreach($users as $user) { 176 | $user->hasVoted($comment); 177 | } 178 | 179 | // Votable 180 | $comments = Comment::with('voters')->get(); 181 | 182 | foreach($comments as $comment) { 183 | $comment->isVotedBy($user); 184 | } 185 | ``` 186 | 187 | ### Events 188 | 189 | | **Event** | **Description** | 190 | | --- | --- | 191 | | `Jcc\LaravelVote\Events\Voted` | Triggered when the relationship is created or updated. | 192 | | `Jcc\LaravelVote\Events\CancelVoted` | Triggered when the relationship is deleted. | 193 | 194 | 195 | ## Reference 196 | 197 | - [laravel-follow](https://github.com/overtrue/laravel-follow) 198 | - [laravel-like](https://github.com/overtrue/laravel-like) 199 | 200 | ## License 201 | 202 | [MIT](LICENSE) 203 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jcc/laravel-vote", 3 | "description": "The package helps you to add user based vote system to your model", 4 | "authors": [ 5 | { 6 | "name": "jcc", 7 | "email": "changejian@gmail.com" 8 | } 9 | ], 10 | "require": { 11 | "php": ">=7.2", 12 | "laravel/framework": "^5.5|~6.0|~7.0|~8.0|~9.0", 13 | "symfony/polyfill-php80": "^1.22" 14 | }, 15 | "require-dev": { 16 | "mockery/mockery": "^1.3", 17 | "orchestra/testbench": "^3.5|~4.0|~5.0|~6.0", 18 | "friendsofphp/php-cs-fixer": "^2.18", 19 | "phpstan/phpstan": "^0.12.81" 20 | }, 21 | "license": "MIT", 22 | "autoload": { 23 | "psr-4": { 24 | "Jcc\\LaravelVote\\": "src" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Jcc\\LaravelVote\\Tests\\": "tests" 30 | } 31 | }, 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "Jcc\\LaravelVote\\VoteServiceProvider" 36 | ] 37 | } 38 | }, 39 | "minimum-stability": "stable", 40 | "prefer-stable": true, 41 | "scripts": { 42 | "check-style": "vendor/bin/php-cs-fixer fix --using-cache=no --diff --config=.php_cs --dry-run --ansi", 43 | "fix-style": "vendor/bin/php-cs-fixer fix --using-cache=no --config=.php_cs --ansi", 44 | "test": "vendor/bin/phpunit --colors=always", 45 | "phpstan": "vendor/bin/phpstan analyse src -l 5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/vote.php: -------------------------------------------------------------------------------- 1 | 'votes', 6 | 7 | 'user_foreign_key' => 'user_id', 8 | 9 | 'vote_model' => \Jcc\LaravelVote\Vote::class, 10 | ]; 11 | -------------------------------------------------------------------------------- /migrations/2021_03_13_000000_create_votes_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 16 | $table->unsignedBigInteger(config('vote.user_foreign_key'))->index(); 17 | $table->morphs('votable'); 18 | $table->string('vote_type', 16)->default('up_vote'); // 'up_vote'/'down_vote' 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down() 27 | { 28 | Schema::dropIfExists(config('vote.votes_table')); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/ 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /src/Events/CancelVoted.php: -------------------------------------------------------------------------------- 1 | vote = $vote; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Events/Voted.php: -------------------------------------------------------------------------------- 1 | relationLoaded('voters')) { 24 | return $this->voters->contains($user); 25 | } 26 | 27 | return $this->voters() 28 | ->where(\config('vote.user_foreign_key'), $user->getKey()) 29 | ->when(\is_string($type), function ($builder) use ($type) { 30 | $builder->where('vote_type', (string)new VoteItems($type)); 31 | }) 32 | ->exists(); 33 | } 34 | 35 | return false; 36 | } 37 | 38 | /** 39 | * Return voters. 40 | * 41 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 42 | */ 43 | public function voters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany 44 | { 45 | return $this->belongsToMany( 46 | \config('auth.providers.users.model'), 47 | \config('vote.votes_table'), 48 | 'votable_id', 49 | \config('vote.user_foreign_key') 50 | ) 51 | ->where('votable_type', $this->getMorphClass()); 52 | } 53 | 54 | /** 55 | * @param \Illuminate\Database\Eloquent\Model $user 56 | * 57 | * @return bool 58 | */ 59 | public function isUpVotedBy(Model $user) 60 | { 61 | return $this->isVotedBy($user, VoteItems::UP); 62 | } 63 | 64 | /** 65 | * Return up voters. 66 | * 67 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 68 | */ 69 | public function upVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany 70 | { 71 | return $this->voters()->where('vote_type', VoteItems::UP); 72 | } 73 | 74 | /** 75 | * @param \Illuminate\Database\Eloquent\Model $user 76 | * 77 | * @return bool 78 | */ 79 | public function isDownVotedBy(Model $user) 80 | { 81 | return $this->isVotedBy($user, VoteItems::DOWN); 82 | } 83 | 84 | /** 85 | * Return down voters. 86 | * 87 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 88 | */ 89 | public function downVoters(): \Illuminate\Database\Eloquent\Relations\BelongsToMany 90 | { 91 | return $this->voters()->where('vote_type', VoteItems::DOWN); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Traits/Voter.php: -------------------------------------------------------------------------------- 1 | $object->getMorphClass(), 27 | 'votable_id' => $object->getKey(), 28 | \config('vote.user_foreign_key') => $this->getKey(), 29 | ]; 30 | 31 | /* @var \Illuminate\Database\Eloquent\Model $vote */ 32 | $vote = \app(\config('vote.vote_model')); 33 | 34 | $type = (string)new VoteItems($type); 35 | 36 | /* @var \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder $vote */ 37 | return tap($vote->where($attributes)->firstOr( 38 | function () use ($vote, $attributes, $type) { 39 | $vote->unguard(); 40 | 41 | if ($this->relationLoaded('votes')) { 42 | $this->unsetRelation('votes'); 43 | } 44 | 45 | return $vote->create(\array_merge($attributes, [ 46 | 'vote_type' => $type, 47 | ])); 48 | } 49 | ), function (Model $model) use ($type) { 50 | $model->update(['vote_type' => $type]); 51 | }); 52 | } 53 | 54 | /** 55 | * @param \Illuminate\Database\Eloquent\Model $object 56 | * 57 | * @return bool 58 | */ 59 | public function hasVoted(Model $object, ?string $type = null): bool 60 | { 61 | return ($this->relationLoaded('votes') ? $this->votes : $this->votes()) 62 | ->where('votable_id', $object->getKey()) 63 | ->where('votable_type', $object->getMorphClass()) 64 | ->when(\is_string($type), function ($builder) use ($type) { 65 | $builder->where('vote_type', (string)new VoteItems($type)); 66 | }) 67 | ->count() > 0; 68 | } 69 | 70 | /** 71 | * @param Model $object 72 | * @return bool 73 | * @throws \Exception 74 | */ 75 | public function cancelVote(Model $object): bool 76 | { 77 | /* @var \Jcc\LaravelVote\Vote $relation */ 78 | $relation = \app(\config('vote.vote_model')) 79 | ->where('votable_id', $object->getKey()) 80 | ->where('votable_type', $object->getMorphClass()) 81 | ->where(\config('vote.user_foreign_key'), $this->getKey()) 82 | ->first(); 83 | 84 | if ($relation) { 85 | if ($this->relationLoaded('votes')) { 86 | $this->unsetRelation('votes'); 87 | } 88 | 89 | return $relation->delete(); 90 | } 91 | 92 | return true; 93 | } 94 | 95 | /** 96 | * @return HasMany 97 | */ 98 | public function votes(): HasMany 99 | { 100 | return $this->hasMany(\config('vote.vote_model'), \config('vote.user_foreign_key'), $this->getKeyName()); 101 | } 102 | 103 | /** 104 | * Get Query Builder for votes 105 | * 106 | * @return \Illuminate\Database\Eloquent\Builder 107 | */ 108 | public function getVotedItems(string $model, ?string $type = null) 109 | { 110 | return \app($model)->whereHas( 111 | 'voters', 112 | function ($builder) use ($type) { 113 | return $builder->where(\config('vote.user_foreign_key'), $this->getKey())->when( 114 | \is_string($type), 115 | function ($builder) use ($type) { 116 | $builder->where('vote_type', (string)new VoteItems($type)); 117 | } 118 | ); 119 | } 120 | ); 121 | } 122 | 123 | public function upVote(Model $object): Vote 124 | { 125 | return $this->vote($object, VoteItems::UP); 126 | } 127 | 128 | public function downVote(Model $object): Vote 129 | { 130 | return $this->vote($object, VoteItems::DOWN); 131 | } 132 | 133 | public function hasUpVoted(Model $object) 134 | { 135 | return $this->hasVoted($object, VoteItems::UP); 136 | } 137 | 138 | public function hasDownVoted(Model $object) 139 | { 140 | return $this->hasVoted($object, VoteItems::DOWN); 141 | } 142 | 143 | public function toggleUpVote(Model $object) 144 | { 145 | return $this->hasUpVoted($object) ? $this->cancelVote($object) : $this->upVote($object); 146 | } 147 | 148 | public function toggleDownVote(Model $object) 149 | { 150 | return $this->hasDownVoted($object) ? $this->cancelVote($object) : $this->downVote($object); 151 | } 152 | 153 | public function getUpVotedItems(string $model) 154 | { 155 | return $this->getVotedItems($model, VoteItems::UP); 156 | } 157 | 158 | public function getDownVotedItems(string $model) 159 | { 160 | return $this->getVotedItems($model, VoteItems::DOWN); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Vote.php: -------------------------------------------------------------------------------- 1 | Voted::class, 24 | 'updated' => Voted::class, 25 | 26 | 'deleted' => CancelVoted::class, 27 | ]; 28 | 29 | /** 30 | * @param array $attributes 31 | */ 32 | public function __construct(array $attributes = []) 33 | { 34 | $this->table = \config('vote.votes_table'); 35 | 36 | parent::__construct($attributes); 37 | } 38 | 39 | protected static function boot() 40 | { 41 | parent::boot(); 42 | 43 | self::creating(function (Vote $vote) { 44 | $userForeignKey = \config('vote.user_foreign_key'); 45 | $vote->{$userForeignKey} = $vote->{$userForeignKey} ?: Auth::id(); 46 | }); 47 | } 48 | 49 | public function votable(): MorphTo 50 | { 51 | return $this->morphTo(); 52 | } 53 | 54 | /** 55 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 56 | */ 57 | public function user() 58 | { 59 | return $this->belongsTo(\config('auth.providers.users.model'), \config('vote.user_foreign_key')); 60 | } 61 | 62 | /** 63 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 64 | */ 65 | public function voter() 66 | { 67 | return $this->user(); 68 | } 69 | 70 | /** 71 | * @param \Illuminate\Database\Eloquent\Builder $query 72 | * @param string $type 73 | * 74 | * @return \Illuminate\Database\Eloquent\Builder 75 | */ 76 | public function scopeWithVotableType(Builder $query, string $type) 77 | { 78 | return $query->where('votable_type', \app($type)->getMorphClass()); 79 | } 80 | 81 | /** 82 | * @param \Illuminate\Database\Eloquent\Builder $query 83 | * @param string $type 84 | * 85 | * @return \Illuminate\Database\Eloquent\Builder 86 | */ 87 | public function scopeWithVoteType(Builder $query, string $type) 88 | { 89 | return $query->where('vote_type', (string)new VoteItems($type)); 90 | } 91 | 92 | public function isUp(): bool 93 | { 94 | return $this->vote_type === VoteItems::UP; 95 | } 96 | 97 | public function isDown(): bool 98 | { 99 | return $this->vote_type === VoteItems::DOWN; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/VoteItems.php: -------------------------------------------------------------------------------- 1 | value = $value; 25 | } 26 | 27 | /** 28 | * @return string[] 29 | */ 30 | public static function getValues(): array 31 | { 32 | return [self::UP, self::DOWN]; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function __toString() 39 | { 40 | return $this->value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/VoteServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Jcc\LaravelVote; 13 | 14 | use Illuminate\Support\ServiceProvider; 15 | 16 | class VoteServiceProvider extends ServiceProvider 17 | { 18 | /** 19 | * Application bootstrap event. 20 | */ 21 | public function boot() 22 | { 23 | $this->publishes([ 24 | \dirname(__DIR__) . '/config/vote.php' => config_path('vote.php'), 25 | ], 'config'); 26 | 27 | $this->publishes([ 28 | \dirname(__DIR__) . '/migrations/' => database_path('migrations'), 29 | ], 'migrations'); 30 | 31 | if ($this->app->runningInConsole()) { 32 | $this->loadMigrationsFrom(\dirname(__DIR__) . '/migrations/'); 33 | } 34 | } 35 | 36 | /** 37 | * Register the service provider. 38 | */ 39 | public function register() 40 | { 41 | $this->mergeConfigFrom( 42 | \dirname(__DIR__) . '/config/vote.php', 43 | 'vote' 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Book.php: -------------------------------------------------------------------------------- 1 | User::class]); 21 | } 22 | 23 | public function test_voteItems() 24 | { 25 | self::assertEquals('up_vote', (string)(new VoteItems('up_vote'))); 26 | self::assertEquals('down_vote', (string)(new VoteItems('down_vote'))); 27 | 28 | $invalidValue = 'foobar'; 29 | $this->expectException(UnexpectValueException::class); 30 | $this->expectDeprecationMessage("Unexpect Value: {$invalidValue}"); 31 | new VoteItems($invalidValue); 32 | } 33 | 34 | public function test_basic_features() 35 | { 36 | /** @var User $user */ 37 | $user = User::create(['name' => 'jcc']); 38 | /** @var Post $post */ 39 | $post = Post::create(['title' => 'Hello world!']); 40 | 41 | $user->vote($post, VoteItems::UP); 42 | 43 | Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { 44 | $vote = $event->vote; 45 | self::assertTrue($vote->isUp()); 46 | self::assertFalse($vote->isDown()); 47 | return $vote->votable instanceof Post 48 | && $vote->user instanceof User 49 | && $vote->user->id === $user->id 50 | && $vote->votable->id === $post->id; 51 | }); 52 | 53 | self::assertTrue($user->hasVoted($post)); 54 | self::assertTrue($user->hasUpVoted($post)); 55 | self::assertFalse($user->hasDownVoted($post)); 56 | 57 | self::assertTrue($post->isVotedBy($user)); 58 | self::assertTrue($post->isUpVotedBy($user)); 59 | self::assertFalse($post->isDownVotedBy($user)); 60 | 61 | self::assertTrue($user->cancelVote($post)); 62 | 63 | Event::fake(); 64 | 65 | $user->vote($post, VoteItems::DOWN); 66 | Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { 67 | $vote = $event->vote; 68 | self::assertFalse($vote->isUp()); 69 | self::assertTrue($vote->isDown()); 70 | return $vote->votable instanceof Post 71 | && $vote->user instanceof User 72 | && $vote->user->id === $user->id 73 | && $vote->votable->id === $post->id; 74 | }); 75 | 76 | self::assertTrue($user->hasVoted($post)); 77 | self::assertFalse($user->hasUpVoted($post)); 78 | self::assertTrue($user->hasDownVoted($post)); 79 | 80 | self::assertTrue($post->isVotedBy($user)); 81 | self::assertFalse($post->isUpVotedBy($user)); 82 | self::assertTrue($post->isDownVotedBy($user)); 83 | 84 | /** @var User $user */ 85 | $user = User::create(['name' => 'jcc']); 86 | /** @var Post $post */ 87 | $post = Post::create(['title' => 'Hello world!']); 88 | $user->vote($post, VoteItems::UP); 89 | Event::fake(); 90 | $user->vote($post, VoteItems::DOWN); 91 | Event::assertDispatched(Voted::class, function ($event) use ($user, $post) { 92 | $vote = $event->vote; 93 | self::assertFalse($vote->isUp()); 94 | self::assertTrue($vote->isDown()); 95 | return $vote->votable instanceof Post 96 | && $vote->user instanceof User 97 | && $vote->user->id === $user->id 98 | && $vote->votable->id === $post->id; 99 | }); 100 | } 101 | 102 | public function test_cancelVote_features() 103 | { 104 | $user1 = User::create(['name' => 'jcc']); 105 | $user2 = User::create(['name' => 'allen']); 106 | 107 | $post = Post::create(['title' => 'Hello world!']); 108 | 109 | $user1->vote($post, VoteItems::DOWN); 110 | $user2->vote($post, VoteItems::UP); 111 | 112 | $user1->cancelVote($post); 113 | $user2->cancelVote($post); 114 | 115 | self::assertFalse($user1->hasVoted($post)); 116 | self::assertFalse($user1->hasDownVoted($post)); 117 | self::assertFalse($user1->hasUpVoted($post)); 118 | 119 | self::assertFalse($user1->hasVoted($post)); 120 | self::assertFalse($user2->hasUpVoted($post)); 121 | self::assertFalse($user2->hasDownVoted($post)); 122 | } 123 | 124 | public function test_upVoted_to_downVoted_each_other_features() 125 | { 126 | $user1 = User::create(['name' => 'jcc']); 127 | $user2 = User::create(['name' => 'allen']); 128 | $post = Post::create(['title' => 'Hello world!']); 129 | 130 | $upModel = $user1->vote($post, VoteItems::UP); 131 | self::assertTrue($user1->hasUpVoted($post)); 132 | self::assertFalse($user1->hasDownVoted($post)); 133 | 134 | $downModel = $user1->vote($post, VoteItems::DOWN); 135 | self::assertFalse($user1->hasUpVoted($post)); 136 | self::assertTrue($user1->hasDownVoted($post)); 137 | self::assertTrue($user1->hasDownVoted($post)); 138 | self::assertEquals($upModel->id, $downModel->id); 139 | 140 | $downModel = $user2->vote($post, VoteItems::DOWN); 141 | self::assertFalse($user2->hasUpVoted($post)); 142 | self::assertTrue($user2->hasDownVoted($post)); 143 | 144 | $upModel = $user2->vote($post, VoteItems::UP); 145 | self::assertTrue($user2->hasUpVoted($post)); 146 | self::assertFalse($user2->hasDownVoted($post)); 147 | self::assertEquals($upModel->id, $downModel->id); 148 | } 149 | 150 | public function test_aggregations() 151 | { 152 | $user = User::create(['name' => 'jcc']); 153 | 154 | $post1 = Post::create(['title' => 'Hello world!']); 155 | $post2 = Post::create(['title' => 'Hello everyone!']); 156 | $post3 = Post::create(['title' => 'Hello players!']); 157 | $book1 = Book::create(['title' => 'Learn laravel.']); 158 | $book2 = Book::create(['title' => 'Learn symfony.']); 159 | $book3 = Book::create(['title' => 'Learn yii2.']); 160 | 161 | $user->vote($post1, VoteItems::UP); 162 | $user->vote($post2, VoteItems::UP); 163 | $user->vote($post3, VoteItems::DOWN); 164 | 165 | $user->vote($book1, VoteItems::UP); 166 | $user->vote($book2, VoteItems::UP); 167 | $user->vote($book3, VoteItems::DOWN); 168 | 169 | self::assertSame(6, $user->votes()->count()); 170 | self::assertSame(4, $user->votes()->withVoteType(VoteItems::UP)->count()); 171 | self::assertSame(2, $user->votes()->withVoteType(VoteItems::DOWN)->count()); 172 | 173 | self::assertSame(3, $user->votes()->withVotableType(Book::class)->count()); 174 | self::assertSame(1, $user->votes()->withVoteType(VoteItems::DOWN)->withVotableType(Book::class)->count()); 175 | } 176 | 177 | public function test_vote_same_model() 178 | { 179 | $user1 = User::create(['name' => 'jcc']); 180 | $user2 = User::create(['name' => 'allen']); 181 | $user3 = User::create(['name' => 'taylor']); 182 | 183 | $user1->vote($user2, VoteItems::UP); 184 | $user3->vote($user1, VoteItems::DOWN); 185 | 186 | self::assertTrue($user1->hasVoted($user2)); 187 | self::assertTrue($user2->isVotedBy($user1)); 188 | 189 | self::assertTrue($user1->hasUpVoted($user2)); 190 | self::assertTrue($user2->isUpVotedBy($user1)); 191 | 192 | self::assertTrue($user3->hasVoted($user1)); 193 | self::assertTrue($user1->isVotedBy($user3)); 194 | 195 | self::assertTrue($user3->hasDownVoted($user1)); 196 | self::assertTrue($user1->isDownVotedBy($user3)); 197 | } 198 | 199 | public function test_object_voters() 200 | { 201 | $user1 = User::create(['name' => 'jcc']); 202 | $user2 = User::create(['name' => 'allen']); 203 | $user3 = User::create(['name' => 'taylor']); 204 | 205 | $post = Post::create(['title' => 'Hello world!']); 206 | 207 | $user1->vote($post, VoteItems::UP); 208 | $user2->vote($post, VoteItems::DOWN); 209 | 210 | self::assertCount(2, $post->voters); 211 | self::assertSame('jcc', $post->voters[0]['name']); 212 | self::assertSame('allen', $post->voters[1]['name']); 213 | 214 | $sqls = $this->getQueryLog(function () use ($post, $user1, $user2, $user3) { 215 | self::assertTrue($post->isVotedBy($user1)); 216 | self::assertTrue($post->isVotedBy($user2)); 217 | self::assertFalse($post->isVotedBy($user3)); 218 | }); 219 | 220 | self::assertEmpty($sqls->all()); 221 | } 222 | 223 | public function test_object_votes_with_custom_morph_class_name() 224 | { 225 | $user1 = User::create(['name' => 'jcc']); 226 | $user2 = User::create(['name' => 'allen']); 227 | $user3 = User::create(['name' => 'taylor']); 228 | 229 | $post = Post::create(['title' => 'Hello world!']); 230 | 231 | Relation::morphMap([ 232 | 'posts' => Post::class, 233 | ]); 234 | 235 | $user1->vote($post, VoteItems::UP); 236 | $user2->vote($post, VoteItems::DOWN); 237 | 238 | self::assertCount(2, $post->voters); 239 | self::assertSame('jcc', $post->voters[0]['name']); 240 | self::assertSame('allen', $post->voters[1]['name']); 241 | } 242 | 243 | public function test_eager_loading() 244 | { 245 | $user = User::create(['name' => 'jcc']); 246 | 247 | $post1 = Post::create(['title' => 'Hello world!']); 248 | $post2 = Post::create(['title' => 'Hello everyone!']); 249 | $book1 = Book::create(['title' => 'Learn laravel.']); 250 | $book2 = Book::create(['title' => 'Learn symfony.']); 251 | 252 | $user->vote($post1, VoteItems::UP); 253 | $user->vote($post2, VoteItems::DOWN); 254 | $user->vote($book1, VoteItems::UP); 255 | $user->vote($book2, VoteItems::DOWN); 256 | 257 | // start recording 258 | $sqls = $this->getQueryLog(function () use ($user) { 259 | $user->load('votes.votable'); 260 | }); 261 | 262 | self::assertSame(3, $sqls->count()); 263 | 264 | // from loaded relations 265 | $sqls = $this->getQueryLog(function () use ($user, $post1) { 266 | $user->hasVoted($post1); 267 | }); 268 | 269 | self::assertEmpty($sqls->all()); 270 | } 271 | 272 | /** 273 | * @param \Closure $callback 274 | * 275 | * @return \Illuminate\Support\Collection 276 | */ 277 | protected function getQueryLog(\Closure $callback): \Illuminate\Support\Collection 278 | { 279 | $sqls = \collect([]); 280 | DB::listen(function ($query) use ($sqls) { 281 | $sqls->push(['sql' => $query->sql, 'bindings' => $query->bindings]); 282 | }); 283 | 284 | $callback(); 285 | 286 | return $sqls; 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /tests/Post.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'testing'); 30 | $app['config']->set('database.connections.testing', [ 31 | 'driver' => 'sqlite', 32 | 'database' => ':memory:', 33 | 'prefix' => '', 34 | ]); 35 | } 36 | 37 | /** 38 | * run package database migrations. 39 | */ 40 | public function setUp(): void 41 | { 42 | parent::setUp(); 43 | $this->loadMigrationsFrom(__DIR__ . '/migrations'); 44 | $this->loadMigrationsFrom(dirname(__DIR__) . '/migrations'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/User.php: -------------------------------------------------------------------------------- 1 | increments('id'); 16 | $table->string('title'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down() 25 | { 26 | Schema::dropIfExists('books'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/migrations/2021_03_13_000000_create_posts_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 16 | $table->string('title'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down() 25 | { 26 | Schema::dropIfExists('posts'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/migrations/2021_03_13_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 16 | $table->string('name'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down() 25 | { 26 | Schema::dropIfExists('users'); 27 | } 28 | } 29 | --------------------------------------------------------------------------------