Membuat CRUD Kategori dan Artikel dengan Laravel
CRUD kategori dan artikel di Laravel adalah latihan yang bagus untuk memahami route, controller, model, migration, validasi, Blade, dan relasi database. Jawaban cepatnya: buat model Category dan Article, hubungkan artikel ke kategori dengan foreign key, daftarkan Route::resource(), validasi request di controller, lalu tampilkan form Blade dengan @csrf dan @method.
Halaman ini cocok setelah kamu membaca Pengenalan Laravel, MVC, Resource Controller, dan Validasi Laravel. Kita akan membuat versi sederhana yang bisa dijadikan pondasi blog admin.
Fitur yang Akan Dibuat
Project ini punya dua data utama:
- Kategori artikel.
- Artikel yang terhubung ke kategori.
Fitur minimalnya:
- tambah, lihat, edit, hapus kategori,
- tambah, lihat, edit, hapus artikel,
- validasi input,
- slug artikel,
- status
draft atau published,
- daftar artikel dengan nama kategori.
Membuat Model dan Migration
Jalankan perintah:
php artisan make:model Category -m
php artisan make:model Article -m
php artisan make:controller CategoryController --resource
php artisan make:controller ArticleController --resource
Migration kategori:
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
Migration artikel:
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('excerpt')->nullable();
$table->longText('body');
$table->enum('status', ['draft', 'published'])->default('draft');
$table->timestamps();
});
Jalankan migration:
Kalau migration gagal, baca Laravel Migration Gagal: Cara Membaca Error SQL.
Mengatur Model
Isi app/Models/Category.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Category extends Model
{
protected $fillable = ['name', 'slug'];
public function articles(): HasMany
{
return $this->hasMany(Article::class);
}
}
Isi app/Models/Article.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Article extends Model
{
protected $fillable = [
'category_id',
'title',
'slug',
'excerpt',
'body',
'status',
];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}
Relasi ini membuat kita bisa memanggil $article->category->name di Blade.
Mendaftarkan Route
Di routes/web.php:
<?php
use App\Http\Controllers\ArticleController;
use App\Http\Controllers\CategoryController;
use Illuminate\Support\Facades\Route;
Route::resource('categories', CategoryController::class);
Route::resource('articles', ArticleController::class);
Lihat hasil route:
Kalau controller tidak ditemukan, cek Target Class Does Not Exist di Laravel.
CRUD Kategori
Contoh CategoryController:
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class CategoryController extends Controller
{
public function index()
{
$categories = Category::latest()->paginate(10);
return view('categories.index', compact('categories'));
}
public function create()
{
return view('categories.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:100', 'unique:categories,name'],
]);
Category::create([
'name' => $validated['name'],
'slug' => Str::slug($validated['name']),
]);
return redirect()->route('categories.index')
->with('success', 'Kategori berhasil dibuat.');
}
public function edit(Category $category)
{
return view('categories.edit', compact('category'));
}
public function update(Request $request, Category $category)
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:100', 'unique:categories,name,' . $category->id],
]);
$category->update([
'name' => $validated['name'],
'slug' => Str::slug($validated['name']),
]);
return redirect()->route('categories.index')
->with('success', 'Kategori berhasil diperbarui.');
}
public function destroy(Category $category)
{
$category->delete();
return redirect()->route('categories.index')
->with('success', 'Kategori berhasil dihapus.');
}
}
Untuk latihan awal, kita memakai validasi langsung di controller. Kalau aturan validasi makin banyak, pindahkan ke Form Request.
CRUD Artikel
Contoh bagian penting ArticleController:
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class ArticleController extends Controller
{
public function index()
{
$articles = Article::with('category')->latest()->paginate(10);
return view('articles.index', compact('articles'));
}
public function create()
{
$categories = Category::orderBy('name')->get();
return view('articles.create', compact('categories'));
}
public function store(Request $request)
{
$validated = $request->validate([
'category_id' => ['required', 'exists:categories,id'],
'title' => ['required', 'string', 'max:160'],
'excerpt' => ['nullable', 'string', 'max:300'],
'body' => ['required', 'string'],
'status' => ['required', 'in:draft,published'],
]);
$validated['slug'] = Str::slug($validated['title']);
Article::create($validated);
return redirect()->route('articles.index')
->with('success', 'Artikel berhasil dibuat.');
}
}
Untuk method edit, update, dan destroy, polanya sama seperti kategori: ambil model lewat route model binding, validasi request, update data, lalu redirect.
resources/views/articles/create.blade.php:
<h1>Tulis Artikel Baru</h1>
@if ($errors->any())
<div>
<strong>Periksa lagi input kamu:</strong>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="{{ route('articles.store') }}">
@csrf
<label>
Kategori
<select name="category_id" required>
<option value="">Pilih kategori</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}" @selected(old('category_id') == $category->id)>
{{ $category->name }}
</option>
@endforeach
</select>
</label>
<label>
Judul
<input type="text" name="title" value="{{ old('title') }}" required>
</label>
<label>
Ringkasan
<textarea name="excerpt">{{ old('excerpt') }}</textarea>
</label>
<label>
Isi Artikel
<textarea name="body" required>{{ old('body') }}</textarea>
</label>
<label>
Status
<select name="status" required>
<option value="draft" @selected(old('status') === 'draft')>Draft</option>
<option value="published" @selected(old('status') === 'published')>Published</option>
</select>
</label>
<button type="submit">Simpan Artikel</button>
</form>
old() membuat input tidak hilang saat validasi gagal. @csrf wajib untuk form POST.
Menampilkan Daftar Artikel
resources/views/articles/index.blade.php:
<h1>Daftar Artikel</h1>
<a href="{{ route('articles.create') }}">+ Artikel Baru</a>
@foreach ($articles as $article)
<article>
<h2>{{ $article->title }}</h2>
<p>Kategori: {{ $article->category->name }}</p>
<p>Status: {{ $article->status }}</p>
<a href="{{ route('articles.edit', $article) }}">Edit</a>
<form method="POST" action="{{ route('articles.destroy', $article) }}">
@csrf
@method('DELETE')
<button type="submit">Hapus</button>
</form>
</article>
@endforeach
{{ $articles->links() }}
Gunakan with('category') di controller agar Laravel tidak menjalankan query kategori berulang-ulang untuk setiap artikel.
Kesalahan yang Sering Terjadi
Lupa @csrf
Form POST, PUT, PATCH, dan DELETE perlu token CSRF. Kalau lupa, sering muncul 419. Baca Cara Mengatasi 419 Page Expired di Laravel.
Lupa @method('DELETE') atau @method('PUT')
HTML form hanya mengenal GET dan POST. Laravel memakai method spoofing agar form bisa dianggap PUT, PATCH, atau DELETE.
Field Tidak Masuk Karena $fillable
Kalau Article::create($validated) tidak menyimpan sebagian field, cek properti $fillable di model.
Slug Duplikat
Str::slug($title) bisa menghasilkan slug yang sama untuk judul yang sama. Untuk project serius, tambahkan logika suffix seperti judul-2.
Route Model Binding Gagal
Pastikan nama parameter route cocok dengan nama model yang di-inject. Jika bingung, baca Route Model Binding Laravel Gagal.
FAQ
Apakah kategori boleh dihapus jika masih punya artikel?
Di contoh ini, cascadeOnDelete() akan menghapus artikel ikut kategori. Untuk aplikasi nyata, kamu mungkin ingin memakai restrictOnDelete() agar kategori tidak bisa dihapus jika masih dipakai.
Apakah slug harus dibuat otomatis?
Tidak wajib, tetapi slug otomatis mengurangi typo dan membuat URL lebih rapi.
Gunakan Form Request ketika validasi mulai panjang, dipakai di banyak tempat, atau controller terasa terlalu ramai.
Apakah perlu upload gambar artikel?
Belum untuk versi dasar. Setelah CRUD utama stabil, kamu bisa menambahkan upload gambar dengan validasi image dan storage Laravel.
Bacaan Terkait