How Variants Work
Shopper uses an attribute-based variant system. A product withtype = ProductType::Variant acts as a parent that holds the shared description, categories, and brand. The parent delegates pricing and stock to its child variants.
The relationship works like this:
A variant’s identity is the combination of its attribute values. The variant “Blue / Large” is defined by two values: Color = Blue and Size = Large. Shopper stores this through a pivot table that links variants to attribute values, so the system can look up which variant matches a customer’s selection.
The parent product itself has no price or stock when it uses variants. All pricing and inventory queries go through the variants.
Model
The model used isShopper\Models\ProductVariant, which extends Shopper\Core\Models\ProductVariant. The core model provides the business logic, relationships, stock management, and pricing. The admin model adds media collections and conversions through Spatie MediaLibrary.
The core model implements Shopper\Core\Contracts\Priceable and Shopper\Core\Models\Contracts\ProductVariant, and uses the following traits:
Extending the Model
To add custom behavior, extend the admin model and update your configuration:config/shopper/models.php:
Database Schema
Product Variants Table
The
sku column uses a composite unique constraint on [product_id, sku]. Two variants on different products can share the same SKU, but variants within the same product must have distinct SKUs.
Attribute Value Pivot Table
This pivot table connects variants to their attribute values, defining which combination of options each variant represents.Relationships
Product
Each variant belongs to a parent product. This is the inverse of theProduct::variants() relationship.
Attribute Values
Thevalues() relationship defines the variant’s identity. A variant named “Blue / Large” has two attribute values attached: one for Color (Blue) and one for Size (Large). This is how the system matches a customer’s selection to a specific variant.
Stock Management
Variants use theHasStock trait for inventory management. Stock is not stored as a column on the variant. Every stock change creates a record in the inventory_histories table, and the current stock is computed as the sum of all mutations. This gives you a complete audit trail of every stock movement.
Every stock mutation is tied to an inventory location (warehouse, store, fulfillment center), so you must always provide an $inventoryId.
Querying Stock
To get the current total stock across all locations:Modifying Stock
To increase stock (for example, when receiving a shipment):Backorder
Theallow_backorder flag indicates whether customers can purchase this variant when it is out of stock. The inStock() method does not check this flag. It only checks whether quantity is available. Your storefront logic should combine both:
Avoiding N+1 Queries
When displaying a product page with multiple variants, accessing$variant->stock on each variant triggers an individual query. Use loadCurrentStock() to batch-load stock in a single query:
$variant->stock without prior batch-loading throws a LazyStockLoadingException with a clear message telling you to use loadCurrentStock().
Pricing
Variants use theHasPrices trait for multi-currency pricing. Each variant can have one price per currency, stored in the prices table through a polymorphic relationship.
To get the price for the store’s default currency:
Price model contains three amount fields:
Media
Variants support two media collections through Spatie MediaLibrary, using the same config-driven collection names as products. This lets you show a different photo when a customer selects “Blue” versus “Red”.
To add a variant thumbnail:
Dimensions
TheHasDimensions trait provides physical measurements for shipping calculations. Each dimension has a value and a unit stored as an enum.
Dimension Enums
Weight:Creating Variants
Single Variant
The simplest way to create a variant with pricing, stock, and attribute values in one transaction is theCreateNewVariant action:
Manual Creation
For more control, create the variant step by step:Generating Variant Combinations
When a product has multiple options, you need to generate all possible combinations. A product with 3 colors and 3 sizes produces 9 variants. Shopper provides tools to automate this.Permutation Helpers
TheArr macro generates the cartesian product of option values:
Bulk Save with SaveProductVariantsAction
TheSaveProductVariantsAction handles creating, updating, and deleting variants in a single transaction. Variants not present in the array are deleted, existing ones are updated, and new ones are created.
variant_id to update a variant instead of creating a new one. Any variant belonging to the product that is not in the array will be deleted.
MapProductOptions
For products with existing attribute assignments,MapProductOptions builds the structured options array you can feed into Arr::permutate():
Retrieving Variants
To get all variants for a product, ordered by position:Observer Behavior
TheProductVariantObserver handles cascade cleanup when a variant is deleted. It removes all associated media files, prices, and inventory history records automatically.
parent::deleting() to preserve this behavior.
Components
You can publish the Livewire components to customize the admin UI for variant management:config/shopper/components/product.php: