How Products Work
Shopper uses a type-driven product system. When you create a product, you choose one of four types, and the system enables the right features automatically.
Standard products hold their own price, stock, and media. They are the simplest product type and work for anything that does not need size/color options.
Variant products are parents that hold the product description, images, and categories, but delegate pricing and stock to their child variants. Each variant represents a specific combination of options (e.g., “Blue / Large”). See the Product Variants documentation for details.
Virtual products behave like Standard products except they skip shipping. The
files media collection lets you attach downloadable files that customers receive after purchase.
External products store a URL to an external site. They have no stock, no shipping, and no checkout flow in Shopper.
You can check a product’s type and capabilities using the following methods:
Model
The model used isShopper\Models\Product, which extends Shopper\Core\Models\Product. The core model provides the business logic, relationships, scopes, stock management, pricing, and reviews. The admin model adds media collections and conversions through Spatie MediaLibrary.
The product model uses SoftDeletes. Deleted products remain in the database and are excluded from queries automatically. The publish() scope filters by visibility and publication date, so storefront queries do not need to handle soft deletes manually.
Extending the Model
To add custom behavior, extend the admin model and update your configuration:config/shopper/models.php:
Database Schema
Relationships
Brand
Each product can belong to a brand. This is a standardBelongsTo relationship.
Categories
Products have a polymorphic many-to-many relationship with categories through theproduct_has_relations table.
Collections
Products can belong to multiple collections. Collections can also be automatic (rule-based), in which case theProductObserver dispatches a SyncProductWithCollectionsJob when a product is saved to evaluate collection rules.
Channels
Products can be assigned to multiple sales channels for multi-channel publishing.Variants
For products withtype = ProductType::Variant, the variants() relationship returns all child variants. Each variant has its own SKU, price, stock, and dimensions.
variants_stock computed attribute returns the total stock across all variants.
Tags
Products can have multiple tags for cross-cutting organization. Unlike categories (hierarchical) or collections (rule-based), tags are simple flat labels.Supplier
Each product can optionally be linked to a supplier.Attributes (Options)
Product attributes are accessed via theoptions() relationship. The name options is used instead of attributes to avoid collision with Eloquent’s $attributes property.
Related Products
You can link products together as related items for cross-selling on the storefront.Discounts
Products can be associated with discounts through a polymorphic relationship.Query Scopes
Published Products
Thepublish() scope filters products that are visible and have a published_at date in the past or present. This is the primary scope for storefront queries.
Filter by Channel
TheforChannel() scope filters products assigned to one or more sales channels.
Publication Check
TheisPublished() method checks if a specific product is currently visible on the storefront.
Media
Products support three media collections through Spatie MediaLibrary. The collection names for the default gallery and thumbnail are defined inconfig/shopper/media.php.
To add a product thumbnail:
Downloadable Files
For virtual products (ebooks, software, courses), thefiles collection stores the downloadable assets that customers receive after purchase. Unlike the thumbnail and gallery collections, the files collection name is not configurable.
Pricing
Products use theHasPrices trait for multi-currency pricing. Each product can have multiple prices, one per currency, stored in the prices table through a polymorphic relationship.
To get the price for a specific currency:
shopper_money_format helper. Prices are stored in cents.
Stock Management
Products use theHasStock trait for inventory management. Stock is not stored as a column on the product. Instead, 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 when modifying stock.
Querying Stock
To get the current total stock across all locations:variants_stock to get the combined stock across all variants:
Modifying Stock
To increase stock (for example, when receiving a shipment from a supplier):Avoiding N+1 Queries
Accessing$product->stock triggers an individual SUM(quantity) query on the inventory_histories table for each product. When iterating over a collection, this results in N separate queries.
Use loadCurrentStock() to batch-load stock for an entire collection in a single query:
Preventing Lazy Stock Loading
Following the same pattern as Laravel’sModel::preventLazyLoading(), you can catch unoptimized stock access during development. When enabled, accessing $product->stock without prior batch-loading throws a LazyStockLoadingException.
Dimensions
TheHasDimensions trait provides computed attributes that return formatted dimension strings combining the value and unit.
weight_value = 1.50 and weight_unit = Weight::KG, $product->weight returns "1.50 kg".
Events
Product lifecycle events are dispatched automatically:ProductObserver also dispatches a SyncProductWithCollectionsJob every time a product is saved, which evaluates automatic collection rules to add or remove the product from rule-based collections.
For the full events reference including payload details, see the Events page.
Creating Products
To create a standard product with a price and category:Retrieving Products
ThefindBySlug static method looks up a product by its slug and throws a ModelNotFoundException if not found:
Permissions
The admin panel generates permissions for product management and its related features:Products
Product Variants
Components
You can publish the Livewire components to customize the admin UI for products:config/shopper/components/product.php where you can replace any page or form component with your own implementation.