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 Extendable Basket Laravel Package

jord-jd/laravel-extendable-basket

An extendable shopping basket for Laravel. Offers a flexible cart with swappable storage, pluggable item handlers, and customizable pricing and discount rules, letting you tailor basket behaviour to your app without rewriting core logic.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and pin expectations

composer require <vendor>/<package>:^6.1
php artisan vendor:publish --provider="<Vendor>\Basket\BasketServiceProvider"
php artisan migrate

v6.1.0 is the release that makes this package viable on modern stacks: it supports Laravel 5.5 through 13 and drops the runtime floor to PHP 7.1. Before you write any code, confirm your app's actual PHP version — if you're on 8.x everything works, but if you maintain a legacy 7.1/7.2 service you are no longer forced to upgrade PHP to use the cart.

2. Look here first

  • The refreshed README setup + usage sections (explicitly rewritten in this release) — the old docs described the pre-6.1 add() contract.
  • The published migration — 6.1.0 fixed a broken cross-version migration and Artisan command setup, so re-publish and diff rather than keeping a migration you scaffolded from an older tag.
  • Your Basket model and BasketItem model, plus any config keys for model swapping.
  • Relation::morphMap() in AppServiceProvider — relevant now that the package persists morph aliases.

3. First use case: guest cart add

use App\Models\Product;

$product = Product::findOrFail($id);

$item = Basket::add($product, 2);   // BasketItem returned (new in 6.1.0)
$item->quantity;                     // 2

The one behavior you should internalize immediately: you no longer need to save the basket before adding an item. A manually constructed basket is persisted automatically on the first add(). For a session-scoped guest cart that means you can build the basket lazily at the edge of your controller and let the first add commit it.

$basket = new Basket(['user_id' => auth()->id()]);  // not yet in DB
$basket->add($product, 1);                          // now persisted automatically

Implementation Patterns

Trust the return value of add()

add() now returns the created or quantity-merged BasketItem, so stop re-querying to get the row or its quantity:

$item = Basket::add($product, $request->integer('quantity'));

return response()->json([
    'id'       => $item->id,
    'quantity' => $item->quantity,   // already merged if the line existed
]);

This makes "add to cart" JSON endpoints and Livewire/Alpine interactions one-liners instead of add-then-fetch.

Validate persistence before you add

The package now rejects unsaved basketable models with a clear exception. Keep that guard at the boundary of your form request rather than scattering try/catch:

$product = Product::find($request->product_id);   // null if not persisted
abort_unless($product, 404);

Basket::add($product, $qty);

For bulk imports/seeders, wrap the loop in a transaction so a single bad model doesn't leave a half-built basket.

Persist early, reconcile late

Two viable workflows now:

  • Lazy basket: construct unsaved, let the first add() persist (6.1.0 behavior). Best for guest carts and single-page checkout.
  • Eager basket: create and save the basket when the session or user is established, then add items. Best when you need a basket id for analytics, abandoned-cart emails, or merging guest → user carts.

Whichever you pick, make the "merge on login" path idempotent — after the merge, add() will quantity-merge rather than duplicate, which is exactly what you want.

Commit morph aliases before you have data

Because the package stores Laravel morph-map aliases when available, decide your alias table up front and keep it stable:

// AppServiceProvider::boot()
Relation::enforceMorphMap([
    'product'  => \App\Models\Product::class,
    'variant'  => \App\Models\ProductVariant::class,
]);

Treat aliases as a schema. Renaming product to catalog_product later means existing basketable_type rows no longer resolve.

Order placement: snapshot, then clear()

DB::transaction(function () use ($basket) {
    $order = Order::createFromBasket($basket);   // reads items + prices here
    $basket->clear();                            // single DELETE
    return $order;
});

clear() removes every item in one query, which is the right default for "place order" and "logout". Because it's a mass delete, BasketItem model events/observers will not fire — if you rely on a deleted observer for stock release or audit logs, iterate and delete individually instead, or hook the order transaction directly.

Render basket totals defensively

Item pricing is resolved on read, and 6.1.0 introduces a clear error when an orphaned item's price is requested. In views, tolerate it rather than 500ing the cart page:

@foreach ($basket->items as $item)
    @continue(! $item->basketable)   {{-- orphaned polymorphic row --}}
    ...
@endforeach

Or catch the specific exception and drop the line, logging the orphan for cleanup.

Gotchas and Tips

The add() return type is a real API change. Code that ignored the return value is unaffected, but anything written as if ($basket->add($p)) or if ($basket->add($p) === true) will now be truthy on an object — which usually still "works" but silently hides failures. Grep your codebase for ->add( and check comparisons.

Auto-persist changes test expectations. Tests that constructed a basket and asserted $basket->exists === false, or counted Basket rows before an explicit save, will now fail. Add RefreshDatabase assertions around the first add() to lock the new behavior in deliberately.

Unsaved basketable exception is new. If you have a seeder, factory, or admin import that adds freshly make()-ed models, it will now throw instead of silently inserting a broken polymorphic reference. Switch those to create() or save() first.

Morph alias migration is the sneaky upgrade hazard. Rows written before you adopted a morph map contain fully-qualified class names; rows written after contain aliases. Matching on add (and orphan detection) compares stored strings, so a mixed table can produce duplicate lines that look like merge failures. If you already have cart data, backfill basketable_type in one pass before enabling aliases:

DB::table('basket_items')->where('basketable_type', Product::class)
    ->update(['basketable_type' => 'product']);

Orphaned rows are skipped, not removed. 6.1.0 makes the package resilient to deleted basketables (skipped when matching additions, clear error when a price is requested), but it does not clean them up. Schedule a small pruning job that deletes basket_items whose basketable is null, otherwise carts accumulate dead weight and your "items count" badge drifts from reality.

Re-publish the migration if you installed from an earlier tag. "Fix the cross-version test migration and Artisan command setup" means migration/command scaffolding changed. Diff the published stub against what's in your repo; a stale migration is a classic source of "works on SQLite, fails on MySQL" column-type mismatches.

PHP 7.1 floor constrains contributions and copy-pasted examples. Since the package must run on 7.1, don't paste Laravel 11-era syntax (arrow functions, typed properties, constructor promotion) into overrides you intend to upstream. It also means the package itself avoids newer constructs — if you're debugging internals, don't expect modern syntax there.

CI matrix tells you what's actually supported. v6.1.0 replaces Travis/Coveralls with a GitHub Actions matrix spanning Laravel 5.5–13. Use it as the source of truth for compatibility claims, and if you hit an issue on an exotic combination (e.g. Laravel 5.6 + PHP 8), check whether that cell exists in the matrix before filing a bug.

Extension points worth knowing. Prefer model swapping in config over subclassing internals; register the alias map in a service provider rather than per-call; and when you need side effects on removal, prefer explicit domain code (stock release at order placement) over relying on clear()'s mass-delete path.

Debugging checklist for "item didn't merge". 1) Is basketable_type identical between the existing row and the incoming model (alias vs FQCN)? 2) Is the model persisted? 3) Are the two adds hitting the same basket (session/user scope)? 4) Log the raw basketable_type value — it answers the first three questions most of the time.

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.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk