loafer/laravel-likeable
A Laravel package for adding like and dislike reactions to Eloquent models. Use traits to make models likeable, attach reactions polymorphically, toggle likes, and query counts or liked items with a clean API.
Read the status line first. This repository is archived (last release 2017-12-28) and the description itself points to cybercog/laravel-love as the successor. Any assessment of day-to-day usage has to start with the adoption decision:
cybercog/laravel-love instead. Same author, same conceptual model, maintained. You get the same Likeable / liker traits plus fixes for modern Laravel.laravel-likeable → keep it, but budget for the migration. It targets the Laravel 5.4–5.6 era; composer resolution against Laravel 8+ is the first thing that will fail.loafer/...) → treat it as a vendored dependency. Pin the exact version, and vendor the migration into your own database/migrations so schema changes are under your control.Minimal steps (assuming a compatible Laravel version):
composer require cybercog/laravel-likeable
# publish config + migration (tag names vary by release; check the README)
php artisan vendor:publish --provider="CyberCog\Likeable\LikeableServiceProvider"
php artisan migrate
Then add the two traits — this is the package's entire mental model: one side is likeable (the thing being rated, e.g. Post), the other is the liker (usually User).
use CyberCog\Likeable\Traits\Likeable;
class Post extends Model
{
use Likeable;
}
use CyberCog\Likeable\Traits\Liker;
class User extends Model
{
use Liker;
}
Where to look first: open the published migration and read the likeables table. It is polymorphic on both sides (likeable_id/likeable_type, liker_id/liker_type) with a boolean-ish liked flag and a unique index. Understanding that single row-per-pair design explains every behaviour you'll hit later — no duplicate rows, like↔dislike is an update, not an insert.
First use case — the classic blog "like this post" button:
$post = Post::first();
$post->like(); // current authenticated user
$post->unlike();
$post->likeToggle(); // idempotent-ish; ideal for a single button endpoint
$post->likes()->count();
$post->isLikedBy($user);
Verify each method name against the installed tag before wiring controllers — the archived README drifted from the successor package, and argument signatures (implicit auth()->user() vs. explicit user) differ between versions.
Thin controller, one route per intent. Do not put likeToggle() behind a GET link. Two small endpoints read better in logs and make idempotency debuggable:
// routes/web.php
Route::post('posts/{post}/like', [PostLikeController::class, 'store']);
Route::delete('posts/{post}/like', [PostLikeController::class, 'destroy']);
public function store(Post $post): PostResource
{
$post->like(); // or ->likeToggle()
return new PostResource($post->loadCount(['likes', 'dislikes']));
}
Prefer the explicit-user form wherever it exists. Anything that resolves auth()->user() internally will blow up in a queued job, an artisan command, an import script, or an unauthenticated API route. If the release you're on only offers the implicit form, wrap it:
Auth::login($user);
$post->like();
Auth::logout();
…or call the underlying service class directly if the package exposes one (app(LikeableService::class)->like($post, $user) in the newer API). Bind your own thin wrapper (PostLiker) around it so that when you migrate to Laravel Love you change one class, not forty call sites.
Expose state via API resources, not via N+1. A feed of 50 posts must not fire 50 count queries:
$posts = Post::withCount(['likes', 'dislikes'])->paginate(20);
// in PostResource
'likes_count' => $this->likes_count,
'dislikes_count' => $this->dislikes_count,
'is_liked' => $this->isLikedBy($request->user()),
is_liked per row is still one query per row unless you eager-load the relation — either eager-load likes constrained to the current user, or accept the cost on paginated pages and cache the counters.
Denormalise counts once you have traffic. withCount on a hot feed will not survive. Add likes_count/dislikes_count columns to the parent table and keep them in sync in your own service layer or via model events on the pivot-ish rows. The package does not do this for you.
Authorisation belongs outside the package. It has no opinion on who may like what. Add a policy:
public function like(User $user, Post $post): bool
{
return $user->id !== $post->poster_id;
}
Queue/import pattern. For bulk operations (seeding, backfills, migrating from an old votes table), bypass the trait and insert directly into the likeables table in chunks — thousands of individual like() calls will each issue their own insert plus a isLiked check.
Migration path to Laravel Love. Same author, same traits, but table and namespace renames. Plan it as: install Love alongside, write a data migration copying likeables → the new table, swap use statements, delete the old package. The public API is close enough that a well-abstracted wrapper (see above) makes this a day's work rather than a sprint.
Archived means "your problem now." No security patches, no PHP 8.1+ / Laravel 9+ compatibility work, no fixes for doctrine/dbal or enum/morph issues. If composer refuses to install, do not reach for --ignore-platform-reqs on a production app; vendor the package into a local path repository you control.
The implicit-auth footgun is the #1 support issue. $post->like() with no logged-in user throws or silently no-ops depending on version. Always assert authentication in the controller before calling.
like() is not idempotent; likeToggle() is what you want on a button. Double-submits (double-click, retried request) will hit the unique index and throw a QueryException on like(). Either use the toggle or catch QueryException and treat a duplicate-key violation as success. Also wrap toggle in a transaction if you're also updating a cached counter — two concurrent toggles can interleave and leave the counter off by one.
Morph maps are load-bearing. The table stores fully-qualified class names. Rename or namespace-shift a model after launch and every existing row silently orphans. Define Relation::morphMap() before going to production, and never change it afterwards.
No cascade on delete. Deleting a Post or a User leaves dangling likeables rows. Counts on other models can drift, and a re-created ID can inherit stale rows. Add a deleting observer that cleans up, or a DB-level FK if your polymorphic columns permit it. Soft deletes are worse: rows persist and counts keep including them.
Counts include dislikes unless you filter. $post->likes()->count() should already filter by the flag, but verify with ->toSql() against your installed version — this is the exact spot where the archived code and the successor diverge, and it silently produces "likes = likes + dislikes" in some forks.
UUID/ULID models will likely break it. The 2017 schema assumes auto-incrementing integer keys on both sides of the polymorphic pair. If you use UUIDs, edit the published migration before running it and check whether the config exposes a flag for this (Laravel Love added one; this package may not).
Unliking a dislike and disliking a like are two different operations. If your UI has a single like/dislike toggle pair, make sure you call undislike() before like(), otherwise the unique index rejects the second insert. The toggle methods exist precisely because this is easy to get wrong.
Debugging recipe: tail the query log (DB::listen(fn($q) => Log::info($q->sql, $q->bindings))) and count queries per page load; then inspect the raw table with select * from likeables where likeable_type = ? and likeable_id = ?. Almost every bug is either (a) a stale/missing row, (b) a class-name mismatch from a rename, or (c) an unexpected auth()->user().
Extension points, in order of usefulness: your own wrapper service (abstracts the API for the Love migration), a model observer on the parent for counter maintenance, a policy for authorisation, and — if you fork — overriding the trait methods in your model rather than editing vendor code.
How can I help you explore Laravel packages today?