Skip to main content
Diagram struktur folder deploy PurwaExam di shared hosting cPanel

Arsitektur Teknis PurwaExam

Studi Kasus Teknis Version 1.0

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