diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index db5635a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: php - -php: - - 7.1 - - 7.2 - - 7.3 - -dist: trusty -sudo: false - -install: travis_retry composer install --no-interaction --prefer-source - -script: vendor/bin/phpunit --verbose diff --git a/README.md b/README.md index c34bf77..507d2f1 100644 --- a/README.md +++ b/README.md @@ -36,18 +36,43 @@ php artisan vendor:publish ### Traits -#### `Overtrue\LaravelFollow\Followable` +#### `Overtrue\LaravelFollow\Traits\Follower` + +Add the Follower trait to your user model: ```php -use Illuminate\Notifications\Notifiable; -use Illuminate\Contracts\Auth\MustVerifyEmail; -use Illuminate\Foundation\Auth\User as Authenticatable; +use Overtrue\LaravelFavorite\Traits\Favoriter; + +class User extends Authenticatable +{ + use Follower; + + <...> +} +``` + +#### `Overtrue\LaravelFollow\Followable` + +Then add the Followable trait to your followable model, for example `App\User`: + +```php use Overtrue\LaravelFollow\Followable; class User extends Authenticatable { + use Followable; <...> +} +``` + +or any other model: + +```php +use Overtrue\LaravelFollow\Followable; + +class Channel extends Model +{ use Followable; <...> } @@ -68,8 +93,6 @@ $user1->rejectFollowRequestFrom($user2); $user1->isFollowing($user2); $user2->isFollowedBy($user1); $user2->hasRequestedToFollow($user1); - -$user1->areFollowingEachOther($user2); ``` #### Get followings: @@ -120,7 +143,7 @@ $user->notApprovedFollowers()->count(); List with `*_count` attribute: ```php -$users = User::withCount(['followings', 'followers'])->get(); +$users = User::withCount(['followings', 'followables'])->get(); // or $users = User::withCount(['approvedFollowings', 'approvedFollowers'])->get(); @@ -135,7 +158,7 @@ foreach($users as $user) { ### Attach user follow status to followable collection -You can use `Followable::attachFollowStatus(Collection $followables)` to attach the user favorite status, it will set `has_followed` attribute to each model of `$followables`: +You can use `Follower::attachFollowStatus(Collection $followables)` to attach the user favorite status, it will set `has_followed` attribute to each model of `$followables`: #### For model @@ -238,7 +261,7 @@ foreach($users as $user) { $user->isFollowing(2); } -$users = User::with('followers')->get(); +$users = User::with('followables')->get(); foreach($users as $user) { $user->isFollowedBy(2); diff --git a/composer.json b/composer.json index 40c72fc..baaa8aa 100644 --- a/composer.json +++ b/composer.json @@ -22,10 +22,10 @@ } }, "require-dev": { - "mockery/mockery": "^1.4", + "mockery/mockery": "^1.5", "phpunit/phpunit": "^9.5", - "orchestra/testbench": "^7.0", - "friendsofphp/php-cs-fixer": "^3.0" + "orchestra/testbench": "^7.4", + "friendsofphp/php-cs-fixer": "^3.8" }, "extra": { "laravel": { diff --git a/config/follow.php b/config/follow.php index ada5aa3..0387eb6 100644 --- a/config/follow.php +++ b/config/follow.php @@ -2,7 +2,22 @@ return [ /* - * Table name for followers records. + * Use uuid as primary key. */ - 'relation_table' => 'user_follower', + 'uuids' => false, + + /* + * User tables foreign key name. + */ + 'user_foreign_key' => 'user_id', + + /* + * Table name for followers table. + */ + 'followables_table' => 'followables', + + /** + * Model class name for followers table. + */ + 'followables_model' => \Overtrue\LaravelFollow\Followable::class, ]; diff --git a/migrations/2020_04_04_000000_create_user_follower_table.php b/migrations/2020_04_04_000000_create_user_follower_table.php deleted file mode 100644 index 7b55978..0000000 --- a/migrations/2020_04_04_000000_create_user_follower_table.php +++ /dev/null @@ -1,24 +0,0 @@ -increments('id'); - $table->unsignedBigInteger('following_id')->index(); - $table->unsignedBigInteger('follower_id')->index(); - $table->timestamp('accepted_at')->nullable()->index(); - $table->timestamps(); - }); - } - - public function down() - { - Schema::dropIfExists(config('follow.relation_table', 'user_follower')); - } -} diff --git a/migrations/2022_05_02_000000_create_followables_table.php b/migrations/2022_05_02_000000_create_followables_table.php new file mode 100644 index 0000000..cf3a66a --- /dev/null +++ b/migrations/2022_05_02_000000_create_followables_table.php @@ -0,0 +1,31 @@ +id(); + $table->unsignedBigInteger(config('follow.user_foreign_key', 'user_id'))->index()->comment('user_id'); + if (config('follow.uuids')) { + $table->uuidMorphs('followable'); + } else { + $table->morphs('followable'); + } + + $table->timestamp('accepted_at')->nullable(); + $table->timestamps(); + + $table->index(['followable_type', 'accepted_at']); + }); + } + + public function down() + { + Schema::dropIfExists(config('follow.followables_table', 'followables')); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e47284c..b2daa31 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,21 +1,13 @@ - - - - ./tests/ - - - - - src/ - - + + + + src/ + + + + + ./tests/ + + diff --git a/src/Events/Event.php b/src/Events/Event.php index 299fec3..34c3f69 100644 --- a/src/Events/Event.php +++ b/src/Events/Event.php @@ -2,26 +2,21 @@ namespace Overtrue\LaravelFollow\Events; -use Overtrue\LaravelFollow\UserFollower; +use Overtrue\LaravelFollow\Followable; class Event { - /** - * @var int|string - */ - public $followingId; + public int|string $followable_id; + public int|string $followable_type; + public int|string $follower_id; + public int|string $user_id; - /** - * @var int|string - */ - public $followerId; + protected Followable $relation; - protected UserFollower $relation; - - public function __construct(UserFollower $relation) + public function __construct(Followable $follower) { - $this->relation = $relation; - $this->followerId = $relation->follower_id; - $this->followingId = $relation->following_id; + $this->follower_id = $this->user_id = $follower->{\config('follow.user_foreign_key', 'user_id')}; + $this->followable_id = $follower->followable_id; + $this->followable_type = $follower->followable_type; } } diff --git a/src/Followable.php b/src/Followable.php index d34c0c4..534163c 100644 --- a/src/Followable.php +++ b/src/Followable.php @@ -2,245 +2,95 @@ namespace Overtrue\LaravelFollow; -use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Pagination\CursorPaginator; -use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Support\Enumerable; -use Illuminate\Support\LazyCollection; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Database\Eloquent\Relations\Pivot; +use Illuminate\Support\Str; +use Overtrue\LaravelFollow\Events\Followed; +use Overtrue\LaravelFollow\Events\Unfollowed; +use function config; /** - * @property \Illuminate\Database\Eloquent\Collection $followings - * @property \Illuminate\Database\Eloquent\Collection $followers + * @property int|string $followable_id; + * @property int|string $followable_type; + * @property int|string $user_id; + * @method HasMany of(Model $model) + * @method HasMany followedBy(Model $model) + * @method HasMany withType(string $type) */ -trait Followable +class Followable extends Model { - /** - * @return bool - */ - public function needsToApproveFollowRequests(): bool - { - return false; - } - - /** - * @param \Illuminate\Database\Eloquent\Model|int $user - * - * @return array - */ - public function follow($user): array - { - /** @var \Illuminate\Database\Eloquent\Model|\Overtrue\LaravelFollow\Followable $user */ - $isPending = $user->needsToApproveFollowRequests() ?: false; + protected $guarded = []; - $this->followings()->syncWithPivotValues($user, [ - 'accepted_at' => $isPending ? null : now() - ], false); - - return ['pending' => $isPending]; - } - - /** - * @param \Illuminate\Database\Eloquent\Model|int $user - */ - public function unfollow($user) - { - $this->followings()->detach($user); - } + protected $dispatchesEvents = [ + 'created' => Followed::class, + 'deleted' => Unfollowed::class, + ]; - /** - * @param \Illuminate\Database\Eloquent\Model|int $user - * - */ - public function toggleFollow($user) - { - $this->isFollowing($user) ? $this->unfollow($user) : $this->follow($user); - } + protected $dates = ['accepted_at']; - /** - * @param \Illuminate\Database\Eloquent\Model|int $user - */ - public function rejectFollowRequestFrom($user) + public function __construct(array $attributes = []) { - $this->followers()->detach($user); - } + $this->table = config('follow.followables_table', 'followables'); - /** - * @param \Illuminate\Database\Eloquent\Model|int $user - */ - public function acceptFollowRequestFrom($user) - { - $this->followers()->updateExistingPivot($user, ['accepted_at' => now()]); + parent::__construct($attributes); } - /** - * @param \Illuminate\Database\Eloquent\Model|int $user - */ - public function hasRequestedToFollow($user): bool + protected static function boot() { - if ($user instanceof Model) { - $user = $user->getKey(); - } - - if ($this->relationLoaded('followings')) { - return $this->followings - ->where('pivot.accepted_at', '===', null) - ->contains($user); - } - - return $this->followings() - ->wherePivot('accepted_at', null) - ->where($this->getQualifiedKeyName(), $user) - ->exists(); - } + parent::boot(); - /** - * @param \Illuminate\Database\Eloquent\Model|int $user - */ - public function isFollowing($user): bool - { - if ($user instanceof Model) { - $user = $user->getKey(); - } - - if ($this->relationLoaded('followings')) { - return $this->followings - ->where('pivot.accepted_at', '!==', null) - ->contains($user); - } - - return $this->followings() - ->wherePivot('accepted_at', '!=', null) - ->where($this->getQualifiedKeyName(), $user) - ->exists(); - } + self::saving(function ($follower) { + $userForeignKey = config('follow.user_foreign_key', 'user_id'); + $follower->setAttribute($userForeignKey, $follower->{$userForeignKey} ?: auth()->id()); - /** - * @param \Illuminate\Database\Eloquent\Model|int $user - */ - public function isFollowedBy($user): bool - { - if ($user instanceof Model) { - $user = $user->getKey(); - } - - if ($this->relationLoaded('followers')) { - return $this->followers - ->where('pivot.accepted_at', '!==', null) - ->contains($user); - } - - return $this->followers() - ->wherePivot('accepted_at', '!=', null) - ->where($this->getQualifiedKeyName(), $user) - ->exists(); - } - - /** - * @param \Illuminate\Database\Eloquent\Model|int $user - */ - public function areFollowingEachOther($user): bool - { - /* @var \Illuminate\Database\Eloquent\Model $user*/ - return $this->isFollowing($user) && $this->isFollowedBy($user); - } - - public function scopeOrderByFollowersCount($query, string $direction = 'desc') - { - return $query->withCount('followers')->orderBy('followers_count', $direction); - } - - public function scopeOrderByFollowersCountDesc($query) - { - return $this->scopeOrderByFollowersCount($query, 'desc'); + if (config('follow.uuids')) { + $follower->setAttribute($follower->getKeyName(), $follower->{$follower->getKeyName()} ?: (string) Str::orderedUuid()); + } + }); } - public function scopeOrderByFollowersCountAsc($query) + public function followable(): MorphTo { - return $this->scopeOrderByFollowersCount($query, 'asc'); + return $this->morphTo(); } - public function followers(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + public function user(): BelongsTo { - return $this->belongsToMany( - __CLASS__, - \config('follow.relation_table', 'user_follower'), - 'following_id', - 'follower_id' - )->withPivot('accepted_at')->withTimestamps()->using(UserFollower::class); + return $this->belongsTo(config('auth.providers.users.model'), config('follow.user_foreign_key', 'user_id')); } - public function followings(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + public function follower(): BelongsTo { - return $this->belongsToMany( - __CLASS__, - \config('follow.relation_table', 'user_follower'), - 'follower_id', - 'following_id' - )->withPivot('accepted_at')->withTimestamps()->using(UserFollower::class); + return $this->user(); } - public function scopeApprovedFollowers(Builder $query): \Illuminate\Database\Eloquent\Relations\BelongsToMany + public function scopeWithType(Builder $query, string $type): Builder { - return $this->followers()->wherePivotNotNull('accepted_at'); + return $query->where('followable_type', app($type)->getMorphClass()); } - public function scopeNotApprovedFollowers(Builder $query): \Illuminate\Database\Eloquent\Relations\BelongsToMany + public function scopeOf(Builder $query, Model $model): Builder { - return $this->followers()->wherePivotNull('accepted_at'); + return $query->where('followable_type', $model->getMorphClass()) + ->where('followable_id', $model->getKey()); } - public function scopeApprovedFollowings(Builder $query): \Illuminate\Database\Eloquent\Relations\BelongsToMany + public function scopeFollowedBy(Builder $query, Model $follower): Builder { - return $this->followings()->wherePivotNotNull('accepted_at'); + return $query->where(config('follow.user_foreign_key', 'user_id'), $follower->getKey()); } - public function scopeNotApprovedFollowings(Builder $query): \Illuminate\Database\Eloquent\Relations\BelongsToMany + public function scopeAccepted(Builder $query): Builder { - return $this->followings()->wherePivotNull('accepted_at'); + return $query->whereNotNull('accepted_at'); } - public function attachFollowStatus($followables, callable $resolver = null) + public function scopeNotAccepted(Builder $query): Builder { - $returnFirst = false; - - switch (true) { - case $followables instanceof Model: - $returnFirst = true; - $followables = \collect([$followables]); - break; - case $followables instanceof LengthAwarePaginator: - $followables = $followables->getCollection(); - break; - case $followables instanceof Paginator: - case $followables instanceof CursorPaginator: - $followables = \collect($followables->items()); - break; - case $followables instanceof LazyCollection: - $followables = \collect(\iterator_to_array($followables->getIterator())); - break; - case \is_array($followables): - $followables = \collect($followables); - break; - } - - \abort_if(!($followables instanceof Enumerable), 422, 'Invalid $followables type.'); - - $followed = UserFollower::where('follower_id', $this->getKey())->get(); - - $followables->map(function ($followable) use ($followed, $resolver) { - $resolver = $resolver ?? fn ($m) => $m; - $followable = $resolver($followable); - - if ($followable && \in_array(Followable::class, \class_uses($followable))) { - $item = $followed->where('following_id', $followable->getKey())->first(); - $followable->setAttribute('has_followed', !!$item); - $followable->setAttribute('followed_at', $item ? $item->created_at : null); - $followable->setAttribute('follow_accepted_at', $item ? $item->accepted_at : null); - } - }); - - return $returnFirst ? $followables->first() : $followables; + return $query->whereNull('accepted_at'); } } diff --git a/src/Traits/Followable.php b/src/Traits/Followable.php new file mode 100644 index 0000000..0b1e34f --- /dev/null +++ b/src/Traits/Followable.php @@ -0,0 +1,106 @@ +followables()->followedBy($follower)->get()->each->delete(); + } + + public function acceptFollowRequestFrom(Model $follower): void + { + if (!in_array(Follower::class, \class_uses($follower))) { + throw new \InvalidArgumentException('The model must use the Follower trait.'); + } + + $this->followables()->followedBy($follower)->get()->each->update(['accepted_at' => \now()]); + } + + public function isFollowedBy(Model $follower): bool + { + if (!in_array(Follower::class, \class_uses($follower))) { + throw new \InvalidArgumentException('The model must use the Follower trait.'); + } + + if ($this->relationLoaded('followables')) { + return $this->followables->whereNotNull('accepted_at')->contains($follower); + } + + return $this->followables()->accepted()->followedBy($follower)->exists(); + } + + public function scopeOrderByFollowersCount($query, string $direction = 'desc') + { + return $query->withCount('followers')->orderBy('followers_count', $direction); + } + + public function scopeOrderByFollowersCountDesc($query) + { + return $this->scopeOrderByFollowersCount($query, 'desc'); + } + + public function scopeOrderByFollowersCountAsc($query) + { + return $this->scopeOrderByFollowersCount($query, 'asc'); + } + + public function followables(): HasMany + { + /** + * @var Model $this + */ + return $this->hasMany( + config('favorite.followables_model', \Overtrue\LaravelFollow\Followable::class), + 'followable_id', + )->where('followable_type', $this->getMorphClass()); + } + + public function followers(): BelongsToMany + { + return $this->belongsToMany( + config('auth.providers.users.model'), + config('follow.followables_table', 'followables'), + 'followable_id', + config('favorite.user_foreign_key', 'user_id') + )->where('followable_type', $this->getMorphClass()) + ->withPivot(['accepted_at']); + } + + public function approvedFollowers(): BelongsToMany + { + return $this->followers()->whereNotNull('accepted_at'); + } + + public function notApprovedFollowers(): BelongsToMany + { + return $this->followers()->whereNull('accepted_at'); + } +} diff --git a/src/Traits/Follower.php b/src/Traits/Follower.php new file mode 100644 index 0000000..b48bb4f --- /dev/null +++ b/src/Traits/Follower.php @@ -0,0 +1,162 @@ + "mixed"])] + public function follow(Model $followable): array + { + if (!in_array(Followable::class, class_uses($followable))) { + throw new InvalidArgumentException('The followable model must use the Followable trait.'); + } + + /** @var \Illuminate\Database\Eloquent\Model|\Overtrue\LaravelFollow\Traits\Followable $followable */ + $isPending = $followable->needsToApproveFollowRequests() ?: false; + + $this->followings()->updateOrCreate([ + 'followable_id' => $followable->getKey(), + 'followable_type' => $followable->getMorphClass(), + ], [ + 'accepted_at' => $isPending ? null : now() + ]); + + return ['pending' => $isPending]; + } + + public function unfollow(Model $followable): void + { + if (!in_array(Followable::class, class_uses($followable))) { + throw new InvalidArgumentException('The followable model must use the Followable trait.'); + } + + $this->followings()->of($followable)->get()->each->delete(); + } + + public function toggleFollow(Model $followable): void + { + $this->isFollowing($followable) ? $this->unfollow($followable) : $this->follow($followable); + } + + public function isFollowing(Model $followable): bool + { + if (!in_array(Followable::class, class_uses($followable))) { + throw new InvalidArgumentException('The followable model must use the Followable trait.'); + } + + if ($this->relationLoaded('followings')) { + return $this->followings + ->whereNotNull('accepted_at') + ->where('followable_id', $followable->getKey()) + ->where('followable_type', $followable->getMorphClass()) + ->isNotEmpty(); + } + + return $this->followings()->of($followable)->accepted()->exists(); + } + + public function hasRequestedToFollow(Model $followable): bool + { + if (!in_array(\Overtrue\LaravelFollow\Traits\Followable::class, \class_uses($followable))) { + throw new InvalidArgumentException('The followable model must use the Followable trait.'); + } + + if ($this->relationLoaded('followings')) { + return $this->followings->whereNull('accepted_at') + ->where('followable_id', $followable->getKey()) + ->where('followable_type', $followable->getMorphClass()) + ->isNotEmpty(); + } + + return $this->followings()->of($followable)->notAccepted()->exists(); + } + + public function followings(): HasMany + { + /** + * @var Model $this + */ + return $this->hasMany( + config('favorite.followables_model', \Overtrue\LaravelFollow\Followable::class), + config('favorite.user_foreign_key', 'user_id'), + $this->getKeyName() + ); + } + + public function approvedFollowings(): HasMany + { + return $this->followings()->accepted(); + } + + public function notApprovedFollowings(): HasMany + { + return $this->followings()->notAccepted(); + } + + public function attachFollowStatus($followables, callable $resolver = null) + { + $returnFirst = false; + + switch (true) { + case $followables instanceof Model: + $returnFirst = true; + $followables = collect([$followables]); + break; + case $followables instanceof LengthAwarePaginator: + $followables = $followables->getCollection(); + break; + case $followables instanceof Paginator: + case $followables instanceof CursorPaginator: + $followables = collect($followables->items()); + break; + case $followables instanceof LazyCollection: + $followables = collect(iterator_to_array($followables->getIterator())); + break; + case is_array($followables): + $followables = collect($followables); + break; + } + + abort_if(!($followables instanceof Enumerable), 422, 'Invalid $followables type.'); + + $followed = $this->followings()->get(); + + $followables->map(function ($followable) use ($followed, $resolver) { + $resolver = $resolver ?? fn ($m) => $m; + $followable = $resolver($followable); + + if ($followable && in_array(Followable::class, class_uses($followable))) { + $item = $followed->where('followable_id', $followable->getKey()) + ->where('followable_type', $followable->getMorphClass()) + ->first(); + $followable->setAttribute('followed_at', $item ? $item->created_at : null); + $followable->setAttribute('follow_accepted_at', $item ? $item->accepted_at : null); + } + }); + + return $returnFirst ? $followables->first() : $followables; + } +} diff --git a/src/UserFollower.php b/src/UserFollower.php deleted file mode 100644 index 67b4638..0000000 --- a/src/UserFollower.php +++ /dev/null @@ -1,23 +0,0 @@ - Followed::class, - 'deleted' => Unfollowed::class, - ]; -} diff --git a/tests/Channel.php b/tests/Channel.php new file mode 100644 index 0000000..4687ffd --- /dev/null +++ b/tests/Channel.php @@ -0,0 +1,13 @@ + User::class]); } - public function test_basic_features() + public function test_user_can_follow_users() { $user1 = User::create(['name' => 'user1']); $user2 = User::create(['name' => 'user2']); @@ -29,7 +29,9 @@ public function test_basic_features() Event::assertDispatched( Followed::class, function ($event) use ($user1, $user2) { - return $event->followingId === $user2->id && $event->followerId === $user1->id; + return $event->followable_type === $user2->getMorphClass() + && $event->followable_id === $user2->id + && $event->user_id === $user1->id; } ); @@ -41,7 +43,40 @@ function ($event) use ($user1, $user2) { Event::assertDispatched( Unfollowed::class, function ($event) use ($user1, $user2) { - return $event->followingId === $user2->id && $event->followerId === $user1->id; + return $event->followable_type === $user2->getMorphClass() + && $event->followable_id === $user2->id + && $event->user_id === $user1->id; + } + ); + } + + public function test_user_can_follow_channels() + { + $user = User::create(['name' => 'user1']); + $channel = Channel::create(['name' => 'Laravel']); + + $user->follow($channel); + + Event::assertDispatched( + Followed::class, + function ($event) use ($user, $channel) { + return $event->followable_type === $channel->getMorphClass() + && $event->followable_id === $channel->id + && $event->user_id === $user->id; + } + ); + + $this->assertTrue($user->isFollowing($channel)); + $this->assertTrue($channel->isFollowedBy($user)); + + $user->unfollow($channel); + + Event::assertDispatched( + Unfollowed::class, + function ($event) use ($user, $channel) { + return $event->followable_type === $channel->getMorphClass() + && $event->followable_id === $channel->id + && $event->user_id === $user->id; } ); } @@ -78,7 +113,7 @@ public function test_user_can_get_unfollowed_users() $user1UnfollowedUsers = User::whereNotIn( 'id', function ($q) use ($user1) { - $q->select('following_id')->from('user_follower')->where('follower_id', $user1->id); + $q->select('followable_id')->from('followables')->where('user_id', $user1->id); } )->where('id', '<>', $user1->id)->get()->toArray(); $this->assertCount(2, $user1UnfollowedUsers); @@ -133,7 +168,7 @@ function () use ($user1, $user2, $user3, $user4) { $this->assertSame(3, $sqls->count()); // with eager loading - $user4->load('followers'); + $user4->load('followables'); $sqls = $this->getQueryLog( function () use ($user1, $user2, $user3, $user4) { $user4->isFollowedBy($user1); @@ -142,29 +177,10 @@ function () use ($user1, $user2, $user3, $user4) { } ); $this->assertSame(0, $sqls->count()); - - // -- follow each other - $user4->follow($user1); - // without loading - $sqls = $this->getQueryLog( - function () use ($user1, $user2, $user3, $user4) { - $user1->areFollowingEachOther($user4); - } - ); - $this->assertSame(1, $sqls->count()); - - // with eager loading - $user1->load('followings', 'followers'); - $sqls = $this->getQueryLog( - function () use ($user1, $user2, $user3, $user4) { - $user1->areFollowingEachOther($user4); - } - ); - $this->assertSame(0, $sqls->count()); } /** - * @param \Closure $callback + * @param \Closure $callback * * @return \Illuminate\Support\Collection */ @@ -192,7 +208,9 @@ public function test_attach_follow_status() $user1->follow($user2); $user1->follow($user3); $user1->follow($user4); + $user2->follow($user4); + $user3->follow($user4); $users = User::all(); @@ -205,41 +223,41 @@ function () use ($user1, $users) { $this->assertSame(1, $sqls->count()); - $this->assertFalse($users[0]->has_followed); - $this->assertTrue($users[1]->has_followed); + $this->assertNull($users[0]->followed_at); + $this->assertNotNull($users[1]->followed_at); $this->assertInstanceOf(Carbon::class, $users[1]->followed_at); - $this->assertTrue($users[2]->has_followed); - $this->assertTrue($users[3]->has_followed); + $this->assertNotNull($users[2]->followed_at); + $this->assertNotNull($users[3]->followed_at); // paginator $users = User::paginate(); $user1->attachFollowStatus($users); $users = $users->toArray()['data']; - $this->assertFalse($users[0]['has_followed']); - $this->assertTrue($users[1]['has_followed']); - $this->assertTrue($users[2]['has_followed']); - $this->assertTrue($users[3]['has_followed']); + $this->assertNull($users[0]['followed_at']); + $this->assertNotNull($users[1]['followed_at']); + $this->assertNotNull($users[2]['followed_at']); + $this->assertNotNull($users[3]['followed_at']); // cursor paginator $users = User::cursorPaginate(); $user1->attachFollowStatus($users); $users = $users->toArray()['data']; - $this->assertFalse($users[0]['has_followed']); - $this->assertTrue($users[1]['has_followed']); - $this->assertTrue($users[2]['has_followed']); - $this->assertTrue($users[3]['has_followed']); + $this->assertNull($users[0]['followed_at']); + $this->assertNotNull($users[1]['followed_at']); + $this->assertNotNull($users[2]['followed_at']); + $this->assertNotNull($users[3]['followed_at']); // cursor $users = User::cursor(); $users = $user1->attachFollowStatus($users)->toArray(); - $this->assertFalse($users[0]['has_followed']); - $this->assertTrue($users[1]['has_followed']); - $this->assertTrue($users[2]['has_followed']); - $this->assertTrue($users[3]['has_followed']); + $this->assertNull($users[0]['followed_at']); + $this->assertNotNull($users[1]['followed_at']); + $this->assertNotNull($users[2]['followed_at']); + $this->assertNotNull($users[3]['followed_at']); // with custom resolver $users = \collect(['creator' => $user2], ['creator' => $user3], ['creator' => $user4]); @@ -287,7 +305,7 @@ public function test_order_by_followers() $mostPopularUser = User::orderByFollowersCountDesc()->first(); // same as: - // $mostPopularUser = Post::withCount('followers')->orderByDesc('followers_count')->first(); + // $mostPopularUser = Post::withCount('followables')->orderByDesc('followers_count')->first(); $this->assertSame($user1->name, $mostPopularUser->name); $this->assertEquals(3, $mostPopularUser->followers_count); } @@ -305,10 +323,10 @@ public function test_repeat_actions() $user1->follow($user3); $user1->follow($user4); - $this->assertDatabaseHas('user_follower', ['follower_id' => $user1->id, 'following_id' => $user2->id]); - $this->assertDatabaseHas('user_follower', ['follower_id' => $user1->id, 'following_id' => $user3->id]); - $this->assertDatabaseHas('user_follower', ['follower_id' => $user1->id, 'following_id' => $user4->id]); + $this->assertDatabaseHas('followables', ['user_id' => $user1->id, 'followable_id' => $user2->id, 'followable_type' => $user2->getMorphClass()]); + $this->assertDatabaseHas('followables', ['user_id' => $user1->id, 'followable_id' => $user3->id, 'followable_type' => $user3->getMorphClass()]); + $this->assertDatabaseHas('followables', ['user_id' => $user1->id, 'followable_id' => $user4->id, 'followable_type' => $user4->getMorphClass()]); - $this->assertDatabaseCount('user_follower', 3); + $this->assertDatabaseCount('followables', 3); } } diff --git a/tests/PrivacyTest.php b/tests/PrivacyTest.php index 24c9878..2792c45 100644 --- a/tests/PrivacyTest.php +++ b/tests/PrivacyTest.php @@ -2,6 +2,7 @@ namespace Tests; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Overtrue\LaravelFollow\Events\Followed; use Overtrue\LaravelFollow\Events\Unfollowed; diff --git a/tests/User.php b/tests/User.php index 751bc07..1d46a29 100644 --- a/tests/User.php +++ b/tests/User.php @@ -3,11 +3,13 @@ namespace Tests; use Illuminate\Database\Eloquent\Model; -use Overtrue\LaravelFollow\Followable; +use Overtrue\LaravelFollow\Traits\Followable; +use Overtrue\LaravelFollow\Traits\Follower; class User extends Model { use Followable; + use Follower; protected $fillable = ['name', 'private']; diff --git a/tests/migrations/2022_05_02_000000_create_channels_table.php b/tests/migrations/2022_05_02_000000_create_channels_table.php new file mode 100644 index 0000000..03342d5 --- /dev/null +++ b/tests/migrations/2022_05_02_000000_create_channels_table.php @@ -0,0 +1,21 @@ +increments('id'); + $table->string('name'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::drop('channels'); + } +}