Variants, SKUs & options
Every sellable unit is a ProductVariant with its own sku, price, currency, tax class and stock. Every product gets one default variant when it is created — a real, sellable variant, not a placeholder that later variants replace. Set it up rather than leaving it at zero: price it for a simple product, or make it your first option variant and add the others next to it:
use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Shops\Products\ProductVariant;
// The price cast writes the currency column from the Money.
$small = $product->defaultVariant; // set up the default variant rather than leaving it at zero
$small->update(['sku' => 'WATER-0.5L', 'price' => Money::ofMinor(199, 'EUR'), 'stock' => 50]);
$large = $product->variants()->create([
'sku' => 'WATER-1L', 'price' => Money::ofMinor(299, 'EUR'), 'stock' => 30, 'position' => 1,
]);
$product->price; // 1.99 EUR — the default (lowest-position) variant
// Re-pricing in another currency: set currency BEFORE price — the cast refuses
// to silently re-denominate a column that already holds another code.
$small->update(['currency' => 'USD', 'price' => Money::ofMinor(219, 'USD')]);
$product->defaultVariant; // the lowest-position variant (WATER-0.5L)
$small->inStock(10); // bool — respects track_stock and the reserved quantity
$small->availableStock(); // stock - reserved
ProductVariant::query()->inStock(2); // untracked, or at least 2 availableThe default variant
Every product gets one on created: SKU from the fallback-locale slug plus -DEFAULT (e.g. SPARKLING-WATER-DEFAULT), a zero price in the product’s shop currency (or the configured default), tax class standard, stock tracking on and zero stock. $product->defaultVariant and $product->price read the lowest-position variant, which is this one unless you reorder. Set its price and receive stock before selling it. SKUs are unique per product.
Options
Options (Size, Colour) and their values model the axes of variation; a variant is the combination of one value per option. Resolve a variant from a set of option values:
use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Shops\Products\Product;
$shirt = Product::create(['name' => 'T-Shirt']);
$size = $shirt->options()->create(['name' => 'Size', 'position' => 0]);
$small = $size->values()->create(['value' => 'S', 'position' => 0]);
$large = $size->values()->create(['value' => 'L', 'position' => 1]);
// The default variant is a real, sellable variant — make it the first option variant:
$s = $shirt->defaultVariant;
$s->update(['sku' => 'TS-S', 'price' => Money::ofMinor(1990, 'EUR')]);
$s->optionValues()->attach($small);
$l = $shirt->variants()->create([
'sku' => 'TS-L', 'price' => Money::ofMinor(1990, 'EUR'), 'position' => 1,
]);
$l->optionValues()->attach($large);
$shirt->variantFor([$small->id]); // $s — exactly these option-value ids, or null$option->product(); // BelongsTo<Product>
$option->values(); // HasMany<ProductOptionValue>, ordered by position
$value->option(); // BelongsTo<ProductOption>
$value->variants(); // BelongsToMany<ProductVariant>
$variant->product(); // BelongsTo<Product>
$variant->optionValues(); // BelongsToMany<ProductOptionValue>Options define what can be bought. For descriptive, filterable data such as material or weight, use the spec sheet (attributes) instead.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.