MVC Sederhana secara Manual

Memisahkan model, view, dan controller tanpa framework.

Daftar isi

MVC (model-view-controller) adalah pola yang memecah aplikasi menjadi tiga peran, yaitu model untuk data, view untuk tampilan, dan controller untuk menghubungkan keduanya. Topik ini membangun ketiga peran tersebut secara manual dalam tiga berkas kecil tanpa framework, lengkap dengan pengujian lewat CLI dan server bawaan PHP. Setelah membacanya, batas tanggung jawab setiap peran dapat dituliskan sebagai kode yang benar-benar berjalan.

Tiga peran yang dipisahkan#

Pada aplikasi satu berkas, pengambilan data, pengolahan, dan pencetakan HTML berada di tempat yang sama. Kode semacam itu bekerja, tetapi perubahan tampilan menuntut pembacaan ulang seluruh berkas, dan logika data tidak dapat diuji tanpa merender halaman. MVC memisahkan ketiga urusan itu sehingga setiap peran dapat dibaca, diuji, dan diubah secara terpisah.

Pembagian perannya dapat diringkas sebagai berikut.

PeranTanggung jawabContoh pada topik ini
ModelMenyimpan dan mengambil dataKelas ModelCatatan yang mengembalikan array catatan
ViewMenyajikan data menjadi keluaranFungsi tampilkan_daftar yang mencetak HTML
ControllerMenerima permintaan dan mengikat keduanyaBerkas index.php

Diagram berikut memperlihatkan arah aliran data antarperan untuk satu permintaan.

Controllerindex.phpModelModelCatatan.phpViewTampilan.php1. permintaan2. minta data3. array data4. kirim data5. respons
Alur data pada MVC sederhana

Urutan pada diagram tetap sama untuk aplikasi yang lebih besar, sedangkan yang berubah hanya isi setiap kotak. Controller tetap menjadi titik masuk permintaan, model tetap satu-satunya yang menyentuh data, dan view tetap satu-satunya yang mencetak HTML.

Model sebagai sumber data#

Model adalah tempat satu-satunya yang berhak membaca atau menulis data. Kelas pada contoh ini mengembalikan array tetap agar topik dapat dijalankan tanpa basis data. Bentuk antarmukanya sengaja dibuat sama dengan model sungguhan, yaitu metode yang dipanggil dan array yang dikembalikan.

php ModelCatatan.php
<?php
declare(strict_types=1);

final class ModelCatatan
{
    // sumber data tetap agar contoh dapat dijalankan tanpa basis data
    public function semua(): array
    {
        return [
            ['judul' => 'Belanja mingguan', 'isi' => 'Beras, minyak goreng, dan telur.'],
            ['judul' => 'Jadwal piket', 'isi' => 'Senin Ali, Rabu Budi, Jumat Citra.'],
        ];
    }
}

Karena view hanya mengenal array, mengganti sumber data dari array tetap ke basis data tidak menuntut perubahan apa pun pada view maupun controller. Batas inilah yang membuat model layak diuji sendirian, misalnya dengan memanggil semua dari skrip CLI tanpa menyentuh HTTP sama sekali.

View sebagai penyaji tampilan#

View berisi markah HTML beserta pencetakan nilai yang diberikan kepadanya. View tidak mengambil data sendiri dan tidak berisi keputusan bisnis, sehingga isinya murni pekerjaan penyajian. Fungsi berikut menyajikan daftar catatan sebagai halaman HTML.

php Tampilan.php
<?php
declare(strict_types=1);

// view: menyajikan data yang sudah diberikan, tanpa mengambil data sendiri
function tampilkan_daftar(array $catatan): void
{
    echo '<!DOCTYPE html>' . PHP_EOL;
    echo '<html lang="id">' . PHP_EOL;
    echo '<head><meta charset="utf-8"><title>Daftar Catatan</title></head>' . PHP_EOL;
    echo '<body>' . PHP_EOL;
    echo '<h1>Daftar Catatan</h1>' . PHP_EOL;
    echo '<ul>' . PHP_EOL;

    foreach ($catatan as $c) {
        echo '<li>'
            . htmlspecialchars($c['judul'], ENT_QUOTES, 'UTF-8')
            . ': '
            . htmlspecialchars($c['isi'], ENT_QUOTES, 'UTF-8')
            . '</li>' . PHP_EOL;
    }

    echo '</ul>' . PHP_EOL;
    echo '</body>' . PHP_EOL;
    echo '</html>' . PHP_EOL;
}

