Cara Membuat Nomor Surat Otomatis Format Resmi di PHP & MySQL (Anti Nomor Ganda)
Di instansi pemerintah, sekolah, atau kantor, nomor surat keluar biasanya punya format baku, misalnya:
007/005/DISKOMINFO/X/2026
Artinya: surat ke-7 tahun ini, kode klasifikasi 005 (undangan), dibuat oleh DISKOMINFO, bulan X (Oktober), tahun 2026.
Kalau penomoran masih manual di buku agenda, dua masalah klasik sering muncul: nomor ganda (dua staf mengambil nomor yang sama) dan nomor loncat. Di tutorial ini kita bikin penomoran otomatis yang aman dengan PHP & MySQL. Contohnya diambil dari aplikasi Sistem Arsip Surat Masuk & Keluar yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Struktur tabel
Simpan nomor urut sebagai angka di kolom terpisah, jangan cuma menyimpan nomor lengkapnya. Menghitung nomor berikutnya jadi mudah dan tidak perlu memotong-motong string.
CREATE TABLE klasifikasi (
id INT AUTO_INCREMENT PRIMARY KEY,
kode VARCHAR(10) NOT NULL UNIQUE, -- 005, 800, 900, ...
nama VARCHAR(100) NOT NULL
) ENGINE=InnoDB;
CREATE TABLE surat_keluar (
id INT AUTO_INCREMENT PRIMARY KEY,
no_urut INT NOT NULL, -- 1, 2, 3, ... reset tiap tahun
nomor_surat VARCHAR(100) NOT NULL UNIQUE, -- 007/005/DISKOMINFO/X/2026
tgl_surat DATE NOT NULL,
klasifikasi_id INT,
tujuan VARCHAR(150) NOT NULL,
perihal VARCHAR(255) NOT NULL,
INDEX (tgl_surat)
) ENGINE=InnoDB;
Kolom nomor_surat diberi UNIQUE sebagai pengaman terakhir: kalau ada bug, database tetap menolak nomor ganda.
2. Mengubah bulan ke angka romawi
<?php
function romawi(int $bulan): string
{
return ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII'][$bulan];
}
echo romawi(10); // X
Untuk bulan saja (1–12), array lookup jauh lebih sederhana daripada algoritma konversi romawi umum.
3. Fungsi pembuat nomor surat
Nomor urut berikutnya = nomor urut terbesar di tahun yang sama + 1. Pakai MAX(), jangan COUNT(). Kalau ada surat yang dihapus, COUNT() akan menghasilkan nomor yang sudah pernah dipakai.
<?php
function nomor_surat_keluar(PDO $pdo, string $tglSurat, string $kodeKlasifikasi, string $kodeInstansi): array
{
$t = strtotime($tglSurat);
$tahun = date('Y', $t);
$st = $pdo->prepare('SELECT COALESCE(MAX(no_urut), 0) FROM surat_keluar WHERE YEAR(tgl_surat) = ?');
$st->execute([$tahun]);
$urut = (int) $st->fetchColumn() + 1;
$nomor = sprintf('%03d/%s/%s/%s/%s',
$urut, // 007 (3 digit, diisi nol)
$kodeKlasifikasi, // 005
$kodeInstansi, // DISKOMINFO
romawi((int) date('n', $t)), // X
$tahun // 2026
);
return [, ];
}
%03d membuat angka selalu 3 digit (7 jadi 007). Karena filter YEAR(tgl_surat) = ?, nomor urut otomatis mulai dari 001 lagi di awal tahun.
4. Kunci dengan transaksi supaya tidak ada nomor ganda
Fungsi di atas masih punya celah. Kalau dua staf menekan tombol simpan bersamaan, keduanya bisa membaca MAX(no_urut) yang sama sebelum salah satunya tersimpan. Solusinya: bungkus dalam transaksi dan kunci baris-baris tahun itu dengan FOR UPDATE.
<?php
try {
$pdo->beginTransaction();
// Kunci semua surat keluar di tahun yang sama sampai commit
$pdo->prepare('SELECT id FROM surat_keluar WHERE YEAR(tgl_surat) = ? FOR UPDATE')
->execute([date('Y', strtotime($tglSurat))]);
[$urut, $nomor] = nomor_surat_keluar($pdo, $tglSurat, $kodeKlasifikasi, 'DISKOMINFO');
$pdo->prepare('INSERT INTO surat_keluar (no_urut, nomor_surat, tgl_surat, klasifikasi_id, tujuan, perihal)
VALUES (?,?,?,?,?,?)')
->execute([$urut, $nomor, $tglSurat, $klasifikasiId, $tujuan, $perihal]);
$pdo->commit();
echo "Nomor surat: $nomor";
} catch (Throwable $e) {
$pdo->rollBack();
echo 'Gagal membuat nomor: ' . $e->getMessage();
}
Staf kedua akan menunggu sebentar sampai transaksi staf pertama selesai, lalu mendapatkan nomor berikutnya.
Pastikan tabel memakai engine InnoDB. Engine MyISAM tidak mendukung transaksi dan penguncian baris.
5. Kunci nomor saat surat diedit
Begitu nomor terbit, tanggal surat dan klasifikasi sebaiknya tidak bisa diubah karena keduanya membentuk nomor. Di form edit, tampilkan field itu sebagai disabled dan di sisi server hanya terima field yang boleh berubah (tujuan, perihal, sifat). Jangan mengandalkan atribut disabled di HTML saja, karena pengguna bisa memanipulasi form lewat DevTools.
Bonus: nomor agenda surat masuk
Surat masuk biasanya hanya butuh nomor agenda sederhana yang berurutan per tahun (0001, 0002, ...). Polanya sama:
$st = $pdo->prepare('SELECT COALESCE(MAX(CAST(no_agenda AS UNSIGNED)), 0) FROM surat_masuk WHERE YEAR(tgl_terima) = ?');
$st->execute([date('Y', strtotime($tglTerima))]);
$agenda = str_pad((string) ($st->fetchColumn() + 1), 4, '0', STR_PAD_LEFT); // 0155
Ringkasan
- Simpan nomor urut (angka) terpisah dari nomor lengkap (teks).
- Hitung nomor berikutnya dengan
MAX() + 1per tahun, bukanCOUNT(). - Format dengan
sprintf('%03d/...')dan konversi bulan ke romawi. - Bungkus pembuatan nomor dalam transaksi + FOR UPDATE, dan beri UNIQUE pada kolom nomor.
- Kunci tanggal dan klasifikasi setelah nomor terbit.
Download source code lengkapnya
Semua logika di atas sudah dipakai di Sistem Arsip Surat Masuk & Keluar dengan Disposisi: nomor agenda dan nomor surat otomatis, upload scan surat yang terlindungi, disposisi pimpinan ke bagian sampai ditindaklanjuti, cetak lembar disposisi, dan buku agenda yang bisa diekspor ke Excel. Gratis dan langsung jalan di XAMPP.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.