liquidstyle/laravel-likeable
Add like and unlike functionality to any Eloquent model with this lightweight Laravel package. It stores likes in a polymorphic table, offers simple trait-based integration, and lets you query liked models and counts with minimal setup.
Install and wire up
composer require <vendor>/<wishlist-package>:^2.2
php artisan vendor:publish --provider="<Vendor>\Wishlist\WishlistServiceProvider"
php artisan migrate
Publish both the config file and migrations in one pass; the wishlist table typically ships with a polymorphic wishlistable relation plus a unique composite index on (user_id, wishlistable_type, wishlistable_id), and you want that index in place before you start writing toggles in a loop.
Where to look first
config/wishlist.php — model bindings, table names, guest-list behaviour, and the route/middleware toggles. This is the file that answers most "why isn't it saving?" questions.HasWishlist (or similarly named) trait — this is the contract you attach to User (and optionally to Product for reverse lookups). Read its method list before writing any controller code; it almost certainly already exposes add/remove/toggle/has/clear.First use case
The 80% case is: authenticated user toggles a product in and out of their wishlist from a product card, then sees the list on /wishlist.
// User model
use <Vendor>\Wishlist\Concerns\HasWishlist;
class User extends Authenticatable
{
use HasWishlist;
}
// Controller
public function toggle(Product $product)
{
auth()->user()->toggleWishlist($product);
return back();
}
// Blade
@if (auth()->user()->hasInWishlist($product))
<form method="POST" action="{{ route('wishlist.toggle', $product) }}">@csrf ...</form>
@endif
Get that loop working end-to-end before touching guests, merging, or API responses — it validates your trait wiring, migration, and config in one shot.
Thin controller, fat trait. Resist wrapping every call in a repository. The package's value is that $user->addToWishlist($product) is already the domain action. Put your policy/authorisation in a FormRequest or policy, not in an adapter layer around the package.
Toggles belong in POST, not GET. A wishlist toggle is state-changing, so it should be a POST/DELETE with CSRF, returning back() for HTML or a small JSON payload for AJAX. Pattern that scales well:
public function toggle(Request $request, Product $product)
{
$added = $request->user()->toggleWishlist($product);
return $request->expectsJson()
? response()->json(['wishlisted' => $added, 'count' => $request->user()->wishlist()->count()])
: back();
}
Eager-load the relation on listing pages. $user->wishlist() returns a morph-to-many; on an index page you almost always want ->with('wishlistable') so the product data comes along. If your package exposes a query builder, use it rather than ->wishlist()->get() followed by lazy loads.
Check-in-list rendering. For grids of 24+ products you do not want one query per card. Either load the user's wishlisted IDs once into a collection and use ->contains('id', $product->id), or use the package's hasInWishlist only where the count is small. This is the single biggest performance decision in a wishlist feature.
Guest wishlists → merge on login. If the config allows session-backed lists, the standard workflow is: guest adds items to a session list, then on Login/Registered events you merge the session list into the persisted list. Wire this in an event listener, not in the login controller — it keeps socialite/API login flows covered too. Decide the conflict policy up front (union vs. replace) and document it.
API surface. For a JSON API, return the wishlist as a resource collection with a stable shape (id, product_id, added_at) rather than serialising the raw pivot. Clients that key off array indices will break the first time you change ordering.
Testing. Use the trait's public methods to seed state in factories/test setup rather than inserting rows directly — that way a schema or method-name change in a minor release shows up as a failing test rather than a silent production bug. Add one feature test per route and one test for the guest-merge path.
The v2.2.6 "typo fix" needs a diff check. A typo fix in "Wishlist Methods" is usually a docblock or exception-message correction — harmless. But if the typo was in a public method name (e.g. addToWhishlist → addToWishlist), any calling code using the old spelling breaks. Before bumping: read the commit, not just the release note. Then grep -rn "Whishlist\|wishlist\b" app/ resources/ tests/ for the misspelling, and let your test suite (which, per above, calls the real methods) confirm nothing is orphaned. Pin with ^2.2.6 rather than ^2.2 if you want certainty about which typo state you're on.
Unique constraint collisions. Double-submits (double-clicked toggle buttons) will throw a QueryException on the composite unique index. Either use insertOrIgnore-style semantics in your controller, disable the button on submit, or catch and treat as idempotent success. Do not silently swallow all query exceptions — only the duplicate-key one.
Morph map surprises. If you register a morph map (Relation::enforceMorphMap([...])), older rows written with FQCNs will no longer resolve. Migrate existing wishlistable_type values in the same deploy as the morph map registration, and add the wishlist's own model to the map if it appears in any polymorphic relation.
Soft deletes and orphans. If Product is soft-deletable, wishlist rows survive and your listing query will either error or silently drop items depending on how the morph relation is loaded. Add a whereHas('wishlistable') (or a scheduled prune) and decide whether a deleted product should remove the wishlist entry.
Guest sessions and cookie lifetime. Session-backed guest lists evaporate when the session expires — usually fine, but communicate it in the UI ("sign in to save your list") rather than letting users lose items without warning. Also confirm the package's guest mode isn't enabled in an API-only context where there's no session.
Extension points, in order of preference. (1) Config-level model swap — subclass the package's wishlist model and rebind in config/wishlist.php; (2) model events on your own subclass (created, deleted) for analytics/notifications; (3) publishing and editing the migration only before first deploy. Avoid editing vendor files or forking for anything short of a genuine bug — this package is shipping patch releases, so staying on the upstream branch is worth more than the flexibility.
Debugging checklist for "it won't save": is the trait on the model the package expects (User, not a custom guard model)? Is the morph type column wide enough? Is the item you're passing actually a persisted model instance (not new Product)? Is guest mode off? That covers roughly every report you'll see in the first week.
How can I help you explore Laravel packages today?