Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Favorites Laravel Package

sugar-agency/laravel-favorites

A Laravel package for adding favorites (bookmarks/likes) to Eloquent models. Likely provides polymorphic relationships and helper methods to favorite, unfavorite, and check favorited status. Verify exact API and features against the package's README before use.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and migrate

composer require sugar-agency/laravel-favorites
php artisan vendor:publish --tag="favorites-migrations"   # confirm the tag in the service provider
php artisan migrate

Do this on a fresh branch and inspect the published migration before running it — you want to know whether the table keys on user_id (bigint/uuid) and whether it has a unique index, because both affect how you extend it later.

2. Read four files before writing any code

The package is small (a handful of source files), so vendor-reading is cheap and worth more than the README:

  • src/ concerns/traits — the exact verb names (favorite, unfavorite, toggleFavorite, isFavorited, favorites()) and their signatures
  • src/Models/Favorite.php — the pivot model, its fillable fields and relations
  • database/migrations/* — column names, index strategy, nullability of the user column
  • src/*ServiceProvider.php — event listeners (the login/session-transfer hook lives here) and any bindings you can override

3. Wire the traits

Add the favoriter trait to User (or any model that can favorite), and the favoritable trait to the models that can be favorited (Product, Post, Article). Keep a note of which trait goes where — this is the single most common first-install mistake.

4. First use case: guest bookmarks that survive login

The package's differentiator is session-based favorites that transfer to the authenticated user. So make your first end-to-end test exactly that:

  1. As a guest, POST to a toggle route and assert the item is favorited.
  2. $this->post('/login', ...) (or actingAs) in the same test session.
  3. Assert the favorite now exists in the favorites table against the user and the session copy is gone.

If that test passes, the package is behaving as advertised and you can build UI on top of it.

Implementation Patterns

Thin controller + toggle endpoint

One route for both guest and authenticated users; let the package resolve the favoriter.

Route::post('/products/{product}/favorite', [FavoriteController::class, 'toggle']);

public function toggle(Request $request, Product $product)
{
    $state = $request->user()?->toggleFavorite($product)
        ?? favorites()->toggle($product);   // guest path — use whatever the package exposes

    return response()->json([
        'favorited' => (bool) $state,
        'count'     => $product->favorites()->count(),
    ]);
}

Keep the route outside auth middleware. If you gate it behind auth you lose the entire session-transfer feature.

Return state, not just a redirect

Front-ends (Blade + Alpine, Inertia, or a fetch call) need to know the new state. Always return favorited and a count so the button and counter can update without a reload.

Hydrate initial state without N+1

Do not call isFavorited() inside a @foreach over 50 products. Prefetch the favorited IDs once:

$favoritedIds = $user->favorites()
    ->where('favoritable_type', (new Product)->getMorphClass())
    ->whereIn('favoritable_id', $products->pluck('id'))
    ->pluck('favoritable_id')
    ->flip();

Then in the view check isset($favoritedIds[$product->id]). This is the highest-value integration tip in the whole package.

Counters for "X people saved this"

Use the morph relation for counts rather than a denormalized column until you actually need it:

Product::withCount('favorites')->paginate();
// $product->favorites_count

If you later add a cached favorites_count column, remember to update it in the same transaction as the toggle and backfill existing rows.

API resources

Normalize the shape once so web and API agree:

class ProductResource extends JsonResource {
    public function toArray($request) {
        return [
            'id'            => $this->id,
            'is_favorited'  => $this->when(isset($this->is_favorited), fn () => (bool) $this->is_favorited),
            'favorites_count' => $this->whenCounted('favorites'),
        ];
    }
}

Multiple favoritable types, one endpoint

Because it's a morph relation, a single generic route works:

Route::post('/favorites/{type}/{id}', ...);
// resolve via Relation::getMorphedModel($type) and abort_unless it uses the trait

Validate the type against an allow-list — never let the client pass an arbitrary class name into the morph map.

Transfer on login: verify the hook, and handle non-standard auth

The transfer happens via an event listener on login. If you use Breeze/Fortify/Sanctum, test it explicitly. For token-based or SPA auth where no session survives, the session concept is meaningless — call the transfer yourself right after authentication, or skip it and require auth for favoriting in that context.

Jobs and console commands

There is no current user or session in a queue worker. Never rely on the ambient favoriter there; always pass a User instance explicitly and operate on $user->favorites().

Use it for more than "likes"

The same table backs "save for later", "wishlist", and "remember this". If you need to distinguish them (a heart and a bookmark on the same post), you'll need an extra discriminator column or a second table — the package stores one favorite per (user, favoritable) pair, so semantics are mutually exclusive by default.

Gotchas and Tips

Session driver matters more than you think. With SESSION_DRIVER=array (the default in many test setups and some deployments), guest favorites vanish between requests and the transfer feature silently does nothing. Use database or redis in any environment where you care, and write a feature test that spans two requests, not one.

The transfer only fires on login. If a guest registers a new account, does the transfer run? Check the listener list in the service provider — registration often dispatches Registered, not Login. If it's missing, add your own listener that calls the transfer method. This is the most likely gap in a package of this size.

Morph maps will break existing rows. If you call Relation::enforceMorphMap() (recommended for smaller indexes), do it before the first favorite is written, or write a migration that rewrites favoritable_type from FQCNs to aliases. Otherwise old rows silently stop resolving.

Orphaned rows. Deleting a Product leaves its favorites behind unless you handle it. Add a deleting hook on the favoritable model, a FK with cascadeOnDelete, or a scheduled prune. Also decide how soft-deleted models should behave — favorites pointing at trashed records will still count.

Duplicate favorites. If there's no unique index on (user_id, favoritable_type, favoritable_id), concurrent double-clicks create duplicates and your counts drift. Add the index in your own migration; also consider wrapping the toggle in DB::transaction plus firstOrCreate/delete rather than trusting a single call.

Multi-guard and multi-auth. The package almost certainly resolves the favoriter via the default guard / Auth::user(). If you have an admin guard, a customer guard, or two user types sharing the table, verify which one it picks and whether user_id collides across guards. This is a common source of "favorites show up for the wrong account" bugs.

Cache the isFavorited check for hot pages. It's one indexed query per item, but on a homepage with 100 cards it's 100 queries if done naively. Use the prefetch pattern above, or a small per-request memo keyed by model class + id.

Test helpers. $this->actingAs($user) covers the authenticated path; for guests use $this->withSession([...]) and follow up with a second request in the same test to prove persistence.

Extension points worth knowing. You can usually override the Favorite model via config or a container binding — check the service provider. That's the cleanest place to add SoftDeletes, extra columns (collection, note), or audit fields, and it lets you keep the package's traits while owning the schema.

Adoption risk is real. Zero dependents and no stars means you are the QA team. Before shipping: pin the version in composer.json, read the whole src/ directory (it's an afternoon at most), and add your own feature tests for toggle, guest persistence, transfer-on-login, and orphan cleanup. If the package stops receiving updates, the traits are thin enough to vendor into app/Support/ in a day.

Debugging checklist when a favorite "doesn't stick": session driver → route is behind auth middleware → morph map mismatch → unique index swallowing the insert → the model you're checking uses the wrong trait → the login listener didn't fire. Walk them in that order; it resolves almost every report.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings