Skip to content
drago-exPublic

About

🛒 Simple shopping cart.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Drago Commerce

Reusable storefront and checkout components for Nette applications.

License: MIT PHP version Tests Coding Style

Requirements

  • PHP >= 8.3, < 9
  • Nette Framework and Composer
  • A Dibi database connection
  • Node.js and Vite only if you use the optional frontend assets

Installation

composer require drago-ex/commerce

Run the package migrations with drago-ex/migration:

php vendor/bin/migration db:migrate vendor/drago-ex/commerce/migrations

Optional demo data for development:

php vendor/bin/migration db:migrate vendor/drago-ex/commerce/migrations-demo

Never edit migrations that have already run on a shared or production database; add a new migration instead.

What it does

  • 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.

Configuration

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\CheckoutSteps

Set the Nette session expiration to at least one day. Cart, checkout, and discount-code data expire after one day:

session:
	expiration: 14 days

Available 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

Use in a presenter

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 and createComponentProductDetail() for a product detail page. Set the product ID on the detail control before returning it.
  • Cart: createComponentMiniCart() for the cart link and createComponentShoppingCart() for the cart page.
  • Checkout: createComponentDelivery(), createComponentCustomer(), and createComponentSummaryOrder() 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;
}

Templates

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>

Checkout and prices

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
			- @checkoutSteps

Products 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.

Order states

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 paid

A 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.

Events

Register callable listeners on the EventDispatcher service to customize behavior:

  • ProductAddedToCart — adjust a product's price before it is added or repriced.
  • CartItemChanged and CartItemRemoved — react to cart updates.
  • CustomerUpdated and DeliveryOptionsChanged — 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.

Order confirmation email

Optional. Install nette/mail, configure its mailer and set commerce.orderEmail.from to enable it:

composer require nette/mail
commerce:
	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.latte

templateFile 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.


Optional add-ons

The package works without them. Set them up by hand as described below when the project needs them.

Frontend assets

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);

About

🛒 Simple shopping cart.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages