Arsitektur Teknis PurwaExam
Asal-Usul: Dari APIK ke PurwaExam
PurwaExam adalah generasi penerus APIK (Aplikasi Ujian Kompetensi), sistem lama Universitas Brawijaya berbasis CodeIgniter 3 HMVC. Konsep struktur akademik (rumpun → sub-rumpun → Learning Outcome), bank soal per Learning Outcome, dan token akses ujian diwariskan langsung — tapi seluruh model data dan implementasinya dibangun ulang dari nol di atas Laravel 13 dan Filament v4, untuk mengakomodasi kebutuhan yang tidak ada di APIK: ujian akhir per kelas dengan bank soal rahasia.
Stack
- Backend: Laravel 13, PHP 8.3
- Admin panel: Filament v4 — resource, relation manager, importer bawaan dipakai penuh, minim custom UI dari nol
- Autorisasi: spatie/laravel-permission (role & permission per panel)
- Audit: spatie/laravel-activitylog — dicatat di level model (
ExamPackage,ExamBatch,ExamToken,Question,User) - Backup: spatie/laravel-backup, dump database harian, retensi 30 hari
Model Kepemilikan Soal
Satu method jadi sumber kebenaran tunggal untuk “soal mana yang boleh dipakai di ujian ini”:
Question::eligibleForExam(LearningOutcome $lo, Exam $exam): HasMany
Dipakai identik di tiga tempat: hitungan stok soal (ExamComponent::availableQuestionCount()), snapshot soal saat ujian dimulai (AttemptService::snapshotQuestions()), dan preview live di form admin. Untuk ujian akhir (kelas_id terisi), hasilnya union soal milik seluruh dosen pengampu kelas. Untuk ujian kompetensi (program_id, tanpa kelas_id), tidak ada pembatasan — kolaboratif lintas dosen by design.
Dua Tabel Pivot Dosen — Kenapa Perlu Disinkron Manual
course_lecturer (dosen ↔ mata kuliah, mengatur akses Bank Soal & Learning Outcome) dan kelas_lecturer (dosen ↔ kelas, mengatur akses edit Kelas) adalah dua tabel terpisah yang tidak otomatis sinkron. Menugaskan dosen lewat form Kelas cuma menulis ke kelas_lecturer.
Perbaikan: Kelas::syncLecturersToCourse(), union-only lewat syncWithoutDetaching (tidak pernah mencabut akses), dipanggil dari afterCreate()/afterSave() di halaman Filament — bukan event model saved(), karena event itu terpanggil sebelum Filament selesai menyimpan relasi form-nya sendiri.
Deploy di Shared Hosting Tanpa Custom Document Root
Kalau document root domain terkunci ke folder bawaan (tidak bisa diarahkan ke public/), struktur yang dipakai:
/home/USER/nama-app/ → kode aplikasi penuh, tidak web-accessible
/home/USER/domain.com/ → HANYA isi public/, + index.php yang dimodifikasi
index.php di folder domain butuh path require disesuaikan (../nama-app/vendor/autoload.php, bukan ../vendor/autoload.php) karena kedua folder sejajar, bukan induk-anak. Hasil npm run build wajib disinkronkan ke dua tempat: nama-app/public/build/ (dibaca PHP untuk manifest Vite) dan domain.com/build/ (diakses browser). Symlink storage juga tidak bisa pakai artisan storage:link default — harus dibuat manual menyasar folder document root yang benar.
Ekstensi PHP yang Sering Terlewat di Shared Hosting
zip dan intl tidak selalu aktif default. config/backup.php mereferensikan ZipArchive di level file config yang dievaluasi setiap request — tanpa guard extension_loaded('zip'), satu baris ini bisa menjatuhkan seluruh aplikasi, bukan cuma perintah backup.
Referensi Terkait
- Narasi lengkap studi kasus: PurwaExam: Studi Kasus Migrasi Sistem Ujian ke Laravel 13 & Filament
- Sistem pendahulunya: Definisi dan Fitur APIK (Aplikasi Ujian Kompetensi)