Tutorial Laravel 10 - Part #11 - Membuat CRUD dengan Beberapa Tabel yang Mempunyai Relasi

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