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

Products Favorite Laravel Package

baks-dev/products-favorite

A Laravel/PHP module that adds a product favorites (wishlist) feature to your application. Install via Composer with baks-dev/products-favorite. Requires PHP 8.4+ and is released under the MIT license. Tests run with phpunit using the products-favorite group.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a single-purpose module: it stores which products a user has favourited. The README (Russian only) gives you the install line and a test group — everything else you learn from the source. Budget 30–60 minutes for a discovery pass before writing feature code.

1. Install and pin the version

composer require baks-dev/products-favorite:7.4.8

Pin it exactly. With zero stars, zero dependents, and patch releases that carry no changelog beyond a version string, you do not want ^7.4 silently dragging in a behavioural change. Note the hard requirement: PHP 8.4+. On 8.2/8.3 the solver will fail or, worse, Composer will resolve an older tag.

2. Discover what you actually got

composer show baks-dev/products-favorite          # namespace, autoload, requires
php artisan vendor:publish --list                 # which tags exist (config, migrations, views, translations)
php artisan about                                 # is the provider registered?
ls vendor/baks-dev/products-favorite/src
ls vendor/baks-dev/products-favorite/src/*/Resources   # migrations, config, translations

Read, in this order:

  • composer.json — the PSR-4 root namespace and any extra.laravel.providers / dont-discover entries.
  • The service provider(s) — what is bound in the container, whether migrations load via loadMigrationsFrom, whether the provider is auto-discovered.
  • src/*/Resources/config/*.php — table name, user model/guard, cascade rules.
  • The migrations — table name, foreign keys, unique index on (user_id, product_id) (or its absence tells you a lot).
  • tests/ — the executable spec. This is your real documentation.

3. Run the shipped tests first

php bin/phpunit --group=products-favorite

If your phpunit.xml does not declare that group, or the package's test bootstrap expects its own harness, you will get "No tests executed" — treat that as a red flag, not a success, and inspect what the tests cover manually.

4. First use case — a toggle endpoint

The fastest proof of life is an authenticated toggle route. Wire it, hit it with php artisan tinker or a test, and confirm a row lands in the favourites table:

// routes/web.php
Route::middleware('auth')->group(function () {
    Route::post('/products/{product}/favorite', [FavoriteController::class, 'toggle'])
        ->name('products.favorite.toggle');
});

Do this before designing any UI. If the toggle does not round-trip, the rest of the assessment is moot.


Implementation Patterns

Treat the package as the persistence + relation layer and keep your own HTTP/UI layer thin on top. That is the pattern that survives a package swap.

Thin controller, idempotent service call

Do not let controllers talk to the model directly if the package exposes a service or facade — but if it does not, wrap it once so you have a single seam to change later.

final class FavoriteController
{
    public function toggle(Product $product, FavoriteService $favorites): JsonResponse
    {
        $isFavorite = $favorites->toggle(auth()->id(), $product->id);

        return response()->json([
            'favorited' => $isFavorite,
            'count'     => $favorites->countFor($product->id),
        ]);
    }
}

Make the operation idempotent by intent: toggle for click handlers, and separate add/remove for API consumers who retry. If the package only offers one of these, implement the other as firstOrCreate / delete on the relation rather than relying on a toggle in a retried job.

Authorization via policy, not middleware

Favourites are per-user; enforce ownership in a policy so both the web controller and any JSON API share one gate:

public function delete(User $user, Favorite $favorite): bool
{
    return $favorite->user_id === $user->id;
}

Never accept user_id from the request body. Bind to auth()->id() — or, if the package's config exposes the user model/guard, respect that config rather than hard-coding App\Models\User.

Read-side: a scoped "is favourited" flag for lists

The common day-to-day need is a product listing where each card shows a heart. Avoid an N+1 by loading the set once and mapping in PHP:

$favoriteIds = Favorite::query()
    ->where('user_id', auth()->id())
    ->whereIn('product_id', $products->pluck('id'))
    ->pluck('product_id')
    ->all();

$products->each(fn ($p) => $p->is_favorited = in_array($p->id, $favoriteIds, true));

Do this instead of Product::withExists('favorite') unless you have verified the inverse relation name — the relation name is the number-one source of confusion in undocumented packages. Grep src/ for belongsTo, hasMany, and belongsToMany before guessing.

Frontend integration pattern (Blade + Alpine / Livewire)

The package is backend-only, so the UI is yours. Keep the interaction optimistic and degrade gracefully:

<button
    type="button"
    data-favorite-toggle
    data-url="{{ route('products.favorite.toggle', $product) }}"
    data-favorited="{{ $product->is_favorited ? 'true' : 'false' }}"
    aria-pressed="{{ $product->is_favorited ? 'true' : 'false' }}"
>
    <span class="sr-only">{{ __('products-favorite::messages.toggle') }}</span>
</button>

Add a @csrf meta tag and a fetch handler; flip aria-pressed immediately, roll back on non-2xx. For Livewire, put the toggle in a single action method and call $this->dispatch('$refresh') — but be aware the package's state lives in the DB, so you must re-read it, not cache it in the component.

Events for the rest of the app

If the package fires domain events (grep for dispatch(, Event::, or a src/*/Event directory), hook them for notifications, analytics, or cache busting. If it does not, that is your single best extension point: emit your own event from the service wrapper.

