conedevelopment/bazar
Bazar is a Laravel e-commerce package that gives you products, categories, carts, orders, payments, and coupons on a flexible database-driven foundation, making it easier to build and customize online stores quickly.
Bazar is a headless domain layer, not a storefront. It gives you Eloquent models, contracts, a cart manager, and a checkout manager. You bring the routes, controllers, views, and payment integration.
composer require conedevelopment/bazar
php artisan vendor:publish --tag=bazar
php artisan migrate
Publish both the config and migrations (tags may differ slightly by version — run php artisan vendor:publish and pick the Bazar provider). Treat the published migrations as your schema; edit them freely before you have data.
Read these files first, in order
config/bazar.php — currency, model bindings, cart/checkout settings. This is the package's control panel.src/Contracts/ — Buyable, Sellable, Cart, Order, etc. These tell you what the package expects from your models.src/Models/ — Product, Variant, Price, Cart, CartItem, Order, OrderItem. This is where the behavior lives.src/Cart/ and src/Checkout/ — the two managers you'll call from your app.database/migrations/ — the actual columns. Read them; they resolve most API questions.First use case: product → cart → order
use Bazar\Models\Product;
use Bazar\Facades\Cart;
use Bazar\Facades\Checkout;
// 1. Catalog: product with a variant and a price
$product = Product::create([
'name' => 'T-Shirt',
'slug' => 't-shirt',
]);
$variant = $product->variants()->create(['name' => 'Large']);
$product->prices()->create(['price' => 2500]); // verify unit in config/migration
// or attach to the variant if your catalog prices at variant level
// 2. Cart (session-backed by default)
Cart::add($variant, 2);
$cart = Cart::get();
$total = Cart::total();
// 3. Checkout: cart → order
$order = Checkout::create($cart, auth()->user(), [
'billing_address' => [...],
'shipping_address' => [...],
]);
The key mental model: Product is buyable, Variant is sellable, Price is polymorphic. Your cart line points at a purchasable entity, and the order copies that data.
Custom models via config, not forks. The intended extension path is to subclass Bazar\Models\Product (or any model), then point the corresponding key in config/bazar.php at your class. Keep the table names identical unless you also edit migrations. This is how you add media, categories, SEO fields, or custom scopes without fighting the package.
namespace App\Models;
class Product extends \Bazar\Models\Product
{
public function media() { return $this->morphMany(Media::class, 'mediable'); }
}
// config/bazar.php
'models' => [
'product' => App\Models\Product::class,
// ...
],
Catalog modeling. Model variants as the sellable unit whenever you have sizes/colors; use product-level prices only for single-SKU items. Prices are rows, not columns — that's what makes multiple currencies and sale prices possible. Add a compare_at_price or sale_price column to the prices migration if you need strikethrough pricing, and compute it in a presenter rather than mutating price.
Cart lifecycle. The cart is session-scoped by default. The pattern that works well:
Login/Registered, find the guest cart, merge items into the user's cart, then delete the guest cart. Do this in a queued listener so you don't slow down auth.total column. Read Cart::total() / the cart model's accessors. If you cache totals, invalidate on every item mutation.Checkout as an orchestrated pipeline. Bazar creates the order and fires events; it does not charge cards. The reliable flow is:
validate cart → Checkout::create(...) → redirect to payment provider
→ webhook hits your controller → verify signature → mark order paid → clear cart
Hook into CheckoutStarted / CheckoutCompleted (and any order events your version exposes) for side effects: inventory decrement, confirmation email, CRM sync, coupon redemption. Queue all of them — these events fire inside the request.
Order status and fulfillment. Keep status transitions in your own service/action class rather than scattering $order->update([...]) in controllers. Listeners should be idempotent, because payment webhooks retry. Store the payment provider's transaction ID on the order (add a column) and guard on it.
Headless integration. Wrap Bazar in thin API controllers returning JSON resources. Bind routes by slug for products, by cart ID for carts, and require auth for orders. If you're building a SPA, the cart endpoints are the only stateful part — everything else is CRUD over your catalog.
Testing. Use RefreshDatabase, create products/variants/prices via factories, and drive Cart/Checkout directly in feature tests. Test the cart merge-on-login listener and the webhook idempotency path explicitly — those are the two places bugs hide.
config/bazar.php and the prices migration: packages in this space differ between integer minor units (cents) and decimals. Getting this wrong silently makes every product 100× too expensive. Centralize formatting in one helper.Price rows plus a currency-aware cart/checkout resolver — plan for it early rather than retrofitting.CheckoutCompleted run inside the checkout request. Implement ShouldQueue and make them idempotent; a slow mailer can time out a checkout.updateOrCreate (or increment) a single line, not create duplicates. If you build custom "add to cart" logic, enforce a unique index on (cart_id, purchasable_type, purchasable_id).dd() the cart manager and read src/Cart and src/Checkout — it's a small codebase and very readable.composer.json and read the changelog before composer update, especially around checkout signatures and event names.How can I help you explore Laravel packages today?