From 449a542571e09e7f7a31cf2163bacee7f68c7203 Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Sun, 31 May 2026 20:18:53 +0330 Subject: [PATCH] feat: Many category and Many sub-category media --- app/Http/Controllers/MediaController.php | 102 +++++++++++------- app/Models/Category.php | 5 + app/Models/Media.php | 10 +- app/Models/SubCategory.php | 6 +- ...000_create_media_category_pivot_tables.php | 79 ++++++++++++++ ...drop_category_columns_from_media_table.php | 36 +++++++ 6 files changed, 190 insertions(+), 48 deletions(-) create mode 100644 database/migrations/2026_05_31_100000_create_media_category_pivot_tables.php create mode 100644 database/migrations/2026_05_31_100100_drop_category_columns_from_media_table.php diff --git a/app/Http/Controllers/MediaController.php b/app/Http/Controllers/MediaController.php index 597eae1..0866d81 100644 --- a/app/Http/Controllers/MediaController.php +++ b/app/Http/Controllers/MediaController.php @@ -4,6 +4,7 @@ use App\Models\Media; use App\Models\Category; +use App\Models\SubCategory; use App\Models\Tag; use Illuminate\Support\Facades\DB; use Illuminate\Http\Request; @@ -18,8 +19,10 @@ public function store(Request $request) 'title' => 'required|string|max:255', 'caption' => 'nullable|string', 'type' => 'required|in:audio,video', - 'category_id' => 'nullable|exists:categories,id', - 'subcategory_id' => 'nullable|exists:sub_categories,id', + 'category_ids' => 'nullable|array', + 'category_ids.*' => 'integer|exists:categories,id', + 'subcategory_ids' => 'nullable|array', + 'subcategory_ids.*' => 'integer|exists:sub_categories,id', 'image_id' => 'nullable|exists:images,id', 'duration' => 'nullable|integer', 'is_premium' => 'nullable|boolean', @@ -31,8 +34,6 @@ public function store(Request $request) 'tags.*' => 'string', ]); - $categoryId = $data['category_id'] ?? null; - $path = null; if ($request->hasFile('file')) { $path = $request->file('file')->store('media', 'public'); @@ -46,12 +47,14 @@ public function store(Request $request) 'file_path' => $path, 'external_url' => $data['external_url'] ?? null, 'image_id' => $data['image_id'] ?? null, - 'category_id' => $categoryId, - 'subcategory_id' => $data['subcategory_id'] ?? null, 'duration' => $data['duration'] ?? null, 'visibility' => $data['visibility'] ?? 'public', 'is_premium'=> $data['is_premium'] ?? false ]); + + $media->categories()->sync($data['category_ids'] ?? []); + $media->subCategories()->sync($data['subcategory_ids'] ?? []); + if (!empty($data['tags'])) { $tagIds = []; @@ -64,12 +67,12 @@ public function store(Request $request) } return response()->json([ 'message' => 'Media created successfully', - 'media' => $media->load(['image', 'category', 'subCategory', 'tags']), + 'media' => $media->load(['image', 'categories', 'subCategories', 'tags']), ]); } public function index(Request $request) { - $query = Media::with(['image', 'category', 'subCategory', 'myNote', 'tags','comments']) + $query = Media::with(['image', 'categories', 'subCategories', 'myNote', 'tags','comments']) ->where(function ($q) { $q->where('visibility', 'public') ->orWhere('user_id', auth()->id()); @@ -83,12 +86,16 @@ public function index(Request $request) if ($request->filled('categories')) { $categories = explode(',', $request->categories); - $query->whereIn('category_id', $categories); + $query->whereHas('categories', function ($q) use ($categories) { + $q->whereIn('categories.id', $categories); + }); } if ($request->filled('subcategories')) { $subcategories = explode(',', $request->subcategories); - $query->whereIn('subcategory_id', $subcategories); + $query->whereHas('subCategories', function ($q) use ($subcategories) { + $q->whereIn('sub_categories.id', $subcategories); + }); } /* @@ -164,9 +171,12 @@ public function index(Request $request) $q->where('title', 'LIKE', "%$search%") ->orWhere('caption', 'LIKE', "%$search%") - ->orWhereHas('category', function ($c) use ($search) { + ->orWhereHas('categories', function ($c) use ($search) { $c->where('name', 'LIKE', "%$search%"); }) + ->orWhereHas('subCategories', function ($s) use ($search) { + $s->where('name', 'LIKE', "%$search%"); + }) ->orWhereHas('tags', function ($t) use ($search) { $t->where('name', 'LIKE', "%$search%"); }); @@ -266,21 +276,28 @@ public function filters(Request $request) |-------------------------------------------------------------------------- */ - $categories = Category::select( - 'categories.id', - 'categories.name', - DB::raw('COUNT(media.id) as media_count') - ) - ->leftJoin('media', function ($join) { - $join->on('categories.id', '=', 'media.category_id') - ->where(function ($q) { - $q->where('media.visibility', 'public') - ->orWhere('media.user_id', auth()->id()); - }); - }) - ->groupBy('categories.id', 'categories.name') + $visibleMedia = function ($q) { + $q->where(function ($inner) { + $inner->where('media.visibility', 'public') + ->orWhere('media.user_id', auth()->id()); + }); + }; + + $categories = Category::query() + ->withCount(['media as media_count' => $visibleMedia]) ->orderByDesc('media_count') - ->get(); + ->get(['id', 'name']); + + /* + |-------------------------------------------------------------------------- + | 1️⃣.5 Subcategories with media count + |-------------------------------------------------------------------------- + */ + + $subcategories = SubCategory::query() + ->withCount(['media as media_count' => $visibleMedia]) + ->orderByDesc('media_count') + ->get(['id', 'category_id', 'name']); /* @@ -314,8 +331,9 @@ public function filters(Request $request) ->get(); return response()->json([ - 'categories' => $categories, - 'durations' => $durations, + 'categories' => $categories, + 'subcategories' => $subcategories, + 'durations' => $durations, ]); } @@ -323,8 +341,8 @@ public function show($id) { $media = Media::with([ 'image', - 'category', - 'subCategory', + 'categories', + 'subCategories', 'myNote', 'tags', 'comments' => function($query) { @@ -361,8 +379,8 @@ public function show($id) 'updated_at' => $media->updated_at, 'is_premium' => $media->is_premium, 'image' => $media->image, - 'category' => $media->category, - 'sub_category' => $media->subCategory, + 'categories' => $media->categories, + 'sub_categories' => $media->subCategories, 'tags' => $media->tags, 'myNote' => $media->myNote, 'is_saved' => $media->is_saved, @@ -444,8 +462,10 @@ public function update(Request $request, $id) 'caption' => 'nullable|string', 'type' => 'nullable|in:audio,video', - 'category_id' => 'nullable|exists:categories,id', - 'subcategory_id' => 'nullable|exists:sub_categories,id', + 'category_ids' => 'nullable|array', + 'category_ids.*' => 'integer|exists:categories,id', + 'subcategory_ids' => 'nullable|array', + 'subcategory_ids.*' => 'integer|exists:sub_categories,id', 'is_premium' => 'nullable|boolean', 'image_id' => 'nullable|exists:images,id', 'duration' => 'nullable|integer', @@ -456,8 +476,6 @@ public function update(Request $request, $id) 'tags.*' => 'string', ]); - $categoryId = $data['category_id'] ?? $media->category_id; - // --- handle file replace --- if ($request->hasFile('file')) { Storage::disk('public')->delete($media->file_path); @@ -469,8 +487,6 @@ public function update(Request $request, $id) 'title' => $data['title'] ?? $media->title, 'caption' => $data['caption'] ?? $media->caption, 'type' => $data['type'] ?? $media->type, - 'category_id' => $categoryId, - 'subcategory_id' => array_key_exists('subcategory_id', $data) ? $data['subcategory_id'] : $media->subcategory_id, 'is_premium' => $data['is_premium'] ?? $media->is_premium, 'duration' => $data['duration'] ?? $media->duration, 'external_url' => $data['external_url'] ?? $media->external_url, @@ -490,6 +506,14 @@ public function update(Request $request, $id) // --- update media --- $media->update($updateData); + if (array_key_exists('category_ids', $data)) { + $media->categories()->sync($data['category_ids'] ?? []); + } + + if (array_key_exists('subcategory_ids', $data)) { + $media->subCategories()->sync($data['subcategory_ids'] ?? []); + } + if (isset($data['tags'])) { $tagIds = []; @@ -503,7 +527,7 @@ public function update(Request $request, $id) return response()->json([ 'message' => 'Media updated successfully', - 'media' => $media->load(['image', 'category' , 'tags']), + 'media' => $media->load(['image', 'categories', 'subCategories', 'tags']), ]); } @@ -540,7 +564,7 @@ public function toggleSaveMedia($id) // GET saved public function saved() { - return auth()->user()->savedMedia()->with(['image','category', 'subCategory', 'myNote' , 'tags'])->get(); + return auth()->user()->savedMedia()->with(['image','categories', 'subCategories', 'myNote' , 'tags'])->get(); } public function storeNote(Request $request, $mediaId) diff --git a/app/Models/Category.php b/app/Models/Category.php index e2f5ae5..9e7ed7b 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -17,5 +17,10 @@ public function subcategories() { return $this->hasMany(SubCategory::class); } + + public function media() + { + return $this->belongsToMany(Media::class, 'category_media'); + } } diff --git a/app/Models/Media.php b/app/Models/Media.php index b1aa826..613e04e 100644 --- a/app/Models/Media.php +++ b/app/Models/Media.php @@ -13,8 +13,6 @@ class Media extends Model protected $fillable = [ 'user_id', 'image_id', - 'category_id', - 'subcategory_id', 'title', 'caption', 'type', @@ -45,14 +43,14 @@ public function image() return $this->belongsTo(Image::class); } - public function category() + public function categories() { - return $this->belongsTo(Category::class); + return $this->belongsToMany(Category::class, 'category_media'); } - public function subCategory() + public function subCategories() { - return $this->belongsTo(SubCategory::class, 'subcategory_id'); + return $this->belongsToMany(SubCategory::class, 'media_sub_category'); } public function tags() { diff --git a/app/Models/SubCategory.php b/app/Models/SubCategory.php index e9eb0d2..10dc58d 100644 --- a/app/Models/SubCategory.php +++ b/app/Models/SubCategory.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; class SubCategory extends Model { @@ -17,8 +17,8 @@ public function category(): BelongsTo return $this->belongsTo(Category::class); } - public function media(): HasMany + public function media(): BelongsToMany { - return $this->hasMany(Media::class, 'subcategory_id'); + return $this->belongsToMany(Media::class, 'media_sub_category'); } } diff --git a/database/migrations/2026_05_31_100000_create_media_category_pivot_tables.php b/database/migrations/2026_05_31_100000_create_media_category_pivot_tables.php new file mode 100644 index 0000000..b7ac73e --- /dev/null +++ b/database/migrations/2026_05_31_100000_create_media_category_pivot_tables.php @@ -0,0 +1,79 @@ +id(); + $table->foreignId('media_id')->constrained('media')->cascadeOnDelete(); + $table->foreignId('category_id')->constrained('categories')->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['media_id', 'category_id']); + }); + + Schema::create('media_sub_category', function (Blueprint $table) { + $table->id(); + $table->foreignId('media_id')->constrained('media')->cascadeOnDelete(); + $table->foreignId('sub_category_id')->constrained('sub_categories')->cascadeOnDelete(); + $table->timestamps(); + + $table->unique(['media_id', 'sub_category_id']); + }); + + // Backfill the new pivots from the existing single columns. + if (Schema::hasColumn('media', 'category_id')) { + DB::table('media') + ->whereNotNull('category_id') + ->orderBy('id') + ->select('id', 'category_id') + ->chunk(200, function ($rows) { + $now = now(); + $insert = $rows->map(fn ($row) => [ + 'media_id' => $row->id, + 'category_id' => $row->category_id, + 'created_at' => $now, + 'updated_at' => $now, + ])->all(); + + DB::table('category_media')->insertOrIgnore($insert); + }); + } + + if (Schema::hasColumn('media', 'subcategory_id')) { + DB::table('media') + ->whereNotNull('subcategory_id') + ->orderBy('id') + ->select('id', 'subcategory_id') + ->chunk(200, function ($rows) { + $now = now(); + $insert = $rows->map(fn ($row) => [ + 'media_id' => $row->id, + 'sub_category_id' => $row->subcategory_id, + 'created_at' => $now, + 'updated_at' => $now, + ])->all(); + + DB::table('media_sub_category')->insertOrIgnore($insert); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('media_sub_category'); + Schema::dropIfExists('category_media'); + } +}; diff --git a/database/migrations/2026_05_31_100100_drop_category_columns_from_media_table.php b/database/migrations/2026_05_31_100100_drop_category_columns_from_media_table.php new file mode 100644 index 0000000..cd20d4a --- /dev/null +++ b/database/migrations/2026_05_31_100100_drop_category_columns_from_media_table.php @@ -0,0 +1,36 @@ +dropForeign(['category_id']); + $table->dropColumn('category_id'); + + $table->dropForeign(['subcategory_id']); + $table->dropColumn('subcategory_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('media', function (Blueprint $table) { + $table->unsignedBigInteger('category_id')->nullable()->after('image_id'); + $table->foreign('category_id')->references('id')->on('categories')->nullOnDelete(); + + $table->unsignedBigInteger('subcategory_id')->nullable()->after('category_id'); + $table->foreign('subcategory_id')->references('id')->on('sub_categories')->nullOnDelete(); + }); + } +};