laravel-shop/docs/01-products.md

12 KiB

Product Management

Overview

The Laravel Shop package provides a complete product management system with support for:

  • Multi-language content through HasMetaTranslation trait
  • Flexible pricing with ProductPrice model
  • Stock management and reservations
  • Product variants (parent/child relationships)
  • Categories, attributes, and actions
  • Product relations (related, upsell, cross-sell)

Creating Products

Minimal Product Creation

The absolute minimum to create a product:

use Blax\Shop\Models\Product;

$product = Product::create([
    'slug' => 'my-product',
]);

This will automatically:

  • Generate a random slug if not provided (e.g., 'new-product-abc12345')
  • Initialize meta field as empty JSON object
  • Set status to 'draft'
  • Set type to 'simple'

Basic Product Creation

$product = Product::create([
    'slug' => 'blue-hoodie',
    'sku' => 'HOOD-BLU-001',
    'type' => 'simple',
    'status' => 'published',
    'is_visible' => true,
    'featured' => false,
]);

// Add translated content (stored in meta column)
$product->setLocalized('name', 'Blue Hoodie', 'en');
$product->setLocalized('description', 'Comfortable cotton hoodie', 'en');
$product->setLocalized('short_description', 'Cotton hoodie', 'en');

Advanced Product Creation

$product = Product::create([
    // Basic Info
    'slug' => 'premium-headphones',
    'sku' => 'HEAD-PREM-001',
    'type' => 'simple',
    'status' => 'published',
    'is_visible' => true,
    'featured' => true,
    'published_at' => now(),
    'sort_order' => 10,
    
    // Sale Period
    'sale_start' => now(),
    'sale_end' => now()->addDays(7),
    
    // Stock Management
    'manage_stock' => true,
    'low_stock_threshold' => 10,
    
    // Physical Properties
    'weight' => 0.5, // kg
    'length' => 20,  // cm
    'width' => 15,   // cm
    'height' => 10,  // cm
    'virtual' => false,
    'downloadable' => false,
    
    // Tax
    'tax_class' => 'standard',
]);

// Add translations
$product->setLocalized('name', 'Premium Wireless Headphones', 'en');
$product->setLocalized('name', 'Auriculares Premium Inalámbricos', 'es');

$product->setLocalized('description', 'High-quality wireless headphones with noise cancellation', 'en');
$product->setLocalized('short_description', 'Premium wireless headphones', 'en');

// Add custom meta data
$product->meta = (object)[
    'brand' => 'AudioPro',
    'color' => 'black',
    'warranty' => '2 years',
];
$product->save();

Product Pricing

Products use the ProductPrice model for flexible pricing. Each product must have at least one price to be purchasable.

Creating Product Prices

use Blax\Shop\Models\ProductPrice;

// Create a default price
$price = ProductPrice::create([
    'purchasable_type' => Product::class,
    'purchasable_id' => $product->id,
    'currency' => 'USD',
    'unit_amount' => 4999, // $49.99 in cents
    'is_default' => true,
    'active' => true,
    'type' => 'one_time',
]);

// Add sale price
$price->update([
    'sale_unit_amount' => 3999, // $39.99
]);

Multi-Currency Pricing

// USD price (default)
ProductPrice::create([
    'purchasable_type' => Product::class,
    'purchasable_id' => $product->id,
    'currency' => 'USD',
    'unit_amount' => 4999,
    'is_default' => true,
    'active' => true,
]);

// EUR price
ProductPrice::create([
    'purchasable_type' => Product::class,
    'purchasable_id' => $product->id,
    'currency' => 'EUR',
    'unit_amount' => 4499,
    'is_default' => false,
    'active' => true,
]);

Recurring Prices (Subscriptions)

ProductPrice::create([
    'purchasable_type' => Product::class,
    'purchasable_id' => $product->id,
    'currency' => 'USD',
    'unit_amount' => 999, // $9.99/month
    'type' => 'recurring',
    'interval' => 'month',
    'interval_count' => 1,
    'trial_period_days' => 7,
    'is_default' => true,
    'active' => true,
]);

Get Current Price

