Skip to content

Instantly share code, notes, and snippets.

@atomjoy
Last active February 19, 2025 19:28
Show Gist options
  • Save atomjoy/3d9f5fb216a5193e8560ff4a35c2e835 to your computer and use it in GitHub Desktop.
Save atomjoy/3d9f5fb216a5193e8560ff4a35c2e835 to your computer and use it in GitHub Desktop.
Product Attribute Management in Laravel, storing Product attributes such as Size, Color, Weight.

Product Attribute Management in Laravel

Product Attribute Management in Laravel, storing Product attributes such as Size, Color, Weight.

Product variants (many to many)

Products (id, name, desc, visible)
# 1, Shirt, Description, true
# 2, Laptop, Description, true

Skus (id, price, slug, product_id, stock_qty, on_stock = true)
# 1, 5.00, 'sku-100', 1, 55 // S-Blue
# 2, 6.00, 'sku-200', 1, 66 // S-Red
# 3, 7.00, 'sku-300', 1, 77 // M-Blue
# 4, 8.00, 'sku-400', 1, 88 // M-Red
# 10, 500, 'sku-501', 2, 3  // Intel-32GB
# 11, 600, 'sku-502', 2, 5  // Intel-64GB
# 12, 500, 'sku-601', 2, 8  // Amd-32GB
# 13, 600, 'sku-602', 2, 4  // Amd-64GB

Attributes (id, product_id, name)
# 1, 1, Size
# 2, 1, Color
# 3, 2, Procesor
# 4, 2, Ram

Properties (id, attribute_id, name)
# 1, 1, S
# 2, 1, M
# 3, 2, Blue
# 4, 2, Red
# 5, 3, Intel
# 6, 3, Amd
# 7, 4, 32GB
# 8, 4, 64GB

# Pivot table (or with 'property_id' not 'value')
attribute_sku (id, attribute_id, sku_id, value)
# 1,1,1,S
# 2,2,1,Blue
# 3,1,2,S
# 4,2,2,Red
# 5,1,3,M
# 6,2,3,Blue
# 7,1,4,M
# 8,2,4,Red
# 21,3,10,Intel
# 22,4,10,32GB
# 23,3,11,Intel
# 24,4,11,64GB
# 25,3,12,Amd
# 26,4,12,32GB
# 27,3,13,Amd
# 28,4,13,64GB

Models

Product

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Product extends Model
{
	/** @use HasFactory<\Database\Factories\ProductFactory> */
	// use HasFactory;

	protected $with = [];

	protected $guarded = [];

	protected function casts(): array
	{
		return [
			'created_at' => 'datetime:Y-m-d H:i:s',
		];
	}

	public function skus(): HasMany
	{
		return $this->hasMany(Sku::class, 'product_id');
	}
}

Sku

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Sku extends Model
{
	/** @use HasFactory<\Database\Factories\SkuFactory> */
	// use HasFactory;

	protected $with = ['attributes', 'images', 'product'];

	protected $guarded = [];

	protected function casts(): array
	{
		return [
			'created_at' => 'datetime:Y-m-d H:i:s',
		];
	}

	public function product(): BelongsTo
	{
		return $this->belongsTo(Product::class, 'product_id');
	}

	public function attributes(): BelongsToMany
	{
		return $this->belongsToMany(Attribute::class, 'attribute_sku')->withPivot(['value']);
	}

	public function images(): MorphMany
	{
		return $this->morphMany(Image::class, 'imageable')->chaperone();
	}

	public function latestImage(): MorphOne
	{
		return $this->morphOne(Image::class, 'imageable')->latestOfMany();
	}

	public function oldestImage(): MorphOne
	{
		return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
	}

	public function bestImage(): MorphOne
	{
		return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
	}
}

Attribute

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Attribute extends Model
{
	/** @use HasFactory<\Database\Factories\AttributeFactory> */
	// use HasFactory;

	protected $with = [];

	protected $guarded = [];

	protected function casts(): array
	{
		return [
			'created_at' => 'datetime:Y-m-d H:i:s',
		];
	}

	public function product(): BelongsTo
	{
		return $this->belongsTo(Product::class, 'product_id');
	}

	public function skus(): BelongsToMany
	{
		return $this->belongsToMany(Sku::class, 'attribute_sku')->withPivot(['value']);
	}
}

