kilobyteno/laravel-user-guest-like
A Laravel package that lets both authenticated users and guests like your Eloquent models. Add the trait, and get simple methods for liking, unliking, and checking likes, with guest likes tracked automatically and no extra setup required.
Install into a Laravel 12/13 app:
composer require kilobyteno/laravel-user-guest-like
Version pinning: 1.5.0 adds Laravel 13 support and drops Laravel 11. On an L11 codebase stay on
^1.4, or upgrade the app first.
Publish and run the migration (the package ships a likes migration — publish it rather than editing the vendor copy, so you can add indexes):
php artisan vendor:publish --tag="user-guest-like-migrations"
php artisan migrate
Add the trait to every model that can be liked (e.g. Post, Comment):
use Kilobyteno\UserGuestLike\Traits\Likeable;
class Post extends Model
{
use Likeable;
}
Where to look first: the package README for the exact trait/method names, then src/Traits/ and the published migration. The migration tells you more about behaviour than the docs — specifically how a guest is identified (session ID vs. IP) and which columns form the uniqueness constraint.
First use case — a toggle endpoint for a like button:
Route::post('/posts/{post}/like', function (Post $post) {
$post->toggleLike(); // or like()/unlike(); confirm names in the README
return back();
});
Guests work out of the box because the package resolves the current liker from the session when auth()->check() is false — no login wall required.
Treat likes as a toggle, not two endpoints. One POST /like route that delegates to the package's toggle keeps the UI simple and makes double-clicks idempotent-ish. Return the fresh count so the front end can update without a reload:
return response()->json([
'liked' => $post->isLikedByCurrent(), // confirm method name
'count' => $post->likes()->count(),
]);
Eager-load counts for index pages. This is the single biggest performance lever:
$posts = Post::withCount('likes')->paginate(20);
// Blade: {{ $post->likes_count }}
Never call $post->likes()->count() inside a loop — that's an N+1 per row.
Guard against "did I like this" N+1 too. If each card renders a filled/unfilled heart, either eager load the current user's liked IDs once and pass a set to the view, or expose a scoped query in a service class:
$likedIds = Like::where('user_id', auth()->id())
->where('likeable_type', Post::class)
->pluck('likeable_id')
->flip();
Livewire / Inertia front ends. Keep the component dumb: dispatch to the package, then $this->post->refresh() or return an Inertia prop with the updated count. Avoid caching the count in a public property that can drift from the DB after another user likes the same post.
Merge guest likes on login. In your Login/Registered listener, before regenerating the session, grab the guest's likes by the package's guest key and re-point them at auth()->id(). De-duplicate against existing rows or the unique index will throw. This is the pattern that makes the "guest can like, then sign up" funnel work without losing data.
Rate-limit the endpoint. Guest likes are cheap to spam. A throttle:30,1 middleware on the toggle route plus a DB unique constraint is a reasonable baseline.
Testing. Use actingAs($user) for authenticated cases and a plain $this->post(...) for guest cases — the guest identity will persist across requests in the same test because the session does. Assert on the DB with assertDatabaseHas('likes', [...]) rather than on rendered HTML.
composer update on a Laravel 11 app will now fail resolution (or, worse, silently pin you to 1.4.x if the constraint is loose). Check composer why-not kilobyteno/laravel-user-guest-like 1.5.0 before blaming the package.firstOrCreate-style logic or catch QueryException with SQLSTATE 23000 and re-read the state.likes rows survive delete() on the likeable model unless you add a deleting observer or a DB-level cascade. Orphaned rows inflate counts on re-used IDs (rare, but it happens with soft deletes + re-seeding). Add a scheduled prune job for likeable_type values no longer in use.likeable_type is string(255) — truncated class names produce silently non-matching queries.(likeable_type, likeable_id) and on (user_id, likeable_type, likeable_id) for the "has liked" lookups; the shipped migration may not include both.likes_count column updated on write, or cache with a short TTL and accept slight staleness. withCount is fine to hundreds of thousands of rows, not millions.Like model is the natural place to hook — bind a subclass via config if the package exposes a model config key, so you can add soft deletes, a source column, or an event listener without forking. Fire your own PostLiked/PostUnliked events from your controller rather than expecting the package to.How can I help you explore Laravel packages today?