Aplikasi web punya tiga kelompok pekerjaan yang sifatnya sangat berbeda: mengolah data, menyusun tampilan, dan mengatur alur permintaan. Kalau ketiganya dicampur dalam satu berkas, aplikasi cepat menjadi sulit dirawat. Pola Model-View-Controller (MVC) memisahkan ketiganya menjadi tiga peran yang jelas, dan Laravel sejak awal dirancang mengikuti pola ini. Pada chapter ini kita pelajari konsep MVC, alurnya dalam satu request, lokasi tiap peran di struktur direktori Laravel, lalu memetakan fitur nyata — halaman daftar tulisan blog — ke Model, View, dan Controller.
Apa itu Pola MVC #
MVC membagi aplikasi menjadi tiga peran:
- Model — pemegang data dan logika bisnis. Model merepresentasikan entitas aplikasi (misalnya tulisan blog, pengguna, pesanan) dan bertugas membaca serta menulis ke database. Di Laravel, peran ini dipegang class Eloquent di
app/Models. - View — lapisan presentasi. View menentukan bagaimana data ditampilkan: struktur HTML, format JSON untuk API, atau template email. Di Laravel, view ditulis sebagai berkas Blade di
resources/views. - Controller — pengatur alur. Controller menerima request, memanggil Model untuk mengambil data, lalu menyerahkan data ke View untuk dirender. Controller sebaiknya tetap ramping: tugasnya mengoordinasikan, bukan menampung semua logika.
Inti MVC adalah arah ketergantungan yang sehat: View bergantung pada data yang diberikan Controller, sementara Model tidak tahu apa-apa tentang HTML. Berkat pemisahan ini, kita bisa mengubah tampilan tanpa menyentuh logika data, dan menguji logika data tanpa membuka browser.
Pola MVC sendiri bukan penemuan Laravel. Pola ini lahir jauh sebelum era web dan diadopsi hampir semua framework web modern — Rails, Django, Symfony — karena terbukti menjaga aplikasi tetap teratur saat jumlah fitur bertambah. Keunggulan Laravel di sisi ini adalah konsistensi: tempat tiap peran sudah ditentukan, sehingga kita tidak perlu berdebat soal struktur setiap kali memulai proyek baru.
Alur Request dalam MVC #
Pola MVC paling mudah dipahami dengan mengikuti perjalanan satu request. Diagram berikut melukis alur request GET /posts untuk fitur daftar tulisan blog:
Berikut penjelasan tiap tahap alur tersebut:
- Browser mengirim request
GET /postske server. - Router mencocokkan URL
/postsdengan definisi diroutes/web.php, lalu menyerahkan request ke controller yang tertera. - Controller meminta data ke Model:
Post::latest('id')->get(). - Model menjalankan query ke tabel
postsdan mengembalikan kumpulan objekPost. - Controller menyerahkan data ke View beserta nama view:
view('posts.index', ['posts' => $posts]). - View merender HTML, hasilnya dikirim kembali ke browser sebagai response.
Lokasi MVC di Struktur Direktori Laravel #
Laravel memetakan tiap peran MVC ke lokasi direktori yang tetap, sehingga kita selalu tahu ke mana harus mencari kode:
| Peran MVC | Lokasi bawaan | Contoh berkas |
|---|---|---|
| Model | app/Models/ | app/Models/Post.php |
| Controller | app/Http/Controllers/ | app/Http/Controllers/PostController.php |
| View | resources/views/ | resources/views/posts/index.blade.php |
| Definisi route | routes/ | routes/web.php |
| Skema database | database/migrations/ | create_posts_table |
Lokasi-lokasi tersebut adalah konvensi bawaan, bukan keharusan mutlak — penjelasan lengkap tiap direktori bisa dibaca di dokumentasi struktur direktori . Yang membuat konvensi berharga: anggota tim baru langsung tahu ke mana harus melihat tanpa membaca seluruh basis kode.
Studi Kasus: Daftar Tulisan Blog #
Mari kita wujudkan fitur “daftar tulisan” dari awal hingga teruji. Laravel menyediakan generator artisan untuk membuat kerangka class, termasuk Model beserta migration-nya:
php artisan make:model Post --migrationOutput yang Dihasilkan:
INFO Model [app/Models/Post.php] created successfully.
INFO Migration [database/migrations/2026_10_13_154002_create_posts_table.php] created successfully.Migration adalah berkas PHP yang mendefinisikan skema tabel posts — bagian dari peran Model dalam arti luas. Method up() dijalankan ketika migration dieksekusi: Schema::create() membentuk tabel dengan kolom id sebagai primary key, judul sebagai string, isi sebagai teks panjang, serta timestamps() yang membuat kolom created_at dan updated_at. Method down() menjadi cara membatalkan perubahan — pasangan maju-mundur inilah yang membuat skema database bisa dikendalikan seperti kode pada umumnya:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('judul');
$table->text('isi');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('posts');
}
};Model Post sendiri ramping karena Eloquent sudah menyediakan perilaku dasar; properti $fillable mendaftarkan kolom yang boleh diisi secara massal:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['judul', 'isi'];
}Controller kemudian mengambil data dan menyerahkannya ke view:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\View\View;
class PostController extends Controller
{
/**
* Tampilkan daftar seluruh tulisan.
*/
public function index(): View
{
$posts = Post::latest('id')->get();
return view('posts.index', [
'posts' => $posts,
]);
}
}Route menghubungkan URL /posts ke method index di routes/web.php:
<?php
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/posts', [PostController::class, 'index']);Terakhir, view Blade di resources/views/posts/index.blade.php merender data menjadi HTML — direktif @foreach mengulang tiap tulisan, dan {{ $post->judul }} mencetak nilai sekaligus meng-escape-nya:
<!DOCTYPE html>
<html lang="id">
<head><title>Daftar Tulisan</title></head>
<body>
<h1>Daftar Tulisan</h1>
@foreach ($posts as $post)
<article>
<h2>{{ $post->judul }}</h2>
<p>{{ $post->isi }}</p>
</article>
@endforeach
</body>
</html>Untuk membuktikan ketiga peran bekerja sebagai satu kesatuan, kita menulis feature test yang menampilkan dua tulisan ke database, melakukan request GET /posts, lalu memeriksa judul kedua tulisan muncul di response:
<?php
namespace Tests\Feature;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostMvcTest extends TestCase
{
use RefreshDatabase;
public function test_halaman_daftar_tulisan_menampilkan_data_dari_model(): void
{
Post::create([
'judul' => 'Tulisan pertama',
'isi' => 'Ini isi tulisan pertama kita tentang Laravel.',
]);
Post::create([
'judul' => 'Belajar MVC di Laravel',
'isi' => 'Model, View, dan Controller bekerja sama di sini.',
]);
$response = $this->get('/posts');
$response->assertStatus(200);
$response->assertSee('Tulisan pertama');
$response->assertSee('Belajar MVC di Laravel');
}
}php artisan test tests/Feature/PostMvcTest.phpOutput yang Dihasilkan:
PASS Tests\Feature\PostMvcTest
✓ halaman daftar tulisan menampilkan data dari model 0.16s
Tests: 1 passed (3 assertions)
Duration: 0.24sTest yang lolos tersebut menegaskan keseluruhan alur: data masuk lewat Model, request dikoordinasikan Controller, dan View merender data hingga terlihat di response. Angka 3 assertions mencerminkan tiga pemeriksaan yang dijalankan: status response harus 200, dan masing-masing judul tulisan harus muncul di halaman. Detail tiap teknik — migration, Eloquent, Blade — kita dalami pada Section masing-masing; sementara itu, pertanyaan alami berikutnya adalah: secara persis, urutan apa yang Laravel lalui dari request browser sampai response terkirim? Itulah topik chapter Lifecycle Request Laravel berikutnya.