Craft Commerce Online Store Setup
Craft Commerce — commercial plugin for CMS Craft CMS, built on Yii2 PHP framework. Unlike monolithic platforms, Craft Commerce doesn't impose theme or catalog structure: everything is built through flexible field system, sections, and Twig templates. This makes it choice for non-standard catalogs, complex custom attributes, and headless architectures.
Craft Commerce Architecture
Craft Commerce works on top of Craft CMS using:
- Elements — basic data type (Products are Elements with extended fields)
- Field Layout — field constructor without rigid schema
- Twig — templating with full PHP API access
- GraphQL — optional API for headless mode (Craft Pro)
- Queue — Redis/Database queue for background tasks
Key Commerce entities:
Product Types → Products → Variants
↓
Sales / Discounts
↓
Line Items → Order → Transaction
Installation and Setup
# Create project via Craft Starter
composer create-project craftcms/craft mystore
cd mystore
# Install Commerce
composer require craftcms/commerce
# Apply migrations
php craft setup
php craft migrate/all
php craft commerce/install
Configure .env:
CRAFT_ENVIRONMENT=production
SECURITY_KEY=your-256-bit-key
DB_DRIVER=mysql
DB_SERVER=127.0.0.1
DB_PORT=3306
DB_DATABASE=craftstore
DB_USER=craftuser
DB_PASSWORD=securepassword
DB_TABLE_PREFIX=craft_
# Commerce-specific settings
STRIPE_PUBLISHABLE_KEY=pk_live_xxx
STRIPE_SECRET_KEY=sk_live_xxx
Product Types
In Commerce, no single product schema — each Product Type defines own field structure:
// craft/config/project/commerce-productTypes.yaml
productTypes:
clothing:
name: Clothing
handle: clothing
hasVariants: true
hasVariantTitleField: false
titleFormat: '{product.title} — {size.label} / {color.label}'
hasDimensions: true
taxCategory: standard
shippingCategory: standard
variantFieldLayouts:
- fields:
- sku
- price
- stock
Create Product Type programmatically:
use craft\commerce\elements\Product;
use craft\commerce\models\ProductType;
$productType = new ProductType();
$productType->name = 'Clothing';
$productType->handle = 'clothing';
$productType->hasVariants = true;
$productType->hasDimensions = true;
Craft::$app->getPlugins()->getPlugin('commerce')
->getProductTypes()
->saveProductType($productType);
Variants and Attribute Matrix
Craft Commerce supports variants as separate Elements with full field set:
{# templates/shop/product.twig #}
{% set product = craft.products.id(productId).one() %}
<h1>{{ product.title }}</h1>
{# Variant matrix for clothing #}
{% set variantMatrix = {} %}
{% for variant in product.variants %}
{% set size = variant.size.label %}
{% set color = variant.color.label %}
{% set variantMatrix = variantMatrix | merge({
(size): (variantMatrix[size] ?? {}) | merge({
(color): {
id: variant.id,
price: variant.price | commerceCurrency('USD'),
stock: variant.hasUnlimitedStock ? '∞' : variant.stock,
sku: variant.sku,
}
})
}) %}
{% endfor %}
<div x-data="variantSelector({{ variantMatrix | json_encode }})">
{# Alpine.js variant selector component #}
</div>
Cart and Checkout
Craft Commerce uses session for cart. Basic operations:
{# Add to cart #}
<form method="post">
{{ csrfInput() }}
{{ actionInput('commerce/cart/update-cart') }}
{{ hiddenInput('purchasableId', variant.id) }}
{{ hiddenInput('qty', 1) }}
{{ redirectInput('/shop/cart') }}
<button type="submit">Add to cart</button>
</form>
{# Get cart #}
{% set cart = craft.commerce.carts.cart %}
{% for item in cart.lineItems %}
<div>{{ item.purchasable.title }} × {{ item.qty }} = {{ item.subtotal | commerceCurrency }}</div>
{% endfor %}
<strong>Total: {{ cart.totalPrice | commerceCurrency }}</strong>
Checkout controller (PHP):
// Custom checkout controller
namespace modules\store\controllers;
use craft\commerce\Plugin as Commerce;
use craft\web\Controller;
class CheckoutController extends Controller
{
public function actionComplete(): \yii\web\Response
{
$this->requirePostRequest();
$order = Commerce::getInstance()->getCarts()->getCart();
// Add custom data to order
$order->setFieldValue('deliveryNote', $this->request->getBodyParam('deliveryNote'));
if (!Craft::$app->getElements()->saveElement($order)) {
return $this->asFailure('Failed to save order');
}
return $this->redirect('/shop/order-confirmation?number=' . $order->number);
}
}
Payment Gateways
Craft Commerce supports several gateway plugins:
# Stripe
composer require craftcms/commerce-stripe
# PayPal
composer require craftcms/commerce-paypal
# Braintree
composer require craftcms/commerce-braintree
Configure gateway via config/commerce-gateways.php:
return [
'stripe' => [
'type' => \craft\commerce\stripe\gateways\PaymentIntents::class,
'name' => 'Stripe',
'publishableKey' => getenv('STRIPE_PUBLISHABLE_KEY'),
'apiKey' => getenv('STRIPE_SECRET_KEY'),
'webhookSigningSecret' => getenv('STRIPE_WEBHOOK_SECRET'),
'testMode' => getenv('CRAFT_ENVIRONMENT') !== 'production',
],
];
GraphQL API for Headless
Craft Pro + Commerce provides full GraphQL API:
# Query products with variants
query ShopProducts($type: [String], $limit: Int, $offset: Int) {
products(type: $type, limit: $limit, offset: $offset) {
id
title
slug
variants {
id
sku
price
stock
... on clothing_Variant {
size { label value }
color { label value }
}
}
images: featuredImage {
url(width: 800, height: 800, format: "webp")
alt
}
}
}
Promotions and Discounts
Craft Commerce separates Sales (price discounts) and Discounts (coupons and automatic discounts):
use craft\commerce\models\Sale;
$sale = new Sale();
$sale->name = 'Summer Sale';
$sale->description = '20% off entire summer collection';
$sale->apply = Sale::APPLY_BY_PERCENT;
$sale->applyAmount = -0.20; // -20%
$sale->allProducts = false;
$sale->categoryIds = [5, 8]; // Category IDs
$sale->dateFrom = new \DateTime('2024-06-01');
$sale->dateTo = new \DateTime('2024-08-31');
$sale->enabled = true;
Commerce::getInstance()->getSales()->saveSale($sale);
Multi-Currency
Craft Commerce Pro supports multi-currency natively:
// config/commerce.php
return [
'paymentCurrencies' => [
'USD' => ['rate' => 1],
'EUR' => ['rate' => 0.92],
'RUB' => ['rate' => 90.5],
'UAH' => ['rate' => 37.2],
],
];
{# Currency switcher #}
{% set currentCurrency = craft.commerce.paymentCurrencies.primaryPaymentCurrency %}
<form method="post">
{{ csrfInput() }}
{{ actionInput('commerce/payment-currencies/set') }}
<select name="currency" onchange="this.form.submit()">
{% for currency in craft.commerce.paymentCurrencies.allPaymentCurrencies %}
<option value="{{ currency.iso }}" {{ currency.iso == currentCurrency.iso ? 'selected' }}>
{{ currency.iso }} — {{ currency.symbol }}
</option>
{% endfor %}
</select>
</form>
Performance and Caching
Craft CMS uses own Data Cache + Element Query Cache. For product pages:
{# Cache query for 1 hour #}
{% cache globally using key 'product-' ~ product.id for 1 hour %}
{# Expensive query with variants, images, related products #}
{% include '_partials/product-full' with { product: product } %}
{% endcache %}
Redis configuration for Element Cache:
// config/app.php
return [
'components' => [
'cache' => [
'class' => yii\redis\Cache::class,
'redis' => [
'hostname' => '127.0.0.1',
'port' => 6379,
'database' => 1,
],
'defaultDuration' => 3600,
],
],
];
Setup Timeline
- Basic install + 1 Product Type + checkout: 1–2 weeks
- Full store 3–5 product types, discounts, multi-currency: 4–6 weeks
- Headless store (Craft GraphQL + Next.js frontend): 6–10 weeks
- Migration from another platform to Craft Commerce: 4–8 weeks (depends on catalog volume and business logic complexity)







