PHP code example of aliziodev / laravel-product-catalog

1. Go to this page and download the library: Download aliziodev/laravel-product-catalog library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

aliziodev / laravel-product-catalog example snippets


use Aliziodev\ProductCatalog\Models\Product;
use Aliziodev\ProductCatalog\Models\ProductVariant;
use Aliziodev\ProductCatalog\Enums\ProductType;
use Aliziodev\ProductCatalog\Enums\InventoryPolicy;
use Aliziodev\ProductCatalog\Facades\ProductCatalog;

// 1. Create product
$product = Product::create(['name' => 'T-Shirt', 'code' => 'TS-001', 'type' => ProductType::Simple]);

// 2. Create variant
$variant = $product->variants()->create(['sku' => 'TS-001-WHT', 'price' => 150000, 'is_default' => true]);

// 3. Set stock
$variant->inventoryItem()->create(['quantity' => 100, 'policy' => InventoryPolicy::Track]);

// 4. Publish
$product->publish();

// 5. Query
Product::published()->inStock()->with('variants')->get();

// config/product-catalog.php
return [

    // The Eloquent model used throughout the package (search drivers, API controller).
    // Override when extending the base Product model in your application.
    // Your model must extend Aliziodev\ProductCatalog\Models\Product.
    'model' => \Aliziodev\ProductCatalog\Models\Product::class,

    // Prefix for all package tables. Change BEFORE running migrations.
    'table_prefix' => env('PRODUCT_CATALOG_TABLE_PREFIX', 'catalog_'),

    'inventory' => [
        // Built-in: 'database' (tracks stock in DB), 'null' (always in stock).
        // Register custom drivers via ProductCatalog::extend().
        'driver' => env('PRODUCT_CATALOG_INVENTORY_DRIVER', 'database'),
    ],

    'slug' => [
        // Regenerate the slug prefix when the product name changes.
        'auto_generate'    => true,
        'separator'        => '-',
        // Length of the permanent random suffix (4–32). Recommended: 8.
        'route_key_length' => (int) env('PRODUCT_CATALOG_ROUTE_KEY_LENGTH', 8),
    ],

    'search' => [
        // Built-in: 'database' (default) or 'scout'.
        'driver' => env('PRODUCT_CATALOG_SEARCH_DRIVER', 'database'),
    ],

    'routes' => [
        // Set true to register the built-in read-only catalog API routes.
        'enabled'    => env('PRODUCT_CATALOG_ROUTES_ENABLED', false),
        'prefix'     => env('PRODUCT_CATALOG_ROUTES_PREFIX', 'catalog'),
        'middleware' => ['api'],
    ],
];

use Aliziodev\ProductCatalog\Models\Product;
use Aliziodev\ProductCatalog\Enums\ProductType;

// Simple product (single SKU)
$product = Product::create([
    'name'              => 'Wireless Mouse',
    'code'              => 'WM-001',        // optional parent SKU / product code
    'type'              => ProductType::Simple,
    'short_description' => 'Ergonomic wireless mouse, 2.4 GHz.',
    'meta_title'        => 'Wireless Mouse — Best Price',
    'meta'              => ['warranty' => '1 year'],
]);

// Lifecycle
$product->publish();    // draft → published, fires ProductPublished event
$product->unpublish();  // published → draft
$product->archive();    // → archived, fires ProductArchived event

// State checks
$product->isPublished();
$product->isDraft();
$product->isArchived();
$product->isSimple();
$product->isVariable();

use Aliziodev\ProductCatalog\Models\ProductVariant;
use Aliziodev\ProductCatalog\Enums\ProductType;

// Variable product
$product = Product::create([
    'name' => 'Running Shoes',
    'code' => 'RS-AIR',
    'type' => ProductType::Variable,
]);

