Pada saat membuat project dengan studi kasus tertentu, pastinya kita akan berurusan dengan tabel-tabel yang saling ber-relasi. Oleh karena itu membuat CRUD dengan beberapa tabel yang ber-relasi ini akan sangat penting untuk dipelajari.
Sebelum kita memulai membahas tentang Membuat CRUD dengan Beberapa Tabel di Laravel, saya asumsikan bahwa Anda telah menginstall project laravel dari awal dan juga telah melakukan konfigurasi database. Jika belum, Anda dapat mengikuti Tutorial Instalasi Laravel 10 yang telah diposting sebelumnya.
Struktur Tabel
Kita mempunyai Product, Brand, Category. Relasinya adalah Brand mempunyai banyak Product. Product bisa masuk ke dalam beberapa Category, dan juga sebaliknya Category pasti mempunyai banyak Product. Struktur tabelnya kurang lebih seperti berikut:
brands
id - integer
name - string
categories
id - integer
name - string
category_product
category_id - integer
product_id - integer
products
id - integer
brand_id - integer
sku - string
name - string
price - decimal
stock - integer
Membuat Model dan Migration
Pada tahapan ini kita akan membuat file mgiration, model dan mendefinisian relasi antar model tersebut.
Brand Model dan Migration
Jalankan perintah berikut untuk membuat model Brand dan file migrationnya:
php artisan make:model Brand -m
File migration untuk Brand (xxxxx_create_brands_table.php) akan berisi class migration seperti berikut:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('brands', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
$table->string('name');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('brands');
}
};
sedangkan untuk model Brand (app/Models/Brand.php) akan seperti berikut:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Models\Product;
class Brand extends Model
{
use HasFactory;
protected $fillable = [
'slug',
'name',
];
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}
Di dalam class Model Brand di atas pada method products() mendefinisikan bahwa Brand mempunyai banyak Product.
Category Model dan Migration
Jalankan perintah berikut untuk membuat model Category dan file migrationnya:
php artisan make:model Category -m
File migration untuk Category (xxxx_create_categories_tabel.php) akan berisi class migration seperti berikut:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
$table->string('name');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('categories');
}
};
sedangkan untuk model Category (app/Models/Category.php) akan seperti berikut:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use App\Models\Product;
class Category extends Model
{
use HasFactory;
protected $fillable = [
'slug',
'name',
];
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class);
}
}
Product Model dan Migration
Jalankan perintah berikut untuk membuat model Product dan file migrationnya:
php artisan make:model Product -m
File migration xxxx_create_products_table.php akan berisi class migratio seperti berikut:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('brand_id')->nullable()->constrained();
$table->string('sku')->index();;
$table->string('name');
$table->decimal('price', 15, 2)->nullable();
$table->integer('stock')->default(0);
$table->timestamps();
});
Schema::create('category_product', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained();
$table->foreignId('product_id')->constrained();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('category_product');
Schema::dropIfExists('products');
}
};
Pada class migration di atas terdapat proses pembuatan tabel pivot category_product
. Sedangkan untuk class model Product akan menjadi seperti berikut:
<?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 Product extends Model
{
use HasFactory;
protected $fillable = [
'brand_id',
'sku',
'name',
'price',
'stock',
];
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class);
}
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class);
}
}
Pada model Product tersebut kita definisikan relasi ke model Brand pada method brand()
dan juga relasi ke model Category pada method categories()
.
Membuat Controller
Buat ProductController
resource dengan perintah:
php artisan make:controller ProductController -r
Perintah di atas akan membuat file app/Http/Controllers/ProductController.php
yang di dalamnya terdapat method: index
, create
, store
, show
, edit
, update
, dan destroy
.
Membuat Route
Untuk routes (routes/web.php) yang perlu didefinisikan masih sama persis dengan tutorial CRUD single tabel. Routenya adalah seperti berikut:
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController;
Route::get('products', [ProductController::class, 'index'])->name('products.index');
Route::get('products/create', [ProductController::class, 'create'])->name('products.create');
Route::post('products', [ProductController::class, 'store'])->name('products.store');
Route::get('products/{id}/edit', [ProductController::class, 'edit'])->name('products.edit');
Route::put('products/{id}', [ProductController::class, 'update'])->name('products.update');
Route::delete('products/{id}', [ProductController::class, 'destroy'])->name('products.destroy');
Membuat Validasi dengan Form Request
Disini kita membutuhkan validasi input saat store produk dan update produk. Pertama, kita akan membuat validasi dengan Form Request untuk StoreProductRequest
dengan perintah:
php artisan make:request StoreProductRequest
Update file app/Http/Requests/StoreProductRequest.php
menjadi seperti berikut:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'sku' => ['required', 'unique:products', 'max:100'],
'name' => ['required', 'max:100'],
'price' => ['required', 'numeric', 'min:1'],
'stock' => ['required', 'numeric', 'min:0'],
'brand_id' => ['required'],
'category_ids' => ['required', 'array', 'min:2']
];
}
}
Ada yang sedikit berbeda pada validasi input category_ids
di atas, yaitu kita akan mengharuskan user memilih minimal 2 category pada saat input produk baru.
Kemudian, kita akan membuat UpdateProductRequest
dengan perintah:
php artisan make:request UpdateProductRequest
Update file app/Http/Requests/UpdateProductRequest.php
menjadi seperti berikut:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateProductRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'sku' => [
'required',
'max:100',
Rule::unique('products')->ignore($this->id),
],
'name' => ['required', 'max:100'],
'price' => ['required', 'numeric', 'min:1'],
'stock' => ['required', 'numeric', 'min:0'],
'brand_id' => ['required'],
'category_ids' => ['required', 'array', 'min:2']
];
}
}
Pada validasi update produk ini ada sedikit perbedaan. Yaitu saat validasi unique
untuk SKU
. Jadi pada saat update, kita perlu menambahkan ignore
terhadap id produk itu sendiri. Karena kalau tidak di ignore, maka akan mentog di validasi SKU harus unik, padahal SKU tersebut adalah milik dari produk yang sedang diedit.
Membuat Form Create Produk
Buat form sederhana untuk create produk dan simpan pada resources/views/products/create.blade.php
:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
</head>
<body>
<div id="app">
<div class="main-wrapper">
<div class="main-content">
<div class="container">
<form method="post" action="{{ route('products.store') }}">
@csrf
<div class="card mt-5">
<div class="card-header">
<h3>New Product</h3>
</div>
<div class="card-body">
@if ($errors->any())
<div class="alert alert-danger">
<div class="alert-title"><h4>Whoops!</h4></div>
There are some problems with your input.
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if (session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<div class="mb-3">
<label class="form-label">SKU</label>
<input type="text" class="form-control" name="sku" value="{{ old('sku') }}" placeholder="#SKU">
</div>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="name" value="{{ old('name') }}" placeholder="Name">
</div>
<div class="mb-3">
<label class="form-label">Brand</label>
<select name="brand_id" class="form-control">
<option value="">-- Brand --</option>
@foreach ($brands as $brandID => $name)
<option value="{{ $brandID }}" @selected(old('brand_id') == $brandID)>
{{ $name }}
</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label class="form-label">Category</label>
@foreach ($categories as $categoryID => $categoryName)
<div class="form-check">
<input class="form-check-input" type="checkbox" name="category_ids[]" value="{{ $categoryID }}">
<label class="form-check-label">
{{ $categoryName }}
</label>
</div>
@endforeach
</div>
<div class="mb-3">
<label class="form-label">Price</label>
<input type="text" class="form-control" name="price" value="{{ old('price') }}" placeholder="Price">
</div>
<div class="mb-3">
<label class="form-label">Stock</label>
<input type="text" class="form-control" name="stock" value="{{ old('stock') }}" placeholder="Stock">
</div>
</div>
<div class="card-footer">
<button class="btn btn-primary" type="submit">Create</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
Kemudian update method create
pada ProductController
menjadi seperti berikut:
public function create(): Response
{
$brands = Brand::orderBy('name', 'asc')->get()->pluck('name', 'id');
$categories = Category::orderBy('name', 'asc')->get()->pluck('name', 'id');
return response(view('products.create', ['brands' => $brands, 'categories' => $categories]));
}
Variabel $brands
dan $categories
yang di pass saat render view akan digunakan untuk membuat tampilan pilihan brand dan kategori.
Tampilan form create produk:
Membuat Fungsi Store Produk
Ini adalah proses menyimpan input dari form produk ke dalam tabel products
di database. Proses ini akan dihandle oleh method store
yang ada di dalam ProductController
. Pertama, kita perlu melakukan validasi input dengan StoreProductRequest
yang telah kita buat di atas. Kemudian, jika inputan tersebut lolos validasi maka kita panggil model Product
untuk menyimpan ke tabel products
. Kode program method store
akan menjadi seperti berikut:
public function store(StoreProductRequest $request): RedirectResponse
{
{
$params = $request->validated();
if ($product = Product::create($params)) {
$product->categories()->sync($params['category_ids']);
return redirect(route('products.index'))->with('success', 'Added!');
}
}
}
Untuk mengaitkan Product dengan Brand kita cukup membuat select
input dengan nama field brand_id
. Sedangkan untuk mengaitkan Product dengan Category, kita perlu membuat field checkbox
kategori dengan nama category_ids[]
kemudian pada saat setelah create product, kita perlu menambahkan perintah sync
dengan category_ids
.
Membuat Fungsi List Produk
Untuk menampilkan data produk ini akan dihandle oleh method index
dalam ProductController
dengan kode program seperti berikut:
public function index(): Response
{
$products = Product::all();
return response(view('products.index', ['products' => $products]));
}
Pertama, panggil Product::all()
untuk melakukan query mengambil semua data dalam tabel products
. Selanjutnya data tersebut akan di assign sebagai parameter pada saat render view resources/views/products/index.blade.php
.
Adapun kode program untuk index.blade.php
adalah seperti berikut:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
</head>
<body>
<div id="app">
<div class="main-wrapper">
<div class="main-content">
<div class="container">
<div class="card mt-5">
<div class="card-header">
<h3>List Product</h3>
</div>
<div class="card-body">
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if (session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<p>
<a class="btn btn-primary" href="{{ route('products.create') }}">New Product</a>
</p>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>ID</th>
<th>SKU</th>
<th>Name</th>
<th>Brand</th>
<th>Categories</th>
<th>Price</th>
<th>Stock</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@forelse ($products as $product)
<tr>
<td>{{ $product->id }}</td>
<td>{{ $product->sku }}</td>
<td>{{ $product->name }}</td>
<td>{{ ($product->brand != null) ? $product->brand->name : '' }}</td>
<td>{{ implode(', ', $product->categories->pluck('name')->toArray()) }}</td>
<td>{{ $product->price }}</td>
<td>{{ $product->stock }}</td>
<td>
<a href="{{ route('products.edit', ['id' => $product->id]) }}" class="btn btn-secondary btn-sm">edit</a>
<a href="#" class="btn btn-sm btn-danger" onclick="
event.preventDefault();
if (confirm('Do you want to remove this?')) {
document.getElementById('delete-row-{{ $product->id }}').submit();
}">
delete
</a>
<form id="delete-row-{{ $product->id }}" action="{{ route('products.destroy', ['id' => $product->id]) }}" method="POST">
<input type="hidden" name="_method" value="DELETE">
@csrf
</form>
</td>
</tr>
@empty
<tr>
<td colspan="8">
No record found!
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
Tampilan list produk:
Membuat Fungsi Edit Produk
Untuk form edit produk ini akan dihandle oleh method edit
dalam ProductController
. Kode programnya adalah seperti berikut:
public function edit(string $id): Response
{
$product = Product::findOrFail($id);
$brands = Brand::orderBy('name', 'asc')->get()->pluck('name', 'id');
$categories = Category::orderBy('name', 'asc')->get()->pluck('name', 'id');
return response(view('products.edit', ['product' => $product, 'brands' => $brands, 'categories' => $categories]));
}
Adapun kode program untuk form edit adalah sebagai berikut:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
</head>
<body>
<div id="app">
<div class="main-wrapper">
<div class="main-content">
<div class="container">
<form method="post" action="{{ route('products.update', $product->id) }}">
@method('PUT')
@csrf
<div class="card mt-5">
<div class="card-header">
<h3>Edit Product</h3>
</div>
<div class="card-body">
@if ($errors->any())
<div class="alert alert-danger">
<div class="alert-title"><h4>Whoops!</h4></div>
There are some problems with your input.
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if (session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<div class="mb-3">
<label class="form-label">SKU</label>
<input type="text" class="form-control" name="sku" value="{{ old('sku', $product->sku) }}" placeholder="#SKU">
</div>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="name" value="{{ old('name', $product->name) }}" placeholder="Name">
</div>
<div class="mb-3">
<label class="form-label">Price</label>
<input type="text" class="form-control" name="price" value="{{ old('price', $product->price) }}" placeholder="Price">
</div>
<div class="mb-3">
<label class="form-label">Brand</label>
<select name="brand_id" class="form-control">
<option value="">-- Brand --</option>
@foreach ($brands as $brandID => $name)
<option value="{{ $brandID }}" @selected(old('brand_id') == $brandID || $product->brand_id == $brandID)>
{{ $name }}
</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label class="form-label">Category</label>
@php
$selectedCategoryIDs = $product->categories->pluck('id')->toArray();
@endphp
@foreach ($categories as $categoryID => $categoryName)
<div class="form-check">
<input class="form-check-input" type="checkbox" name="category_ids[]" value="{{ $categoryID }}" @checked(in_array($categoryID, $selectedCategoryIDs))>
<label class="form-check-label">
{{ $categoryName }}
</label>
</div>
@endforeach
</div>
<div class="mb-3">
<label class="form-label">Stock</label>
<input type="text" class="form-control" name="stock" value="{{ old('stock', $product->stock) }}" placeholder="Stock">
</div>
</div>
<div class="card-footer">
<button class="btn btn-primary" type="submit">Update</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
Tampilan form edit produk:
Membuat Fungsi Update Produk
Fungsi update produk ini akan dihandle oleh method update
pada ProductController
. Kode programnya adalah seperti berikut:
public function update(UpdateProductRequest $request, string $id): RedirectResponse
{
$product = Product::findOrFail($id);
$params = $request->validated();
if ($product->update($params)) {
$product->categories()->sync($params['category_ids']);
return redirect(route('products.index'))->with('success', 'Updated!');
}
}
Jika berhasil melakukan update data, maka akan di-redirect ke halaman list produk dengan menampilkan success message.
Membuat Fungsi Delete Produk
Sebelum melakukan proses penghapusan data, program akan menampilkan dialog konfirmasi penghapusan ke user.
Fungsi delete produk akan dihandel oleh method destroy
pada ProductController
. Kode programnya adalah seperti berikut:
public function destroy(string $id): RedirectResponse
{
$product = Product::findOrFail($id);
$product->categories()->detach();
if ($product->delete()) {
return redirect(route('products.index'))->with('success', 'Deleted!');
}
return redirect(route('products.index'))->with('error', 'Sorry, unable to delete this!');
}
Pertama akan mengecek data produk berdasarkan id yang diberikan. Jika data produk valid, baru dilakukan penghapusan. Jika sukses hapus maka akan di-redirect ke halaman list produk dengan menampilkan success message.
Baris kode $product->categories()->detach();
digunakan untuk menghilangkan relasi Product
dengan Category
dengan cara menghapus data pada tabel pivot category_product
. Apabila ini tidak dilakukan, maka penghapusan produk yang punya relasi ke kategori akan gagal karena terhalang oleh constraint
database.
Source Code
Source code lengkap tutorial ini dapat di download di repo github: https://github.com/gieart87/tutorial-laravel10/tree/feature/part-11-crud-multiple-table-ber-relasi
Tulis Komentar