// App\Listeners\ForgetFavoriteCounts
public function handle(ProductFavorited $event): void
{
    Cache::forget("products:{$event->productId}:favorites");
}

Testing pattern

Write your own feature tests rather than relying on the shipped ones — they prove the package's internals, not your integration:

public function test_toggling_creates_and_removes_a_favorite(): void
{
    $user = User::factory()->create();
    $product = Product::factory()->create();

    $this->actingAs($user)->post(route('products.favorite.toggle', $product))->assertOk();
    $this->assertDatabaseHas('product_favorites', ['user_id' => $user->id, 'product_id' => $product->id]);

    $this->actingAs($user)->post(route('products.favorite.toggle', $product))->assertOk();
    $this->assertDatabaseMissing('product_favorites', ['product_id' => $product->id]);
}

Run these in CI alongside --group=products-favorite so an upstream patch that breaks you fails your build, not production.


Gotchas and Tips

PHP 8.4 is a hard floor, not a suggestion. If your app or CI image is on 8.3, you cannot install this at all. Check platform.php in composer.json and the CI matrix before promising the feature.

Zero stars and zero dependents means you are the QA team. There is no issue tracker history, no Stack Overflow corpus, and patch releases 7.4.4 → 7.4.8 whose entire changelog is "v7.4.8". Consequences:

  • Pin the exact version; upgrade by hand, one patch at a time, with your feature tests green before and after.
  • Fork it into your VCS if you plan to modify anything. Do not patch vendor/.
  • Read the diff of src/ between tags (git diff v7.4.4..v7.4.8 -- src) — the release notes will never tell you what changed.

The migration may not be published. Many modular packages load migrations from the provider; others require vendor:publish. Always run php artisan migrate --pretend before migrate in a shared environment, and check whether the table name collides with an existing favorites / wishlists table you already have. If you need a custom table name, publish the config and verify the model actually reads it.

Unique constraint vs. race conditions. Two rapid clicks (or a double-submitted form) can produce a duplicate row or a QueryException on a unique index. Mitigate with firstOrCreate semantics, a unique DB index on (user_id, product_id), and a disabled/aria-busy button while the request is in flight. Confirm the shipped migration has that index — if it does not, add it in your own migration.

Auth guard mismatch. If your app uses a multi-guard setup (admin, api via Sanctum), check whether the package resolves the user from auth()->user() or from a configurable guard. The silent failure mode is favourites saved against guard A and listed against guard B — the user sees an empty wishlist. Add a test with actingAs($user, 'sanctum') if you serve an API.

Orphaned favourites. Unless the migration declares onDelete('cascade') on product_id, deleting a product leaves rows behind, and listing favourites then hits missing models. Check the FK, and if it is missing, either add the cascade yourself or filter with whereHas('product') on read.

Caching counts. A "12 people saved this" badge will hammer the table. Cache per product and invalidate from the add/remove path — but remember invalidation must fire from every write path, including seeders and admin tooling. Centralise writes in one service method for exactly this reason.

Translations are Russian. If the package ships Resources/lang/ru, publish and add en (and your locales) under the same key structure — otherwise you will get raw keys or Russian strings in an otherwise English UI. Run php artisan vendor:publish --tag=...translations if the tag exists; otherwise copy the directory into lang/vendor/.

Lumen / non-standard bootstraps. Service provider auto-discovery depends on extra.laravel.providers in the package's composer.json. If it is absent, register it manually in bootstrap/providers.php (Laravel 11+) or config/app.php, and register the migrations path yourself.

Debugging checklist when a favourite "doesn't stick":

  1. Route::has(...) and CSRF token present — 419 is the most common cause, not the package.
  2. Log::info(auth()->id()) in the controller — null user means the favourite was never attributable.
  3. php artisan tinker → create a row directly via the package model; if that works, the bug is your controller; if not, it's config/migration.
  4. DB::listen(...) or Telescope to see the actual insert/delete SQL and the table it targets.

Extension points to look for before writing your own: a publishable config (table, user model, guard), a bound interface in the service provider you can bind() over in your own provider, and any events. If none of these exist, the pragmatic extension point is your own FavoriteService wrapper — one class, one seam, and the package becomes replaceable in an afternoon.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package