netflex/commerce
Netflex Commerce is a Laravel/PHP package for adding e-commerce capabilities to your app, offering product, cart, and order management with an Eloquent-friendly foundation for building customizable storefronts and checkout flows.
Install the framework, not this repo. netflex/commerce is a read-only subtree split — it exists so Composer can resolve the component, not so you can depend on it or send PRs to it. In a real project you install:
composer require netflex/framework
…and you get netflex/commerce pulled in transitively, version-locked to the framework. If you composer require netflex/commerce directly you'll end up with two sources of truth and a version-skew bug you'll spend a day on. All issues and patches go to the monorepo.
Where to look first, in this order:
src/Commerce directory — that's the actual source of this split, and it's where the class list lives. Bookmark it; you'll read it more than the docs.config/commerce.php (or whatever the framework's publish tag drops) — payment provider map, order status enum, VAT/mva settings, cart/session driver.Order model and its row/line-item class. Almost everything you'll do in a day is "get an order, mutate its rows, transition its status."First use case: render a cart from a session and hand it to a checkout view. Roughly:
// controller
$cart = Order::cart(); // the "unfinished order" bound to the session
return view('checkout.cart', [
'order' => $cart,
'rows' => $cart->rows, // line items w/ product, qty, unit price, VAT
'total' => $cart->total, // check whether this returns minor units
]);
Then add-to-cart, recalculate, and redirect to a payment gateway. That one loop — cart → rows → totals → gateway → callback → order status — is the whole package in miniature. Get it working end-to-end on a staging store before touching VAT rules or discounts.
Set up a sandbox store and a fake/offline payment gateway on day one. Commerce code is almost impossible to reason about statically because prices, VAT, and shipping costs are resolved at runtime from store config plus API data. The feedback loop is the tooling.
Treat the order as an aggregate root, and route all mutation through it. The recurring good pattern in Netflex commerce work is: never touch OrderRow records directly from a controller. Add methods on your own Order subclass or a small service:
$order->addProduct($product, qty: 2);
$order->applyVoucher($code);
$order->recalculate(); // explicit — don't rely on magic recalc on save
This matters because totals, VAT, shipping thresholds, and discount eligibility often cascade — changing one row can change shipping cost and thus the total. Scattered $row->save() calls produce orders whose total disagrees with the sum of their rows, and that's the #1 commerce bug class.
Cart-as-draft-order. Most flow-control questions ("where's the cart?") resolve to "the cart is an order in draft state." Prefer a single repository/service that finds or creates the current cart rather than sprinkling session reads:
class CurrentCart
{
public function get(): Order
{
return Order::firstOrCreateDraft(session()->getId());
}
}
Merging a guest cart into a customer cart at login is then a single method on that class — and you will need it, so build the seam early.
Extend by subclassing and binding, not by editing. Payment gateways, shipping calculators, and (usually) VAT resolvers are designed for inheritance. The pattern:
// config/commerce.php
'payment' => [
'providers' => [
'mypsp' => App\Commerce\MyPspGateway::class,
],
],
Then implement the gateway contract: authorize, capture, refund, and a handleCallback(Request) that is idempotent. Use Laravel's container binding / config map rather than if (class_exists(...)) branching.
Idempotent webhook handler, always. Payment callbacks get retried. Wrap the status transition in a transaction and guard on current state:
DB::transaction(function () use ($order, $payload) {
$order = Order::lockForUpdate()->findOrFail($payload['order_id']);
if ($order->isPaid()) return; // already processed
$order->markPaid($payload['transaction_id']);
});
Use events for the side-effects, keep the order model dumb. Order-placed / order-paid / order-cancelled events are where you hang mail, ERP export, gift-card issuance, and analytics. That keeps the checkout controller readable and gives you a replay path when an integration breaks.
Local dev workflow: php artisan config:clear (or config:cache if you're emulating prod — see gotchas), a seeded test product, and an offline/manual gateway that marks orders paid without a network call. Point the framework at a staging Netflex store, never production, and keep a commerce.php override in config/ that's .env-driven.
Testing pattern: fake the outbound HTTP layer (Http::fake()) and assert on your order state transitions and event dispatches, not on the package's internals. Snapshot the rendered order totals array rather than individual fields — totals are the contract.
This repo is a mirror — pin the framework, not this package. Version constraints here are generated; PRs get closed; issues get lost. If you find yourself debugging an installed copy under vendor/netflex/commerce, remember your fix has to be re-applied against netflex/framework (or better, you subclass it in app/).
Zero stars and zero dependents means zero community surface. There's no StackOverflow corpus for this. Budget time to read the source. Conversely: because it's a first-party framework component rather than a random package, the code quality is generally higher than the star count suggests — don't let the metadata scare you off, but do pin exact versions in composer.lock and read changelogs on every framework bump.
Money is probably integers in minor units. Verify this on day one with a dd() on a total. Floats plus VAT rounding = off-by-one-øre bugs that surface only at scale, and Norwegian mva handling rounds per line, not per order, which quietly breaks if you recalculate totals yourself. Let the package compute; you display.
VAT/mva and currency are the sharp edges. Prices may be stored inclusive or exclusive of tax depending on store settings, and the conversion point is easy to get wrong. Always check whether a price accessor returns gross or net before feeding it to a gateway. Test with a mixed-VAT basket (domestic + cross-border) early, not the week before launch.
config:cache in production will bite you. Anything that reads config at boot — provider maps, status enums — is frozen. Ship config changes through a deploy that re-runs php artisan config:cache, and never let an admin UI mutate config/commerce.php at runtime. If you need per-store dynamic settings, put them in the DB and read them through a service.
Order mutations often persist remotely, not locally. If the order is backed by the Netflex API, save() is a network call. Consequences: no transactions across it, watch out for N+1 when iterating rows in a Blade loop (eager-load or pre-fetch rows into arrays), and treat every save as fallible — wrap checkout in try/catch and surface a recoverable error rather than a 500.
Session and cookie lifetimes kill carts. Cart abandonment that looks like a conversion problem is often a session-driver problem (or SESSION_LIFETIME shorter than your "remember my cart" promise). Persist the cart key in a long-lived cookie and reconcile on return.
Debugging checklist when totals look wrong: (1) is the order recalculate()d after the last mutation? (2) are discount rows applied before or after VAT? (3) did shipping cost change when a row was removed? (4) is a stale cached order object in the container/session being read? Clearing the cart and re-adding is a fast way to distinguish state corruption from a calculation bug.
Status transitions aren't always guarded. Don't assume markShipped() refuses a cancelled order. Add your own state-machine assertions in a service layer — cheap, and it prevents the "shipped an order that was refunded" support ticket.
Extension point to reach for first: the gateway contract. It's the best-documented seam by necessity (every PSP integration uses it), so it's your template for how the package expects you to extend anything else — subclass, bind in config, implement a contract, keep the package frozen.
How can I help you explore Laravel packages today?