Cara Membuat Seleksi PPDB Otomatis per Jalur & Kuota di PHP & MySQL (Zonasi, Prestasi, Afirmasi)
Setiap musim PPDB, panitia sekolah menghadapi pertanyaan yang sama: siapa yang lolos di setiap jalur, berdasarkan apa, dan kalau nilainya sama siapa yang didahulukan? Kalau perhitungannya masih manual di Excel, rawan salah urut dan sulit dijelaskan ke orang tua.
Di tutorial ini kita bangun logika seleksi PPDB otomatis dengan PHP & MySQL: skor berbeda untuk tiap jalur, pengurutan dengan tie-breaker yang adil, lalu penetapan diterima, cadangan, dan tidak diterima sesuai kuota. Contohnya diambil dari aplikasi PPDB Online yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Struktur tabel
CREATE TABLE jalur (
id INT AUTO_INCREMENT PRIMARY KEY,
kode VARCHAR(20) NOT NULL UNIQUE, -- zonasi, prestasi, afirmasi, mutasi
nama VARCHAR(60) NOT NULL,
kuota SMALLINT NOT NULL
) ENGINE=InnoDB;
CREATE TABLE pendaftar (
id INT AUTO_INCREMENT PRIMARY KEY,
jalur_id INT NOT NULL,
nama VARCHAR(100) NOT NULL,
jarak_km DECIMAL(5,2) NOT NULL DEFAULT 0,
nilai_rata DECIMAL(5,2) NOT NULL DEFAULT 0,
poin_prestasi DECIMAL(5,2) NOT NULL DEFAULT 0,
status ENUM('draft','menunggu','perbaikan','terverifikasi','diterima','cadangan','tidak_diterima','daftar_ulang') NOT NULL DEFAULT 'draft',
skor DECIMAL(6,2) ,
peringkat ,
dikirim_at DATETIME
) ENGINEInnoDB;
Hanya pendaftar yang berkasnya sudah terverifikasi yang ikut seleksi. Pendaftar yang masih menunggu verifikasi atau diminta perbaikan belum dihitung.
2. Rumus skor per jalur
Setiap jalur punya prioritas berbeda. Dengan match di PHP 8 rumusnya tetap ringkas dan mudah dibaca:
<?php
function hitung_skor(array $p, string $kodeJalur): float
{
$rapor = (float) $p['nilai_rata'];
return match ($kodeJalur) {
// Jarak paling menentukan: 0 km = 100, tiap 1 km dikurangi 10 (minimal 0), ditambah 10% nilai rapor
'zonasi' => round(max(0, 100 - (float) $p['jarak_km'] * 10) + $rapor * 0.1, 2),
// Nilai rapor + poin prestasi yang sudah disetujui verifikator
'prestasi' => round($rapor + (float) $p['poin_prestasi'], 2),
// Afirmasi & perpindahan tugas: nilai rapor
default => round($rapor, 2),
};
}
Contoh jalur zonasi, jarak 0,8 km dengan rapor 91,5: 100 − 8 + 9,15 = 101,15.
Rumus di atas hanya contoh. Sesuaikan dengan juknis PPDB daerahmu. Yang penting, rumusnya ditulis di satu fungsi sehingga mudah diubah dan diaudit.
3. Mengurutkan dengan tie-breaker yang adil
Dua pendaftar bisa memiliki skor persis sama. Tentukan aturan pemecah yang jelas dan konsisten:
- Skor lebih tinggi
- Zonasi: jarak lebih dekat. Jalur lain: nilai rapor lebih tinggi
- Yang lebih dulu mengirim formulir
Operator spaceship (<=>) pada array membandingkan elemen satu per satu dari kiri ke kanan, sehingga cocok untuk pengurutan bertingkat:
<?php
usort($calon, function ($a, $b) use ($kodeJalur) {
$kunciA = [$a['skor'], $kodeJalur === 'zonasi' ? -$a['jarak_km'] : $a['nilai_rata'], $b['dikirim_at']];
$kunciB = [$b['skor'], $kodeJalur === 'zonasi' ? -$b['jarak_km'] : $b['nilai_rata'], $a['dikirim_at']];
return $kunciB <=> $kunciA; // urutan menurun (terbaik di atas)
});
Dua trik kecil di sini:
-jarak_km: jarak lebih kecil harus dianggap "lebih baik", jadi nilainya dibalik.- Posisi
dikirim_atditukar ($bdi kunci A dan$adi kunci B) supaya yang lebih awal mengirim berada di atas, walaupun urutan keseluruhannya menurun.
4. Tetapkan diterima, cadangan, dan tidak diterima
Setelah urut, tinggal potong sesuai kuota. Cadangan diambil dari persentase kuota (misalnya 10%):
<?php
$pdo->beginTransaction();
foreach ($daftarJalur as $j) {
$st = $pdo->prepare("SELECT * FROM pendaftar WHERE jalur_id = ?
AND status IN ('terverifikasi','diterima','cadangan','tidak_diterima')");
$st->execute([$j['id']]);
$calon = $st->fetchAll(PDO::FETCH_ASSOC);
foreach ($calon as &$c) $c['skor'] = hitung_skor($c, $j['kode']);
unset($c);
urutkan($calon, $j['kode']); // usort dari langkah 3
$cadangan = (int) ceil($j['kuota'] * 10 / 100);
$up = $pdo->prepare('UPDATE pendaftar SET skor = ?, peringkat = ?, status = ? WHERE id = ?');
foreach ($calon as => ) {
= < [] ?
: ( < [] + ? : );
->([[], + , , []]);
}
}
->();
Perhatikan klausa status IN ('terverifikasi','diterima','cadangan','tidak_diterima'). Dengan begitu seleksi bisa dijalankan ulang kapan saja (misalnya setelah ada verifikasi susulan) dan hasilnya selalu konsisten. Pendaftar yang sudah daftar ulang tidak ikut diurutkan ulang.
Bungkus seluruh proses dalam transaksi supaya tidak ada keadaan setengah jadi, misalnya jalur zonasi sudah diproses tetapi jalur prestasi belum.
5. Tahan hasil sampai jadwal pengumuman
Panitia boleh menjalankan seleksi lebih awal sebagai simulasi, tetapi calon siswa baru boleh melihat hasilnya saat pengumuman. Tentukan tahap dari jadwal, lalu "samarkan" status di halaman calon siswa:
<?php
function tahap(array $jadwal): string
{
$now = date('Y-m-d H:i:s');
if ($now < $jadwal['buka']) return 'belum';
if ($now <= $jadwal['tutup']) return 'pendaftaran';
if ($now < $jadwal['pengumuman']) return 'seleksi';
return 'pengumuman';
}
// Di halaman status calon siswa
$hasilSeleksi = ['diterima', 'cadangan', 'tidak_diterima'];
$tampil = tahap($jadwal) === 'pengumuman' ? $p['status']
: (in_array($p['status'], $hasilSeleksi, true) ? 'terverifikasi' : $p['status']);
Ringkasan
- Hanya pendaftar terverifikasi yang diseleksi.
- Tulis rumus skor setiap jalur di satu fungsi supaya mudah disesuaikan dengan juknis.
- Urutkan dengan tie-breaker bertingkat memakai
<=>pada array. - Potong sesuai kuota, ambil cadangan dari persentase kuota, dan jalankan dalam transaksi supaya bisa diulang dengan aman.
- Tahan hasil sampai jadwal pengumuman.
Download source code lengkapnya
Semua logika di atas sudah dipakai di PPDB Online PHP Native & MySQL: pendaftaran mandiri dengan formulir 4 langkah, unggah berkas yang terlindungi, verifikasi dan permintaan perbaikan, seleksi otomatis per jalur, pengumuman terjadwal, kartu pendaftaran, dan daftar ulang. Gratis dan langsung jalan di XAMPP.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.