Teks “café” yang tampil sebagai “café”, tanda tanya hitam di halaman web, atau kolom database yang rusak diam-diam — hampir semuanya berakar pada satu masalah yang sama: character encoding. Encoding adalah aturan bagaimana karakter diterjemahkan menjadi byte saat disimpan dan dikirim, lalu diterjemahkan kembali menjadi karakter saat dibaca; ketika kedua pihak memakai aturan yang berbeda, teks rusak tanpa pesan error apa pun. PHP menempatkan tanggung jawab ini penuh pada kita: string PHP hanyalah urutan byte mentah, tanpa penanda encoding apa pun. Chapter ini membongkar cara kerja encoding dari ASCII hingga UTF-8, lalu mempraktikkan deteksi BOM, pemeriksaan validitas, dan perbaikan korupsi klasik — fondasi bagi seluruh chapter internasionalisasi di section ini.
Apa Itu Character Encoding #
Komputer hanya menyimpan angka, sehingga setiap karakter perlu dipetakan ke angka — huruf A menjadi angka 65, misalnya. Encoding adalah kamus dua arah tersebut: aturan yang menetapkan karakter mana direpresentasikan byte mana, berapa byte dipakai per karakter, dan bagaimana byte-byte itu diurutkan. Dua sistem yang bertukar teks harus bersepakat memakai kamus yang sama; jika penulis memakai satu kamus dan pembaca memakai kamus lain, byte yang sama ditafsirkan menjadi karakter yang berbeda.
Karena string PHP adalah urutan byte tanpa label encoding, PHP sendiri tidak tahu apakah "caf\xC3\xA9" dimaksudkan sebagai teks UTF-8 atau sekadar deretan byte tanpa makna khusus. Fungsi seperti strlen() menghitung byte; fungsi mb_* dari ekstensi mbstring-lah yang mampu menafsirkan byte sebagai karakter menurut encoding tertentu. Karena itu, keputusan encoding harus dijaga oleh kita — konsisten di seluruh aplikasi, dan pilihan terbaik untuk aplikasi modern adalah UTF-8.
Dari ASCII ke Latin-1 hingga UTF-8 #
Tiga generasi encoding menutup kebutuhan yang terus membesar. ASCII cukup untuk bahasa Inggris dengan 128 karakter dalam satu byte; Latin-1 (ISO-8859-1) memanfaatkan sisa byte hingga 256 karakter untuk huruf beraksen Eropa Barat; UTF-8 menjangkau seluruh karakter Unicode — aksen, aksara Asia, hingga emoji — dengan panjang byte yang bervariasi:
| Encoding | Ukuran per karakter | Cakupan |
|---|---|---|
| ASCII | 1 byte (7 bit) | 128 karakter: huruf Latin, angka, tanda baca, kontrol |
| ISO-8859-1 (Latin-1) | 1 byte | 256 karakter: ASCII + huruf beraksen Eropa Barat |
| UTF-8 | 1–4 byte | Seluruh Unicode: aksen, aksara CJK, simbol, emoji |
Desain UTF-8 cerminan dari warisan ASCII: karakter ASCII tetap ditulis satu byte dengan nilai sama, dan setiap karakter non-ASCII ditandai byte pembuka khusus yang diikuti byte lanjutan. Kita bisa menyaksikan strukturnya langsung dengan bin2hex() dan strlen():
<?php
foreach (["A", "é", "€", "😀"] as $karakter) {
printf("%s → 0x%s (%d byte)\n", $karakter, bin2hex($karakter), strlen($karakter));
}Output yang Dihasilkan:
A → 0x41 (1 byte)
é → 0xc3a9 (2 byte)
€ → 0xe282ac (3 byte)
😀 → 0xf09f9880 (4 byte)Huruf A menempati satu byte 0x41 — sama persis seperti ASCII. Huruf é menempati dua byte 0xC3 0xA9: byte pembuka 0xC3 mengumumkan “karakter dua byte menyusul”, dan 0xA9 adalah byte lanjutannya. Simbol € memakai tiga byte, dan emoji 😀 memakai empat — panjang yang bervariasi inilah yang membuat fungsi byte seperti strlen() tidak bisa dipakai untuk menghitung karakter.
Menghitung Byte versus Karakter #
Dua pertanyaan yang sering tertukar: “berapa panjang string ini?” dan “berapa karakter teks ini?” Ketika teks mengandung karakter multi-byte, keduanya berbeda jawaban:
<?php
$teks = "Siti menulis di café";
echo strlen($teks), "\n";
echo mb_strlen($teks), "\n";
echo bin2hex("é"), "\n";Output yang Dihasilkan:
21
20
c3a9String "Siti menulis di café" terdiri dari 20 karakter, tetapi 21 byte — huruf é menyumbang dua byte — dan bin2hex("é") memperlihatkan byte c3a9 yang menjadi alasannya. Fungsi mb_strlen() menafsirkan byte menurut encoding (default UTF-8) lalu menghitung karakternya, sedangkan strlen() hanya menghitung byte.
Byte Order Mark (BOM) #
BOM adalah tiga byte EF BB BF yang kadang disisipkan editor di awal berkas UTF-8 untuk menandai encoding-nya. Pada berkas teks biasa, BOM lebih sering merepotkan: ia muncul sebagai karakter tak terlihat sebelum baris pertama, merusak perbandingan string, dan bocor menjadi output aneh di awal respons web. Deteksi dan penghapusannya sederhana:
<?php
const UTF8_BOM = "\xEF\xBB\xBF";
$isiDenganBom = UTF8_BOM . "nama,saldo\nBudi,150000\n";
$adaBom = str_starts_with($isiDenganBom, UTF8_BOM);
$isiBersih = $adaBom ? substr($isiDenganBom, strlen(UTF8_BOM)) : $isiDenganBom;
var_dump($adaBom);
echo "Baris pertama: ", explode("\n", $isiBersih)[0], "\n";
echo "Tiga byte pertama setelah BOM dihapus: 0x", bin2hex(substr($isiBersih, 0, 3)), "\n";Output yang Dihasilkan:
bool(true)
Baris pertama: nama,saldo
Tiga byte pertama setelah BOM dihapus: 0x6e616dFungsi str_starts_with() memeriksa kehadiran BOM, dan substr() menghapus tepat tiga byte itu sebelum isi dipakai. Contoh di atas memakai isi CSV karena BOM paling sering menghantui berkas tersebut — header nama,saldo yang tampak benar di editor tiba-tiba membuat array_combine() atau pembaca header gagal karena byte EF BB BF menempel di depan kata nama. Satu pengecualian yang layak diketahui: berkas CSV yang ditujukan untuk Microsoft Excel justru sengaja diberi BOM UTF-8 agar Excel mengenali UTF-8 secara benar.
Memeriksa dan Mendeteksi Encoding #
Ketika data tiba dari sumber yang tidak diketahui encoding-nya, ada dua pertanyaan berbeda yang perlu dijawab. Pertama: “apakah byte ini valid untuk encoding tertentu?” — dijawab oleh mb_check_encoding(). Kedua: “encoding apa yang paling mungkin dipakai?” — dijawab oleh mb_detect_encoding():
<?php
$teksValid = "Halo, café";
$byteLatin1 = "caf\xE9";
var_dump(mb_check_encoding($teksValid, "UTF-8"));
var_dump(mb_check_encoding($byteLatin1, "UTF-8"));Output yang Dihasilkan:
bool(true)
bool(false)String pertama lolos karena seluruh bytenya membentuk urutan UTF-8 yang sah. String kedua gagal: byte tunggal 0xE9 adalah byte pembuka karakter tiga byte dalam UTF-8, tetapi tidak diikuti byte lanjutan — urutan yang mustahil. mb_check_encoding() cocok dipasang sebagai gerbang validasi: teks yang gagal pemeriksaan sebaiknya ditolak atau dikarantina, bukan diteruskan.
<?php
$teksAscii = "Selamat datang";
$teksUtf8 = "Selamat datang, café";
$byteLatin1 = "caf\xE9";
var_dump(mb_detect_encoding($teksAscii, ["ASCII", "UTF-8"], true));
var_dump(mb_detect_encoding($teksUtf8, ["ASCII", "UTF-8"], true));
var_dump(mb_detect_encoding($byteLatin1, ["UTF-8", "ISO-8859-1"], true));Output yang Dihasilkan:
string(5) "ASCII"
string(5) "UTF-8"
string(10) "ISO-8859-1"Teks murni ASCII terdeteksi sebagai ASCII; teks beraksen UTF-8 terdeteksi sebagai UTF-8; dan byte Latin-1 0xE9 terdeteksi sebagai ISO-8859-1 karena UTF-8 gugur lebih dulu dalam daftar kandidat. Selalu berikan daftar kandidat secara eksplisit dan nyalakan parameter ketiga (strict) — tanpa keduanya, hasil deteksi melenceng jauh dari kenyataan.
Korupsi Klasik: Double Encoding #
Korupsi encoding paling sering dijumpai berbentuk double encoding: teks UTF-8 disalahartikan sebagai Latin-1, lalu di-encode ulang menjadi UTF-8 — setiap byte asal berubah menjadi karakter terpisah. Mari kita lihat prosesnya dengan input tetap:
<?php
$teksBenar = "café";
$teksRusak = mb_convert_encoding($teksBenar, "UTF-8", "ISO-8859-1");
echo "Asli : ", bin2hex($teksBenar), " → $teksBenar\n";
echo "Rusak: ", bin2hex($teksRusak), " → $teksRusak\n";Output yang Dihasilkan:
Asli : 636166c3a9 → café
Rusak: 636166c383c2a9 → caféTeks asli café adalah byte 63 61 66 C3 A9. Pemanggilan mb_convert_encoding() di atas sengaja berbohong: ia diberi tahu bahwa sumbernya Latin-1 padahal aslinya UTF-8, sehingga dua byte C3 A9 (satu huruf é) ditafsirkan sebagai dua karakter Latin-1 à dan ©, lalu masing-masing di-encode ulang menjadi dua byte UTF-8 — hasilnya byte 63 61 66 C3 83 C2 A9 yang tampil sebagai café. Karakteristik inilah tanda tangannya: setiap karakter beraksen berubah jadi dua karakter aneh berawalan Ã.
Memperbaikinya berarti membalik proses tersebut: konversi teks rusak dari UTF-8 kembali ke ISO-8859-1 (mb_convert_encoding($teksRusak, "ISO-8859-1", "UTF-8")) akan mengembalikan byte UTF-8 yang asli — berhasil hanya jika seluruh karakter yang terlibat berada di bawah 256. Pencegahan selalu lebih murah daripada perbaikan: pastikan seluruh rantai — berkas sumber, koneksi database, dan aplikasi — berbicara UTF-8, sehingga konversi silang tidak pernah terjadi.
Deklarasikan UTF-8 di Semua Lapisan #
Aplikasi bebas masalah encoding bukan yang “kebetulan cocok”, melainkan yang menyatakan UTF-8 secara eksplisit di setiap lapisan yang dilaluinya:
<?php
// 1. Respons HTTP untuk halaman web — panggil sebelum output apa pun
header("Content-Type: text/html; charset=utf-8");
// 2. Encoding internal untuk seluruh fungsi mbstring
mb_internal_encoding("UTF-8");
// 3. Koneksi MySQL melalui PDO — selalu utf8mb4, bukan utf8
$pdo = new PDO(
"mysql:host=localhost;dbname=toko;charset=utf8mb4",
"pengguna",
"YOUR_DATABASE_PASSWORD"
);Empat deklarasi lain hidup di luar kode PHP: simpan seluruh berkas sumber .php sebagai UTF-8 di editor, nyatakan <meta charset="utf-8"> pada dokumen HTML, buat tabel MySQL dengan kumpulan karakter utf8mb4, dan tulis deklarasi encoding saat membuat berkas XML. Nama charset utf8mb4 pada MySQL perlu diperhatikan — utf8 lama di MySQL hanya menampung karakter hingga tiga byte sehingga emoji dan karakter langka gagal tersimpan, sementara utf8mb4 adalah UTF-8 yang sebenarnya.
Ringkasan #
Encoding adalah kontrak byte dan karakter yang harus disepakati seluruh rantai data. Kini kita tahu mengapa strlen() dan mb_strlen() berbeda, bagaimana UTF-8 menyusun karakter multi-byte, bagaimana BOM dideteksi dan dihapus, bagaimana validitas byte diperiksa, serta bagaimana korupsi double encoding terjadi dan dicegah. Dengan fondasi byte ini, kita siap masuk lebih jauh ke model karakter Unicode dan praktik UTF-8 yang benar pada Chapter Unicode dan UTF-8
.