Reusable storefront and checkout components for Nette applications.
- PHP >= 8.3, < 9
- Nette Framework and Composer
- A Dibi database connection
- Node.js and Vite only if you use the optional frontend assets
composer require drago-ex/commerceRun the package migrations with drago-ex/migration:
php vendor/bin/migration db:migrate vendor/drago-ex/commerce/migrationsOptional demo data for development:
php vendor/bin/migration db:migrate vendor/drago-ex/commerce/migrations-demoNever edit migrations that have already run on a shared or production database; add a new migration instead.
- Product listing and detail pages, including product variants and stock.
- Session shopping cart with quantity updates and discount codes.
- Checkout for delivery, payment, customer details, and order confirmation.
- Transactional order creation with atomic stock reservation and order-line snapshots.
- Events for customizing product pricing and reacting to checkout changes.
Register the extension and the phone-number input bridge in your Nette configuration:
extensions:
phoneNumberInput: Nepada\Bridges\PhoneNumberInputDI\PhoneNumberInputExtension
commerce: Drago\Commerce\DI\CommerceExtension
commerce:
currency: CZK
moneyFormat: cs_CZ
moneyFractionDigits: 0
services:
- Drago\Commerce\Domain\Checkout\CheckoutProcess
- Drago\Commerce\Domain\Checkout\CheckoutStepsSet the Nette session expiration to at least one day. Cart, checkout, and discount-code data expire after one day:
session:
expiration: 14 daysAvailable commerce options:
| Option | Default | Description |
|---|---|---|
currency |
EUR |
Currency used for prices |
moneyFormat |
de_DE |
Locale used to format prices |
moneySymbol |
'' |
Optional currency-symbol override |
moneyFractionDigits |
2 |
Number of displayed decimal places |
itemsPerPage |
12 |
Products per page; 0 disables pagination |
defaultRegionCode |
none | Default phone region, e.g. CZ, or ['autoDetect', 'CZ'] to detect and fall back |
allowedRegionPhoneNumber |
any | One or more allowed phone-region codes |
countries |
[CZ, SK] |
Delivery countries offered in the customer form, ISO 3166-1 alpha-2 codes; the first one is preselected unless GeoLite2 finds another of them |
validatePostCode |
true |
Validate and format the postal code for the chosen country (countries brick/postcode does not know keep it as written) |
vatPayer |
false |
The shop is a VAT payer: prices include VAT ("incl. VAT" at the price) and an order line keeps the VAT rate of its product |
vatRates |
[21, 12, 0] |
VAT rates in percent offered for a product; the first one is the default; a configured list replaces it |
geoLite2Path |
none | Path to a GeoLite2 City database for detecting the phone region and the country |
Add the CommerceControl trait and create factories for the controls you use. The trait injects the controls and configures checkout navigation.
use Drago\Commerce\UI\CommerceControl;
use Drago\Commerce\UI\Product\ProductDetailControl;
use Drago\Commerce\UI\ShoppingCart\MiniCartControl;
use Drago\Commerce\Domain\Checkout\CheckoutProcess;
final class ShopPresenter extends BasePresenter
{
use CommerceControl;
public function __construct(
private CheckoutProcess $checkoutProcess,
) {
parent::__construct();
}
protected function createComponentMiniCart(): MiniCartControl
{
$control = $this->miniCartControl;
$control->translator = $this->getTranslator();
return $control;
}
}Create a component factory only for controls you want to display. Each factory returns the matching property injected by CommerceControl:
- Storefront:
createComponentProduct()for the product listing andcreateComponentProductDetail()for a product detail page. Set the product ID on the detail control before returning it. - Cart:
createComponentMiniCart()for the cart link andcreateComponentShoppingCart()for the cart page. - Checkout:
createComponentDelivery(),createComponentCustomer(), andcreateComponentSummaryOrder()for delivery and payment, customer details, and final order review.
When you use translations, set the control's translator in its factory. The listing links a product detail as detail with the slug parameter (the address of the product); the detail factory selects the product by it (an unknown slug is a 404):
protected function createComponentProductDetail(): ProductDetailControl
{
$control = $this->productDetailControl;
$control->setProductSlug((string) $this->getParameter('slug'));
$control->translator = $this->getTranslator();
return $control;
}Render the controls in your Latte templates. Put the mini cart in the layout and wrap controls in snippets when they should update with Naja:
{snippet cart}
{control miniCart}
{/snippet}
{control product}
{control productDetail}
{snippet shoppingCart}
{control shoppingCart}
{/snippet}
{snippet delivery}
{control delivery}
{/snippet}
{snippet customer}
{control customer}
{/snippet}
{snippet summaryOrder}
{control summaryOrder}
{/snippet}Product descriptions are written in Texy: the detail prints them as HTML made safe by
TexyFormatter of drago-ex/texy-toolbar, the listing shows the beginning
of their text. The formatter comes from the texy extension of that package when the project registers it,
otherwise commerce registers it alone.
The order-completion page is provided by the application as the done action. Each control supports a custom template file through templateControl:
$control->templateControl = __DIR__ . '/templates/Delivery/custom.latte';A custom order-summary template must include the hidden orderToken field in the sendOrder form so checkout can verify that the customer is confirming the displayed order:
<form n:name="sendOrder">
<input n:name="orderToken">
<input n:name="send">
</form>The default checkout actions are default, shoppingCart, delivery, customer, summary, and done. Register CheckoutProcess and use it in startup() to redirect customers to the first checkout step whose requirements are not met:
public function startup(): void
{
parent::startup();
$target = $this->checkoutProcess->getRedirectTargetForAction($this->getAction());
if ($target !== null && $target !== $this->getAction()) {
$this->redirect($target);
}
}To use different presenter action names, replace the default CheckoutSteps and CheckoutProcess service entries with named definitions. The action names must match the actions in your presenter:
services:
checkoutSteps:
factory: Drago\Commerce\Domain\Checkout\CheckoutSteps
arguments:
- {delivery: shipping, customer: billing}
checkoutProcess:
factory: Drago\Commerce\Domain\Checkout\CheckoutProcess
arguments:
- @Drago\Commerce\Service\ShoppingCartSession
- @Drago\Commerce\Service\OrderSession
- @checkoutStepsProducts with active variants must be purchased through a variant. A variant can use the product price and discount, or have its own price; variant-specific prices do not receive the product discount. Checkout refreshes catalog and delivery prices before order placement and asks the customer to review changes.
A placed order is new. OrderStatusService changes its state along OrderStatus, and its payment separately:
| State | Can change to |
|---|---|
new |
processing, cancelled |
processing |
shipped, cancelled |
shipped |
completed |
completed, cancelled |
— |
$orderStatusService->change($orderId, OrderStatus::Shipped);
$orderStatusService->markPaid($orderId); // paid_at, false when already paidA change that is not allowed throws OrderStatusException. Cancelling returns the ordered items to stock and the use of the discount code. Whether an order is paid is kept apart from its state (paid_at), as a bank transfer is paid before shipping and cash on delivery after it.
Register callable listeners on the EventDispatcher service to customize behavior:
ProductAddedToCart— adjust a product's price before it is added or repriced.CartItemChangedandCartItemRemoved— react to cart updates.CustomerUpdatedandDeliveryOptionsChanged— react to checkout changes.OrderPlaced— react after the order has been saved.OrderStatusChanged— react after the state of an order has changed (e.g. send a shipping email).
Listeners run synchronously. An OrderPlaced listener failure is logged and does not undo the saved order.
Optional. Install nette/mail, configure its mailer and set commerce.orderEmail.from to enable it:
composer require nette/mailcommerce:
orderEmail:
from: 'Shop <shop@example.com>'
mailer: mail.mailer # service implementing Nette\Mail\Mailer
storeName: 'My Shop'
storeEmail: support@example.com
# templateFile: %appDir%/Mail/order-email.lattetemplateFile points to a custom Latte template of type Drago\Commerce\Mail\OrderEmailTemplate. Texts go through the configured translator; drago-ex/translator finds the bundled translations automatically, other translators can load src/Commerce. The language is taken from the presenter's lang parameter and passed to the translator's setTranslate() when available.
The email is sent synchronously; a failure is logged and does not affect the order. Each order stores the carrier and payment names as they were when it was placed.
The package works without them. Set them up by hand as described below when the project needs them.
Add the package to your package.json, then import its assets and initialize the Naja integration:
{
"type": "module",
"dependencies": {
"drago-commerce": "file:vendor/drago-ex/commerce"
}
}import naja from 'naja';
import Commerce from 'drago-commerce';
import 'drago-commerce/styles';
naja.initialize();
new Commerce().initialize(naja);