// Get the current price (considers sale prices and dates)
$currentPrice = $product->getCurrentPrice(); // Returns float

// Check if product is on sale
if ($product->isOnSale()) {
    echo "On sale!";
}

// Get default price
$defaultPrice = $product->defaultPrice()->first();

Stock Management

Enable Stock Management

$product->update([
    'manage_stock' => true,
    'low_stock_threshold' => 10,
]);

Increase/Decrease Stock

// Increase stock
$product->increaseStock(50);

// Decrease stock
$product->decreaseStock(1);

// Get available stock
$available = $product->getAvailableStock();

// Check if in stock
if ($product->isInStock()) {
    echo "In stock!";
}

// Check if low stock
if ($product->isLowStock()) {
    echo "Low stock warning!";
}

Stock Claims (Reservations)

use Blax\Shop\Models\ProductStock;

// Claim stock temporarily (for bookings)
$claim = $product->claimStock(
    quantity: 2,
    reference: $cart,
    from: now(),
    until: now()->addDays(3),
    note: 'Cart reservation'
);

// Release claim
$product->releaseStock($cart);

// Get active claims
$claims = $product->stocks()
    ->where('type', 'claimed')
    ->where('status', 'pending')
    ->get();

Stock History

// Get all stock records
$stockRecords = $product->stocks()->get();

// Filter by type
$increases = $product->stocks()->where('type', 'increase')->get();
$decreases = $product->stocks()->where('type', 'decrease')->get();
$reservations = $product->stocks()->where('type', 'reservation')->get();

Product Variants

Create Parent Product

$parent = Product::create([
    'type' => 'variable',
    'slug' => 'hoodie',
    'status' => 'published',
]);

$parent->setLocalized('name', 'Hoodie', 'en');

Create Variants

// Small variant
$small = Product::create([
    'type' => 'simple',
    'slug' => 'hoodie-small',
    'sku' => 'HOOD-S',
    'parent_id' => $parent->id,
    'status' => 'published',
]);

ProductPrice::create([
    'purchasable_type' => Product::class,
    'purchasable_id' => $small->id,
    'currency' => 'USD',
    'unit_amount' => 4999,
    'is_default' => true,
    'active' => true,
]);

// Medium variant
$medium = Product::create([
    'type' => 'simple',
    'slug' => 'hoodie-medium',
    'sku' => 'HOOD-M',
    'parent_id' => $parent->id,
    'status' => 'published',
]);

ProductPrice::create([
    'purchasable_type' => Product::class,
    'purchasable_id' => $medium->id,
    'currency' => 'USD',
    'unit_amount' => 4999,
    'is_default' => true,
    'active' => true,
]);

// Get all variants
$variants = $parent->children()->get();

Categories

Create Categories

use Blax\Shop\Models\ProductCategory;

$category = ProductCategory::create([
    'name' => 'Electronics',
    'slug' => 'electronics',
    'description' => 'Electronic products',
    'is_visible' => true,
    'sort_order' => 1,
]);

// Create child category
$subCategory = ProductCategory::create([
    'name' => 'Headphones',
    'slug' => 'headphones',
    'parent_id' => $category->id,
    'is_visible' => true,
    'sort_order' => 1,
]);

Attach Products to Categories

// Attach single category
$product->categories()->attach($category->id);

// Attach multiple categories
$product->categories()->attach([$category1->id, $category2->id]);

// Sync categories (removes others)
$product->categories()->sync([$category1->id, $category2->id]);

// Get product categories
$categories = $product->categories()->get();

Query Products by Category

// Get products in category
$products = Product::byCategory($category->id)->get();

// Get category tree
$tree = ProductCategory::getTree();

// Get visible categories
$visible = ProductCategory::visible()->get();

// Get root categories
$roots = ProductCategory::roots()->get();

Product Attributes

Add Attributes

use Blax\Shop\Models\ProductAttribute;

// Add color attribute
ProductAttribute::create([
    'product_id' => $product->id,
    'key' => 'Color',
    'value' => 'Blue',
    'sort_order' => 1,
]);