// Define options
$colorOption = $product->options()->create(['name' => 'Color', 'position' => 1]);
$red  = $colorOption->values()->create(['value' => 'Red',  'position' => 1]);
$blue = $colorOption->values()->create(['value' => 'Blue', 'position' => 2]);

$sizeOption = $product->options()->create(['name' => 'Size', 'position' => 2]);
$size42 = $sizeOption->values()->create(['value' => '42', 'position' => 1]);
$size43 = $sizeOption->values()->create(['value' => '43', 'position' => 2]);

// Create variant
$variant = ProductVariant::create([
    'product_id'    => $product->id,
    'sku'           => 'RS-AIR-RED-42',
    'price'         => 850000,
    'compare_price' => 1000000,    // original price (for sale badge)
    'cost_price'    => 500000,     // internal cost
    'weight'        => 0.350,
    'length'        => 30,
    'width'         => 15,
    'height'        => 12,
    'is_default'    => true,
    'is_active'     => true,
    'meta'          => ['barcode' => '8991234567890'],
]);

// Attach option values to variant
$variant->optionValues()->sync([$red->id, $size42->id]);

// Auto-generate SKU from product code + option values
$variant->load('optionValues');
$suggested = $product->buildVariantSku($variant); // "RS-AIR-RED-42"

// Human-readable label
$variant->displayName();        // "Red / 42"

// Pricing helpers
$variant->isOnSale();           // true — compare_price > price
$variant->discountPercentage(); // 15 (int)

use Aliziodev\ProductCatalog\Facades\ProductCatalog;
use Aliziodev\ProductCatalog\Enums\InventoryReason;

$inventory = ProductCatalog::inventory(); // resolves configured driver

// Set absolute quantity
$inventory->set($variant, 50);

// Adjust (positive = restock, negative = deduct)
$inventory->adjust($variant, -5, InventoryReason::SALE, $order); // $order is optional reference model

// Query
$inventory->getQuantity($variant);        // available quantity (total − reserved)
$inventory->isInStock($variant);          // true
$inventory->canFulfill($variant, 10);     // true

// Built-in drivers:
// 'database' (default) — tracks stock in catalog_inventory_items
// 'null'               — always in stock, no DB writes (digital/unlimited goods)
// To use null driver: PRODUCT_CATALOG_INVENTORY_DRIVER=null in .env
// For per-variant unlimited stock use InventoryPolicy::Allow instead (more granular)

// Direct model helpers (InventoryItem)
$item = $variant->inventoryItem;
$item->availableQuantity();  // quantity - reserved_quantity
$item->reserve(3);           // increment reserved_quantity (no audit trail)
$item->release(3);           // decrement reserved_quantity (no audit trail)
$item->isLowStock();         // true if availableQuantity <= low_stock_threshold

use Aliziodev\ProductCatalog\Facades\ProductCatalog;
use Aliziodev\ProductCatalog\Enums\InventoryReason;

$inventory = ProductCatalog::inventory();

// 1. Customer places order — hold stock
$inventory->reserve($variant, 3, InventoryReason::ORDER_PLACED, $order);
// reserved_quantity: +3, total quantity: unchanged, available: −3

// 2a. Order cancelled — release the hold
$inventory->release($variant, 3, InventoryReason::ORDER_CANCELLED, $order);
// reserved_quantity: −3, total quantity: unchanged, available: +3

// 2b. Order fulfilled — convert reservation to permanent deduction
$inventory->commit($variant, 3, InventoryReason::ORDER_FULFILLED, $order);
// reserved_quantity: −3, total quantity: −3, available: unchanged

// reserve() throws InventoryException when available stock < requested
// commit() throws InventoryException when reserved_quantity < requested

use Aliziodev\ProductCatalog\Enums\InventoryReason;

// Restock
InventoryReason::PURCHASE        // 'purchase'
InventoryReason::RETURN_ITEM     // 'return'

// Deduction
InventoryReason::SALE            // 'sale'
InventoryReason::DAMAGE          // 'damage'
InventoryReason::EXPIRY          // 'expiry'