Setiap nilai dari model dibungkus htmlspecialchars sebelum dicetak, sehingga isi catatan tidak pernah dapat mengubah struktur halaman. Pemanggilan itu berada di view karena tampilan adalah titik keluaran terakhir sebelum HTML dikirim ke browser. Cara mengurangi pengulangan pemanggilan htmlspecialchars seperti di atas dibahas pada topik berikutnya.

Controller sebagai penghubung#

Controller menerima permintaan, meminta data dari model, lalu menyerahkan data tersebut ke view. Controller yang sehat berisi susunan panggilan yang pendek, karena detail pengambilan data ada di model dan detail penyajian ada di view.

php index.php
<?php
declare(strict_types=1);

// controller: mengikat model dan view tanpa berisi detail keduanya
require 'ModelCatatan.php';
require 'Tampilan.php';

$model = new ModelCatatan();
$catatan = $model->semua();

tampilkan_daftar($catatan);
text Keluaran
<!DOCTYPE html>
<html lang="id">
<head><meta charset="utf-8"><title>Daftar Catatan</title></head>
<body>
<h1>Daftar Catatan</h1>
<ul>
<li>Belanja mingguan: Beras, minyak goreng, dan telur.</li>
<li>Jadwal piket: Senin Ali, Rabu Budi, Jumat Citra.</li>
</ul>
</body>
</html>

Keluaran di atas dihasilkan ketika index.php dijalankan lewat CLI, yaitu tanpa permintaan HTTP sama sekali, dan tetap berupa halaman HTML yang utuh. Keadaan itu menegaskan bahwa pemisahan peran berjalan: pengujian data dan tampilan tidak menuntut server web. Pada proyek nyata, pemuatan kelas seperti di atas digantikan autoloader sebagaimana dibahas pada modul Namespace, Composer, dan PSR.

Menjalankan lewat server bawaan#

Perintah berikut menjalankan server bawaan dari folder aplikasi, lalu meminta halaman yang sama melalui HTTP.

bash
# jalankan server bawaan pada port 8110 dari folder aplikasi
php -S localhost:8110 >/dev/null 2>&1 &
sleep 1
# minta halaman daftar catatan
curl -s http://localhost:8110/
# hentikan server
kill %1
text Keluaran
<!DOCTYPE html>
<html lang="id">
<head><meta charset="utf-8"><title>Daftar Catatan</title></head>
<body>
<h1>Daftar Catatan</h1>
<ul>
<li>Belanja mingguan: Beras, minyak goreng, dan telur.</li>
<li>Jadwal piket: Senin Ali, Rabu Budi, Jumat Citra.</li>
</ul>
</body>
</html>

Respons HTTP berisi markah yang sama persis dengan keluaran CLI, karena controller melakukan pekerjaan yang sama untuk keduanya. Perbedaannya hanya pada asal permintaan, yaitu pemanggilan langsung berbanding permintaan dari browser. Dari titik ini, menambah halaman berarti menambah metode model, fungsi view, dan panggilan di controller, tanpa mengubah polanya.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat memisahkan peran pertama kali.

  • Model ikut mencetak HTML. Kelas yang mengambil data sekaligus menampilkan kembali sulit diuji dan mengunci format keluaran ke satu tampilan. Perbaikannya adalah membuat model hanya mengembalikan array atau objek data.
  • View berisi kueri atau keputusan bisnis. View yang memanggil basis data sendiri membuat urutan alur MVC terbalik dan menyembunyikan sumber datanya. Perbaikannya adalah menyerahkan seluruh data dari controller, sedangkan view hanya mencetak.
  • Controller menampung seluruh logika aplikasi. Berkas controller yang memanjang tanpa batas menandakan logika yang sebenarnya milik kelas model atau layanan lain. Perbaikannya adalah memindahkan logika ke kelas yang dipanggil controller.

Ringkasan#

  • MVC memisahkan data, tampilan, dan penghubung sehingga setiap peran dapat dibaca dan diuji secara terpisah.
  • Model hanya mengembalikan data, view hanya mencetak keluaran dengan htmlspecialchars pada setiap nilai, dan controller hanya menyusun panggilan di antara keduanya.
  • Keluaran CLI dan respons HTTP sama persis karena pemisahan peran tidak bergantung pada cara aplikasi dipanggil.
  • Pada aplikasi nyata, pemuatan berkas manual digantikan autoloader dan array tetap digantikan kueri PDO, tanpa mengubah batas antarperan.

Referensi#