Image

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Image extends Model
{
	/** @use HasFactory<\Database\Factories\ImageFactory> */
	// use HasFactory;

	protected $with = [];

	protected $guarded = [];

	protected function casts(): array
	{
		return [
			'created_at' => 'datetime:Y-m-d H:i:s',
		];
	}

	/**
	 * Get the parent model (skus or other).
	 */
	public function imageable(): MorphTo
	{
		return $this->morphTo();
	}
}

Separate the attributes from each other

$product->skus->pluck('attributes')->flatten()->groupBy('name')->map(function ($item) {
	return $item->keyBy('pivot.value');
});

Links

https://laracasts.com/discuss/channels/laravel/best-way-to-store-product-attributes-like-size-color-etc?page=1&replyId=752232
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Products
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->nullable()->default('product-' . uniqid());
$table->text('description')->nullable()->default('');
$table->unsignedTinyInteger('on_stock')->nullable()->default(1);
$table->unsignedTinyInteger('visible')->nullable()->default(1);
$table->unsignedInteger('sorting')->nullable()->default(0);
$table->timestamps();
$table->unique('name');
$table->unique('slug');
});
// Product attributes
Schema::create('attributes', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('product_id');
$table->string('name');
$table->timestamps();
$table->unique(['product_id', 'name']);
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
});
// Attributes properties
Schema::create('props', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('attribute_id');
$table->string('name');
$table->timestamps();
$table->unique(['attribute_id', 'name']);
$table->foreign('attribute_id')->references('id')->on('attributes')->onDelete('cascade');
});
// Product variants
Schema::create('skus', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('product_id');
$table->unsignedBigInteger('price')->default(0);
$table->string('slug')->nullable()->default('sku-' . uniqid());
$table->unsignedBigInteger('stock_quantity')->nullable()->default(1);
$table->unsignedTinyInteger('on_stock')->nullable()->default(1);
$table->unsignedTinyInteger('visible')->nullable()->default(1);
$table->timestamps();
$table->unique('slug');
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
});
// Pivot table product variants and attributes
Schema::create('attribute_sku', function (Blueprint $table) {
$table->id('id');
$table->foreignId('attribute_id')->constrained('attributes')->onDelete('cascade');
$table->foreignId('sku_id')->constrained('skus')->onDelete('cascade');
$table->string('value');
$table->timestamps();
$table->unique(['attribute_id', 'sku_id']);
// Or with props without value
// $table->unsignedBigInteger('prop_id');
// $table->foreign('prop_id')->references('id')->on('props')->onUpdate('cascade')->onDelete('cascade');
});
// Sku images
Schema::create('images', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('imageable_id');
$table->string('imageable_type');
$table->string('url');
$table->unsignedBigInteger('likes')->nullable()->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('images');
Schema::dropIfExists('attribute_sku');
Schema::dropIfExists('skus');
Schema::dropIfExists('props');
Schema::dropIfExists('attributes');
Schema::dropIfExists('products');
}
};
[
{
"id":6,
"sku":"sku-65cde5760ca3c",
"product_id":10,
"price":55,
"order":1,
"quantity":10,
"attributes":[
{
"id":1,
"name":"Color",
"pivot":{
"sku_id":6,
"attribute_id":1,
"value":"red"
}
},
{
"id":2,
"name":"Size",
"pivot":{
"sku_id":6,
"attribute_id":2,
"value":"small"
}
}
]
},
{
"id":7,
"sku":"sku-65cde576963e1",
"product_id":10,
"price":56,
"order":1,
"quantity":10,
"attributes":[
{
"id":1,
"name":"Color",
"pivot":{
"sku_id":7,
"attribute_id":1,
"value":"red"
}
},
{
"id":2,
"name":"Size",
"pivot":{
"sku_id":7,
"attribute_id":2,
"value":"medium"
}
}
]
},
{
"id":8,
"sku":"sku-65cde577349dc",
"product_id":10,
"price":57,
"order":1,
"quantity":20,
"attributes":[
{
"id":1,
"name":"Color",
"pivot":{
"sku_id":8,
"attribute_id":1,
"value":"blue"
}
},
{
"id":2,
"name":"Size",
"pivot":{
"sku_id":8,
"attribute_id":2,
"value":"small"
}
}
]
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment