laravel-shop/docs/01-products.md

593 lines
12 KiB
Markdown
Raw Normal View History

2025-11-21 10:49:41 +00:00
# Product Management
2025-11-25 16:25:20 +00:00
## 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)
2025-11-21 10:49:41 +00:00
## Creating Products
### Minimal Product Creation
The absolute minimum to create a product:
```php
use Blax\Shop\Models\Product;
$product = Product::create([
'slug' => 'my-product',
]);
```
This will automatically:
2025-11-25 16:25:20 +00:00
- Generate a random slug if not provided (e.g., 'new-product-abc12345')
- Initialize meta field as empty JSON object
2025-11-21 10:49:41 +00:00
- Set status to 'draft'
- Set type to 'simple'
### Basic Product Creation
```php
$product = Product::create([
'slug' => 'blue-hoodie',
'sku' => 'HOOD-BLU-001',
'type' => 'simple',
'status' => 'published',
2025-11-22 08:55:58 +00:00
'is_visible' => true,
2025-11-21 10:49:41 +00:00
'featured' => false,
]);
2025-11-25 16:25:20 +00:00
// Add translated content (stored in meta column)
2025-11-21 10:49:41 +00:00
$product->setLocalized('name', 'Blue Hoodie', 'en');
$product->setLocalized('description', 'Comfortable cotton hoodie', 'en');
$product->setLocalized('short_description', 'Cotton hoodie', 'en');
```
### Advanced Product Creation
```php
$product = Product::create([
// Basic Info
'slug' => 'premium-headphones',
'sku' => 'HEAD-PREM-001',
'type' => 'simple',
'status' => 'published',
2025-11-22 08:55:58 +00:00
'is_visible' => true,
2025-11-21 10:49:41 +00:00
'featured' => true,
'published_at' => now(),
'sort_order' => 10,
2025-11-25 16:25:20 +00:00
// Sale Period
2025-11-21 10:49:41 +00:00
'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');
2025-11-25 16:25:20 +00:00
// Add custom meta data
$product->meta = (object)[
'brand' => 'AudioPro',
'color' => 'black',
'warranty' => '2 years',
];
$product->save();
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
## Product Pricing
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
Products use the `ProductPrice` model for flexible pricing. Each product must have at least one price to be purchasable.
### Creating Product Prices
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
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
```php
// 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)
```php
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
```php
// 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
```php
$product->update([
'manage_stock' => true,
'low_stock_threshold' => 10,
2025-11-21 10:49:41 +00:00
]);
```
2025-11-25 16:25:20 +00:00
### Increase/Decrease Stock
```php
// 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 Reservations
```php
use Blax\Shop\Models\ProductStock;
// Reserve stock temporarily
$reservation = $product->reserveStock(
quantity: 2,
reference: $cart,
until: now()->addMinutes(15),
note: 'Cart reservation'
);
// Release reservation
$reservation->update(['status' => 'completed']);
// Get active reservations
$reservations = $product->reservations()->get();
```
### Stock History
```php
// 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
2025-11-21 10:49:41 +00:00
```php
$parent = Product::create([
'type' => 'variable',
'slug' => 'hoodie',
2025-11-25 16:25:20 +00:00
'status' => 'published',
2025-11-21 10:49:41 +00:00
]);
2025-11-25 16:25:20 +00:00
$parent->setLocalized('name', 'Hoodie', 'en');
```
### Create Variants
```php
// Small variant
2025-11-21 10:49:41 +00:00
$small = Product::create([
'type' => 'simple',
'slug' => 'hoodie-small',
'sku' => 'HOOD-S',
'parent_id' => $parent->id,
2025-11-25 16:25:20 +00:00
'status' => 'published',
]);
ProductPrice::create([
'purchasable_type' => Product::class,
'purchasable_id' => $small->id,
'currency' => 'USD',
'unit_amount' => 4999,
'is_default' => true,
'active' => true,
2025-11-21 10:49:41 +00:00
]);
2025-11-25 16:25:20 +00:00
// Medium variant
2025-11-21 10:49:41 +00:00
$medium = Product::create([
'type' => 'simple',
'slug' => 'hoodie-medium',
'sku' => 'HOOD-M',
'parent_id' => $parent->id,
2025-11-25 16:25:20 +00:00
'status' => 'published',
2025-11-21 10:49:41 +00:00
]);
2025-11-25 16:25:20 +00:00
ProductPrice::create([
'purchasable_type' => Product::class,
'purchasable_id' => $medium->id,
'currency' => 'USD',
'unit_amount' => 4999,
'is_default' => true,
'active' => true,
2025-11-21 10:49:41 +00:00
]);
2025-11-25 16:25:20 +00:00
// Get all variants
$variants = $parent->children()->get();
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
## Categories
### Create Categories
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
use Blax\Shop\Models\ProductCategory;
$category = ProductCategory::create([
'name' => 'Electronics',
'slug' => 'electronics',
'description' => 'Electronic products',
'is_visible' => true,
'sort_order' => 1,
2025-11-21 10:49:41 +00:00
]);
2025-11-25 16:25:20 +00:00
// Create child category
$subCategory = ProductCategory::create([
'name' => 'Headphones',
'slug' => 'headphones',
'parent_id' => $category->id,
'is_visible' => true,
'sort_order' => 1,
]);
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
### Attach Products to Categories
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
// 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
```php
// 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();
2025-11-21 10:49:41 +00:00
```
## Product Attributes
2025-11-25 16:25:20 +00:00
### Add Attributes
2025-11-21 10:49:41 +00:00
```php
use Blax\Shop\Models\ProductAttribute;
2025-11-25 16:25:20 +00:00
// Add color attribute
2025-11-21 10:49:41 +00:00
ProductAttribute::create([
'product_id' => $product->id,
2025-11-25 16:25:20 +00:00
'key' => 'Color',
'value' => 'Blue',
2025-11-21 10:49:41 +00:00
'sort_order' => 1,
]);
2025-11-25 16:25:20 +00:00
// Add size attribute
2025-11-21 10:49:41 +00:00
ProductAttribute::create([
'product_id' => $product->id,
2025-11-25 16:25:20 +00:00
'key' => 'Size',
'value' => 'Large',
2025-11-21 10:49:41 +00:00
'sort_order' => 2,
]);
2025-11-25 16:25:20 +00:00
// Get product attributes
$attributes = $product->attributes()->get();
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
## Product Actions
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
Product actions allow you to trigger events when certain things happen (e.g., on purchase).
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
### Create Product Actions
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
use Blax\Shop\Models\ProductAction;
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
// Send email on purchase
ProductAction::create([
2025-11-21 10:49:41 +00:00
'product_id' => $product->id,
2025-11-25 16:25:20 +00:00
'action_type' => 'SendWelcomeEmail',
'event' => 'purchased',
'parameters' => [
'template' => 'welcome',
'delay' => 0,
],
'active' => true,
'sort_order' => 1,
2025-11-21 10:49:41 +00:00
]);
2025-11-25 16:25:20 +00:00
// Grant access on purchase
ProductAction::create([
2025-11-21 10:49:41 +00:00
'product_id' => $product->id,
2025-11-25 16:25:20 +00:00
'action_type' => 'GrantCourseAccess',
'event' => 'purchased',
'parameters' => [
'course_id' => 123,
],
'active' => true,
'sort_order' => 2,
2025-11-21 10:49:41 +00:00
]);
2025-11-25 16:25:20 +00:00
```
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
### Trigger Actions
```php
// Actions are automatically triggered on events
// You can also manually trigger them:
$product->callActions('purchased', $productPurchase);
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
// On refund
$product->callActions('refunded', $productPurchase);
2025-11-21 10:49:41 +00:00
```
## Product Relations
### Related Products
```php
2025-11-25 16:25:20 +00:00
// Add related products
2025-11-21 10:49:41 +00:00
$product->relatedProducts()->attach($relatedProduct->id, [
'type' => 'related',
]);
2025-11-25 16:25:20 +00:00
// Get related products
$related = $product->relatedProducts()->wherePivot('type', 'related')->get();
2025-11-21 10:49:41 +00:00
```
### Upsells
```php
2025-11-25 16:25:20 +00:00
// Add upsell product
$product->relatedProducts()->attach($upsellProduct->id, [
2025-11-21 10:49:41 +00:00
'type' => 'upsell',
]);
2025-11-25 16:25:20 +00:00
// Get upsell products
$upsells = $product->upsells()->get();
2025-11-21 10:49:41 +00:00
```
### Cross-sells
```php
2025-11-25 16:25:20 +00:00
// Add cross-sell product
$product->relatedProducts()->attach($crossSellProduct->id, [
2025-11-21 10:49:41 +00:00
'type' => 'cross-sell',
]);
2025-11-25 16:25:20 +00:00
// Get cross-sell products
$crossSells = $product->crossSells()->get();
2025-11-21 10:49:41 +00:00
```
## Querying Products
2025-11-25 16:25:20 +00:00
### Scopes
2025-11-21 10:49:41 +00:00
```php
// Published products
2025-11-25 16:25:20 +00:00
$published = Product::published()->get();
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
// Visible products (published + visible + published_at check)
$visible = Product::visible()->get();
2025-11-21 10:49:41 +00:00
// Featured products
2025-11-25 16:25:20 +00:00
$featured = Product::featured()->get();
// In stock products
$inStock = Product::inStock()->get();
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
// 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();
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
### Search
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
// Search by slug, SKU, or name
$results = Product::search('headphones')->get();
2025-11-21 10:49:41 +00:00
// Price range
2025-11-25 16:25:20 +00:00
$results = Product::priceRange(min: 10.00, max: 100.00)->get();
2025-11-21 10:49:41 +00:00
// Order by price
2025-11-25 16:25:20 +00:00
$cheapest = Product::orderByPrice('asc')->get();
$expensive = Product::orderByPrice('desc')->get();
```
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
### Combining Scopes
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
```php
2025-11-21 10:49:41 +00:00
$products = Product::visible()
->inStock()
->byCategory($categoryId)
2025-11-25 16:25:20 +00:00
->priceRange(min: 20, max: 50)
2025-11-21 10:49:41 +00:00
->orderByPrice('asc')
2025-11-25 16:25:20 +00:00
->get();
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
## Product Visibility
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
// Check if product is visible
if ($product->isVisible()) {
// Product is published, visible, and published_at is in past
2025-11-21 10:49:41 +00:00
}
2025-11-25 16:25:20 +00:00
// Set visibility
$product->update([
'status' => 'published',
'is_visible' => true,
'published_at' => now(),
]);
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
## Virtual & Downloadable Products
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
// Virtual product (no shipping)
$product->update([
'virtual' => true,
]);
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
// Downloadable product
$product->update([
'downloadable' => true,
]);
2025-11-21 10:49:41 +00:00
2025-11-25 16:25:20 +00:00
// Both
$product->update([
'virtual' => true,
'downloadable' => true,
]);
2025-11-21 10:49:41 +00:00
```
2025-11-25 16:25:20 +00:00
## API Export
2025-11-21 10:49:41 +00:00
```php
2025-11-25 16:25:20 +00:00
// Get product as API array
2025-11-21 10:49:41 +00:00
$data = $product->toApiArray();
// Returns:
// [
// 'id' => '...',
// 'slug' => '...',
2025-11-25 16:25:20 +00:00
// 'sku' => '...',
// 'name' => '...', // localized
// 'description' => '...', // localized
// 'short_description' => '...', // localized
// 'type' => '...',
2025-11-21 10:49:41 +00:00
// 'price' => 49.99,
2025-11-25 16:25:20 +00:00
// 'sale_price' => null,
// 'is_on_sale' => false,
// 'low_stock' => false,
// 'featured' => false,
// 'virtual' => false,
// 'downloadable' => false,
// 'weight' => 0.5,
// 'dimensions' => [...],
2025-11-21 10:49:41 +00:00
// 'categories' => [...],
// 'attributes' => [...],
// 'variants' => [...],
2025-11-25 16:25:20 +00:00
// 'parent' => null,
// 'created_at' => '...',
// 'updated_at' => '...',
2025-11-21 10:49:41 +00:00
// ]
```