Skip to main content
Products are the core of your e-commerce catalog. Each product has a type that determines its capabilities: a Standard product is a simple physical item, a Variant product has multiple options like size and color, a Virtual product is a digital download, and an External product links to an external URL.

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 is Shopper\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:
Update config/shopper/models.php:

Database Schema

Relationships

Brand

Each product can belong to a brand. This is a standard BelongsTo relationship.
To query products by brand:

Categories

Products have a polymorphic many-to-many relationship with categories through the product_has_relations table.

Collections

Products can belong to multiple collections. Collections can also be automatic (rule-based), in which case the ProductObserver 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 with type = ProductType::Variant, the variants() relationship returns all child variants. Each variant has its own SKU, price, stock, and dimensions.
The variants_stock computed attribute returns the total stock across all variants.
See the Product Variants documentation for the full variant API.

Tags

Products can have multiple tags for cross-cutting organization. Unlike categories (hierarchical) or collections (rule-based), tags are simple flat labels.
See the Product Tags documentation for details.

Supplier

Each product can optionally be linked to a supplier.

Attributes (Options)

Product attributes are accessed via the options() relationship. The name options is used instead of attributes to avoid collision with Eloquent’s $attributes property.
The pivot table stores the selected attribute value and an optional custom value for freeform input.
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.
See the Discounts documentation for details on discount rules and conditions.

Query Scopes

Published Products

The publish() 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

The forChannel() scope filters products assigned to one or more sales channels.

Publication Check

The isPublished() 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 in config/shopper/media.php. To add a product thumbnail:
To add gallery images:
To retrieve the thumbnail URL with a specific conversion:

Downloadable Files

For virtual products (ebooks, software, courses), the files collection stores the downloadable assets that customers receive after purchase. Unlike the thumbnail and gallery collections, the files collection name is not configurable.
To get all downloadable files for a product:

Pricing

Products use the HasPrices 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:
To format the price for display, use the shopper_money_format helper. Prices are stored in cents.
See the Pricing documentation for details on creating and managing prices.

Stock Management

Products use the HasStock 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:
For variant products, use variants_stock to get the combined stock across all variants:
To get stock at a specific point in time, useful for reporting:
To get stock for a specific inventory location:
To check the stock alert threshold:

Modifying Stock

To increase stock (for example, when receiving a shipment from a supplier):
To decrease stock (for example, when fulfilling an order):
To set stock to an exact quantity (for example, after a physical inventory count). This calculates the delta from the current stock and creates a single mutation:
To clear all stock history and optionally set a new starting quantity:
clearStock() deletes all inventory history records for the product. Use it only for resets or corrections, not for regular stock adjustments.
For variant products, stock is managed on each variant individually. See the Product Variants stock management section.

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:
You can also batch-load stock for a specific inventory location:

Preventing Lazy Stock Loading

Following the same pattern as Laravel’s Model::preventLazyLoading(), you can catch unoptimized stock access during development. When enabled, accessing $product->stock without prior batch-loading throws a LazyStockLoadingException.

Dimensions

The HasDimensions trait provides computed attributes that return formatted dimension strings combining the value and unit.
For a product with weight_value = 1.50 and weight_unit = Weight::KG, $product->weight returns "1.50 kg".

Events

Product lifecycle events are dispatched automatically:
The 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:
To add a thumbnail image:

Retrieving Products

The findBySlug static method looks up a product by its slug and throws a ModelNotFoundException if not found:
To get all published products with their relationships:
To get featured products:
To get products by category:

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:
This creates config/shopper/components/product.php where you can replace any page or form component with your own implementation.