Skip to main content
Products are the core of your e-commerce catalog. This documentation covers the Product model structure, relationships, types, and how to interact with products programmatically.

Model

Shopper\Core\Models\Product
The Product model implements several interfaces and uses multiple traits to provide a complete feature set:
use Shopper\Core\Models\Product;

// The model implements
- Shopper\Core\Contracts\HasReviews
- Shopper\Core\Contracts\Priceable
- Shopper\Core\Models\Contracts\Product
- Spatie\MediaLibrary\HasMedia

Extending the Model

To customize the Product model, create your own model that extends the base class:
namespace App\Models;

use Shopper\Core\Models\Product as BaseProduct;

class Product extends BaseProduct
{
    // Add your customizations here
}
Then update the configuration in config/shopper/models.php:
return [
    'product' => \App\Models\Product::class,
];

Database Schema

ColumnTypeNullableDefaultDescription
idbigintnoautoPrimary key
namestringno-Product name
slugstringyesautoURL-friendly identifier (unique)
skustringyesnullStock Keeping Unit (unique)
barcodestringyesnullProduct barcode (unique)
descriptionlongtextyesnullFull product description
typestringyesnullProduct type enum value
is_visiblebooleannofalseVisibility on storefront
featuredbooleannofalseFeatured product flag
security_stockintegeryes0Minimum stock alert threshold
old_price_amountintegeryesnullCompare at price (in cents)
price_amountintegeryesnullCurrent price (in cents)
cost_amountintegeryesnullCost per item (in cents)
backorderbooleannofalseAllow backorders
published_atdatetimenonow()Publication date
seo_titlestring(60)yesnullSEO meta title
seo_descriptionstring(160)yesnullSEO meta description
weight_unitstringyesnullWeight unit enum
weight_valuedecimal(10,2)yesnullWeight value
height_unitstringyesnullHeight unit enum
height_valuedecimal(10,2)yesnullHeight value
width_unitstringyesnullWidth unit enum
width_valuedecimal(10,2)yesnullWidth value
depth_unitstringyesnullDepth unit enum
depth_valuedecimal(10,2)yesnullDepth value
volume_unitstringyesnullVolume unit enum
volume_valuedecimal(10,2)yesnullVolume value
external_idstringyesnullExternal system reference
metadatajsonyesnullAdditional custom data
brand_idbigintyesnullForeign key to brands
parent_idbigintyesnullForeign key for parent product

Product Types

Products are categorized using the ProductType enum:
use Shopper\Core\Enum\ProductType;

ProductType::Standard  // Simple product without variants
ProductType::Variant   // Product with variants (size, color, etc.)
ProductType::Virtual   // Digital/downloadable product
ProductType::External  // Product hosted externally (affiliate)

Type Checking Methods

$product->isStandard();
$product->isVariant();
$product->isVirtual();
$product->isExternal();

Type Capabilities

// Check if product can use shipping
$product->canUseShipping(); // true for Standard and Variant types

// Check if product can have attributes
$product->canUseAttributes(); // true for Standard and Variant types

// Check if product can have variants
$product->canUseVariants(); // true only for Variant type

Relationships

Brand

// Get the product's brand
$product->brand; // Returns Brand model or null

// Query products by brand
Product::query()
    ->whereHas('brand', fn ($q) => $q->where('slug', 'nike'))
    ->get();

Categories

Products have a many-to-many polymorphic relationship with categories:
// Get all categories
$product->categories;

// Attach categories
$product->categories()->attach([$categoryId1, $categoryId2]);

// Sync categories (replaces existing)
$product->categories()->sync([$categoryId1, $categoryId2]);

// Detach all categories
$product->categories()->detach();

Collections

// Get all collections containing this product
$product->collections;

// Add product to collection
$product->collections()->attach($collectionId);

Channels

Products can be assigned to multiple sales channels:
// Get product channels
$product->channels;

// Assign to channel
$product->channels()->attach($channelId);

Variants

For products with type = ProductType::Variant:
// Get all variants
$product->variants;

// Get total stock across all variants
$product->variants_stock;

// Create a variant
$product->variants()->create([
    'name' => 'Blue / Large',
    'sku' => 'TSHIRT-BLU-L',
    'price_amount' => 2999,
]);

Attributes (Options)

Product attributes are accessed via the options() relationship to avoid collision with Eloquent’s $attributes property:
// Get product attributes
$product->options; // Collection of Attribute models

// Access pivot data
foreach ($product->options as $attribute) {
    $valueId = $attribute->pivot->attribute_value_id;
    $customValue = $attribute->pivot->attribute_custom_value;
}

// Attach attributes with values
$product->options()->attach($attributeId, [
    'attribute_value_id' => $valueId,
    'attribute_custom_value' => 'Custom text',
]);
// Get related products
$product->relatedProducts;

// Attach related products
$product->relatedProducts()->attach([$productId1, $productId2]);

Query Scopes

Published Products

use Shopper\Core\Models\Product;

// Get only published and visible products
Product::query()->publish()->get();

// The scope filters by:
// - published_at <= now()
// - is_visible = true

Filter by Channel

// Products available on a specific channel
Product::query()->forChannel($channelId)->get();

// Multiple channels
Product::query()->forChannel([$channel1, $channel2])->get();

Publication Status

// Check if product is currently published
$product->isPublished();

