modularavel/likeable
modularavel/likeable is a Laravel package that adds like functionality to Eloquent models. Let users like posts, comments, and other resources through a reusable trait, with relationship helpers and like counts for quick integration.
v1.0.0 is the first stable tag for modularavel/likeable, so this assessment establishes the baseline. The 1.0.0 version number is the main signal here: the maintainers are declaring the public API (trait methods, relation names, config keys) stable enough to depend on with a normal ^1.0 constraint. The changelog is empty beyond the tag, which is typical for an initial release — there is no upgrade path to worry about, but also no migration guide to lean on, so read the README and the trait source before wiring it into an existing model.
Minimal setup:
composer require modularavel/likeable
php artisan vendor:publish --provider="Modularavel\Likeable\LikeableServiceProvider"
php artisan migrate
Then apply the trait to the model that receives likes (and, if the package follows the usual two-trait split, the liker trait to your User):
use Modularavel\Likeable\Traits\Likeable;
class Post extends Model
{
use Likeable;
}
First use case — the one to validate before building anything else:
$post->like(); // as the authenticated user
$post->unlike();
$post->toggleLike(); // for a single "heart" button
$post->isLikedBy(auth()->user());
$post->likes()->count();
$post->likes; // collection of like records
Look first at: the published migration (confirm the morph columns and indexes actually match your naming), the config/likeable.php file (user model resolution and table names are the two things that break first), and the trait itself to see which methods are public API versus internal helpers.
Trait placement is asymmetric — get it right. The "likeable" trait goes on content models (Post, Comment, Video), while the "liker" trait belongs on User. Mixing these up produces confusing failures where auth()->user()->likes() returns nothing or throws a relation-not-found error. If your app has more than one actor type (users and teams, say), confirm the package resolves the actor via the auth guard or accepts an explicit model argument; v1.0.0's contract is worth pinning down in a test.
Prefer toggleLike() for UI, explicit like()/unlike() for APIs. A toggle endpoint is idempotent-hostile — a double-click or a retried request flips the state back. For a JSON API, make the client send intent (POST /likes, DELETE /likes) and map those to the explicit methods.
Eager-load counts everywhere. The single biggest performance trap is rendering a feed and calling $post->likes()->count() in the loop. Use withCount('likes') on the index query and read $post->likes_count. If you need "did this user like it" across a feed, that is a second query unless the package ships a helper — usually worth writing a small whereLikedBy($user) scope or a single whereIn lookup keyed by likeable ID rather than N isLikedBy() calls.
Wrap the write and the read in a transaction when you fan out. A realistic like endpoint does: insert the like, increment a cached counter, and dispatch a notification event. Do the first two in a transaction and dispatch after commit (DB::afterCommit or ShouldDispatchAfterCommit) so a rollback doesn't leave a phantom notification.
Expose it through a resource, not the raw relation. In an ApiResource, return is_liked and likes_count rather than embedding the full likes collection — the collection leaks user IDs and balloons payload size on popular posts.
Blade usage: a small anonymous component (<x-like-button :model="$post" />) that renders the count plus a state-aware form keeps the toggle logic out of views and gives you one place to add optimistic UI or a Livewire binding later.
Integration tip: if you already have a Reaction or Favorite table, don't run both systems. Pick one and migrate; two parallel polymorphic tables doing the same job will diverge within a month and the reporting queries become unanswerable.
Relation::enforceMorphMap(), existing rows written with fully-qualified class names stop resolving and likes silently vanish from relations. Decide on a morph map before production data exists, or write a one-time backfill.(user_id, likeable_type, likeable_id) is the only reliable guard against double-likes from concurrent requests. Verify the published migration has it; if not, add it in your own follow-up migration. Handle the resulting QueryException (or use firstOrCreate/insertOrIgnore) rather than letting a 500 surface to the user.where likeable_type = ? and likeable_id in (...)) and "who liked this" reads want different index orders. Check EXPLAIN on your real query before the table gets large.withCount is fine to tens of thousands of likes; beyond that, a denormalized likes_count column updated in the same transaction (with a periodic reconciliation job) is the safer bet. Watch for the count drifting after hard deletes — polymorphic relations do not cascade.User uses SoftDeletes, likes from deleted users still count and can break isLikedBy() assumptions. Decide explicitly whether to filter, and if the package doesn't, add a scope.config/likeable.php are usually read at boot; changing them means re-publishing or clearing config cache (php artisan config:clear) — a stale cached config is a frequent "why is it querying the wrong table" answer in local dev.RefreshDatabase plus actingAs($user) covers most of it. Add one test asserting that liking twice does not create two rows, and one asserting the toggle returns to the original state — those two catch the majority of real-world regressions.^1.0 is reasonable for a 1.0.0, but with no changelog history yet, review the diff before taking 1.1.0 until the project demonstrates a track record of documented breaking changes.How can I help you explore Laravel packages today?