// Adjustment / Set
InventoryReason::CORRECTION      // 'correction'
InventoryReason::STOCKTAKE       // 'stocktake'

// Reserve
InventoryReason::ORDER_PLACED    // 'order_placed'
InventoryReason::CART_HOLD       // 'cart_hold'

// Release
InventoryReason::ORDER_CANCELLED // 'order_cancelled'
InventoryReason::CART_RELEASED   // 'cart_released'
InventoryReason::TIMEOUT         // 'timeout'

// Commit
InventoryReason::ORDER_FULFILLED // 'order_fulfilled'

'inventory' => [
    'movement_reasons' => [
        // built-in reasons ...
        'promotion',     // custom reason for your app
        'gift',
    ],
],

use Aliziodev\ProductCatalog\Models\Brand;
use Aliziodev\ProductCatalog\Models\Category;
use Aliziodev\ProductCatalog\Models\Tag;

// Brand
$brand = Brand::create(['name' => 'Nike', 'slug' => 'nike']);
$product->update(['brand_id' => $brand->id]);

// Category (supports parent–child nesting)
$apparel  = Category::create(['name' => 'Apparel',  'slug' => 'apparel']);
$shoes    = Category::create(['name' => 'Shoes',    'slug' => 'shoes', 'parent_id' => $apparel->id]);

$product->update(['primary_category_id' => $shoes->id]);

// Assign multiple categories
$product->categories()->sync([$apparel->id, $shoes->id]);

// Tags
$tag = Tag::create(['name' => 'new-arrival', 'slug' => 'new-arrival']);
$product->tags()->attach($tag);

// Status scopes
Product::published()->get();
Product::draft()->get();

// Price range (active variants only)
$product->minPrice();      // float|null
$product->maxPrice();      // float|null
$product->priceRange();    // ['min' => 850000.0, 'max' => 1200000.0] | null

// Stock scope — products with at least one purchasable active variant
// NOTE: variants without an inventoryItem record are excluded from this scope.
// Always create an inventoryItem when creating a variant, even for Allow policy.
Product::inStock()->get();

// Search across name, code, short_description, and variant SKUs
Product::search('RS-AIR')->get();

// Filter
Product::forBrand($brand)->published()->get();
Product::withTag($tag)->inStock()->get();

// Low stock alert
use Aliziodev\ProductCatalog\Models\InventoryItem;

InventoryItem::lowStock()->with('variant.product')->get();

$product = Product::create([
    'name' => 'Kaos Polo',
    'meta' => [
        'material'    => 'Katun 100%',
        'origin'      => 'Indonesia',
        'care'        => 'Cuci maks 30°C',
        'weight_gram' => 200,
    ],
]);

$product->meta['material']; // 'Katun 100%'

Schema::create('product_attributes', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('product_id'); // references catalog_products.id
    $table->string('key');
    $table->string('value');
    $table->index(['product_id', 'key']);
});

// app/Models/Product.php
class Product extends BaseProduct
{
    public function attributes(): HasMany
    {
        return $this->hasMany(ProductAttribute::class);
    }
}

Product::published()
    ->whereHas('attributes', fn ($q) =>
        $q->where('key', 'material')->where('value', 'Katun')
    )
    ->get();

use Aliziodev\ProductCatalog\Search\ProductSearchBuilder;

// Fluent API
ProductSearchBuilder::query('kemeja')
    ->inCategory('t-shirts')           // slug or ID
    ->withTags(['sale', 'new-arrival']) // AND logic, slug or ID
    ->forBrand('stylehouse')            // slug or ID
    ->priceBetween(50_000, 500_000)
    ->onlyInStock()
    ->sortBy('price')->sortAscending()
    ->paginate(24);

// Build from HTTP request — maps q, category, brand, tag/tags[],
// min_price, max_price, in_stock, type, sort_by, sort_direction
ProductSearchBuilder::fromRequest($request)->paginate(24);

