Skip to main content

PurwaExam: Studi Kasus Migrasi Sistem Ujian ke Laravel 13 & Filament

4 min 699 words

Dua tabel pivot dosen. Nama mirip. Tidak pernah saling sinkron sejak awal. Satu dosen bisa mengedit kelasnya sendiri, tapi menu Mata Kuliah dan Learning Outcome-nya kosong melompong — padahal dia jelas-jelas mengajar di sana.

Itu salah satu dari beberapa bug nyata yang saya temukan waktu membangun ulang sistem ujian kompetensi jadi PurwaExam. Tulisan ini sisi teknisnya: apa yang sebenarnya rusak, dan bagaimana Laravel 13 plus Filament v4 dipakai untuk membetulkannya.

Dari APIK ke PurwaExam

PurwaExam bukan proyek dari nol. Pendahulunya, APIK (Aplikasi Ujian Kompetensi), dibangun dengan CodeIgniter 3 pola HMVC, cukup solid untuk ujian kompetensi lintas program studi. Struktur rumpun/sub-rumpun/Learning Outcome dan konsep token akses ujian di PurwaExam hari ini diwariskan langsung dari sana.

Yang tidak bisa lagi ditambal di arsitektur lama: kebutuhan ujian akhir per kelas, yang soalnya harus rahasia sampai hari-H — sesuatu yang tidak pernah jadi masalah waktu APIK cuma menangani ujian kompetensi yang memang kolaboratif sejak awal. Dari sanalah keputusan membangun ulang total, bukan menambal, diambil.

Bug: Dua Tabel Pivot yang Diam-Diam Tidak Sinkron

course_lecturer mengatur akses dosen ke mata kuliah. kelas_lecturer mengatur akses dosen ke kelas. Dua hal yang kedengarannya sama tapi ternyata dua tabel berbeda — dan cuma satu yang keisi waktu dosen ditugaskan lewat form Kelas.

Hasilnya: dosen bisa edit kelasnya sendiri (baca dari kelas_lecturer), tapi tidak muncul di daftar Mata Kuliah atau Learning Outcome (baca dari course_lecturer). Dua fitur yang kelihatan tidak berhubungan, ternyata satu akar masalah.

Perbaikannya bukan menghapus salah satu tabel — terlalu berisiko, banyak query lain bergantung padanya. Saya tambahkan satu method, Kelas::syncLecturersToCourse(), dipanggil setelah Filament selesai menyimpan relasi kelas — union-only, cuma menambah akses, tidak pernah mencabut. Kalau dijalankan di event saved() model malah keburu terpanggil sebelum Filament selesai proses form-nya sendiri; harus di afterCreate()/afterSave() milik halaman Filament.

Fitur: Kepemilikan Soal yang Sadar Konteks

Ini yang paling menarik secara desain. Ujian kompetensi dan ujian akhir butuh aturan akses bank soal yang berlawanan — tapi dua-duanya harus lewat jalur kode yang sama, supaya tidak ada celah kalau salah satu lupa diupdate.

Solusinya satu method Eloquent: Question::eligibleForExam(LearningOutcome $lo, Exam $exam). Dipakai persis sama di tiga tempat — hitungan stok soal tersedia, preview live di form admin, dan snapshot soal saat peserta memulai ujian. Untuk ujian akhir (ada kelas_id), hasilnya gabungan soal seluruh dosen pengampu kelas itu — union, bukan cuma satu dosen, karena kelas sering diajar lebih dari satu dosen. Untuk ujian kompetensi (ada program_id, tanpa kelas_id), aturan ini dilepas total — memang harus kolaboratif lintas dosen sejak awal.

Satu titik kebenaran, tiga pemakaian. Kalau logikanya berubah nanti, cukup ubah di satu tempat.

Deploy: Shared Hosting yang Document Root-nya Tidak Bisa Diubah

Rencana awal sederhana: arahkan document root domain langsung ke public/, standar Laravel di mana pun. Ternyata di akun cPanel yang dipakai, kolom itu terkunci — tidak bisa diarahkan ke folder lain sama sekali.

Solusinya: pisahkan jadi dua folder sejajar. Kode aplikasi penuh di satu folder (tidak bisa diakses web), isi public/ saja di folder document root bawaan — lengkap dengan index.php yang path require-nya saya modifikasi manual, karena sekarang dua folder itu sejajar, bukan induk-anak seperti asumsi default Laravel.

Konsekuensinya, hasil build Vite (public/build/) harus ada di dua tempat: satu untuk dibaca PHP di sisi server (manifest.json, tidak perlu bisa diakses browser), satu lagi yang benar-benar diminta browser lewat HTTP. Lupa sinkronkan salah satunya setelah npm run build berikutnya, dan aset CSS/JS langsung pecah di production tanpa pesan error yang jelas di awal.

Ekstensi PHP zip dan intl juga ternyata tidak aktif default di host ini — config/backup.php mereferensikan ZipArchive langsung di level file config, yang dievaluasi PHP di setiap request, bukan cuma saat backup jalan. Tanpa guard extension_loaded('zip'), satu baris config bisa menjatuhkan seluruh situs.

Yang Dipelajari

Proyek ini mengajarkan satu hal yang sering diremehkan: bug paling berbahaya bukan yang bikin aplikasi crash, tapi yang bikin aplikasi jalan normal sambil diam-diam membocorkan hal yang seharusnya rahasia. Dua tabel pivot yang tidak sinkron cuma bikin fitur kelihatan “kurang lengkap”. Bank soal tanpa kepemilikan yang jelas bisa bikin ujian akhir bocor tanpa ada yang sadar sampai hari-H.

Kalau proyek Anda punya sistem lama yang “jalan tapi meragukan” — akses yang kebablasan, dua sumber data yang seharusnya satu, atau rencana deploy yang ternyata tidak cocok dengan hosting sungguhan — itu justru sinyal paling jelas untuk diaudit dulu sebelum ditambal lebih jauh. Sashindo biasa masuk dari titik itu.

Studi kasus teknis PurwaExam — dari dua tabel pivot dosen yang tidak sinkron sampai deploy Laravel di shared hosting tanpa custom document root.

...ita lengkap soal kenapa dan bagaimana rekonstruksi itu dilakukan — termasuk sisi teknisnya — ada di [[PurwaExam: Studi Kasus Migrasi Sistem Ujian ke Laravel 13 & Filament]].

Referenced in this post