vanilo/checkout
Vanilo Checkout provides a flexible, configurable checkout flow for Laravel ecommerce applications, converting carts into orders through steps for customer details, shipping, payment, and confirmation. It integrates with the Vanilo suite and can be customized to fit your store.
Vanilo Checkout is the step-flow engine that sits between a vanilo/cart cart and a vanilo/order order. In a normal Vanilo app it arrives as a transitive dependency of vanilo/framework, so you rarely install it directly:
composer require vanilo/checkout # only if you're assembling modules yourself
php artisan vendor:publish --tag=vanilo-checkout
Where to look first, in order:
config/checkout.php — this is the source of truth for the flow. It defines the ordered steps array and the driver/implementation used to resolve the checkout. If your flow "does the wrong thing," it's almost always here.src/Contracts/Checkout.php and src/Contracts/CheckoutManager.php — the two interfaces you'll code against. The Checkout object is the state bag (cart, customer, shipping/billing address, shipping method, payment method); the CheckoutManager is the flow controller (current/next/previous step, proceed/back).src/Http/ (or the default CheckoutController) and the published views — a working reference implementation of each step.Vanilo\Checkout\Facades\Checkout facade and the checkout middleware group — how routes reach the flow.First use case: wire the default flow end-to-end. Register the checkout routes (the module ships a service provider that can publish/bind them), add a Checkout step controller per route, and load a cart into the checkout before rendering step one:
use Vanilo\Checkout\Facades\Checkout;
Checkout::setCart($cart); // Cart from vanilo/cart
return view(Checkout::getCurrentStep()->getView());
Once you can walk cart → address → shipping → payment → confirm and call Checkout::createOrder() on the last step, you understand 80% of the package.
Step-per-route (the intended pattern). Each step is a GET that renders a view and a POST that validates input, mutates the Checkout state, and calls the manager's proceed()/back(). The manager refuses to advance if a step's guard fails (e.g., no shipping method selected), so keep the step objects as the single place for "is this step satisfied?" logic instead of duplicating it in controllers.
Custom steps. Add your own step by implementing CheckoutStep (or extending the provided abstract step) and registering it in the steps array in config/checkout.php. A typical custom step is "gift message," "age verification," or "delivery date picker." Keep the step class thin: read from the Checkout bag, validate, and stash the result on the session-backed checkout. Then the rest of the flow treats it like any other step.
Cart injection at the boundary. The checkout does not own the cart. Always resolve the cart from vanilo/cart (usually the session/current cart) and setCart() at the start of the flow. If you have multiple carts (e.g., a "buy now" vs. "basket"), enforce a single active cart before entering checkout, otherwise the order created at the end won't match what the user sees.
Order creation is the hand-off point. The last step calls Checkout::createOrder(), which returns a vanilo/order Order. From there you're in vanilo/payment / gateway territory: initiate payment, attach the transaction to the order, and only then show a confirmation. Treat createOrder() as a commit — everything upstream (addresses, methods, amounts) is frozen into the order at that moment.
Headless / API flows. Because the Checkout contract is interface-driven, you can build JSON endpoints that mirror the web steps without touching views: one endpoint per step, returning the step's serialized state and validation errors. Bind your own implementation of the Checkout contract (a non-session-backed one, e.g., keyed by a token) in a service provider if you need stateless operation.
Testing. The checkout is easiest to test by resolving the contract and driving it directly:
$checkout = app(\Vanilo\Checkout\Contracts\Checkout::class);
$checkout->setCart($fakeCart);
$checkout->setShippingAddress($address);
// assert step guards / createOrder behavior
Prefer asserting on the manager's step progression and on the resulting order, not on HTTP view output.
Checkout implementation bound in the container.setCart() expects a vanilo/cart cart. Passing an arbitrary Eloquent model or a "wishlist" object will blow up later when the order tries to read line items. Validate the type at the controller boundary.config/checkout.php steps. The manager reads the steps array at boot. If you clear the config cache after editing steps, remember to re-run php artisan config:cache in production — cached config can silently keep an old step order or a removed step.billingAddress, otherwise you'll ship an order with a stale address.createOrder() is not inherently idempotent. Guard the confirm step with a token, redirect-after-post, or an order-existence check keyed on the checkout identity. Users will double-click "Place order."vanilo/checkout is a small, low-star internal module — fork risk is low but you should still extend via the contracts (Checkout, CheckoutManager, CheckoutStep) and container bindings rather than editing vendor/. When it breaks, reading src/ is faster than searching docs; the docs for this submodule are thin.proceed() "does nothing," log the current step object and its validation result — the manager silently refuses to advance on a failed guard. Instrument the step class rather than the controller to find the missing state.vanilo/order is a common cause of createOrder() returning unexpected types.How can I help you explore Laravel packages today?