// Control which relations are eager-loaded
ProductSearchBuilder::query('laptop')
    ->withRelations(['brand', 'primaryCategory', 'tags', 'defaultVariant'])
    ->paginate(20);

// config/product-catalog.php
'model' => \App\Models\Product::class,  // top-level — used by all subsystems
'search' => [
    'driver' => env('PRODUCT_CATALOG_SEARCH_DRIVER', 'database'),
],

// Custom search driver
use Aliziodev\ProductCatalog\Facades\ProductCatalog;

ProductCatalog::extendSearch('typesense', function ($app) {
    return new \App\Search\TypesenseSearchDriver;
});

// Find by slug (both old and new slugs resolve)
$product = Product::findBySlug('ergonomic-mouse-a1b2c3d4');
$product = Product::findBySlugOrFail('ergonomic-mouse-a1b2c3d4');

// Scope variant
Product::published()->bySlug($slug)->firstOrFail();

// config/product-catalog.php
'routes' => [
    'enabled' => true,
    'prefix'  => 'catalog',
],

use Aliziodev\ProductCatalog\Http\Resources\ProductResource;
use Aliziodev\ProductCatalog\Http\Resources\ProductVariantResource;

$product = Product::with(['brand', 'primaryCategory', 'tags', 'variants'])->findOrFail($id);

return ProductResource::make($product);

use Aliziodev\ProductCatalog\Events\ProductPublished;
use Aliziodev\ProductCatalog\Events\InventoryReserved;

class SendNewProductNotification
{
    public function handle(ProductPublished $event): void
    {
        // $event->product
    }
}

class HandleStockReservation
{
    public function handle(InventoryReserved $event): void
    {
        // $event->variant
        // $event->type          — MovementType::Reserve or MovementType::Release
        // $event->quantity      — positive for reserve, negative for release
        // $event->reservedBefore
        // $event->reservedAfter
        // $event->reason
        // $event->movement      — the InventoryMovement record
        // $event->isReserve()   — true when type is Reserve
        // $event->isRelease()   — true when type is Release
    }
}

use Aliziodev\ProductCatalog\Enums\InventoryPolicy;

$variant->inventoryItem()->create([
    'quantity'           => 0,
    'policy'             => InventoryPolicy::Allow,  // never runs out
    'low_stock_threshold' => null,
]);



namespace App\Models;

use Aliziodev\ProductCatalog\Models\Product as BaseProduct;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;

class Product extends BaseProduct implements HasMedia
{
    use InteractsWithMedia;

    public function registerMediaCollections(): void
    {
        $this->addMediaCollection('featured')
            ->singleFile()
            ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']);

        $this->addMediaCollection('gallery');
    }

    public function registerMediaConversions(?Media $media = null): void
    {
        $this->addMediaConversion('thumb')
            ->width(400)
            ->height(400)
            ->sharpen(5);

        $this->addMediaConversion('webp')
            ->format('webp')
            ->quality(80);
    }
}

// In your controllers / services — always use your extended model
use App\Models\Product;

$product = Product::with('variants')->findOrFail($id);
$product->getFirstMediaUrl('featured', 'thumb'); // ✓ works

// When coming from a variant relationship, re-query:
$product = App\Models\Product::find($variant->product_id);
$product->getFirstMediaUrl('featured', 'thumb'); // ✓ works

// app/Models/ProductVariant.php
namespace App\Models;

use Aliziodev\ProductCatalog\Models\ProductVariant as BaseVariant;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ProductVariant extends BaseVariant
{
    public function product(): BelongsTo
    {
        return $this->belongsTo(Product::class); // App\Models\Product
    }
}

// Upload featured image
$product->addMediaFromRequest('image')->toMediaCollection('featured');

// Upload gallery
$product->addMediaFromRequest('gallery')->toMediaCollection('gallery');

