overtrue/laravel-like
A Laravel package for adding like, unlike, and favorite functionality to Eloquent models. Simple traits let users like any model, check liked status, count likes, and query liked items through clean, expressive APIs.
Install and migrate
composer require overtrue/laravel-like
php artisan vendor:publish --provider="Overtrue\LaravelLike\LikeServiceProvider"
php artisan migrate
Publishing normally gives you a config/like.php (model bindings, UUID/morph settings) and the likes migration. If you don't see the tags you expect, publish by provider as above, or check the package's README for the current tag names — these have shifted between majors.
First place to look
config/like.php — this is where the Like model class and UUID support are configured. Read it before writing any code; nearly every customization hangs off it.likes) and the polymorphic columns (likeable_id / likeable_type).Overtrue\LaravelLike\Traits\Liker and Traits\Likeable — the two traits are the entire public API surface.Wire up your models
// app/Models/User.php
use Overtrue\LaravelLike\Traits\Liker;
class User extends Authenticatable
{
use Liker;
}
// app/Models/Post.php
use Overtrue\LaravelLike\Traits\Likeable;
class Post extends Model
{
use Likeable;
}
First use case
A like/unlike endpoint plus a "did I like this?" flag on the show page:
// routes/web.php
Route::post('/posts/{post}/like', function (Post $post) {
$post->toggleLike(auth()->user());
return back();
});
// in a controller / view
$post->likers()->count(); // total likes
auth()->user()->hasLiked($post); // boolean for the current user
That loop — toggle, count, membership check — covers 90% of what this package is used for, whether you call it "likes", "favorites", "bookmarks", or "upvotes". The semantics are generic; only the table and trait names are like-specific.
Version note for this release
v6.0.0 requires PHP 8.3+ and Laravel 13 only. If you are on Laravel 11 or 12, stay on the ^5.4 line — this is not a version that will install alongside older framework constraints, and there is no compatibility shim.
Controller endpoints (the standard shape)
public function store(Post $post)
{
$post->like(auth()->user()); // idempotent-ish; see Gotchas
return response()->json([
'liked' => true,
'count' => $post->likers()->count(),
]);
}
public function destroy(Post $post)
{
$post->unlike(auth()->user());
return response()->json([
'liked' => false,
'count' => $post->likers()->count(),
]);
}
Return the fresh count from the server rather than incrementing client-side — it keeps the counter honest when two tabs are open.
Toggle for a single button
$post->toggleLike($user);
Use this when the UI is a single optimistic button. Use explicit like/unlike when you have separate buttons and want deterministic state.
Index pages: avoid the N+1
The single biggest day-to-day pattern. hasLiked() per row is a query per row, and likers()->count() per row is another.
$posts = Post::query()
->withCount('likers')
->paginate(20);
// in the resource
'lkes_count' => $this->likers_count,
For the "liked by me" flag across a collection, do one query and hydrate a set:
$likedIds = Like::query()
->where('user_id', auth()->id())
->where('likeable_type', (new Post)->getMorphClass())
->whereIn('likeable_id', $posts->pluck('id'))
->pluck('likeable_id')
->flip();
// $likedIds->has($post->id)
That's two queries for the whole page instead of 40.
Custom Like model via config (main extension point)
// config/like.php
'models' => [
'like' => App\Models\Like::class,
],
// app/Models/Like.php
class Like extends \Overtrue\LaravelLike\Like
{
protected $with = ['user']; // or add scopes, casts, soft deletes
}
This is how you add soft deletes, activity-log observers, an is_public flag, or a user() relation eager-load default. Do it before launch — changing the Like model after data exists means a backfill.
Likers list with pagination
$post->likers()->with('profile')->paginate(30);
Resist the urge to ->get() — popular posts will bite you.
Morph maps
If you use Relation::enforceMorphMap([...]), the likeable_type column will store aliases (post instead of App\Models\Post). That's fine and desirable, but it means the stored values differ from a fresh install. Either adopt morph maps on day one or write a migration to rewrite the column when you introduce them.
Blade / API resource convention
Keep the two fields consistent across every resource so the frontend doesn't special-case:
return [
'id' => $this->id,
'likes_count' => $this->likers_count ?? $this->likers()->count(),
'liked_by_me' => $this->liked_by_me,
];
Non-"like" semantics
Because it is just a polymorphic pivot, the same package handles bookmarks and upvotes. If you need two independent concepts (like and bookmark), do not try to multiplex one likes table with a type column bolted on — install the package twice under different table/model names, or use two separate tables. Multiplexing breaks every unique constraint assumption.
v6 breaks on Laravel ≤12 — plan the upgrade
The release notes are minimal but the constraint change is not: Laravel 13 only, PHP 8.3 minimum, plus a bumped testbench baseline. Practically this means:
composer.json for php: ^8.3 and laravel/framework: ^13.0 before bumping.composer update.laravel-like and pin ^5.x will block the upgrade. Run composer why-not overtrue/laravel-like 6.0.0 early.Duplicate rows under fast toggling
Two rapid clicks hitting like() concurrently can both pass an existence check and either double-insert or throw a unique-constraint QueryException. Defend with:
try {
$post->like($user);
} catch (\Illuminate\Database\QueryException $e) {
// already liked — treat as success
}
or do the write yourself with firstOrCreate, and make sure the migration actually has a unique index on (user_id, likeable_id, likeable_type). If you customised the migration, confirm the index survived.
No built-in counter cache
likers()->count() is a COUNT(*) every time. On a feed page this is fine with withCount, but if you display a total on a hot page, add a likes_count column on the parent model and maintain it in Like model events. Be aware that means drift is now your problem — add a nightly reconciliation command.
Orphaned likes when content is deleted
Polymorphic rows aren't cleaned up by the parent's deletion. Add explicit cleanup:
static::deleted(function (Post $post) {
Like::where('likeable_type', $post->getMorphClass())
->where('likeable_id', $post->id)
->delete();
});
...or a scheduled command that sweeps likes whose parent no longer exists. Skipping this leaves you with silently inflated counts if IDs are ever reused, and a table that grows forever.
No self-like protection
Nothing prevents a user from liking their own post. Enforce it in a policy or a form request — it's a product rule, not a package rule.
N+1 creeping back in
The two most common regressions: adding auth()->user()->hasLiked($post) inside a Blade @foreach, and adding a likers accessor to an API resource that's serialized in a collection. Grep for hasLiked( and likers()->count() before any release; both are almost always wrong in a loop.
Testing
No factories ship for the Like model, so write your own or use the relation API directly. In tests:
$this->actingAs($user);
$this->post->like($user);
$this->assertTrue($user->hasLiked($this->post));
With RefreshDatabase the pivots reset cleanly — but if you're asserting exact counts, watch out for tests that like from multiple users without fresh setup.
Config cache
If you change the like model binding or UUID setting in config/like.php, run php artisan config:clear — a cached config will silently keep the old model class and you'll debug the wrong Like instance.
Naming collision
The traits are Liker and Likeable. If your app already has a Likeable trait or interface (common in CMS-style codebases), alias on import: use Overtrue\LaravelLike\Traits\Likeable as HasLikes;.
How can I help you explore Laravel packages today?