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

Simple Commerce Laravel Package

duncanmcclean/simple-commerce

Simple Commerce is a lightweight e-commerce package for Laravel and Statamic. It adds products, carts, checkout, orders, payments, coupons, shipping and tax tools, letting you build a custom store without a heavyweight platform.

View on GitHub
Deep Wiki
Context7

Getting Started

Simple Commerce is a Statamic addon first and a Laravel package second: everything (products, orders, customers, coupons) is a Statamic entry, so your first hour is really about collection design, not e-commerce plumbing.

Minimum viable install:

composer require duncanmcclean/simple-commerce
php artisan simple-commerce:setup   # recent 7.x; otherwise publish config tag manually

If your version doesn't ship the setup command, publish the config (vendor:publish --tag=simple-commerce-config) and create three collections yourself: products, orders, coupons. Orders and products need blueprints matching the fieldsets the package ships with — copy them from the package's resources/blueprints rather than hand-rolling, or the cart will silently refuse to persist line items.

Look here first, in this order:

  1. config/simple-commerce.php — collection handles, currency, gateways, shipping methods, tax rates, notifications, and route registration all live in one file.
  2. src/Contracts/ — PaymentGateway, ShippingMethod, PurchaseRule, TaxEngine. These four interfaces are the entire extension surface; skim them before you write any custom logic.
  3. routes/web.php in the package — the !/simple-commerce/... action endpoints that the Antlers tags post to.

First use case — a working storefront in three templates, using the bundled dummy/manual gateway so you can complete an order before touching Stripe:

{{ sc:products limit="12" }}
  <a href="{{ url }}">{{ title }}</a>
  <form method="POST" action="{{ sc:cart:add }}">
    {{ csrf_field }}
    <input type="hidden" name="product" value="{{ id }}">
    <input type="number" name="quantity" value="1" min="1">
    <button>Add to cart</button>
  </form>
{{ /sc:products }}
{{ sc:cart }}
  {{ items }}
    {{ product:title }} × {{ quantity }}
    <form method="POST" action="{{ sc:cart:remove }}">
      {{ csrf_field }}<input type="hidden" name="item" value="{{ id }}">
      <button>Remove</button>
    </form>
  {{ /items }}
  Total: {{ total }}
{{ /sc:cart }}
{{ sc:cart:checkout redirect="/checkout/thank-you" error_redirect="/checkout" }}
  {{ csrf_field }}
  <input type="email" name="customer[email]" required>
  <input type="text"  name="customer[name]">
  <input type="text"  name="shipping[name]">
  <input type="text"  name="shipping[address1]">
  <input type="text"  name="shipping[city]">
  <input type="text"  name="shipping[postal_code]">
  <select name="gateway">
    {{ sc:gateways }}<option value="{{ handle }}">{{ display }}</option>{{ /gateways }}
  </select>
  <button>Pay</button>
{{ /sc:cart:checkout }}

That's a complete order lifecycle — cart in session, order entry written on checkout, receipt email dispatched. Swap the gateway handle to stripe afterwards; nothing else changes.

Implementation Patterns

Template-driven (the 80% path). The sc: tags are thin wrappers over action routes, so the whole storefront is Blade/Antlers. Keep the cart page hitting {{ sc:cart:update }} with item + quantity hidden inputs for quantity changes rather than rebuilding the cart server-side. Because everything is a session cart, you get free "cart survives navigation" behaviour and zero state management.

Programmatic carts (controllers, APIs, headless). Same objects, no templates:

use DuncanMcClean\SimpleCommerce\Facades\Cart;

$cart = Cart::current();               // session-bound
$cart->add($product, $variant, 2);
$cart->setShippingMethod('standard-post');
$cart->setCustomer($customer);
$cart->setCoupon($coupon);

Use this when you need conditional logic the tags can't express — e.g. adding a bundle by expanding it into multiple add() calls in one request.

Variants. Variants are an array field on the product entry, not separate entries. Price resolution always goes through $product->variant($sku); the cart stores the variant identifier alongside the line item. If you have more than a handful of option combinations, generate the variant array programmatically on product save — hand-editing a 60-row grid in the CP is where data-entry mistakes come from.

