Penafian: berikut ini adalah deskripsi bagaimana saya memahami pola mirip MVC dalam konteks aplikasi web berbasis PHP. Semua tautan eksternal yang digunakan dalam konten ada di sana untuk menjelaskan istilah dan konsep, dan tidak menyiratkan kredibilitas saya sendiri pada subjek.
Hal pertama yang harus saya jelaskan adalah: modelnya adalah layer .
Kedua: ada perbedaan antara MVC klasik dan apa yang kami gunakan dalam pengembangan web. Inilah sedikit jawaban lama yang saya tulis, yang menjelaskan secara singkat bagaimana perbedaannya.
Apa yang BUKAN model:
Model ini bukan kelas atau objek apa pun. Ini adalah kesalahan yang sangat umum terjadi (saya juga melakukannya, meskipun jawaban aslinya ditulis ketika saya mulai belajar sebaliknya) , karena sebagian besar kerangka kerja melanggengkan kesalahpahaman ini.
Baik itu teknik Pemetaan Objek-Relasional (ORM) maupun abstraksi tabel database. Siapa pun yang memberi tahu Anda sebaliknya kemungkinan besar akan berusaha 'menjual' ORM baru atau seluruh kerangka kerja.
Apa itu model:
Dalam adaptasi MVC tepat, M berisi semua logika bisnis domain dan lapisan Model yang sebagian besar terbuat dari tiga jenis struktur:
Objek Domain
Objek domain adalah wadah logis dari informasi domain murni; biasanya mewakili entitas logis dalam ruang domain masalah. Biasa disebut sebagai logika bisnis .
Ini adalah tempat Anda menentukan cara memvalidasi data sebelum mengirim faktur, atau menghitung total biaya pesanan. Pada saat yang sama, Objek Domain benar-benar tidak menyadari penyimpanan - baik dari mana (database SQL, REST API, file teks, dll.) Atau bahkan jika mereka disimpan atau diambil.
Pemetaan Data
Objek-objek ini hanya bertanggung jawab atas penyimpanan. Jika Anda menyimpan informasi dalam database, ini akan menjadi tempat tinggal SQL. Atau mungkin Anda menggunakan file XML untuk menyimpan data, dan Pemeta Data Anda menguraikan dari dan ke file XML.
Jasa
Anda dapat menganggapnya sebagai "Objek Domain tingkat tinggi", tetapi alih-alih logika bisnis, Layanan bertanggung jawab atas interaksi antara Objek Domain dan Pemetaan . Struktur ini pada akhirnya menciptakan antarmuka "publik" untuk berinteraksi dengan logika bisnis domain. Anda dapat menghindarinya, tetapi dengan membocorkan beberapa logika domain ke Controllers .
Ada jawaban terkait untuk subjek ini dalam pertanyaan implementasi ACL - mungkin berguna.
Komunikasi antara lapisan model dan bagian lain dari triad MVC harus terjadi hanya melalui Layanan . Pemisahan yang jelas memiliki beberapa manfaat tambahan:
- membantu menegakkan prinsip tanggung jawab tunggal (SRP)
- menyediakan 'ruang gerak' tambahan jika terjadi perubahan logika
- membuat pengontrol sesederhana mungkin
- memberikan cetak biru yang jelas, jika Anda membutuhkan API eksternal
Bagaimana cara berinteraksi dengan model?
Prasyarat: menonton kuliah "Global State and Singletons" dan "Don't Look Things!" dari Pembicaraan Kode Bersih.
Mendapatkan akses ke instance layanan
Agar instance View dan Controller (apa yang bisa Anda sebut: "lapisan UI") untuk mengakses layanan ini, ada dua pendekatan umum:
- Anda dapat menyuntikkan layanan yang diperlukan dalam konstruktor pandangan dan pengontrol Anda secara langsung, lebih disukai menggunakan wadah DI.
- Menggunakan pabrik untuk layanan sebagai ketergantungan wajib untuk semua pandangan dan pengontrol Anda.
Seperti yang Anda duga, wadah DI adalah solusi yang jauh lebih elegan (meskipun bukan yang paling mudah bagi pemula). Kedua pustaka, yang saya rekomendasikan mempertimbangkan fungsi ini akan menjadi komponen mandiri DependencyInjection atau Auryn milik Syfmony .
Baik solusi menggunakan pabrik dan wadah DI akan memungkinkan Anda juga berbagi contoh berbagai server untuk dibagikan antara pengontrol yang dipilih dan melihat siklus permintaan-respons yang diberikan.
Perubahan status model
Sekarang Anda dapat mengakses ke lapisan model di pengontrol, Anda harus mulai benar-benar menggunakannya:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
Pengontrol Anda memiliki tugas yang sangat jelas: ambil input pengguna dan, berdasarkan input ini, ubah status logika bisnis saat ini. Dalam contoh ini negara yang diubah antara adalah "pengguna anonim" dan "pengguna masuk".
Pengontrol tidak bertanggung jawab untuk memvalidasi input pengguna, karena itu adalah bagian dari aturan bisnis dan pengontrol jelas tidak memanggil permintaan SQL, seperti apa yang akan Anda lihat di sini atau di sini (jangan membenci mereka, mereka salah arah, bukan jahat).
Menampilkan pengguna perubahan-negara.
Oke, pengguna telah login (atau gagal). Sekarang apa? Kata pengguna masih tidak menyadarinya. Jadi, Anda harus benar-benar menghasilkan respons dan itu adalah tanggung jawab pandangan.
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
Dalam kasus ini, tampilan menghasilkan salah satu dari dua kemungkinan respons, berdasarkan kondisi lapisan model saat ini. Untuk kasus penggunaan yang berbeda, Anda akan memiliki pandangan memilih template yang berbeda untuk di-render, berdasarkan sesuatu seperti "artikel yang dipilih saat ini".
Lapisan presentasi sebenarnya bisa sangat rumit, seperti dijelaskan di sini: Memahami Tampilan MVC di PHP .
Tapi saya hanya membuat API REST!
Tentu saja, ada beberapa situasi, ketika ini adalah pembunuhan yang berlebihan.
MVC hanyalah solusi konkret untuk prinsip Pemisahan Kekhawatiran . MVC memisahkan antarmuka pengguna dari logika bisnis, dan di UI memisahkan pemisahan input pengguna dan presentasi. Ini sangat penting. Walaupun sering orang menggambarkannya sebagai "triad", itu sebenarnya tidak terdiri dari tiga bagian independen. Strukturnya lebih seperti ini:
Ini berarti, bahwa, ketika logika lapisan presentasi Anda hampir tidak ada, pendekatan pragmatis adalah menjaganya sebagai lapisan tunggal. Itu juga secara substansial dapat menyederhanakan beberapa aspek lapisan model.
Menggunakan pendekatan ini contoh login (untuk API) dapat ditulis sebagai:
public function postLogin(Request $request)
{
$email = $request->get('email');
$data = [
'status' => 'ok',
];
try {
$identity = $this->identification->findIdentityByEmailAddress($email);
$token = $this->identification->loginWithPassword(
$identity,
$request->get('password')
);
} catch (FailedIdentification $exception) {
$data = [
'status' => 'error',
'message' => 'Login failed!',
]
}
return new JsonResponse($data);
}
Meskipun ini tidak berkelanjutan, ketika Anda memiliki logika yang rumit untuk merender badan respons, penyederhanaan ini sangat berguna untuk skenario yang lebih sepele. Tetapi berhati-hatilah , pendekatan ini akan menjadi mimpi buruk, ketika mencoba untuk digunakan dalam basis kode besar dengan logika presentasi yang kompleks.
Bagaimana cara membangun model?
Karena tidak ada satu kelas "Model" (seperti dijelaskan di atas), Anda benar-benar tidak "membangun model". Alih-alih, Anda mulai membuat Layanan , yang dapat melakukan metode tertentu. Dan kemudian mengimplementasikan Objek Domain dan Pemetaan .
Contoh metode layanan:
Dalam kedua pendekatan di atas ada metode login ini untuk layanan identifikasi. Seperti apa bentuknya sebenarnya. Saya menggunakan versi yang sedikit dimodifikasi dari fungsi yang sama dari perpustakaan , yang saya tulis .. karena saya malas:
public function loginWithPassword(Identity $identity, string $password): string
{
if ($identity->matchPassword($password) === false) {
$this->logWrongPasswordNotice($identity, [
'email' => $identity->getEmailAddress(),
'key' => $password, // this is the wrong password
]);
throw new PasswordMismatch;
}
$identity->setPassword($password);
$this->updateIdentityOnUse($identity);
$cookie = $this->createCookieIdentity($identity);
$this->logger->info('login successful', [
'input' => [
'email' => $identity->getEmailAddress(),
],
'user' => [
'account' => $identity->getAccountId(),
'identity' => $identity->getId(),
],
]);
return $cookie->getToken();
}
Seperti yang Anda lihat, pada tingkat abstraksi ini, tidak ada indikasi dari mana data itu diambil. Mungkin database, tetapi juga mungkin hanya obyek tiruan untuk tujuan pengujian. Bahkan pemetaan data, yang sebenarnya digunakan untuk itu, disembunyikan dalam private
metode layanan ini.
private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}
Cara membuat pemetaan
Untuk mengimplementasikan abstraksi kegigihan, pendekatan yang paling fleksibel adalah membuat pemetaan data khusus .
Dari: PoEAA Buku
Dalam praktiknya mereka diimplementasikan untuk interaksi dengan kelas atau superclasses tertentu. Katakanlah Anda memiliki Customer
dan Admin
dalam kode Anda (keduanya mewarisi dari User
superclass). Keduanya mungkin akhirnya memiliki mapper yang cocok dan terpisah, karena mengandung bidang yang berbeda. Tetapi Anda juga akan berakhir dengan operasi bersama dan umum digunakan. Misalnya: memperbarui "yang terakhir terlihat online" . Dan alih-alih membuat pemetaan yang ada menjadi lebih berbelit-belit, pendekatan yang lebih pragmatis adalah memiliki "Pengguna Mapper" umum, yang hanya memperbarui stempel waktu itu.
Beberapa komentar tambahan:
Tabel dan model basis data
Meskipun kadang-kadang ada hubungan langsung 1: 1: 1 antara tabel database, Domain Object , dan Mapper , dalam proyek yang lebih besar mungkin lebih jarang terjadi daripada yang Anda harapkan:
Informasi yang digunakan oleh objek Domain tunggal mungkin dipetakan dari tabel yang berbeda, sedangkan objek itu sendiri tidak memiliki kegigihan dalam database.
Contoh: jika Anda menghasilkan laporan bulanan. Ini akan mengumpulkan informasi dari berbagai tabel, tetapi tidak ada MonthlyReport
tabel ajaib dalam database.
Seorang Mapper tunggal dapat memengaruhi banyak tabel.
Contoh: saat Anda menyimpan data dari User
objek, Objek Domain ini dapat berisi kumpulan objek domain lainnya - Group
instance. Jika Anda mengubahnya dan menyimpannya User
, Data Mapper harus memperbarui dan / atau memasukkan entri dalam beberapa tabel.
Data dari Objek Domain tunggal disimpan di lebih dari satu tabel.
Contoh: dalam sistem besar (pikirkan: jaringan sosial berukuran sedang), mungkin pragmatis untuk menyimpan data autentikasi pengguna dan data yang sering diakses secara terpisah dari potongan konten yang lebih besar, yang jarang diperlukan. Dalam hal ini Anda mungkin masih memiliki satu User
kelas, tetapi informasi yang dikandungnya akan bergantung pada apakah rincian lengkap diambil.
Untuk setiap Objek Domain, mungkin ada lebih dari satu mapper
Contoh: Anda memiliki situs berita dengan basis kode bersama untuk perangkat lunak manajemen publik dan publik. Tapi, sementara kedua antarmuka menggunakan Article
kelas yang sama , manajemen membutuhkan lebih banyak info yang terisi di dalamnya. Dalam hal ini Anda akan memiliki dua pemetaan yang terpisah: "internal" dan "eksternal". Setiap melakukan kueri yang berbeda, atau bahkan menggunakan basis data yang berbeda (seperti pada master atau slave).
Tampilan bukan templat
Lihat contoh di MVC (jika Anda tidak menggunakan variasi pola MVP) bertanggung jawab untuk logika presentasi. Ini berarti bahwa setiap Tampilan biasanya akan menyulap setidaknya beberapa templat. Ia memperoleh data dari Model Layer dan kemudian, berdasarkan informasi yang diterima, memilih templat dan menetapkan nilai.
Salah satu manfaat yang Anda peroleh dari hal ini adalah penggunaan kembali. Jika Anda membuat ListView
kelas, maka, dengan kode yang ditulis dengan baik, Anda dapat memiliki kelas yang sama menyerahkan presentasi daftar pengguna dan komentar di bawah artikel. Karena mereka berdua memiliki logika presentasi yang sama. Anda cukup mengganti templat.
Anda bisa menggunakan templat PHP asli atau menggunakan mesin templating pihak ketiga. Mungkin juga ada beberapa perpustakaan pihak ketiga, yang dapat sepenuhnya menggantikan Lihat contoh.
Bagaimana dengan versi lama dari jawabannya?
Satu-satunya perubahan besar adalah bahwa, apa yang disebut Model dalam versi lama, sebenarnya adalah Layanan . Sisa dari "analogi perpustakaan" terus berjalan dengan baik.
Satu-satunya kelemahan yang saya lihat adalah bahwa ini akan menjadi perpustakaan yang benar-benar aneh, karena itu akan mengembalikan Anda informasi dari buku itu, tetapi tidak membiarkan Anda menyentuh buku itu sendiri, karena kalau tidak abstraksi akan mulai "bocor". Saya mungkin harus memikirkan analogi yang lebih pas.
Apa hubungan antara instance View dan Controller ?
Struktur MVC terdiri dari dua lapisan: ui dan model. Struktur utama pada lapisan UI adalah view dan controller.
Saat Anda berurusan dengan situs web yang menggunakan pola desain MVC, cara terbaik adalah memiliki hubungan 1: 1 antara tampilan dan pengontrol. Setiap tampilan mewakili seluruh halaman di situs web Anda dan memiliki pengontrol khusus untuk menangani semua permintaan yang masuk untuk tampilan tertentu.
Misalnya, untuk mewakili artikel yang dibuka, Anda harus \Application\Controller\Document
dan \Application\View\Document
. Ini akan berisi semua fungsi utama untuk lapisan UI, ketika berurusan dengan artikel (tentu saja Anda mungkin memiliki beberapa komponen XHR yang tidak terkait langsung dengan artikel) .