├── .github └── FUNDING.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── composer.json ├── config └── social.php ├── database └── migrations │ └── create_socials_table.php └── src ├── Console └── Commands │ └── InstallPackageCommand.php ├── Contracts └── Follows │ ├── CanFollowContract.php │ └── FollowableContract.php ├── Events ├── Event.php ├── Followed.php └── UnFollowed.php ├── Exceptions └── .gitkeep ├── Facades └── Social.php ├── Models ├── Follows.php └── Like.php ├── Providers └── SocialServiceProvider.php ├── Social.php └── Traits ├── Follows ├── CanFollow.php └── Followable.php └── Like ├── CanLike.php ├── LikeCounterable.php └── Likeable.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://idpay.ir/laravelir'] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | node_modules/ 3 | npm-debug.log 4 | yarn-error.log 5 | composer.lock 6 | *.meta.* 7 | _ide_* 8 | .phpunit.result.cache 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `miladimos/laravel-social` will be documented in this file. 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Starts](https://img.shields.io/github/stars/miladimos/laravel-social?style=flat&logo=github)](https://github.com/miladimos/laravel-social/forks) 2 | [![Forks](https://img.shields.io/github/forks/miladimos/laravel-social?style=flat&logo=github)](https://github.com/miladimos/laravel-social/stargazers) 3 | 4 | 5 | # Laravel social package 6 | 7 | A toolkit package for social networks 8 | 9 | ## Installation 10 | 11 | 1. Run the command below to add this package: 12 | 13 | ``` 14 | composer require miladimos/laravel-social 15 | ``` 16 | 17 | 2. Open your config/socials.php and add the following to the providers array: 18 | 19 | ```php 20 | Miladimos\Social\Providers\SocialServiceProvider::class, 21 | ``` 22 | 23 | 3. Run the command below to install package: 24 | 25 | ``` 26 | php artisan social:install 27 | ``` 28 | 29 | 4. Run the command below to migrate database: 30 | 31 | ``` 32 | php artisan migrate 33 | ``` 34 | 35 | # Features 36 | 37 | ## Tag: 38 | 39 | First add `Taggable` trait to models that you want have tags 40 | 41 | ```php 42 | 43 | namespace App\Models; 44 | 45 | use Illuminate\Database\Eloquent\Factories\HasFactory; 46 | use Miladimos\Social\Traits\Taggable; 47 | use Illuminate\Database\Eloquent\Model; 48 | 49 | class Post extends Model 50 | { 51 | use HasFactory, 52 | Taggable; 53 | } 54 | 55 | ``` 56 | 57 | Second you can work with tags: 58 | 59 | ```php 60 | namespace App\Http\Controller; 61 | 62 | use App\Models\Post; 63 | use Miladimos\Social\Models\Tag; 64 | 65 | class YourController extends Controller 66 | { 67 | public function index() 68 | { 69 | // first you can create custom tags 70 | $tag = Tag::create(['name' => 'tag']); 71 | 72 | $post = Post::first(); 73 | 74 | $post->tags; // return attached tags 75 | 76 | $post->attach($tag); // attach one tag 77 | 78 | $post->detach($tag); // detach one tag 79 | 80 | $post->syncTags([$tags]); // sync tags 81 | 82 | $tag->taggables; // return morph relation to tagged model 83 | } 84 | } 85 | 86 | ``` 87 | tag model have soft deletes trait. 88 | 89 | 90 | ## Like 91 | 92 | ## Bookmark 93 | 94 | ## Follow 95 | 96 | ## Category 97 | 98 | First add `Taggable` trait to models that you want have attachments 99 | 100 | ```php 101 | 102 | namespace App\Models; 103 | 104 | use Illuminate\Database\Eloquent\Factories\HasFactory; 105 | use Illuminate\Database\Eloquent\Model; 106 | use Miladimos\Social\Traits\Taggable; 107 | 108 | class Post extends Model 109 | { 110 | use HasFactory, 111 | Taggable; 112 | } 113 | 114 | ``` 115 | 116 | ### Methods 117 | 118 | in controllers you have these methods: 119 | 120 | ```php 121 | 122 | namespace App\Http\Controllers; 123 | 124 | use App\Models\Post; 125 | 126 | class PostController extends Controller 127 | { 128 | public function index() 129 | { 130 | $post = Post::find(1); 131 | 132 | $post->likes // return all likes 133 | 134 | 135 | } 136 | } 137 | 138 | ``` 139 | 140 | #### Features 141 | 142 | Like 143 | 144 | Favorite 145 | 146 | Bookmark 147 | 148 | Follow \ Unfollow 149 | 150 | Comment 151 | 152 | $post = Post::find(1); 153 | 154 | $post->comment('This is a comment'); 155 | 156 | $post->commentAsUser($user, 'This is a comment from someone else'); 157 | $comment = $post->comments->first(); 158 | 159 | $comment->approve(); 160 | 161 | Auto Approve Comments implements Commentator needsCommentApproval false 162 | 163 | // Retrieve all comments 164 | $comments = $post->comments; 165 | 166 | // Retrieve only approved comments 167 | $approved = $post->comments()->approved()->get(); 168 | 169 | Vote / Rate System 170 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miladimos/laravel-social", 3 | "description": "a simple toolkit for social networks", 4 | "homepage": "https://github.com/miladimos/laravel-social", 5 | "type": "library", 6 | "version" : "0.6.3", 7 | "keywords": [ 8 | "laravel", 9 | "laravel-package", 10 | "laravel-social", 11 | "laravel support", 12 | "laravel packages", 13 | "like", 14 | "subscribe", 15 | "laravel-like", 16 | "laravel-follow" 17 | ], 18 | "authors": [ 19 | { 20 | "name": "miladimos", 21 | "email": "miladimos@outlook.com", 22 | "role": "maintainer", 23 | "homepage": "https://github.com/miladimos" 24 | } 25 | ], 26 | "autoload": { 27 | "psr-4": { 28 | "Miladimos\\Social\\": "src/" 29 | } 30 | }, 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "Miladimos\\Social\\Providers\\SocialServiceProvider" 35 | ] 36 | } 37 | }, 38 | "require": { 39 | "php": ">=7.4|^8.0" 40 | }, 41 | "license": "MIT" 42 | } 43 | -------------------------------------------------------------------------------- /config/social.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'model' => Miladimos\Social\Models\Follows::class, 8 | 'table' => 'social_follows', 9 | 'followable_morphs' => 'followable', // follower 10 | 'followingable_morphs' => 'followingable', // following 11 | 'need_follows_to_approved' => false, 12 | ], 13 | 14 | 15 | 'likes' => [ 16 | 'model' => Miladimos\Social\Models\Like::class, 17 | 18 | 'liker_foreign_key' => 'liker_id', 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /database/migrations/create_socials_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->morphs('followable'); // follower 14 | $table->morphs('followingable'); // following 15 | $table->timestamp('requested_at')->nullable(); 16 | $table->timestamp('accepted_at')->nullable(); 17 | $table->timestamps(); 18 | }); 19 | 20 | Schema::create(config('social.likes.table'), function (Blueprint $table) { 21 | $table->id(); 22 | $table->foreignId(config('social.likes.liker_foreign_key'))->index()->comment('user_id'); 23 | $table->morphs(config('social.likes.morphs')); 24 | $table->timestamps(); 25 | 26 | $table->unique(['likeable_id', 'likeable_type'], 'likes'); 27 | }); 28 | } 29 | 30 | public function down(): void 31 | { 32 | Schema::dropIfExists(config('social.follows.table')); 33 | Schema::dropIfExists(config('social.likes.table')); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/Console/Commands/InstallPackageCommand.php: -------------------------------------------------------------------------------- 1 | line("\t... Welcome To Social Package Installer ..."); 18 | 19 | //config 20 | if (File::exists(config_path('social.php'))) { 21 | $confirm = $this->confirm("social.php already exist. Do you want to overwrite?"); 22 | if ($confirm) { 23 | $this->publishConfig(); 24 | } else { 25 | $this->error("you must overwrite config file"); 26 | exit; 27 | } 28 | } else { 29 | $this->publishConfig(); 30 | } 31 | 32 | if (!empty(File::glob(database_path('migrations\*_create_socials_table.php')))) { 33 | $list = File::glob(database_path('migrations\*_create_socials_table.php')); 34 | collect($list)->each(function ($item) { 35 | File::delete($item); 36 | $this->warn("Deleted: " . $item); 37 | }); 38 | $this->publishMigration(); 39 | } else { 40 | $this->publishMigration(); 41 | } 42 | 43 | $this->info("Social Package Successfully Installed. Star me on Github :) \n"); 44 | $this->info("\t\tGood Luck."); 45 | } 46 | 47 | private function publishConfig() 48 | { 49 | $this->call('vendor:publish', [ 50 | '--provider' => "Miladimos\Social\Providers\SocialServiceProvider", 51 | '--tag' => 'social-config', 52 | '--force' => true 53 | ]); 54 | } 55 | 56 | private function publishMigration() 57 | { 58 | $this->call('vendor:publish', [ 59 | '--provider' => "Miladimos\Social\Providers\SocialServiceProvider", 60 | '--tag' => 'social-migrations', 61 | '--force' => true 62 | ]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Contracts/Follows/CanFollowContract.php: -------------------------------------------------------------------------------- 1 | follower_id = $this->user_id = $follower->{\config('follow.user_foreign_key', 'user_id')}; 22 | $this->followable_id = $follower->followable_id; 23 | $this->followable_type = $follower->followable_type; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Events/Followed.php: -------------------------------------------------------------------------------- 1 | table = config('social.follows.table', 'social_follows'); 17 | 18 | parent::__construct($attributes); 19 | } 20 | 21 | // follower 22 | public function followable(): MorphTo 23 | { 24 | return $this->morphTo(); 25 | } 26 | 27 | // following 28 | public function followingable(): MorphTo 29 | { 30 | return $this->morphTo(); 31 | } 32 | 33 | public function needApprove() 34 | { 35 | return config('social.follows.need_follows_to_approved'); 36 | } 37 | 38 | public function scopeAccepted($query) 39 | { 40 | return $query->whereNotNull('accepted_at'); 41 | } 42 | 43 | /** 44 | * Returns the entity that this entity is following. 45 | * 46 | * @return \Illuminate\Database\Eloquent\Model 47 | */ 48 | public function followable() 49 | { 50 | return $this->morphTo(); 51 | } 52 | 53 | /** 54 | * Returns the entity that followed this entity. 55 | * 56 | * @return \Illuminate\Database\Eloquent\Model 57 | */ 58 | public function follower() 59 | { 60 | return $this->morphTo(); 61 | } 62 | 63 | /** 64 | * Finds the entities that are followers for the given type. 65 | * 66 | * @param \Illuminate\Database\Eloquent\Model|string $type 67 | * 68 | * @return \Illuminate\Database\Eloquent\Builder 69 | */ 70 | public function scopeWhereFollowerType(Builder $query, $type) 71 | { 72 | // Determine if the given type is a valid class 73 | if (class_exists($type)) { 74 | $type = new $type(); 75 | } 76 | 77 | // Determine if the given type is an instance of an 78 | // Eloquent Model and if it is, we'll obtain the 79 | // corresponding morphed class name from it. 80 | if (is_a($type, Model::class)) { 81 | $type = $type->getMorphClass(); 82 | } 83 | 84 | return $query->where('follower_type', $type); 85 | } 86 | 87 | /** 88 | * Finds the entities that are being followed for the given type. 89 | * 90 | * @param \Illuminate\Database\Eloquent\Model|string $type 91 | * 92 | * @return \Illuminate\Database\Eloquent\Builder 93 | */ 94 | public function scopeWhereFollowableType(Builder $query, $type) 95 | { 96 | // Determine if the given type is a valid class 97 | if (class_exists($type)) { 98 | $type = new $type(); 99 | } 100 | 101 | // Determine if the given type is an instance of an 102 | // Eloquent Model and if it is, we'll obtain the 103 | // corresponding morphed class name from it. 104 | if (is_a($type, Model::class)) { 105 | $type = $type->getMorphClass(); 106 | } 107 | 108 | return $query->where('followable_type', $type); 109 | } 110 | 111 | /** 112 | * Finds the given entity that's following other entities. 113 | * 114 | * @return \Illuminate\Database\Eloquent\Builder 115 | */ 116 | public function scopeWhereFollowerEntity(Builder $query, CanFollowContract $entity) 117 | { 118 | return $query 119 | ->where('follower_id', $entity->getKey()) 120 | ->where('follower_type', $entity->getMorphClass()) 121 | ; 122 | } 123 | 124 | /** 125 | * Finds the given entity that's being followed by other entities. 126 | * 127 | * @return \Illuminate\Database\Eloquent\Builder 128 | */ 129 | public function scopeWhereFollowableEntity(Builder $query, CanBeFollowedContract $entity) 130 | { 131 | return $query 132 | ->where('followable_id', $entity->getKey()) 133 | ->where('followable_type', $entity->getMorphClass()) 134 | ; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Models/Like.php: -------------------------------------------------------------------------------- 1 | table = config('social.likes.table', 'social_likes'); 16 | 17 | parent::__construct($attributes); 18 | } 19 | 20 | public function likeable(): MorphTo 21 | { 22 | return $this->morphTo(); 23 | } 24 | 25 | public function liker() 26 | { 27 | return $this->user(); 28 | } 29 | 30 | public function user() 31 | { 32 | return $this->belongsTo(config('auth.providers.users.model'), config('like.user_foreign_key')); 33 | } 34 | 35 | public function scopeWithType(Builder $query, string $type) 36 | { 37 | return $query->where('likeable_type', app($type)->getMorphClass()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Providers/SocialServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . "/../../config/social.php", 'social'); 15 | 16 | $this->loadMigrationsFrom(__DIR__ . '/../../database/migrations'); 17 | 18 | $this->registerFacades(); 19 | } 20 | 21 | public function boot() 22 | { 23 | $this->registerCommands(); 24 | 25 | $this->registerConfig(); 26 | 27 | $this->registerMigrations(); 28 | } 29 | 30 | private function registerFacades() 31 | { 32 | $this->app->bind('social', function ($app) { 33 | return new Social(); 34 | }, true); 35 | } 36 | 37 | private function registerConfig() 38 | { 39 | $this->publishes([ 40 | __DIR__ . '/../../config/social.php' => config_path('social.php') 41 | ], 'social-config'); 42 | } 43 | 44 | private function registerCommands() 45 | { 46 | if ($this->app->runningInConsole()) { 47 | $this->commands([ 48 | InstallPackageCommand::class, 49 | ]); 50 | } 51 | } 52 | 53 | private function registerMigrations() 54 | { 55 | $this->publishes([ 56 | __DIR__ . '/../../database/migrations/create_socials_table.stub' => database_path('migrations/' . date('Y_m_d_His', time()) . '_create_socials_tables.php'), 57 | ], 'social-migrations'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Social.php: -------------------------------------------------------------------------------- 1 | followings()->delete(); 19 | }); 20 | } 21 | 22 | public function needsToApproveFollowRequests(): bool 23 | { 24 | return config('social.follows.need_follows_to_approved'); 25 | } 26 | 27 | public function followsModel(): string 28 | { 29 | return config('social.follows.model'); 30 | } 31 | 32 | public function followingableMorphs(): string 33 | { 34 | return config('social.follows.followingable_morphs'); 35 | } 36 | 37 | public function followings(): MorphMany 38 | { 39 | return $this->morphMany(config('social.follows.model'), $this->followingableMorphs()); 40 | } 41 | 42 | public function hasAnyFollowings(): bool 43 | { 44 | return (bool) $this->followings()->count(); 45 | } 46 | 47 | public function hasAnyFollowers(): bool 48 | { 49 | return (bool) $this->followers()->count(); 50 | } 51 | 52 | // FollowableContract 53 | public function follow($entity) 54 | { 55 | // $following = $this->findFollowing($entity); 56 | 57 | $isPending = $entity->needsToApproveFollowRequests() ?: false; 58 | 59 | if (!$this->isFollowing($entity) && $this->id != $entity->id) { 60 | return $this->followings()->attach($entity); 61 | } 62 | 63 | $this->followings()->attach($user, [ 64 | 'accepted_at' => $isPending ? null : now() 65 | ]); 66 | 67 | return ['pending' => $isPending]; 68 | } 69 | 70 | public function findFollowing(FollowableContract $entity) 71 | { 72 | return $this->followings()->whereFollowableEntity($entity)->first(); 73 | } 74 | 75 | public function findFollower(CanFollowContract $entity) 76 | { 77 | return $this->followers()->whereFollowingableEntity($entity)->first(); 78 | } 79 | 80 | // FollowableContract $entity 81 | public function isFollowing(Model $model): bool 82 | { 83 | if ($this->relationLoaded('followings')) { 84 | return $this->followings() 85 | ->wherePivot('accepted_at', '!=', null) 86 | ->contains($model); 87 | } 88 | 89 | // return tap($this->relationLoaded('subscriptions') ? $this->subscriptions : $this->subscriptions()) 90 | // ->where('subscribable_id', $object->getKey()) 91 | // ->where('subscribable_type', $object->getMorphClass()) 92 | // ->count() > 0; 93 | } 94 | 95 | public function isFollowedBy(Model $user): bool 96 | { 97 | if ($this->relationLoaded('followers')) { 98 | return $this->followers() 99 | ->wherePivot('accepted_at', '!=', null) 100 | ->contains($user); 101 | } 102 | } 103 | 104 | // FollowableContract $entity 105 | public function unfollow($user) 106 | { 107 | $this->followings()->detach($user); 108 | 109 | $relation = $object->subscriptions() 110 | ->where('subscribable_id', $object->getKey()) 111 | ->where('subscribable_type', $object->getMorphClass()) 112 | ->where(config('social.subscribtions.user_foreign_key'), $this->getKey()) 113 | ->first(); 114 | 115 | if ($relation) { 116 | $relation->delete(); 117 | } 118 | } 119 | 120 | public function toggleFollow($user) 121 | { 122 | $this->isFollowing($user) ? $this->unfollow($user) : $this->follow($user); 123 | } 124 | 125 | public function areFollowingEachOther($user): bool 126 | { 127 | return $this->isFollowing($user) && $this->isFollowedBy($user); 128 | } 129 | 130 | public function followMany(Collection $entities) 131 | { 132 | $entities->each(function (FollowableContract $entity) { 133 | $this->follow($entity); 134 | }); 135 | 136 | return $this->fresh(); 137 | } 138 | 139 | public function unfollowMany(Collection $entities) 140 | { 141 | $entities->each(function (FollowableContract $entity) { 142 | $this->unfollow($entity); 143 | }); 144 | 145 | return $this->fresh(); 146 | } 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | public function rejectFollowRequestFrom($user) 159 | { 160 | $this->followers()->detach($user); 161 | } 162 | 163 | public function acceptFollowRequestFrom($user) 164 | { 165 | $this->followers()->updateExistingPivot($user, ['accepted_at' => now()]); 166 | } 167 | 168 | public function hasRequestedToFollow($user): bool 169 | { 170 | if ($user instanceof Model) { 171 | $user = $user->getKey(); 172 | } 173 | 174 | if ($this->relationLoaded('followings')) { 175 | return $this->followings 176 | ->where('pivot.accepted_at', '===', null) 177 | ->contains($user); 178 | } 179 | 180 | return $this->followings() 181 | ->wherePivot('accepted_at', null) 182 | ->where($this->getQualifiedKeyName(), $user) 183 | ->exists(); 184 | } 185 | 186 | 187 | /** 188 | * Finds the gained followers (created) over the given time period. 189 | * 190 | * @return int 191 | */ 192 | public function scopeGainedFollowers(Builder $query, DateTime $startDate, DateTime $endDate) 193 | { 194 | return $this 195 | ->followers() 196 | ->withoutTrashed() 197 | ->whereBetween('created_at', [$startDate, $endDate]) 198 | ; 199 | } 200 | 201 | /** 202 | * Finds the lost followers (deleted) over the given time period. 203 | * 204 | * @return int 205 | */ 206 | public function scopeLostFollowers(Builder $query, DateTime $startDate, DateTime $endDate) 207 | { 208 | return $this 209 | ->followers() 210 | ->onlyTrashed() 211 | ->whereBetween('deleted_at', [$startDate, $endDate]) 212 | ; 213 | } 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | use function abort_if; 230 | use function class_uses; 231 | use function collect; 232 | use Illuminate\Contracts\Pagination\Paginator; 233 | use Illuminate\Database\Eloquent\Collection; 234 | use Illuminate\Database\Eloquent\Model; 235 | use Illuminate\Database\Eloquent\Relations\HasMany; 236 | use Illuminate\Pagination\CursorPaginator; 237 | use Illuminate\Pagination\LengthAwarePaginator; 238 | use Illuminate\Support\Enumerable; 239 | use Illuminate\Support\LazyCollection; 240 | use function in_array; 241 | use InvalidArgumentException; 242 | use function is_array; 243 | use function iterator_to_array; 244 | use JetBrains\PhpStorm\ArrayShape; 245 | 246 | /** 247 | * @property Collection $followings 248 | */ 249 | trait Follower 250 | { 251 | #[ArrayShape(['pending' => 'mixed'])] 252 | public function follow(Model $followable): array 253 | { 254 | if ($followable->is($this)) { 255 | throw new InvalidArgumentException('Cannot follow yourself.'); 256 | } 257 | 258 | if (! in_array(Followable::class, class_uses($followable))) { 259 | throw new InvalidArgumentException('The followable model must use the Followable trait.'); 260 | } 261 | 262 | /** @var \Illuminate\Database\Eloquent\Model|\Overtrue\LaravelFollow\Traits\Followable $followable */ 263 | $isPending = $followable->needsToApproveFollowRequests() ?: false; 264 | 265 | $this->followings()->updateOrCreate([ 266 | 'followable_id' => $followable->getKey(), 267 | 'followable_type' => $followable->getMorphClass(), 268 | ], [ 269 | 'accepted_at' => $isPending ? null : now(), 270 | ]); 271 | 272 | return ['pending' => $isPending]; 273 | } 274 | 275 | public function unfollow(Model $followable): void 276 | { 277 | if (! in_array(Followable::class, class_uses($followable))) { 278 | throw new InvalidArgumentException('The followable model must use the Followable trait.'); 279 | } 280 | 281 | $this->followings()->of($followable)->get()->each->delete(); 282 | } 283 | 284 | public function toggleFollow(Model $followable): void 285 | { 286 | $this->isFollowing($followable) ? $this->unfollow($followable) : $this->follow($followable); 287 | } 288 | 289 | public function isFollowing(Model $followable): bool 290 | { 291 | if (! in_array(Followable::class, class_uses($followable))) { 292 | throw new InvalidArgumentException('The followable model must use the Followable trait.'); 293 | } 294 | 295 | if ($this->relationLoaded('followings')) { 296 | return $this->followings 297 | ->whereNotNull('accepted_at') 298 | ->where('followable_id', $followable->getKey()) 299 | ->where('followable_type', $followable->getMorphClass()) 300 | ->isNotEmpty(); 301 | } 302 | 303 | return $this->followings()->of($followable)->accepted()->exists(); 304 | } 305 | 306 | public function hasRequestedToFollow(Model $followable): bool 307 | { 308 | if (! in_array(\Overtrue\LaravelFollow\Traits\Followable::class, \class_uses($followable))) { 309 | throw new InvalidArgumentException('The followable model must use the Followable trait.'); 310 | } 311 | 312 | if ($this->relationLoaded('followings')) { 313 | return $this->followings->whereNull('accepted_at') 314 | ->where('followable_id', $followable->getKey()) 315 | ->where('followable_type', $followable->getMorphClass()) 316 | ->isNotEmpty(); 317 | } 318 | 319 | return $this->followings()->of($followable)->notAccepted()->exists(); 320 | } 321 | 322 | public function followings(): HasMany 323 | { 324 | /** 325 | * @var Model $this 326 | */ 327 | return $this->hasMany( 328 | config('follow.followables_model', \Overtrue\LaravelFollow\Followable::class), 329 | config('follow.user_foreign_key', 'user_id'), 330 | $this->getKeyName() 331 | ); 332 | } 333 | 334 | public function approvedFollowings(): HasMany 335 | { 336 | return $this->followings()->accepted(); 337 | } 338 | 339 | public function notApprovedFollowings(): HasMany 340 | { 341 | return $this->followings()->notAccepted(); 342 | } 343 | 344 | public function attachFollowStatus($followables, callable $resolver = null) 345 | { 346 | $returnFirst = false; 347 | 348 | switch (true) { 349 | case $followables instanceof Model: 350 | $returnFirst = true; 351 | $followables = collect([$followables]); 352 | break; 353 | case $followables instanceof LengthAwarePaginator: 354 | $followables = $followables->getCollection(); 355 | break; 356 | case $followables instanceof Paginator: 357 | case $followables instanceof CursorPaginator: 358 | $followables = collect($followables->items()); 359 | break; 360 | case $followables instanceof LazyCollection: 361 | $followables = collect(iterator_to_array($followables->getIterator())); 362 | break; 363 | case is_array($followables): 364 | $followables = collect($followables); 365 | break; 366 | } 367 | 368 | abort_if(! ($followables instanceof Enumerable), 422, 'Invalid $followables type.'); 369 | 370 | $followed = $this->followings()->get(); 371 | 372 | $followables->map(function ($followable) use ($followed, $resolver) { 373 | $resolver = $resolver ?? fn ($m) => $m; 374 | $followable = $resolver($followable); 375 | 376 | if ($followable && in_array(Followable::class, class_uses($followable))) { 377 | $item = $followed->where('followable_id', $followable->getKey()) 378 | ->where('followable_type', $followable->getMorphClass()) 379 | ->first(); 380 | $followable->setAttribute('has_followed', (bool) $item); 381 | $followable->setAttribute('followed_at', $item ? $item->created_at : null); 382 | $followable->setAttribute('follow_accepted_at', $item ? $item->accepted_at : null); 383 | } 384 | }); 385 | 386 | return $returnFirst ? $followables->first() : $followables; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/Traits/Follows/Followable.php: -------------------------------------------------------------------------------- 1 | followers()->delete(); 13 | }); 14 | } 15 | 16 | public function followableMorphs(): string 17 | { 18 | return config('social.follows.followable_morphs'); 19 | } 20 | 21 | public function followers():MorphMany 22 | { 23 | return $this->morphMany(config('social.follows.model'), $this->followableMorphs())->withPivot('accepted_at')->withTimestamps(); 24 | } 25 | 26 | 27 | 28 | 29 | public function needsToApproveFollowRequests(): bool 30 | { 31 | return false; 32 | } 33 | 34 | public function rejectFollowRequestFrom(Model $follower): void 35 | { 36 | if (! in_array(Follower::class, \class_uses($follower))) { 37 | throw new \InvalidArgumentException('The model must use the Follower trait.'); 38 | } 39 | 40 | $this->followables()->followedBy($follower)->get()->each->delete(); 41 | } 42 | 43 | public function acceptFollowRequestFrom(Model $follower): void 44 | { 45 | if (! in_array(Follower::class, \class_uses($follower))) { 46 | throw new \InvalidArgumentException('The model must use the Follower trait.'); 47 | } 48 | 49 | $this->followables()->followedBy($follower)->get()->each->update(['accepted_at' => \now()]); 50 | } 51 | 52 | public function isFollowedBy(Model $follower): bool 53 | { 54 | if (! in_array(Follower::class, \class_uses($follower))) { 55 | throw new \InvalidArgumentException('The model must use the Follower trait.'); 56 | } 57 | 58 | if ($this->relationLoaded('followables')) { 59 | return $this->followables->whereNotNull('accepted_at')->contains($follower); 60 | } 61 | 62 | return $this->followables()->accepted()->followedBy($follower)->exists(); 63 | } 64 | 65 | public function scopeOrderByFollowersCount($query, string $direction = 'desc') 66 | { 67 | return $query->withCount('followers')->orderBy('followers_count', $direction); 68 | } 69 | 70 | public function scopeOrderByFollowersCountDesc($query) 71 | { 72 | return $this->scopeOrderByFollowersCount($query, 'desc'); 73 | } 74 | 75 | public function scopeOrderByFollowersCountAsc($query) 76 | { 77 | return $this->scopeOrderByFollowersCount($query, 'asc'); 78 | } 79 | 80 | 81 | public function approvedFollowers(): BelongsToMany 82 | { 83 | return $this->followers()->whereNotNull('accepted_at'); 84 | } 85 | 86 | public function notApprovedFollowers(): BelongsToMany 87 | { 88 | return $this->followers()->whereNull('accepted_at'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Traits/Like/CanLike.php: -------------------------------------------------------------------------------- 1 | likes()->delete(); 15 | }); 16 | } 17 | 18 | public function likes(): MorphMany 19 | { 20 | // favoriteable 21 | return $this->morphMany(config('social.likes.model'), config('social.likes.user_foreign_key'), $this->getKeyName()); 22 | } 23 | 24 | public function like(Model $object): Like 25 | { 26 | $attributes = [ 27 | 'likeable_type' => $object->getMorphClass(), 28 | 'likeable_id' => $object->getKey(), 29 | config('social.likes.user_foreign_key') => $this->getKey(), 30 | ]; 31 | 32 | $like = \app(config('social.likes.model')); 33 | 34 | return $like->where($attributes)->firstOr( 35 | function () use ($like, $attributes) { 36 | $like->unguard(); 37 | 38 | if ($this->relationLoaded('likes')) { 39 | $this->unsetRelation('likes'); 40 | } 41 | 42 | return $like->create($attributes); 43 | } 44 | ); 45 | } 46 | 47 | public function unlike(Model $object): bool 48 | { 49 | $relation = \app(config('social.likes.model')) 50 | ->where('likeable_id', $object->getKey()) 51 | ->where('likeable_type', $object->getMorphClass()) 52 | ->where(config('social.likes.user_foreign_key'), $this->getKey()) 53 | ->first(); 54 | 55 | if ($relation) { 56 | if ($this->relationLoaded('likes')) { 57 | $this->unsetRelation('likes'); 58 | } 59 | 60 | return $relation->delete(); 61 | } 62 | 63 | return true; 64 | } 65 | 66 | public function toggleLike(Model $object) 67 | { 68 | return $this->hasLiked($object) ? $this->unlike($object) : $this->like($object); 69 | } 70 | 71 | public function hasLiked(Model $object): bool 72 | { 73 | return ($this->relationLoaded('likes') ? $this->likes : $this->likes()) 74 | ->where('likeable_id', $object->getKey()) 75 | ->where('likeable_type', $object->getMorphClass()) 76 | ->count() > 0; 77 | } 78 | 79 | public function likesCount(): int 80 | { 81 | return $this->likes()->count(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Traits/Like/LikeCounterable.php: -------------------------------------------------------------------------------- 1 | likes()->delete(); 15 | // $model->removeLikes(); 16 | }); 17 | } 18 | 19 | private function incrementLikeCount() 20 | { 21 | $counter = $this->likeCounter()->first(); 22 | 23 | if ($counter) { 24 | $counter->count++; 25 | $counter->save(); 26 | } else { 27 | $counter = new LikeCounter; 28 | $counter->count = 1; 29 | $this->likeCounter()->save($counter); 30 | } 31 | } 32 | 33 | /** 34 | * Private. Decrement the total like count stored in the counter 35 | */ 36 | private function decrementLikeCount() 37 | { 38 | $counter = $this->likeCounter()->first(); 39 | 40 | if ($counter) { 41 | $counter->count--; 42 | if ($counter->count) { 43 | $counter->save(); 44 | } else { 45 | $counter->delete(); 46 | } 47 | } 48 | } 49 | 50 | public function like($userId = null) 51 | { 52 | $this->incrementLikeCount(); 53 | } 54 | 55 | public function unlike($userId = null) 56 | { 57 | $this->decrementLikeCount(); 58 | } 59 | 60 | /** 61 | * Populate the $model->likes attribute 62 | */ 63 | public function getLikeCountAttribute() 64 | { 65 | return $this->likeCounter ? $this->likeCounter->count : 0; 66 | } 67 | 68 | 69 | public function removeLikes() 70 | { 71 | LikeCounter::where('likeable_type', $this->morphClass)->where('likeable_id', $this->id)->delete(); 72 | // $this->likeCounter()->delete(); 73 | } 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/Traits/Like/Likeable.php: -------------------------------------------------------------------------------- 1 | likes()->delete(); 15 | // $model->removeLikes(); 16 | }); 17 | } 18 | 19 | public function likes(): MorphMany 20 | { 21 | return $this->morphMany(Like::class, 'likeable'); 22 | } 23 | 24 | public function likesCount(): int 25 | { 26 | return $this->likes()->count(); 27 | } 28 | 29 | public function likedBy(User $user) 30 | { 31 | $this->likes()->create(['likeable_id' => $user->id, 'likeable_type' => get_class($user)]); 32 | } 33 | 34 | public function dislikedBy(User $user) 35 | { 36 | optional($this->likes()->where('user_id', $user->id())->first())->delete(); 37 | } 38 | 39 | public function isLikedBy(Model $user): bool 40 | { 41 | if (\is_a($user, config('auth.providers.users.model'))) { 42 | if ($this->relationLoaded('likers')) { 43 | return $this->likers->contains($user); 44 | } 45 | 46 | return $this->likers()->where(\config('social.likes.user_foreign_key'), $user->getKey())->exists(); 47 | // return $this->likes()->where('user_id', $user->id())->exists(); 48 | 49 | } 50 | 51 | return false; 52 | } 53 | 54 | public function removeLikes() 55 | { 56 | $this->likes()->delete(); 57 | } 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | public function scopeLikedBy($query, $userId = null) 77 | { 78 | if (is_null($userId)) { 79 | $userId = $this->loggedInUserId(); 80 | } 81 | 82 | return $query->whereHas('likes', function ($q) use ($userId) { 83 | $q->where('user_id', '=', $userId); 84 | }); 85 | } 86 | 87 | 88 | /** 89 | * Has the currently logged in user already "liked" the current object 90 | * 91 | * @param string $userId 92 | * @return boolean 93 | */ 94 | public function liked($userId = null) 95 | { 96 | if (is_null($userId)) { 97 | $userId = $this->loggedInUserId(); 98 | } 99 | 100 | return (bool) $this->likes() 101 | ->where('user_id', '=', $userId) 102 | ->count(); 103 | } 104 | 105 | /** 106 | * Private. Increment the total like count stored in the counter 107 | */ 108 | 109 | 110 | /** 111 | * Did the currently logged in user like this model 112 | * Example : if($book->liked) { } 113 | * @return boolean 114 | */ 115 | public function getLikedAttribute() 116 | { 117 | return $this->liked(); 118 | } 119 | 120 | 121 | public function scopeWhereLikedBy($query, $userId = null) 122 | { 123 | if (is_null($userId)) { 124 | $userId = $this->loggedInUserId(); 125 | } 126 | 127 | return $query->whereHas('likes', function ($q) use ($userId) { 128 | $q->where('user_id', '=', $userId); 129 | }); 130 | } 131 | 132 | 133 | } 134 | --------------------------------------------------------------------------------