├── src
├── .gitignore
├── Events
│ ├── Voted.php
│ ├── CancelVoted.php
│ └── Event.php
├── Exceptions
│ └── UnexpectValueException.php
├── VoteItems.php
├── VoteServiceProvider.php
├── Traits
│ ├── Votable.php
│ └── Voter.php
└── Vote.php
├── .DS_Store
├── .gitignore
├── config
└── vote.php
├── tests
├── Book.php
├── Post.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
├── TestCase.php
└── FeatureTest.php
├── phpunit.xml
├── .github
└── workflows
│ ├── php.yml
│ └── lint.yml
├── migrations
└── 2021_03_13_000000_create_votes_table.php
├── LICENSE
├── composer.json
├── .php_cs
└── README.md
/src/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jcc/laravel-vote/HEAD/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | /vendor/
3 | composer.lock
4 | .phpunit.result.cache
5 | .php_cs.cache
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/src/Events/Voted.php:
--------------------------------------------------------------------------------
1 | 'votes',
6 |
7 | 'user_foreign_key' => 'user_id',
8 |
9 | 'vote_model' => \Jcc\LaravelVote\Vote::class,
10 | ];
11 |
--------------------------------------------------------------------------------
/tests/Book.php:
--------------------------------------------------------------------------------
1 | vote = $vote;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./tests/
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/tests/migrations/2021_03_13_000000_create_books_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('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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/TestCase.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 | ;
--------------------------------------------------------------------------------
/src/Traits/Votable.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/FeatureTest.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 |
--------------------------------------------------------------------------------