// Add size attribute
ProductAttribute::create([
    'product_id' => $product->id,
    'key' => 'Size',
    'value' => 'Large',
    'sort_order' => 2,
]);

// Get product attributes
$attributes = $product->attributes()->get();

Product Actions

Product actions allow you to trigger events when certain things happen (e.g., on purchase).

Create Product Actions

use Blax\Shop\Models\ProductAction;

// Send email on purchase
ProductAction::create([
    'product_id' => $product->id,
    'class' => \App\Jobs\SendWelcomeEmail::class,
    'events' => ['purchased'],
    'parameters' => [
        'template' => 'welcome',
        'delay' => 0,
    ],
    'active' => true,
    'defer' => true,
    'sort_order' => 1,
]);

// Grant access on purchase
ProductAction::create([
    'product_id' => $product->id,
    'class' => \App\Jobs\GrantCourseAccess::class,
    'events' => ['purchased'],
    'parameters' => [
        'course_id' => 123,
    ],
    'active' => true,
    'defer' => true,
    'sort_order' => 2,
]);

Trigger Actions

// Actions are automatically triggered on events
// You can also manually trigger them:
$product->callActions('purchased', $productPurchase);

// On refund
$product->callActions('refunded', $productPurchase);

Product Relations

// Add related products
$product->relatedProducts()->attach($relatedProduct->id, [
    'type' => 'related',
]);

// Get related products
$related = $product->relatedProducts()->wherePivot('type', 'related')->get();

Upsells

// Add upsell product
$product->relatedProducts()->attach($upsellProduct->id, [
    'type' => 'upsell',
]);

// Get upsell products
$upsells = $product->upsells()->get();

Cross-sells

// Add cross-sell product
$product->relatedProducts()->attach($crossSellProduct->id, [
    'type' => 'cross-sell',
]);

// Get cross-sell products
$crossSells = $product->crossSells()->get();

Querying Products

Scopes

// Published products
$published = Product::published()->get();

// Visible products (published + visible + published_at check)
$visible = Product::visible()->get();

// Featured products
$featured = Product::featured()->get();

// In stock products
$inStock = Product::inStock()->get();

// Low stock products
$lowStock = Product::lowStock()->get();

// By type
$simple = Product::where('type', 'simple')->get();
$virtual = Product::where('virtual', true)->get();
$downloadable = Product::where('downloadable', true)->get();
// Search by slug, SKU, or name
$results = Product::search('headphones')->get();

// Price range
$results = Product::priceRange(min: 10.00, max: 100.00)->get();

// Order by price
$cheapest = Product::orderByPrice('asc')->get();
$expensive = Product::orderByPrice('desc')->get();

Combining Scopes

$products = Product::visible()
    ->inStock()
    ->byCategory($categoryId)
    ->priceRange(min: 20, max: 50)
    ->orderByPrice('asc')
    ->get();

Product Visibility

// Check if product is visible
if ($product->isVisible()) {
    // Product is published, visible, and published_at is in past
}

// Set visibility
$product->update([
    'status' => 'published',
    'is_visible' => true,
    'published_at' => now(),
]);

Virtual & Downloadable Products

// Virtual product (no shipping)
$product->update([
    'virtual' => true,
]);

// Downloadable product
$product->update([
    'downloadable' => true,
]);

// Both
$product->update([
    'virtual' => true,
    'downloadable' => true,
]);

API Export

// Get product as API array
$data = $product->toApiArray();

// Returns:
// [
//     'id' => '...',
//     'slug' => '...',
//     'sku' => '...',
//     'name' => '...', // localized
//     'description' => '...', // localized
//     'short_description' => '...', // localized
//     'type' => '...',
//     'price' => 49.99,
//     'sale_price' => null,
//     'is_on_sale' => false,
//     'low_stock' => false,
//     'featured' => false,
//     'virtual' => false,
//     'downloadable' => false,
//     'weight' => 0.5,
//     'dimensions' => [...],
//     'categories' => [...],
//     'attributes' => [...],
//     'variants' => [...],
//     'parent' => null,
//     'created_at' => '...',
//     'updated_at' => '...',
// ]