Multi-step checkout. Don't try to collect address, shipping method, and payment in one POST. Split it: POST to your own controller to persist shipping[...] fields onto the cart, redirect to a review step, then let {{ sc:cart:checkout }} do the final charge. This keeps the payment POST small and means a failed card doesn't wipe the address.

Post-purchase side effects via events. This is the right integration seam — not gateway overrides:

Event::listen(OrderPaid::class, function (OrderPaid $event) {
    $order = $event->order;            // the Order entry
    // decrement stock, sync to ERP, tag the customer, fire analytics
});

OrderPaymentFailed, OrderRefunded, and PostCheckout round this out. Actions attached to orders in the CP (Mark as Paid, Refund) go through the same events, so a manual refund in the control panel triggers your listener too — that consistency is the package's best design decision.

Custom rules, rates, and gateways. All four are interface implementations registered in config or boot:

class MinimumOrderValue implements PurchaseRule
{
    public function passes($cart): bool
    {
        return $cart->total() >= 2000;   // £20.00, in minor units
    }
}

Purchase rules are the correct home for stock checks, minimum values, and "no shipping to X" guards. Shipping methods implement the ShippingMethod contract — a name(), a cost calculation receiving the cart, and an availability check. Implement PaymentGateway only if you're integrating a provider that isn't Stripe/PayPal; you need purchase(), completePurchase() for offsite redirects, and a webhook() handler.

Gotchas and Tips

Money is integers in minor units. $cart->total() returns 2599, not 25.99. Format at the edge with the package's money helper/modifier, never by dividing in Blade. This is the single most common source of wrong totals in custom code.

The cart is session state — plan for it. Headless or API-driven clients need a session (or you manage carts manually by ID). Guest-cart-to-user-cart merging on login is not free; listen for Laravel's Login event and re-add items to the authenticated cart.

Abandoned carts become order entries. Every cart is backed by a real entry in the orders collection, so a busy store accumulates thousands of cart-status entries in flat files. Two mitigations: (a) schedule a job that deletes/archives stale carts, (b) put orders on Statamic's Eloquent driver rather than the file driver. On a multi-server deployment, file-based orders are a correctness problem, not just an aesthetic one — you need shared storage or the database driver.

Run php artisan stache:refresh after any bulk product import. Products are indexed by the Stache; if your import writes entries directly, the storefront will show stale or missing products until the index rebuilds. In dev, keep the Stache watcher enabled or you'll chase ghosts.

CSRF and webhooks. The !/simple-commerce/... routes expect a CSRF token — {{ csrf_field }} in every hand-written form. Conversely, Stripe/PayPal webhook routes must be excluded from CSRF verification. If you disable the package's route registration to build your own, you own that middleware exclusion; forgetting it produces silent 419s in the Stripe dashboard and un-paid orders that never get marked paid.

Clear the cart after completion. If your PostCheckout or redirect logic doesn't destroy the cart, customers can re-submit and double-order. Verify this explicitly in staging with a successful test payment.

Stock is not enforced out of the box. Nothing prevents over-selling. Add a PurchaseRule that re-reads stock at checkout time (not at add-to-cart time) and decrements in an OrderPaid listener. Do the check inside a transaction/lock if you expect concurrent checkouts on limited inventory.

Testing. Use the dummy gateway in feature tests, Http::fake() for Stripe, and Statamic's PreventSavingStacheItemsToDisk trait so test runs don't pollute content/. Assert on the resulting order entry's status and paid flag rather than on redirect URLs — gateway return paths differ between inline and offsite flows.

Small community, read the source. At ~163 stars there is very little Stack Overflow coverage; issues and the source are your documentation. The flip side is an active release cadence (7.8.10 landed with an email-validation hardening fix), so pin your version and read the upgrade notes — the 7.x line is Statamic 5 only.

Extend, don't fork. Views, config, and mail templates are all publishable; gateway/rule/shipping/tax interfaces cover nearly every customisation. If you find yourself editing vendor/, you've probably missed one of those four contracts.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console