drago-ex / commerce
Simple shopping cart.
Requires
- php: >=8.3 <9
- ext-intl: *
- brick/money: ^0.5
- brick/postcode: ^0.2
- drago-ex/application: ^2.0
- drago-ex/database: ^3.0
- drago-ex/form: ^2.0
- latte/latte: ^3.0
- nepada/phone-number-input: ^1.0
- nette/application: ^3.1
- nette/di: ^3.0
Requires (Dev)
- geoip2/geoip2: ^2.12
- nette/tester: ^2.3
- phpstan/phpstan-nette: ^2.0
- tracy/tracy: ^2.7
Suggests
- drago-ex/translator: Configures Latte translation support for bundled templates using translation macros.
- geoip2/geoip2: Enables automatic phone-region detection from the visitor's IP via a MaxMind GeoLite2 City database you provide yourself (see README, 'geoLite2Path' config option).
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-18 10:49:58 UTC
README
Simple shopping cart.
Requirements
- PHP >= 8.3
- Nette Framework
- Composer
Installation
composer require drago-ex/commerce
Frontend Assets
Add the Composer package as a local npm dependency:
{
"type": "module",
"dependencies": {
"drago-commerce": "file:vendor/drago-ex/commerce"
}
}
Install JavaScript dependencies:
npm install
Import the Commerce behavior and styles in your Vite entry point:
import naja from 'naja'; import Commerce from 'drago-commerce'; import 'drago-commerce/styles'; naja.initialize(); new Commerce().initialize(naja);
The default integration submits cart quantity changes through Naja and shows a loading spinner during AJAX requests.
Extension Registration
In your config.neon file, register the extension:
extensions: - Nepada\Bridges\PhoneNumberInputDI\PhoneNumberInputExtension commerce: Drago\Commerce\DI\CommerceExtension
Configure Commerce Settings
Still in config.neon, configure the basic commerce settings:
commerce: currency: CZK moneyFormat: cs_CZ moneySymbol: '' moneyFractionDigits: 0 defaultRegionCode: ['autoDetect', 'CZ'] allowedRegionPhoneNumber: CZ postCodeOnRegionPhone: true # geoLite2Path: %appDir%/../data/GeoLite2-City.mmdb
Use Commerce Trait in Your Presenter
Add the CommerceControl trait to your presenter for easy integration of commerce components:
use Drago\Commerce\UI\CommerceControl; class CommercePresenter extends Nette\Application\UI\Presenter { use CommerceControl; // other code }
The trait injects the six commerce controls ($this->deliveryControl, $this->customerControl, $this->summaryOrderControl, $this->shoppingCartControl, $this->miniCartControl, $this->productControl) and configures each checkout-step control for its place in the flow: the step map, completed steps, current step, and the redirect target for the next step. Each control always represents the same fixed step — a DeliveryControl is always the delivery step — so this configuration doesn't need repeating per presenter; a presenter only sets what genuinely varies per use, such as the translator. See "Example Presenter" below.
Inject CheckoutProcess Service
public function __construct( private readonly CheckoutProcess $checkoutProcess ) { parent::__construct(); }
CheckoutProcess is used directly in the presenter for the startup() guard shown below (see "Example Presenter").
Example Presenter
A presenter using the full checkout flow looks like this:
<?php declare(strict_types=1); namespace App\Presentation\Front\Home; use App\Presentation\BasePresenter; use Drago\Commerce\Domain\Checkout\CheckoutProcess; use Drago\Commerce\UI\CommerceControl; use Drago\Commerce\UI\Order\CustomerControl; use Drago\Commerce\UI\Order\DeliveryControl; use Drago\Commerce\UI\Order\SummaryOrderControl; use Drago\Commerce\UI\Product\ProductControl; use Drago\Commerce\UI\ShoppingCart\MiniCartControl; use Drago\Commerce\UI\ShoppingCart\SummaryCartControl; /** @property-read HomeTemplate $template */ final class CommercePresenter extends BasePresenter { use CommerceControl; public function __construct( protected CheckoutProcess $checkoutProcess, ) { parent::__construct(); } // Guards every action in this presenter against being opened directly // (e.g. from a bookmark or back button) when its prerequisites aren't // met. startup() runs before every action, so one override covers the // whole flow. public function startup(): void { parent::startup(); $target = $this->checkoutProcess->getRedirectTargetForAction($this->getAction()); if ($target !== null && $target !== $this->getAction()) { $this->redirect($target); } } // Cart icon/count shown in the layout (e.g. navbar). protected function createComponentMiniCart(): MiniCartControl { $control = $this->miniCartControl; $control->translator = $this->getTranslator(); return $control; } // Product listing — the entry point of the flow. protected function createComponentProduct(): ProductControl { return $this->productControl; } // Cart contents + discount code form. protected function createComponentShoppingCart(): SummaryCartControl { $control = $this->shoppingCartControl; $control->translator = $this->getTranslator(); return $control; } // Carrier + payment method selection. protected function createComponentDelivery(): DeliveryControl { $control = $this->deliveryControl; $control->translator = $this->getTranslator(); return $control; } // Contact + billing form. protected function createComponentCustomer(): CustomerControl { $control = $this->customerControl; $control->translator = $this->getTranslator(); return $control; } // Final review + "place order" button. protected function createComponentSummaryOrder(): SummaryOrderControl { $control = $this->summaryOrderControl; $control->translator = $this->getTranslator(); return $control; } }
translator is optional — set it if you're using drago-ex/translator (or your own Translator implementation) so the bundled templates render translated labels instead of the English defaults.
How a checkout-step control gets configured
CommerceControl::injectCommerceControl() configures each checkout-step control via CheckoutProcess::configureStep():
public function configureStep(BaseControl $control, string $step): BaseControl { $control->setSteps($this->getSteps()); $control->setCompletedSteps($this->getCompletedSteps()); $control->setCurrentStep($step); if ($next = $this->getNextStep($step)) { $control->setLinkRedirectTarget($next); } return $control; }
getNextStep() reads the checkout flow's order from CheckoutSteps, so renaming a step via the customSteps constructor argument (see "Customize Checkout Steps" below) is reflected automatically.
This runs in injectCommerceControl(), which Nette calls right after the presenter is constructed, before startup(). getCompletedSteps() only reads session state (cart contents, the in-progress order) and never the current action, so this is safe regardless of which action is being handled.
If a control needs to represent a different step than its default (e.g. reusing DeliveryControl in a custom flow), call $this->checkoutProcess->configureStep($this->deliveryControl, 'yourStep') in your own presenter — it's a plain method call, safe to call again.
Why the startup() guard exists
CheckoutProcess::getRedirectTargetForAction() checks the actual session state (cart contents, chosen carrier, filled-in customer) against the step being requested, and returns where to send the visitor instead — e.g. someone opening /customer directly with an empty cart gets redirected back to the product listing rather than seeing a broken form.
Calling it from startup() covers every action in the presenter with one override: for any action other than delivery/customer/summary, the resolver's match falls through to default => null — a safe no-op — so nothing needs to be added for other actions such as the product listing or the shopping cart.
Optional Custom Template
Each control/component has a public property called templateControl that lets you specify a custom template file for rendering. Use this if you want to customize the look or layout of the component.
Here's a simple example showing how to set a custom template in the component factory method:
protected function createComponentDelivery(): DeliveryControl { $control = $this->deliveryControl; // Optional: override the default template file $control->templateControl = __DIR__ . '/templates/Delivery/customTemplate.latte'; // Additional setup like steps, current step, etc. $control->setSteps($this->checkoutProcess->getSteps()); // ... return $control; }
Latte Templates
1. Layout (@layout.latte)
Include the mini cart widget in your navbar / header:
{snippet cart} {control miniCart} {/snippet}
2. Product Catalog (default.latte)
{block content} {control product} {/block}
3. Shopping Cart (shoppingCart.latte)
{block content} {snippet shoppingCart} {control shoppingCart} {/snippet} {/block}
4. Shipping & Payment (delivery.latte)
{block content} {snippet delivery} {control delivery} {/snippet} {/block}
5. Customer Details (customer.latte)
{block content} {control customer} {/block}
6. Order Summary (summary.latte)
{block content} {snippet summaryOrder} {control summaryOrder} {/snippet} {/block}
7. Order Confirmation (done.latte)
{block content} <h1>{_'Order completed'}</h1> <p class="alert alert-success">{_'Thank you, your order has been successfully submitted.'}</p> <a n:href="default" class="btn btn-primary">{_'Back to the menu'}</a> {/block}
Register Services
Register the checkout services so Nette DI can create and wire the checkout flow.
The minimal registration below is enough when you keep the default step names and templates; Nette will autowire required dependencies (ShoppingCartSession, OrderSession) into CheckoutProcess.
services: - Drago\Commerce\Domain\Checkout\CheckoutProcess - Drago\Commerce\Domain\Checkout\CheckoutSteps
If you want to override step names or provide a custom CheckoutSteps instance (for localization, branding, or per-step template mapping), use the explicit service configuration shown in the "Customize Checkout Steps (Optional)" section.
Customize Checkout Steps (Optional)
If you want to rename the default checkout steps or add custom ones, you can configure your own instance of CheckoutSteps via the service container and pass it to CheckoutProcess. This gives you full control over step naming (e.g. for localization, branding, or structural changes).
Example configuration in neon:
services: # Register CheckoutSteps with custom step keys checkoutSteps: factory: Drago\Commerce\Domain\Checkout\CheckoutSteps arguments: - # Custom step names (you can omit or override only selected ones) products: 'products' delivery: 'shipping' customer: 'billing' summary: 'summary' shoppingCart: 'shoppingCart' orderDone: 'done' # Register CheckoutProcess with dependencies injected checkoutProcess: factory: Drago\Commerce\Domain\Checkout\CheckoutProcess arguments: - @Drago\Commerce\Service\ShoppingCartSession - @Drago\Commerce\Service\OrderSession - @checkoutSteps
Summary
This way you have a fully configured commerce module ready for extension and use in your Nette application.