cybercog/laravel-likeable
Add like and dislike functionality to Laravel Eloquent models via a polymorphic many-to-many relationship. Includes reusable traits, query scopes, and events, letting users quickly like or dislike posts, comments, and any other model.
Install the package and get a likeable model wired up in a few minutes:
composer require cybercog/laravel-likeable
php artisan vendor:publish --provider="Cog\Likeable\LikeableServiceProvider"
php artisan migrate
The service provider is auto-discovered. Publishing gives you config/likeable.php (model bindings, table names, UUID/ID settings) and the likes migration — read the config file first, since it is the single place that decides which Like model class and which table the package uses.
Then add the two traits:
use Cog\Likeable\Traits\Likeable; // on the thing being liked
use Cog\Likeable\Traits\Liker; // on the User
class Post extends Model
{
use Likeable;
}
class User extends Authenticatable
{
use Liker;
}
First working use case — a toggle endpoint plus an index that shows whether the current user liked each post:
// routes
Route::post('posts/{post}/like', fn (Post $post) => [
'liked' => auth()->user()->toggleLike($post),
]);
// controller
$posts = Post::withCount('likes')->paginate();
// blade
@if (auth()->user()->liked($post)) Liked @endif
Start with like(), unlike(), toggleLike(), liked($model), isLikedBy($user), and the likes() morph-many relation. Those cover ~90% of real usage; the rest of the API (scopes, counts, custom models) is for scale.
Keep the check and the write on the right side of the traits. Liker methods take the model ($user->like($post)), Likeable methods read from the model ($post->isLikedBy($user)). Mixing them up is the most common source of "method not found" during integration.
Feed pages: eager load, then check (this is what 3.1.0 optimises).
$posts = Post::with('likes')->withCount('likes')->paginate(20);
// Each of these is now resolved from the loaded collection — no per-row query.
$posts->each(fn ($post) => $post->isLikedBy($user));
Before 3.1.0 this pattern was still N+1: loading likes didn't help because the check always queried. Now the check consults the loaded relation first, so with('likes') on a feed is genuinely worth doing. Pair it with withCount('likes') so you get the counter without a second relation.
Constrain the eager load when like volume is high. Loading every like on a viral post just to answer "did this user like it" is wasteful. Define a narrowed relation and use it for the UI:
public function likesByMe(): MorphMany
{
return $this->morphMany(Like::class, 'likeable')
->where('user_id', auth()->id());
}
Use with('likes') when the fast path matters and volume is small; use withCount + a constrained relation when posts are popular.
Filtering lists by like state — use the package scopes rather than joining the likes table by hand:
Post::whereLikedBy($user)->paginate(); // liked by this user
Post::whereNotLikedBy($user)->paginate(); // not yet liked
Expose it in API resources as explicit fields, so clients don't need to know about the relation:
return [
'id' => $this->id,
'likes_count' => $this->likes_count,
'is_liked' => $this->isLikedBy(auth()->user()),
];
Prefer a single toggle endpoint over separate like/unlike POSTs. It keeps the client simpler and removes a class of "double-click sends two likes" bugs.
Custom models go through config, not through the trait. If you need extra columns on the like record, extend the package Like model and rebind it in config/likeable.php — don't edit the published migration in place on an existing install.
Events/observers: if you need notifications ("X liked your post"), hook the model events on your own Like model rather than scattering Notification::send in controllers. That keeps the like path the single source of truth.
like()/unlike() in the same request can return the pre-write answer if the relation was loaded earlier. Call $post->load('likes') / $post->unsetRelation('likes') after mutating, or re-fetch. This is the single most likely regression to hit when upgrading from 3.0.x.likes and (b) also mutates like state in the same request.with('likes') loads all likes. On high-traffic models this can be hundreds of rows per item. Benchmark before adding it to a feed; withCount alone is often enough.(likeable_type, likeable_id, user_id) in a follow-up migration so concurrent requests fail loudly instead of double-inserting.liked($model)/isLikedBy($user) expect a real user instance — guard with auth()->check() in Blade and policies, or the null user will surface as a null-constraint error.Relation::enforceMorphMap([...]) early, and add it before you ship, not after.likes_count comes from withCount('likes'); don't expect a column on posts to update itself. If you need one for sorting, keep it in sync via a model observer.deleting observer. Decide explicitly.liker model the same class as the one used to check; is the morph map enforced consistently; is a stale likes relation loaded on the instance. toSql() on $post->likes() is faster than guessing.Like/Liker/Likeable implementations — anything you'd otherwise be tempted to monkey-patch belongs there.How can I help you explore Laravel packages today?