Pendahuluan
Setelah menguasai Eloquent ORM, sekarang saatnya menyatukan semua yang sudah kita pelajari — Routing, Controller, Blade, dan Eloquent — untuk membangun aplikasi CRUD yang benar-benar fungsional.
CRUD (Create, Read, Update, Delete) adalah fondasi hampir semua aplikasi web. Jika kamu bisa membuat CRUD yang bersih di Laravel, kamu sudah siap membangun sistem apapun.
Di artikel ini kita akan menyelesaikan Task Manager secara lengkap dengan fitur:
- ✅ Listing task dengan pagination
- ✅ Form buat task baru dengan validasi
- ✅ Edit task yang sudah ada
- ✅ Hapus task dengan konfirmasi
- ✅ Flash message sukses/error
- ✅ CSRF protection otomatis
Persiapan Awal
Pastikan kamu sudah mengikuti artikel sebelumnya. Model, migration, dan Resource Controller untuk Task sudah dibuat. Jika belum:
# Buat semua yang dibutuhkan sekaligus
php artisan make:model Task -mcr
# Jalankan migration (sesuaikan schema dengan artikel Eloquent sebelumnya)
php artisan migrate
# (Opsional) Isi data dummy
php artisan db:seed --class=TaskSeeder
Verifikasi route yang tersedia:
php artisan route:list --name=tasks
Langkah 1: Update routes/web.php
<?php
// routes/web.php
use App\Http\Controllers\TaskController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return redirect()->route('tasks.index');
});
Route::resource('tasks', TaskController::class);
Langkah 2: Controller Lengkap
<?php
// app/Http/Controllers/TaskController.php
namespace App\Http\Controllers;
use App\Models\Task;
use App\Http\Requests\StoreTaskRequest;
use App\Http\Requests\UpdateTaskRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class TaskController extends Controller
{
// GET /tasks — Tampilkan semua task dengan pagination
public function index(): View
{
$tasks = Task::latest()->paginate(10);
return view('tasks.index', compact('tasks'));
}
// GET /tasks/create — Tampilkan form buat task baru
public function create(): View
{
return view('tasks.create');
}
// POST /tasks — Simpan task baru
public function store(StoreTaskRequest $request): RedirectResponse
{
Task::create($request->validated());
return redirect()
->route('tasks.index')
->with('success', 'Task berhasil ditambahkan! 🎉');
}
// GET /tasks/{task} — Tampilkan detail task
public function show(Task $task): View
{
return view('tasks.show', compact('task'));
}
// GET /tasks/{task}/edit — Tampilkan form edit
public function edit(Task $task): View
{
return view('tasks.edit', compact('task'));
}
// PUT /tasks/{task} — Update task
public function update(UpdateTaskRequest $request, Task $task): RedirectResponse
{
$task->update($request->validated());
return redirect()
->route('tasks.index')
->with('success', 'Task berhasil diperbarui! ✏️');
}
// DELETE /tasks/{task} — Hapus task
public function destroy(Task $task): RedirectResponse
{
$task->delete();
return redirect()
->route('tasks.index')
->with('success', 'Task berhasil dihapus! 🗑️');
}
}
Langkah 3: Form Request Validation
Form Request adalah cara terbaik untuk memvalidasi input di Laravel — memisahkan logika validasi dari Controller agar tetap bersih.
php artisan make:request StoreTaskRequest
php artisan make:request UpdateTaskRequest
<?php
// app/Http/Requests/StoreTaskRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreTaskRequest extends FormRequest
{
// Apakah user boleh melakukan request ini?
public function authorize(): bool
{
return true; // Ubah ke logic auth jika perlu
}
// Aturan validasi
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'description' => 'nullable|string|max:1000',
'priority' => 'required|in:low,medium,high',
'due_date' => 'nullable|date|after_or_equal:today',
];
}
// Pesan error custom (opsional, tapi ramah pengguna)
public function messages(): array
{
return [
'title.required' => 'Judul task wajib diisi.',
'title.max' => 'Judul task maksimal 255 karakter.',
'priority.required' => 'Prioritas wajib dipilih.',
'priority.in' => 'Prioritas tidak valid.',
'due_date.after_or_equal' => 'Tanggal deadline tidak boleh di masa lalu.',
];
}
}
<?php
// app/Http/Requests/UpdateTaskRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateTaskRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'description' => 'nullable|string|max:1000',
'priority' => 'required|in:low,medium,high',
'is_done' => 'boolean',
'due_date' => 'nullable|date',
];
}
}
Langkah 4: Blade Views
Layout Utama
{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Task Manager')</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: sans-serif; background: #f5f5f5; color: #333; }
.container { max-width: 900px; margin: 0 auto; padding: 20px; }
nav { background: #4F46E5; color: white; padding: 15px 20px; }
nav a { color: white; text-decoration: none; font-weight: bold; font-size: 1.2rem; }
.alert { padding: 12px 16px; border-radius: 6px; margin: 16px 0; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
.alert-error { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
.btn { padding: 8px 16px; border-radius: 6px; text-decoration: none; font-size: 0.9rem; cursor: pointer; border: none; }
.btn-primary { background: #4F46E5; color: white; }
.btn-warning { background: #F59E0B; color: white; }
.btn-danger { background: #EF4444; color: white; }
.btn-secondary { background: #6B7280; color: white; }
</style>
</head>
<body>
<nav>
<a href="{{ route('tasks.index') }}">📋 Task Manager</a>
</nav>
<div class="container">
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if (session('error'))
<div class="alert alert-error">{{ session('error') }}</div>
@endif
@yield('content')
</div>
</body>
</html>
Halaman Index
{{-- resources/views/tasks/index.blade.php --}}
@extends('layouts.app')
@section('title', 'Daftar Task')
@section('content')
<div style="display:flex; justify-content:space-between; align-items:center; margin:24px 0 16px;">
<h1>📋 Daftar Task <span style="color:#6B7280; font-size:1rem;">({{ $tasks->total() }} task)</span></h1>
<a href="{{ route('tasks.create') }}" class="btn btn-primary">+ Tambah Task</a>
</div>
@forelse ($tasks as $task)
<div style="background:white; padding:16px; border-radius:8px; margin-bottom:12px; border-left: 4px solid {{ $task->priority === 'high' ? '#EF4444' : ($task->priority === 'medium' ? '#F59E0B' : '#10B981') }};">
<div style="display:flex; justify-content:space-between; align-items:start;">
<div>
<h3 style="{{ $task->is_done ? 'text-decoration:line-through; color:#9CA3AF;' : '' }}">
{{ $task->title }}
</h3>
@if ($task->description)
<p style="color:#6B7280; margin-top:4px;">{{ Str::limit($task->description, 100) }}</p>
@endif
<div style="margin-top:8px; font-size:0.8rem; color:#9CA3AF;">
<span>Prioritas: <strong>{{ ucfirst($task->priority) }}</strong></span>
@if ($task->due_date)
• <span>Deadline: {{ $task->due_date->format('d M Y') }}</span>
@endif
• <span>{{ $task->is_done ? '✅ Selesai' : '⏳ Pending' }}</span>
</div>
</div>
<div style="display:flex; gap:8px; flex-shrink:0;">
<a href="{{ route('tasks.edit', $task) }}" class="btn btn-warning">Edit</a>
<form action="{{ route('tasks.destroy', $task) }}" method="POST"
onsubmit="return confirm('Yakin ingin menghapus task ini?')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Hapus</button>
</form>
</div>
</div>
</div>
@empty
<div style="text-align:center; padding:48px; background:white; border-radius:8px;">
<p style="font-size:1.1rem; color:#6B7280;">Belum ada task. Yuk tambah yang pertama!</p>
<a href="{{ route('tasks.create') }}" class="btn btn-primary" style="margin-top:16px; display:inline-block;">
+ Tambah Task Pertama
</a>
</div>
@endforelse
{{-- Pagination --}}
<div style="margin-top:24px;">
{{ $tasks->links() }}
</div>
@endsection
Form Tambah Task
{{-- resources/views/tasks/create.blade.php --}}
@extends('layouts.app')
@section('title', 'Tambah Task')
@section('content')
<div style="max-width:600px;">
<h1 style="margin:24px 0 16px;">➕ Tambah Task Baru</h1>
<div style="background:white; padding:24px; border-radius:8px;">
<form action="{{ route('tasks.store') }}" method="POST">
@csrf
{{-- Judul --}}
<div style="margin-bottom:16px;">
<label style="display:block; font-weight:600; margin-bottom:4px;">
Judul Task <span style="color:red;">*</span>
</label>
<input type="text" name="title" value="{{ old('title') }}"
placeholder="Contoh: Review kode pull request"
style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem; {{ $errors->has('title') ? 'border-color:red;' : '' }}">
@error('title')
<span style="color:red; font-size:0.85rem;">{{ $message }}</span>
@enderror
</div>
{{-- Deskripsi --}}
<div style="margin-bottom:16px;">
<label style="display:block; font-weight:600; margin-bottom:4px;">Deskripsi</label>
<textarea name="description" rows="3"
placeholder="Tambahkan detail task (opsional)"
style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">{{ old('description') }}</textarea>
@error('description')
<span style="color:red; font-size:0.85rem;">{{ $message }}</span>
@enderror
</div>
{{-- Prioritas --}}
<div style="margin-bottom:16px;">
<label style="display:block; font-weight:600; margin-bottom:4px;">
Prioritas <span style="color:red;">*</span>
</label>
<select name="priority" style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">
<option value="low" {{ old('priority') === 'low' ? 'selected' : '' }}>🟢 Rendah</option>
<option value="medium" {{ old('priority', 'medium') === 'medium' ? 'selected' : '' }}>🟡 Sedang</option>
<option value="high" {{ old('priority') === 'high' ? 'selected' : '' }}>🔴 Tinggi</option>
</select>
@error('priority')
<span style="color:red; font-size:0.85rem;">{{ $message }}</span>
@enderror
</div>
{{-- Tanggal Deadline --}}
<div style="margin-bottom:24px;">
<label style="display:block; font-weight:600; margin-bottom:4px;">Tanggal Deadline</label>
<input type="date" name="due_date" value="{{ old('due_date') }}"
min="{{ date('Y-m-d') }}"
style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">
@error('due_date')
<span style="color:red; font-size:0.85rem;">{{ $message }}</span>
@enderror
</div>
<div style="display:flex; gap:12px;">
<button type="submit" class="btn btn-primary">💾 Simpan Task</button>
<a href="{{ route('tasks.index') }}" class="btn btn-secondary">Batal</a>
</div>
</form>
</div>
</div>
@endsection
Form Edit Task
{{-- resources/views/tasks/edit.blade.php --}}
@extends('layouts.app')
@section('title', 'Edit Task')
@section('content')
<div style="max-width:600px;">
<h1 style="margin:24px 0 16px;">✏️ Edit Task</h1>
<div style="background:white; padding:24px; border-radius:8px;">
<form action="{{ route('tasks.update', $task) }}" method="POST">
@csrf
@method('PUT')
{{-- Judul --}}
<div style="margin-bottom:16px;">
<label style="display:block; font-weight:600; margin-bottom:4px;">Judul Task *</label>
<input type="text" name="title" value="{{ old('title', $task->title) }}"
style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">
@error('title')
<span style="color:red; font-size:0.85rem;">{{ $message }}</span>
@enderror
</div>
{{-- Deskripsi --}}
<div style="margin-bottom:16px;">
<label style="display:block; font-weight:600; margin-bottom:4px;">Deskripsi</label>
<textarea name="description" rows="3"
style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">{{ old('description', $task->description) }}</textarea>
</div>
{{-- Prioritas --}}
<div style="margin-bottom:16px;">
<label style="display:block; font-weight:600; margin-bottom:4px;">Prioritas</label>
<select name="priority" style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">
@foreach (['low' => '🟢 Rendah', 'medium' => '🟡 Sedang', 'high' => '🔴 Tinggi'] as $val => $label)
<option value="{{ $val }}" {{ old('priority', $task->priority) === $val ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</div>
{{-- Status Selesai --}}
<div style="margin-bottom:16px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer;">
<input type="hidden" name="is_done" value="0">
<input type="checkbox" name="is_done" value="1" {{ old('is_done', $task->is_done) ? 'checked' : '' }}>
<span>Tandai sebagai selesai</span>
</label>
</div>
{{-- Tanggal Deadline --}}
<div style="margin-bottom:24px;">
<label style="display:block; font-weight:600; margin-bottom:4px;">Tanggal Deadline</label>
<input type="date" name="due_date"
value="{{ old('due_date', $task->due_date?->format('Y-m-d')) }}"
style="width:100%; padding:8px 12px; border:1px solid #D1D5DB; border-radius:6px; font-size:1rem;">
</div>
<div style="display:flex; gap:12px;">
<button type="submit" class="btn btn-primary">💾 Update Task</button>
<a href="{{ route('tasks.index') }}" class="btn btn-secondary">Batal</a>
</div>
</form>
</div>
</div>
@endsection
Langkah 5: Update Model Task
Pastikan Model Task sudah dikonfigurasi dengan benar:
<?php
// app/Models/Task.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected $fillable = [
'title', 'description', 'priority', 'is_done', 'due_date'
];
protected $casts = [
'is_done' => 'boolean',
'due_date' => 'date',
];
}
Menjalankan Aplikasi
php artisan serve
Buka http://localhost:8000 dan coba semua fitur:
- Tambah beberapa task
- Edit task yang sudah ada
- Tandai task sebagai selesai
- Hapus task
Troubleshooting: Error yang Sering Muncul
CSRF token mismatch (419 Error)
Penyebab: Semua form POST/PUT/DELETE harus menyertakan token CSRF, tapi @csrf tidak ada di form.
Solusi:
<form method="POST" action="{{ route('tasks.store') }}">
@csrf {{-- WAJIB ada di setiap form POST/PUT/DELETE --}}
...
</form>
MethodNotAllowedHttpException untuk PUT/DELETE
Penyebab: Browser hanya mendukung GET dan POST. Untuk PUT, PATCH, DELETE harus menggunakan method spoofing.
Solusi:
<form action="{{ route('tasks.update', $task) }}" method="POST">
@csrf
@method('PUT') {{-- Wajib untuk form edit --}}
...
</form>
<form action="{{ route('tasks.destroy', $task) }}" method="POST">
@csrf
@method('DELETE') {{-- Wajib untuk form hapus --}}
...
</form>
Pesan Validasi Tidak Muncul
Penyebab: Lupa menampilkan error di Blade, atau menggunakan validate() di Controller tapi tidak ada @error di view.
Solusi:
{{-- Tampilkan semua error sekaligus --}}
@if ($errors->any())
<div class="alert alert-error">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{-- Atau tampilkan per field --}}
@error('title')
<span style="color:red;">{{ $message }}</span>
@enderror
Pertanyaan yang Sering Diajukan
Apa bedanya validate() di Controller vs Form Request?
validate() langsung di Controller cepat untuk kasus sederhana. Form Request lebih baik untuk aplikasi nyata karena: logika validasi terpisah dari Controller, bisa reuse, lebih mudah di-test, dan mendukung custom authorization. Gunakan Form Request untuk semua fitur production.
Bagaimana cara menambahkan pagination di Laravel?
// Controller — ganti get() dengan paginate()
$tasks = Task::latest()->paginate(15); // 15 item per halaman
// Blade — tambahkan di bawah loop
{{ $tasks->links() }} // Tampilkan link halaman
Untuk custom styling pagination, jalankan php artisan vendor:publish --tag=laravel-pagination.
Bagaimana cara menggunakan old() di form?
Fungsi old('field_name') mengembalikan nilai input sebelumnya — sangat berguna agar form tidak kosong setelah validasi gagal. Selalu tambahkan old() di semua field form:
<input name="title" value="{{ old('title', $task->title ?? '') }}">
{{-- old('title') → nilai dari request terakhir
$task->title → nilai default saat edit
'' → nilai default kosong saat create --}}
Kesimpulan
Selamat! Aplikasi CRUD Task Manager kamu sekarang sudah berfungsi penuh. Kita sudah membangun:
- Controller lengkap dengan 7 method CRUD
- Form Request Validation yang bersih dan terorganisir
- Blade views: index, create, edit dengan pesan validasi
- Flash message untuk feedback sukses/error
- CSRF protection dan method spoofing
Di artikel berikutnya, kita akan membahas topik yang sangat sering menyebabkan masalah bagi developer Indonesia: Cara Mengatasi Error CORS di Laravel — terutama saat menghubungkan Laravel backend dengan React/Vue frontend.
Kamu sudah berhasil membangun aplikasi web yang nyata! Ini adalah pencapaian yang luar biasa 🏆