// This checks:
// - is_visible is true
// - published_at is set
// - published_at is in the past or present

Media Management

Products use Spatie Media Library for image management:
// Add an image
$product->addMedia($file)->toMediaCollection('products');

// Add thumbnail (single file collection)
$product->addMedia($file)->toMediaCollection('thumbnail');

// Add downloadable files
$product->addMedia($file)->toMediaCollection('files');

// Get all product images
$product->getMedia('products');

// Get thumbnail
$product->getFirstMedia('thumbnail');
$product->getFirstMediaUrl('thumbnail');

Media Collections Configuration

The product registers three media collections:
CollectionDiskSingle FileDescription
productsconfigurednoProduct gallery images
thumbnailconfiguredyesMain product image
filesconfigurednoDownloadable files

Events

Product lifecycle events are dispatched automatically:
use Shopper\Core\Events\Products\ProductCreated;
use Shopper\Core\Events\Products\ProductUpdated;
use Shopper\Core\Events\Products\ProductDeleted;
For the full events reference including payload details and usage examples, see the Events page.

Stock Management

Products use the HasStock trait for inventory management:
// Get current stock
$product->stock;

// For variant products, get combined stock
$product->variants_stock;

// Check stock status
if ($product->stock <= $product->security_stock) {
    // Low stock alert
}

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 (e.g. listing 50 products on a page), this results in 50 separate queries — a classic N+1 problem. Use loadCurrentStock() to batch-load stock for an entire collection in a single query:
use Shopper\Core\Models\Product;

$products = Product::query()->with('variants')->get();

// 1 query instead of N
Product::loadCurrentStock($products);

// For variant-type products, also batch-load variant stock
$variants = $products
    ->filter(fn ($p) => $p->canUseVariants())
    ->flatMap->variants;

ProductVariant::loadCurrentStock($variants);

// Now $product->stock and $variant->stock use preloaded values — no extra queries
foreach ($products as $product) {
    echo $product->name . ': ' . $product->stock;
}
You can also batch-load stock for a specific inventory location:
Product::loadStockForInventory($products, $inventoryId);

Preventing Lazy Stock Loading

Following the same pattern as Laravel’s Model::preventLazyLoading(), you can opt-in to catch unoptimized stock access during development. When enabled, accessing $product->stock without prior batch-loading throws a LazyStockLoadingException:
use Shopper\Core\Models\Product;

// In a service provider boot() method
Product::preventLazyStockLoading(! app()->isProduction());
This helps you catch N+1 stock queries early. The exception message tells you exactly which model and method to use:
Attempted to lazy load stock on model [Shopper\Core\Models\Product]
but lazy stock loading is disabled.
Use [Shopper\Core\Models\Product::loadCurrentStock($collection)]
to batch-load stock before accessing $model->stock.

Pricing

Products use the HasPrices trait:
// Get product price (returns Price model)
$product->price;

// Access amount (in cents)
$product->price_amount;

// Compare at price
$product->old_price_amount;

// Cost
$product->cost_amount;

// Format price for display
shopper_money_format($product->price_amount); // "$29.99"

Creating Products

use Shopper\Core\Models\Product;
use Shopper\Core\Enum\ProductType;

$product = Product::query()->create([
    'name' => 'Classic T-Shirt',
    'sku' => 'TSHIRT-001',
    'type' => ProductType::Standard,
    'price_amount' => 2999, // $29.99 in cents
    'is_visible' => true,
    'published_at' => now(),
    'brand_id' => $brandId,
]);

// Attach to categories
$product->categories()->attach([$categoryId]);

// Add media
$product->addMediaFromUrl('https://example.com/image.jpg')
    ->toMediaCollection('thumbnail');

Retrieving Products

// Get all published products
$products = Product::query()
    ->publish()
    ->with(['brand', 'categories', 'media'])
    ->get();

// Get featured products
$featured = Product::query()
    ->publish()
    ->where('featured', true)
    ->take(8)
    ->get();

// Get products by category
$products = Product::query()
    ->publish()
    ->whereHas('categories', fn ($q) => $q->where('slug', 'clothing'))
    ->get();

// Get products with low stock
$lowStock = Product::query()
    ->whereColumn('stock', '<=', 'security_stock')
    ->get();

Components

Publish Livewire components to customize the admin interface:
php artisan shopper:component:publish product
This publishes to config/shopper/components/product.php:
use Shopper\Livewire;
use Shopper\Livewire\Components;

return [
    'pages' => [
        'product-index' => Livewire\Pages\Product\Index::class,
        'product-edit' => Livewire\Pages\Product\Edit::class,
        'variant-edit' => Livewire\Pages\Product\Variant::class,
        'attribute-index' => Livewire\Pages\Attribute\Browse::class,
    ],
    'components' => [
        'products.form.attributes' => Components\Products\Form\Attributes::class,
        'products.form.edit' => Components\Products\Form\Edit::class,
        'products.form.media' => Components\Products\Form\Media::class,
        'products.form.inventory' => Components\Products\Form\Inventory::class,
        'products.form.variants' => Components\Products\Form\Variants::class,
        // ...
        'slide-overs.add-product' => Livewire\SlideOvers\AddProduct::class,
        'slide-overs.add-variant' => Livewire\SlideOvers\AddVariant::class,
        // ...
    ],
];