Mengelola satu jenis data — misalnya post — hampir selalu berarti tujuh kegiatan yang sama: tampilkan daftar, tampilkan form baru, simpan, tampilkan detail, tampilkan form ubah, perbarui, dan hapus. Menulis tujuh route plus tujuh method satu per satu berulang setiap kali kita menambah entitas baru. Laravel meringkas pekerjaan tersebut menjadi resource controller: satu perintah generator menghasilkan tujuh method standar, dan satu baris Route::resource mendaftarkan tujuh route-nya. Chapter ini membangun CRUD post sungguhan di atas SQLite sampai lolos feature test.
Membuat Controller –resource #
Karena PostController versi dua method dari chapter sebelumnya sudah tidak relevan, hapus dulu berkasnya lalu jalankan generator dengan flag --resource:
rm app/Http/Controllers/PostController.php
php artisan make:controller PostController --resourceOutput yang Dihasilkan:
INFO Controller [app/Http/Controllers/PostController.php] created successfully. Generator kini menghasilkan tujuh method kosong dengan nama tetap:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index()
{
//
}
public function create()
{
//
}
public function store(Request $request)
{
//
}
public function show(string $id)
{
//
}
public function edit(string $id)
{
//
}
public function update(Request $request, string $id)
{
//
}
public function destroy(string $id)
{
//
}
}Nama method tersebut adalah konvensi yang diikat Laravel, bukan sekadar gaya — Route::resource mencocokkan nama-nama ini dengan HTTP method dan URI. Pemetaan tujuh aksinya sebagai berikut:
| HTTP method | URI | Method | Tugas |
|---|---|---|---|
| GET | /posts | index() | Daftar seluruh post |
| GET | /posts/create | create() | Form post baru |
| POST | /posts | store() | Simpan post baru |
| GET | /posts/{post} | show() | Detail satu post |
| GET | /posts/{post}/edit | edit() | Form ubah post |
| PUT/PATCH | /posts/{post} | update() | Perbarui post |
| DELETE | /posts/{post} | destroy() | Hapus post |
Satu Baris Route::resource #
Ganti seluruh route posts lama dengan satu baris di routes/web.php:
<?php
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
Route::resource('posts', PostController::class);Argumen pertama adalah nama resource tanpa garis miring; Laravel membentuk URI dan nama route dari nama tersebut. Bentuk jamak posts menghasilkan parameter {post} tunggal di route detail — Laravel mengubah nama resource menjadi bentuk singular untuk parameter, kebiasaan yang sebaiknya diikuti juga pada nama model Post agar route model binding berjalan mulus tanpa konfigurasi. Kita periksa hasilnya:
php artisan route:list --except-vendor --path=postsOutput yang Dihasilkan:
GET|HEAD posts ................... posts.index › PostController@index
POST posts ................... posts.store › PostController@store
GET|HEAD posts/create .......... posts.create › PostController@create
GET|HEAD posts/{post} .............. posts.show › PostController@show
PUT|PATCH posts/{post} .......... posts.update › PostController@update
DELETE posts/{post} ........ posts.destroy › PostController@destroy
GET|HEAD posts/{post}/edit ......... posts.edit › PostController@edit
Showing [7] routesPerhatikan dua hal pada output di atas. Pertama, setiap route otomatis bernama mengikuti pola posts.aksi (posts.index, posts.store, dst), sehingga kita langsung bisa memanggil route('posts.index'). Kedua, route posts/create muncul sebelum posts/{post} — Laravel mengurutkan pola statis lebih dulu agar /posts/create tidak tertelan pola dinamis {post}.
Menyempitkan dengan only dan except #
Aplikasi nyata tidak selalu butuh ketujuh aksi. Method ->only() memilih sebagian route, sedangkan ->except() membuang sebagian:
Route::resource('posts', PostController::class)->only(['index', 'show']);Output yang Dihasilkan:
GET|HEAD posts .................... posts.index › PostController@index
GET|HEAD posts/{post} ............... posts.show › PostController@show
Showing [2] routesBentuk hanya-baca seperti di atas cocok untuk data yang diubah lewat admin panel atau seeder. Jalankan route:list setiap kali menyempitkan resource — daftar yang tampil adalah kebenaran akhirnya, bukan daftar yang kita niatkan.
Mini-CRUD yang Benar-Benar Jalan #
Sekarang kita isi method-nya dengan implementasi nyata di atas tabel SQLite. Buat dulu migration dan modelnya — detail keduanya dibahas penuh di Section Database dan Migrations serta Eloquent ORM:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});Implementasi controller mengikuti pola koordinator dari chapter Konsep Controller:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class PostController extends Controller
{
public function index(): View
{
return view('posts.index', ['posts' => Post::latest()->get()]);
}
public function create(): View
{
return view('posts.create');
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:100',
'content' => 'required|string',
]);
$post = Post::create($validated);
return redirect()->route('posts.show', $post)
->with('status', 'Post berhasil disimpan.');
}
public function show(Post $post): View
{
return view('posts.show', ['post' => $post]);
}
public function edit(Post $post): View
{
return view('posts.edit', ['post' => $post]);
}
public function update(Request $request, Post $post): RedirectResponse
{
$validated = $request->validate([
'title' => 'required|string|max:100',
'content' => 'required|string',
]);
$post->update($validated);
return redirect()->route('posts.show', $post)
->with('status', 'Post berhasil diperbarui.');
}
public function destroy(Post $post): RedirectResponse
{
$post->delete();
return redirect()->route('posts.index')
->with('status', 'Post berhasil dihapus.');
}
}Dua pola penting ada di sini. Signature show(Post $post) memanfaatkan route model binding — Laravel mencari post berdasarkan {post} di URI dan mengirimkan objeknya sebagai argumen (kita bahas tuntas di chapter Route Model Binding). Method $request->validate() memeriksa input sebelum data disentuh database; gagal validasi otomatis me-redirect pengirim kembali — pembahasan lengkapnya menunggu Section Validasi dan Form.
Alur CRUD tersebut kita kunci dengan feature test yang menelusuri ketujuh aksi secara berurutan:
<?php
namespace Tests\Feature;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostCrudTest extends TestCase
{
use RefreshDatabase;
public function test_alur_crud_lengkap(): void
{
$this->get('/posts')->assertOk()->assertSee('Daftar Post');
$this->get('/posts/create')->assertOk()->assertSee('Tulis Post Baru');
$this->post('/posts', [
'title' => 'Belajar Resource Controller',
'content' => 'Satu baris Route::resource menangani tujuh aksi.',
])->assertRedirect(route('posts.show', Post::first()));
$post = Post::first();
$this->get("/posts/{$post->id}")
->assertOk()
->assertSee('Belajar Resource Controller');
$this->get("/posts/{$post->id}/edit")->assertOk()->assertSee('Ubah Post');
$this->put("/posts/{$post->id}", [
'title' => 'Belajar Resource Controller (revisi)',
'content' => 'Konten diperbarui.',
])->assertRedirect("/posts/{$post->id}");
$this->delete("/posts/{$post->id}")->assertRedirect('/posts');
$this->assertDatabaseMissing('posts', ['id' => $post->id]);
}
}Output yang Dihasilkan:
PASS Tests\Feature\PostCrudTest
✓ alur crud lengkap 0.17s
Tests: 1 passed (17 assertions)
Duration: 0.25sTujuh belas assertion dalam satu test mencakup seluruh siklus: dua halaman terbuka, data tersimpan di SQLite, detail dan form ubah ditemukan, revisi tersimpan, dan baris hilang setelah DELETE. CRUD utuh selesai dalam satu berkas controller dan satu baris Route::resource, persis seperti janji dokumentasi controllers
.
Di ujung lain spektrum ini ada controller yang justru terlalu kecil untuk tujuh method: endpoint dengan satu tugas tunggal. Chapter Single Action Controller berikutnya membahas bentuk paling ringkasnya.