From 522ebdae34a7049d3aaab287e24788ebb5560909 Mon Sep 17 00:00:00 2001 From: Mekan1206 Date: Mon, 9 Feb 2026 00:48:22 +0500 Subject: [PATCH] Refactor product relations to use new `category_product` and `channel_product` tables - Updated queries and relationships in various controllers and models to replace `product_has_relations` with `category_product` and `channel_product`. - Adjusted methods in `ProductFilterer`, `FilterParamsController`, and `FilterController` to reflect the new database structure. - Modified seeder to insert data into the new `channel_product` table. - Updated tests to ensure compatibility with the new product relation structure. --- .../Product/Filter/ProductFilterer.php | 5 +- .../Api/V1/FilterParamsController.php | 7 +-- .../Api/V1/Filters/FilterController.php | 5 +- .../V1/Filters/Helpers/CategoriesFilter.php | 5 +- .../V1/Product/ProductRelatedController.php | 15 +++-- app/Models/Ecommerce/Channel/Channel.php | 5 +- .../Ecommerce/Product/Category/Category.php | 4 +- .../Product/Collection/Collection.php | 5 +- .../Product/Concerns/ProductRelationships.php | 16 ++--- .../Filters/ProductEntrepreneurFilter.php | 5 +- .../Ecommerce/Product/Product/Product.php | 5 +- .../Product/Category/CategoryRepository.php | 5 +- ..._001233_refactor_product_relationships.php | 61 +++++++++++++++++++ database/seeders/new/ProductsTableSeeder.php | 5 +- tests/Feature/Api/V1/ProductTest.php | 2 +- 15 files changed, 102 insertions(+), 48 deletions(-) create mode 100644 database/migrations/2026_02_09_001233_refactor_product_relationships.php diff --git a/app/Helpers/Ecommerce/Product/Filter/ProductFilterer.php b/app/Helpers/Ecommerce/Product/Filter/ProductFilterer.php index 9573ef1..2f1726e 100644 --- a/app/Helpers/Ecommerce/Product/Filter/ProductFilterer.php +++ b/app/Helpers/Ecommerce/Product/Filter/ProductFilterer.php @@ -81,11 +81,10 @@ class ProductFilterer $this->queryBuilder->whereIn( column: 'id', values: ( - fn ($query) => $query->from('product_has_relations') + fn ($query) => $query->from('category_product') ->select('product_id') ->distinct('product_id') - ->where('productable_type', '=', 'category') - ->whereIn('productable_id', explode(',', $this->request->categories)) + ->whereIn('category_id', explode(',', $this->request->categories)) ) ); } diff --git a/app/Http/Controllers/Api/V1/FilterParamsController.php b/app/Http/Controllers/Api/V1/FilterParamsController.php index da97a90..7fb1be9 100644 --- a/app/Http/Controllers/Api/V1/FilterParamsController.php +++ b/app/Http/Controllers/Api/V1/FilterParamsController.php @@ -38,10 +38,9 @@ class FilterParamsController extends Controller $category_id = (int) $request->category_id; $brand_ids = DB::table('products') - ->join('product_has_relations', function ($join) use ($category_id) { - $join->on('products.id', '=', 'product_has_relations.product_id') - ->where('product_has_relations.productable_id', '=', $category_id) - ->where('product_has_relations.productable_type', '=', 'category'); + ->join('category_product', function ($join) use ($category_id) { + $join->on('products.id', '=', 'category_product.product_id') + ->where('category_product.category_id', '=', $category_id); }) ->select(['id', 'brand_id']) ->pluck('brand_id'); diff --git a/app/Http/Controllers/Api/V1/Filters/FilterController.php b/app/Http/Controllers/Api/V1/Filters/FilterController.php index 1309b43..c2be3a2 100644 --- a/app/Http/Controllers/Api/V1/Filters/FilterController.php +++ b/app/Http/Controllers/Api/V1/Filters/FilterController.php @@ -97,9 +97,8 @@ class FilterController extends Controller ->distinct('products.id') ->pluck('products.id'); - return Category::where('is_visible', true)->ordered()->join('product_has_relations', 'categories.id', '=', 'product_has_relations.productable_id') - ->where('product_has_relations.productable_type', '=', 'category') - ->whereIntegerInRaw('product_has_relations.product_id', $products) + return Category::where('is_visible', true)->ordered()->join('category_product', 'categories.id', '=', 'category_product.category_id') + ->whereIntegerInRaw('category_product.product_id', $products) ->get(['id', 'parent_id', 'name']) ->unique('categories.id'); } diff --git a/app/Http/Controllers/Api/V1/Filters/Helpers/CategoriesFilter.php b/app/Http/Controllers/Api/V1/Filters/Helpers/CategoriesFilter.php index afe4a75..19944c7 100644 --- a/app/Http/Controllers/Api/V1/Filters/Helpers/CategoriesFilter.php +++ b/app/Http/Controllers/Api/V1/Filters/Helpers/CategoriesFilter.php @@ -87,9 +87,8 @@ class CategoriesFilter ->distinct('products.id') ->pluck('products.id'); - return $this->queryBuilder->join('product_has_relations', 'categories.id', '=', 'product_has_relations.productable_id') - ->where('product_has_relations.productable_type', '=', 'category') - ->whereIntegerInRaw('product_has_relations.product_id', $products) + return $this->queryBuilder->join('category_product', 'categories.id', '=', 'category_product.category_id') + ->whereIntegerInRaw('category_product.product_id', $products) ->get($this->columns) ->unique('categories.id'); } diff --git a/app/Http/Controllers/Api/V1/Product/ProductRelatedController.php b/app/Http/Controllers/Api/V1/Product/ProductRelatedController.php index 89ee544..5569f49 100644 --- a/app/Http/Controllers/Api/V1/Product/ProductRelatedController.php +++ b/app/Http/Controllers/Api/V1/Product/ProductRelatedController.php @@ -16,18 +16,17 @@ class ProductRelatedController extends Controller */ public function index(Product $product): JsonResponse { - $products = DB::table('product_has_relations') - ->select('product_has_relations.product_id') - ->whereIn('product_has_relations.productable_id', (function ($query) use ($product) { - $query->from('product_has_relations') - ->select('productable_id') - ->distinct('productable_id') - ->where('productable_type', '=', 'category') + $products = DB::table('category_product') + ->select('category_product.product_id') + ->whereIn('category_product.category_id', (function ($query) use ($product) { + $query->from('category_product') + ->select('category_id') + ->distinct('category_id') ->where('product_id', '=', $product->id); })) ->limit(12) ->orderByRaw('RANDOM()') - ->pluck('product_has_relations.product_id') + ->pluck('category_product.product_id') ->unique(); return response()->rest( diff --git a/app/Models/Ecommerce/Channel/Channel.php b/app/Models/Ecommerce/Channel/Channel.php index 83ee49c..a252926 100644 --- a/app/Models/Ecommerce/Channel/Channel.php +++ b/app/Models/Ecommerce/Channel/Channel.php @@ -11,6 +11,7 @@ use App\Repositories\System\Cache\CacheRepository; 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\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphToMany; @@ -179,9 +180,9 @@ class Channel extends Model implements HasMedia, Sortable /** * Products */ - public function products(): MorphToMany + public function products(): BelongsToMany { - return $this->morphToMany(Product::class, 'productable', 'product_has_relations'); + return $this->belongsToMany(Product::class, 'channel_product'); } /** diff --git a/app/Models/Ecommerce/Product/Category/Category.php b/app/Models/Ecommerce/Product/Category/Category.php index fa5826b..78ff825 100644 --- a/app/Models/Ecommerce/Product/Category/Category.php +++ b/app/Models/Ecommerce/Product/Category/Category.php @@ -152,9 +152,9 @@ class Category extends Model implements HasMedia, Sortable /** * Category Products */ - public function products(): MorphToMany + public function products(): BelongsToMany { - return $this->morphToMany(Product::class, 'productable', 'product_has_relations'); + return $this->belongsToMany(Product::class, 'category_product'); } /** diff --git a/app/Models/Ecommerce/Product/Collection/Collection.php b/app/Models/Ecommerce/Product/Collection/Collection.php index bdb2800..b565047 100644 --- a/app/Models/Ecommerce/Product/Collection/Collection.php +++ b/app/Models/Ecommerce/Product/Collection/Collection.php @@ -5,6 +5,7 @@ namespace App\Models\Ecommerce\Product\Collection; use App\Models\Ecommerce\Product\Product\Product; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\MorphToMany; use Spatie\EloquentSortable\Sortable; use Spatie\EloquentSortable\SortableTrait; @@ -109,9 +110,9 @@ class Collection extends Model implements HasMedia, Sortable /** * Products */ - public function products(): MorphToMany + public function products(): BelongsToMany { - return $this->morphToMany(Product::class, 'productable', 'product_has_relations'); + return $this->belongsToMany(Product::class, 'collection_product'); } /** diff --git a/app/Models/Ecommerce/Product/Product/Concerns/ProductRelationships.php b/app/Models/Ecommerce/Product/Product/Concerns/ProductRelationships.php index ec4491e..cb3b81a 100644 --- a/app/Models/Ecommerce/Product/Product/Concerns/ProductRelationships.php +++ b/app/Models/Ecommerce/Product/Product/Concerns/ProductRelationships.php @@ -38,9 +38,9 @@ trait ProductRelationships /** * Related Channels */ - public function channels(): MorphToMany + public function channels(): BelongsToMany { - return $this->morphedByMany(Channel::class, 'productable', 'product_has_relations'); + return $this->belongsToMany(Channel::class, 'channel_product'); } /** @@ -56,25 +56,25 @@ trait ProductRelationships /** * Related products (similar) */ - public function relatedProducts(): MorphToMany + public function relatedProducts(): BelongsToMany { - return $this->morphedByMany(Product::class, 'productable', 'product_has_relations'); + return $this->belongsToMany(Product::class, 'product_related', 'product_id', 'related_product_id'); } /** * Related categories */ - public function categories(): MorphToMany + public function categories(): BelongsToMany { - return $this->morphedByMany(Category::class, 'productable', 'product_has_relations'); + return $this->belongsToMany(Category::class, 'category_product'); } /** * Related Collections */ - public function collections(): MorphToMany + public function collections(): BelongsToMany { - return $this->morphedByMany(Collection::class, 'productable', 'product_has_relations'); + return $this->belongsToMany(Collection::class, 'collection_product'); } /** diff --git a/app/Nova/Resources/Ecommerce/Product/Product/Filters/ProductEntrepreneurFilter.php b/app/Nova/Resources/Ecommerce/Product/Product/Filters/ProductEntrepreneurFilter.php index cbe377c..ddb8eef 100644 --- a/app/Nova/Resources/Ecommerce/Product/Product/Filters/ProductEntrepreneurFilter.php +++ b/app/Nova/Resources/Ecommerce/Product/Product/Filters/ProductEntrepreneurFilter.php @@ -33,9 +33,8 @@ class ProductEntrepreneurFilter extends Filter */ public function apply(NovaRequest $request, $query, $value) { - $vendorProducts = DB::table('product_has_relations') - ->where('productable_type', 'channel') - ->where('productable_id', $value) + $vendorProducts = DB::table('channel_product') + ->where('channel_id', $value) ->pluck('product_id'); $query->whereIntegerInRaw('id', $vendorProducts); diff --git a/app/Nova/Resources/Ecommerce/Product/Product/Product.php b/app/Nova/Resources/Ecommerce/Product/Product/Product.php index 5dd86aa..2e8209b 100644 --- a/app/Nova/Resources/Ecommerce/Product/Product/Product.php +++ b/app/Nova/Resources/Ecommerce/Product/Product/Product.php @@ -123,9 +123,8 @@ class Product extends Resource $user = $request->user(); if ($user->hasRole('vendor')) { - $vendorProducts = DB::table('product_has_relations') - ->where('productable_type', 'channel') - ->where('productable_id', $user->channel()->id) + $vendorProducts = DB::table('channel_product') + ->where('channel_id', $user->channel()->id) ->pluck('product_id'); $query->whereIntegerInRaw('id', $vendorProducts); diff --git a/app/Repositories/Ecommerce/Product/Category/CategoryRepository.php b/app/Repositories/Ecommerce/Product/Category/CategoryRepository.php index 7b7653e..8c2e68e 100644 --- a/app/Repositories/Ecommerce/Product/Category/CategoryRepository.php +++ b/app/Repositories/Ecommerce/Product/Category/CategoryRepository.php @@ -182,9 +182,8 @@ class CategoryRepository ])) { $products = $resource->products()->distinct('products.id')->pluck('products.id'); - return $this->queryBuilder->join('product_has_relations', 'categories.id', '=', 'product_has_relations.productable_id') - ->where('product_has_relations.productable_type', '=', 'category') - ->whereIntegerInRaw('product_has_relations.product_id', $products) + return $this->queryBuilder->join('category_product', 'categories.id', '=', 'category_product.category_id') + ->whereIntegerInRaw('category_product.product_id', $products) ->distinct('categories.id') ->get(['id', 'slug', 'name']); } diff --git a/database/migrations/2026_02_09_001233_refactor_product_relationships.php b/database/migrations/2026_02_09_001233_refactor_product_relationships.php new file mode 100644 index 0000000..a95bc17 --- /dev/null +++ b/database/migrations/2026_02_09_001233_refactor_product_relationships.php @@ -0,0 +1,61 @@ +foreignId('product_id')->constrained()->cascadeOnDelete(); + $table->foreignId('category_id')->constrained()->cascadeOnDelete(); + // No primary key on pivot usually, but maybe composite unique? + // product_has_relations didn't have one explicitly shown but usually pivots do. + // Let's add an index for performance. + $table->unique(['product_id', 'category_id']); + }); + + Schema::create('collection_product', function (Blueprint $table) { + $table->foreignId('product_id')->constrained()->cascadeOnDelete(); + $table->foreignId('collection_id')->constrained()->cascadeOnDelete(); + $table->unique(['product_id', 'collection_id']); + }); + + Schema::create('channel_product', function (Blueprint $table) { + $table->foreignId('product_id')->constrained()->cascadeOnDelete(); + $table->foreignId('channel_id')->constrained()->cascadeOnDelete(); + $table->unique(['product_id', 'channel_id']); + }); + + Schema::create('product_related', function (Blueprint $table) { + $table->foreignId('product_id')->constrained()->cascadeOnDelete(); + $table->foreignId('related_product_id')->constrained('products')->cascadeOnDelete(); + $table->unique(['product_id', 'related_product_id']); + }); + + // 2. Migrate data + // Using raw SQL for efficiency and simplicity + DB::statement("INSERT INTO category_product (product_id, category_id) SELECT product_id, productable_id FROM product_has_relations WHERE productable_type = 'category'"); + DB::statement("INSERT INTO collection_product (product_id, collection_id) SELECT product_id, productable_id FROM product_has_relations WHERE productable_type = 'collection'"); + DB::statement("INSERT INTO channel_product (product_id, channel_id) SELECT product_id, productable_id FROM product_has_relations WHERE productable_type = 'channel'"); + DB::statement("INSERT INTO product_related (product_id, related_product_id) SELECT product_id, productable_id FROM product_has_relations WHERE productable_type = 'product'"); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('category_product'); + Schema::dropIfExists('collection_product'); + Schema::dropIfExists('channel_product'); + Schema::dropIfExists('product_related'); + } +}; diff --git a/database/seeders/new/ProductsTableSeeder.php b/database/seeders/new/ProductsTableSeeder.php index 06d2171..8e81149 100644 --- a/database/seeders/new/ProductsTableSeeder.php +++ b/database/seeders/new/ProductsTableSeeder.php @@ -45,10 +45,9 @@ class ProductsTableSeeder extends Seeder }); $channel = $user ? $user->channel() : tmpostChannel(); - DB::table('product_has_relations')->insert([ + DB::table('channel_product')->insert([ 'product_id' => $data['id'], - 'productable_id' => $channel->id, - 'productable_type' => 'channel', + 'channel_id' => $channel->id, ]); } diff --git a/tests/Feature/Api/V1/ProductTest.php b/tests/Feature/Api/V1/ProductTest.php index acc1abf..6bfc20b 100644 --- a/tests/Feature/Api/V1/ProductTest.php +++ b/tests/Feature/Api/V1/ProductTest.php @@ -217,7 +217,7 @@ test('can list related products', function () { // Then it selects product_ids from those categories. // It doesn't explicitly exclude the main product ID in the query shown in tool output? // Let's check the controller code again. - // It selects `product_has_relations.product_id` where `productable_id` IN (categories of main product). + // It selects `category_product.product_id` where `category_id` IN (categories of main product). // It does NOT seem to exclude the main product ID in the query. // However, `unique()` is used. // If the main product is returned, count would be 3. If excluded, 2.