// Get URLs
$product->getFirstMediaUrl('featured', 'thumb');
$product->getMedia('gallery')->map->getUrl('webp');



namespace App\Inventory;

use Aliziodev\ProductCatalog\Contracts\InventoryProviderInterface;
use Aliziodev\ProductCatalog\Exceptions\InventoryException;
use Aliziodev\ProductCatalog\Models\ProductVariant;
use App\Models\Inventory; // your own inventory model
use Illuminate\Database\Eloquent\Model;

class AppInventoryProvider implements InventoryProviderInterface
{
    public function getQuantity(ProductVariant $variant): int
    {
        return Inventory::where('sku', $variant->sku)->value('quantity') ?? 0;
    }

    public function isInStock(ProductVariant $variant): bool
    {
        return $this->getQuantity($variant) > 0;
    }

    public function canFulfill(ProductVariant $variant, int $quantity): bool
    {
        return $this->getQuantity($variant) >= $quantity;
    }

    public function adjust(
        ProductVariant $variant,
        int $delta,
        string $reason = '',
        ?Model $reference = null,
    ): void {
        $record = Inventory::where('sku', $variant->sku)->firstOrFail();
        $newQty = $record->quantity + $delta;

        if ($newQty < 0) {
            throw InventoryException::insufficientStock(abs($delta), $record->quantity);
        }

        $record->update(['quantity' => $newQty]);
    }

    public function set(
        ProductVariant $variant,
        int $quantity,
        string $reason = '',
        ?Model $reference = null,
    ): void {
        Inventory::updateOrCreate(
            ['sku'      => $variant->sku],
            ['quantity' => max(0, $quantity)]
        );
    }

    public function reserve(
        ProductVariant $variant,
        int $quantity,
        string $reason = '',
        ?Model $reference = null,
    ): void {
        // implement reservation against your external system
    }

    public function release(
        ProductVariant $variant,
        int $quantity,
        string $reason = '',
        ?Model $reference = null,
    ): void {
        // implement release against your external system
    }

    public function commit(
        ProductVariant $variant,
        int $quantity,
        string $reason = '',
        ?Model $reference = null,
    ): void {
        // implement commit (reservation → permanent deduction) against your external system
    }
}

use Aliziodev\ProductCatalog\Facades\ProductCatalog;

public function boot(): void
{
    ProductCatalog::extend('app', function ($app) {
        return new \App\Inventory\AppInventoryProvider;
    });
}

// tests/TestCase.php
use Orchestra\Testbench\TestCase as OrchestraTestCase;
use Aliziodev\ProductCatalog\ProductCatalogServiceProvider;

abstract class TestCase extends OrchestraTestCase
{
    use \Illuminate\Foundation\Testing\RefreshDatabase;

    protected function getPackageProviders($app): array
    {
        return [ProductCatalogServiceProvider::class];
    }

    protected function defineDatabaseMigrations(): void
    {
        $this->loadMigrationsFrom(
            base_path('vendor/aliziodev/laravel-product-catalog/database/migrations')
        );
    }
}

// tests/Feature/CheckoutTest.php
use Aliziodev\ProductCatalog\Models\Product;
use Aliziodev\ProductCatalog\Models\ProductVariant;
use Aliziodev\ProductCatalog\Enums\InventoryPolicy;

it('can add item to cart', function () {
    $variant = ProductVariant::factory()->create(['price' => 150000]);

    // Use Allow policy — no inventory record needed
    $variant->inventoryItem()->create(['quantity' => 0, 'policy' => InventoryPolicy::Allow]);

    // ... your test assertions
});

config(['product-catalog.inventory.driver' => 'null']);
bash
php artisan catalog:install
bash
php artisan vendor:publish --tag=product-catalog-migrations
php artisan migrate
bash
php artisan vendor:publish --tag=product-catalog-config
bash
php artisan catalog:install
bash
composer vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan migrate