# Product Relations ## Overview The Product Relations system enables complex relationships between products through a flexible, type-based association model. Products can be related to each other in various ways for different business purposes like upselling, bundling, cross-selling, and structural groupings. ## Architecture ### Database Structure Product relations are stored in a many-to-many pivot table with type differentiation: ``` products ├── id ├── name ├── type └── ... product_relations (pivot table) ├── product_id → The source product ├── related_product_id → The target product ├── type → ProductRelationType enum ├── sort_order → Optional ordering └── timestamps ``` ### Key Concepts 1. **Directional Relations**: Relations go from `product_id` to `related_product_id` 2. **Typed Relations**: Each relation has a specific type (e.g., UPSELL, RELATED) 3. **Flexible**: Same product can have multiple relation types to different products 4. **Bidirectional Support**: Some relations (POOL/SINGLE) create reverse relations automatically ## Relation Types ### Marketing Relations These relations help with product discovery and sales optimization: #### 1. RELATED (`ProductRelationType::RELATED`) **Purpose**: Products that are commonly viewed or purchased together **Use Case**: "Customers also viewed" or "Similar products" **Example**: ```php // Canon Camera → Canon Lenses (related products) $camera->productRelations()->attach($lens->id, [ 'type' => ProductRelationType::RELATED->value ]); // Access $relatedProducts = $camera->relatedProducts; ``` **Direction**: Can be one-way or bidirectional **Visibility**: Typically shown on product detail pages --- #### 2. UPSELL (`ProductRelationType::UPSELL`) **Purpose**: Higher-tier or premium alternatives to encourage upgrades **Use Case**: Suggesting a better/more expensive product **Example**: ```php // Basic Plan → Premium Plan (upsell) $basicPlan->productRelations()->attach($premiumPlan->id, [ 'type' => ProductRelationType::UPSELL->value ]); // Access $upsells = $basicPlan->upsellProducts; ``` **Direction**: One-way (Basic → Premium, not reverse) **Visibility**: Product pages, cart pages, checkout --- #### 3. CROSS_SELL (`ProductRelationType::CROSS_SELL`) **Purpose**: Complementary products often purchased together **Use Case**: "Frequently bought together" or "Complete your purchase" **Example**: ```php // Laptop → Mouse, Laptop Bag, USB Hub (cross-sells) $laptop->productRelations()->attach([$mouse->id, $bag->id, $hub->id], [ 'type' => ProductRelationType::CROSS_SELL->value ]); // Access $crossSells = $laptop->crossSellProducts; ``` **Direction**: One-way (main product → accessories) **Visibility**: Cart page, checkout, product page --- #### 4. DOWNSELL (`ProductRelationType::DOWNSELL`) **Purpose**: Lower-priced alternatives if customer balks at price **Use Case**: "Too expensive? Try this instead" **Example**: ```php // Premium Plan → Basic Plan (downsell) $premiumPlan->productRelations()->attach($basicPlan->id, [ 'type' => ProductRelationType::DOWNSELL->value ]); // Access $downsells = $premiumPlan->downsellProducts; ``` **Direction**: One-way (Premium → Basic, not reverse) **Visibility**: Shown when customer hesitates or abandons cart --- #### 5. ADD_ON (`ProductRelationType::ADD_ON`) **Purpose**: Optional extras that enhance the main product **Use Case**: Extended warranties, gift wrapping, rush delivery **Example**: ```php // Product → Extended Warranty (add-on) $product->productRelations()->attach($warranty->id, [ 'type' => ProductRelationType::ADD_ON->value ]); // Access $addOns = $product->addOnProducts; ``` **Direction**: One-way (main product → add-on) **Visibility**: Product page, during add-to-cart flow --- ### Structural Relations These relations define product hierarchy and composition: #### 6. VARIATION (`ProductRelationType::VARIATION`) **Purpose**: Different versions/variants of the same base product **Use Case**: Size, color, or configuration variations **Example**: ```php // T-Shirt (Variable) → T-Shirt Small, T-Shirt Medium (variations) $tshirt->productRelations()->attach([$small->id, $medium->id, $large->id], [ 'type' => ProductRelationType::VARIATION->value ]); // Access $variations = $tshirt->variantProducts; ``` **Direction**: One-way (parent → variations) **Visibility**: Product page variant selector --- #### 7. BUNDLE (`ProductRelationType::BUNDLE`) **Purpose**: Group of products sold together as a package **Use Case**: "Starter Kit" or "Complete Package" offerings **Example**: ```php // Starter Bundle → Individual Products $starterKit->productRelations()->attach([ $product1->id, $product2->id, $product3->id ], [ 'type' => ProductRelationType::BUNDLE->value ]); // Access $bundleProducts = $starterKit->bundleProducts; ``` **Direction**: One-way (bundle → components) **Visibility**: Bundle product page showing contents --- ### Pool Relations (Special) These are bidirectional relations for pool/single item management: #### 8. SINGLE (`ProductRelationType::SINGLE`) **Purpose**: Link pool product to its individual component items **Use Case**: Pool product pointing to actual bookable items **Example**: ```php // Parking Pool → Individual Spots (single items) $pool->productRelations()->attach($spot1->id, [ 'type' => ProductRelationType::SINGLE->value ]); // Access $singleItems = $pool->singleProducts; ``` **Direction**: Pool → Single Items **Auto-creates**: Reverse POOL relation --- #### 9. POOL (`ProductRelationType::POOL`) **Purpose**: Link individual items back to their pool container **Use Case**: Single item referencing its parent pool **Example**: ```php // This is automatically created when using attachSingleItems() // Individual Spot → Parking Pool (pool reference) // Access $pools = $spot1->poolProducts; ``` **Direction**: Single Item → Pool **Auto-created**: By `attachSingleItems()` method --- ## Usage Examples ### Basic Relations ```php // Create a product with related products $camera = Product::find(1); // Add related products (one at a time) $camera->productRelations()->attach($lens->id, [ 'type' => ProductRelationType::RELATED->value, 'sort_order' => 1, ]); // Add multiple related products $camera->productRelations()->attach([ $lens->id => ['type' => ProductRelationType::RELATED->value], $tripod->id => ['type' => ProductRelationType::RELATED->value], $bag->id => ['type' => ProductRelationType::RELATED->value], ]); // Retrieve related products $relatedProducts = $camera->relatedProducts; ``` ### Cross-Selling ```php // Set up cross-sells for laptop $laptop->productRelations()->attach([ $mouse->id => ['type' => ProductRelationType::CROSS_SELL->value, 'sort_order' => 1], $bag->id => ['type' => ProductRelationType::CROSS_SELL->value, 'sort_order' => 2], $warranty->id => ['type' => ProductRelationType::CROSS_SELL->value, 'sort_order' => 3], ]); // In cart or checkout $crossSells = $laptop->crossSellProducts()->orderBy('sort_order')->get(); ``` ### Upselling Flow ```php // Basic → Premium upsell path $basicPlan->productRelations()->attach($premiumPlan->id, [ 'type' => ProductRelationType::UPSELL->value ]); // Premium → Enterprise upsell path $premiumPlan->productRelations()->attach($enterprisePlan->id, [ 'type' => ProductRelationType::UPSELL->value ]); // Show upsells if ($cart->contains($basicPlan)) { $suggestedUpgrade = $basicPlan->upsellProducts->first(); } ``` ### Product Variations ```php // Variable product with variations $tshirt = Product::create([ 'name' => 'T-Shirt', 'type' => ProductType::VARIABLE, ]); // Create variations $small = Product::create(['name' => 'T-Shirt Small', 'type' => ProductType::VARIATION]); $medium = Product::create(['name' => 'T-Shirt Medium', 'type' => ProductType::VARIATION]); $large = Product::create(['name' => 'T-Shirt Large', 'type' => ProductType::VARIATION]); // Link variations $tshirt->productRelations()->attach([ $small->id => ['type' => ProductRelationType::VARIATION->value], $medium->id => ['type' => ProductRelationType::VARIATION->value], $large->id => ['type' => ProductRelationType::VARIATION->value], ]); // Display on product page $variations = $tshirt->variantProducts; ``` ### Pool/Single Relations (Special Case) Pool relations are unique because they're bidirectional: ```php // ✅ CORRECT WAY - Use attachSingleItems() $pool = Product::create(['type' => ProductType::POOL]); $spot1 = Product::create(['type' => ProductType::BOOKING]); $spot2 = Product::create(['type' => ProductType::BOOKING]); // This creates BOTH directions automatically: $pool->attachSingleItems([$spot1->id, $spot2->id]); // Now both directions work: $pool->singleProducts; // Returns: [spot1, spot2] $spot1->poolProducts; // Returns: [pool] // ❌ WRONG WAY - Don't use attach() directly $pool->productRelations()->attach($spot1->id, [ 'type' => ProductRelationType::SINGLE->value ]); // This only creates one direction! Missing reverse POOL relation. ``` **What `attachSingleItems()` does:** ```php public function attachSingleItems(array $singleItemIds): void { // 1. Attach SINGLE relations from pool to items $this->productRelations()->attach( array_fill_keys($singleItemIds, ['type' => ProductRelationType::SINGLE->value]) ); // 2. Attach reverse POOL relations from items to pool foreach ($singleItemIds as $singleItemId) { $singleItem = static::find($singleItemId); $singleItem->productRelations()->attach($this->id, [ 'type' => ProductRelationType::POOL->value ]); } } ``` ## Advanced Usage ### Filtering by Relation Type ```php // Get all relations of a specific type $upsells = $product->relationsByType(ProductRelationType::UPSELL); // Or use dedicated method $upsells = $product->upsellProducts; // Get multiple types $suggestions = $product->productRelations() ->whereIn('type', [ ProductRelationType::UPSELL->value, ProductRelationType::CROSS_SELL->value ]) ->get(); ``` ### Custom Queries ```php // Relations with additional constraints $premiumUpsells = $product->upsellProducts() ->where('products.price', '>', 10000) ->orderBy('products.price', 'asc') ->get(); // Limited cross-sells $topCrossSells = $product->crossSellProducts() ->orderByPivot('sort_order') ->limit(3) ->get(); ``` ### Checking Relations ```php // Check if product has any upsells if ($product->upsellProducts()->exists()) { // Show upsell section } // Count related products $relatedCount = $product->relatedProducts()->count(); // Check specific relation $hasRelation = $product->productRelations() ->where('related_product_id', $otherProduct->id) ->where('type', ProductRelationType::RELATED->value) ->exists(); ``` ### Managing Relations ```php // Add relation $product->productRelations()->attach($relatedId, [ 'type' => ProductRelationType::RELATED->value, 'sort_order' => 1, ]); // Update relation $product->productRelations()->updateExistingPivot($relatedId, [ 'sort_order' => 2 ]); // Remove relation $product->productRelations()->detach($relatedId); // Remove all relations of a type $product->relatedProducts()->detach(); // Sync relations (replace all) $product->productRelations()->sync([ $id1 => ['type' => ProductRelationType::RELATED->value], $id2 => ['type' => ProductRelationType::RELATED->value], ]); ``` ## Relation Type Reference | Type | Enum | Direction | Auto-Reverse | Typical Use | |------------|-----------------------------------|-------------|--------------|---------------------------------| | RELATED | `ProductRelationType::RELATED` | One-way | No | Similar products, "also viewed" | | UPSELL | `ProductRelationType::UPSELL` | One-way | No | Premium alternatives | | CROSS_SELL | `ProductRelationType::CROSS_SELL` | One-way | No | Complementary products | | DOWNSELL | `ProductRelationType::DOWNSELL` | One-way | No | Lower-priced alternatives | | ADD_ON | `ProductRelationType::ADD_ON` | One-way | No | Optional extras | | VARIATION | `ProductRelationType::VARIATION` | One-way | No | Product variants | | BUNDLE | `ProductRelationType::BUNDLE` | One-way | No | Package components | | SINGLE | `ProductRelationType::SINGLE` | Pool → Item | Yes (POOL) | Pool single items | | POOL | `ProductRelationType::POOL` | Item → Pool | Yes (SINGLE) | Item's pool reference | ## Best Practices ### 1. Use Appropriate Relation Types ```php // ✅ CORRECT - Semantic meaning $product->productRelations()->attach($accessory->id, [ 'type' => ProductRelationType::CROSS_SELL->value // Complementary ]); // ❌ INCORRECT - Wrong type $product->productRelations()->attach($accessory->id, [ 'type' => ProductRelationType::UPSELL->value // Accessory isn't an upgrade ]); ``` ### 2. Use Helper Methods ```php // ✅ CORRECT - Dedicated method $upsells = $product->upsellProducts; // ❌ VERBOSE - Manual filtering $upsells = $product->productRelations() ->wherePivot('type', ProductRelationType::UPSELL->value) ->get(); ``` ### 3. Sort Order for Display ```php // ✅ CORRECT - Use sort_order $product->productRelations()->attach($items, [ $item1->id => ['type' => ProductRelationType::CROSS_SELL->value, 'sort_order' => 1], $item2->id => ['type' => ProductRelationType::CROSS_SELL->value, 'sort_order' => 2], ]); $crossSells = $product->crossSellProducts()->orderByPivot('sort_order')->get(); ``` ### 4. Always Use attachSingleItems() for Pools ```php // ✅ CORRECT - Bidirectional $pool->attachSingleItems([$item1->id, $item2->id]); // ❌ INCORRECT - One-way only $pool->productRelations()->attach($item1->id, [ 'type' => ProductRelationType::SINGLE->value ]); ``` ### 5. Eager Load Relations ```php // ✅ CORRECT - Avoid N+1 $products = Product::with('crossSellProducts')->get(); foreach ($products as $product) { $product->crossSellProducts; // Already loaded } // ❌ INCORRECT - N+1 queries $products = Product::all(); foreach ($products as $product) { $product->crossSellProducts; // Query per product } ``` ### 6. Validate Relation Logic ```php // ✅ CORRECT - Check business logic if ($premiumPlan->price > $basicPlan->price) { $basicPlan->productRelations()->attach($premiumPlan->id, [ 'type' => ProductRelationType::UPSELL->value ]); } // ❌ INCORRECT - No validation $basicPlan->productRelations()->attach($cheaperPlan->id, [ 'type' => ProductRelationType::UPSELL->value // Upsell to cheaper product?? ]); ``` ## Common Patterns ### Product Page Relations Display ```php // Show all related products $relatedProducts = $product->relatedProducts()->limit(4)->get(); // Show upsell if available $upsell = $product->upsellProducts()->first(); // Show add-ons $addOns = $product->addOnProducts; ``` ### Cart Cross-Sells ```php $cart = $user->currentCart(); $allCrossSells = collect(); foreach ($cart->items as $item) { $crossSells = $item->purchasable->crossSellProducts; $allCrossSells = $allCrossSells->merge($crossSells); } // Remove duplicates and products already in cart $uniqueCrossSells = $allCrossSells ->unique('id') ->reject(fn($p) => $cart->items->contains('purchasable_id', $p->id)); ``` ### Upsell at Checkout ```php $cartTotal = $cart->getTotal(); // Find upsells for products in cart $upsellOpportunities = $cart->items ->map(fn($item) => $item->purchasable->upsellProducts) ->flatten() ->unique('id'); // Filter to affordable upsells $affordableUpsells = $upsellOpportunities->filter( fn($upsell) => $upsell->price <= $cartTotal * 1.2 // Max 20% more ); ``` ### Bundle Product Display ```php // Show bundle contents $bundle = Product::find($id); $components = $bundle->bundleProducts; $totalValue = $components->sum('price'); $bundlePrice = $bundle->price; $savings = $totalValue - $bundlePrice; // "Save $50 when you buy the bundle!" ``` ## Troubleshooting ### Relations Not Showing **Check:** 1. Relation type is correct: `ProductRelationType::RELATED->value` 2. Using correct method: `relatedProducts` not `productRelations` 3. Pivot data exists: Check `product_relations` table ### Pool/Single Relations Not Bidirectional **Solution:** ```php // Use dedicated method $pool->attachSingleItems($itemIds); // NOT regular attach() ``` ### Duplicate Relations **Prevent:** ```php // Check before adding if (!$product->relatedProducts()->where('id', $relatedId)->exists()) { $product->productRelations()->attach($relatedId, [ 'type' => ProductRelationType::RELATED->value ]); } ``` ### N+1 Query Issues **Solution:** ```php // Eager load $products = Product::with([ 'crossSellProducts', 'upsellProducts', 'relatedProducts' ])->get(); ``` ## Related Documentation - [Pool Products](./ProductTypes/02-pool-products.md) - POOL/SINGLE relations in detail - [Product Types](./ProductTypes/) - Understanding different product types