UTF-8 dari Browser ke MariaDB - Menjaga Teks Tetap Utuh di Aplikasi PHP
Sebuah form menerima nama dengan benar, PHP mencetaknya dengan benar dalam pesan debug, dan MariaDB menyimpan sesuatu yang masih tampak masuk akal. Beberapa waktu kemudian, halaman menampilkan simbol pengganti, pencarian gagal menemukan kata yang terlihat sama, atau sebuah emoji menghilang. Lapisan mana yang mengubah teks itu?
Tidak ada satu "tombol UTF-8" untuk seluruh web application. Teks melintasi beberapa batas: browser ke HTTP request, byte request ke string PHP, PHP ke koneksi database, koneksi ke kolom, lalu byte tersimpan kembali ke HTML response. Setiap batas memiliki kontraknya sendiri. Satu asumsi yang keliru bisa tetap tersembunyi sementara test suite yang hanya memakai ASCII terus lulus.
Artikel ini menyusun audit per lapisan untuk aplikasi PHP kecil yang memakai PDO dan MariaDB. Fokusnya adalah sistem baru atau sistem yang memang sudah berbasis UTF-8. Memperbaiki data legacy adalah persoalan forensik tersendiri karena mengganti label tidak dapat mengungkap bagaimana byte yang rusak dahulu ditafsirkan.
Bedakan karakter, byte, dan aturan perbandingan
Unicode memberikan code point kepada karakter. UTF-8 adalah encoding yang merepresentasikan Unicode scalar value sebagai rangkaian byte. Keduanya berkaitan, tetapi bukan sinonim. WHATWG Encoding Standard mendefinisikan encoding sebagai pemetaan antara scalar value dan byte, mewajibkan UTF-8 untuk format dan konteks baru, serta menjelaskan mengapa ketidaksepakatan encoding antara produsen dan konsumen dapat memengaruhi ketepatan dan keamanan.
Collation database menjawab pertanyaan lain: bagaimana string dibandingkan dan diurutkan? Dua kolom dapat sama-sama memakai utf8mb4, tetapi menerapkan aturan berbeda untuk huruf besar-kecil, aksen, atau pengurutan linguistik. Font menambahkan lapisan independen lain dengan menentukan glyph yang tampil di layar. Kotak kosong bisa menjadi masalah font; kemunculan U+FFFD, replacement character, biasanya mengarah pada rangkaian byte yang tidak valid atau keliru di-decode. Kedua gejala itu saja belum menunjukkan lokasi kesalahan awal.
Pemisahan ini menghasilkan kebiasaan debugging yang berguna: pada setiap batas, tanyakan karakter apa yang dimaksud, byte apa yang ada, encoding apa yang dinyatakan, dan kebijakan perbandingan apa yang aktif. "Di sini terlihat baik-baik saja" adalah bukti yang lebih lemah daripada empat jawaban tersebut.
Nyatakan UTF-8 pada batas web
Sebuah HTML response perlu menyatakan encoding secara konsisten. PHP dapat mengirim media type dan charset dalam HTTP header, sedangkan dokumen membawa deklarasi metadata di bagian awal:
<?php
header('Content-Type: text/html; charset=UTF-8');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Text boundary test</title>
</head>
HTML Standard mendefinisikan <meta charset="utf-8"> sebagai deklarasi encoding dokumen dan menyarankan agar informasi encoding ditempatkan lebih awal. HTTP response dan HTML sebaiknya sepakat, bukan mengandalkan deteksi browser. Source file PHP, template, file terjemahan, dan fixture impor juga perlu disimpan sebagai UTF-8; label response yang benar tidak melakukan transcoding terhadap source file yang ditulis dengan encoding lain.
Request membutuhkan kontrak yang sama jelasnya. Form HTML modern yang disajikan sebagai UTF-8 biasanya mengirim teks dengan UTF-8, sedangkan JSON API umumnya memakai UTF-8 sebagai bagian dari ekosistem web JSON. Meski begitu, aplikasi tetap perlu menentukan apa yang diterima dan menolak input malformed pada batas yang terkendali, bukan menebak-nebak di antara beberapa encoding.
Validasi input yang diharapkan, jangan mengonversi secara membabi buta
String PHP adalah rangkaian byte; PHP tidak melekatkan tipe UTF-8 permanen pada setiap string. Fungsi mbstring mb_check_encoding dapat memeriksa apakah sebuah byte stream valid untuk encoding yang disebutkan:
function requireUtf8(string $value): string
{
if (!mb_check_encoding($value, 'UTF-8')) {
throw new InvalidArgumentException('Expected valid UTF-8 text.');
}
return $value;
}
Validitas sengaja memberikan hasil yang sempit. Artinya, byte tersebut membentuk UTF-8 yang sah. Hasil itu tidak menyatakan bahwa teks telah dinormalisasi, aman dimasukkan ke HTML, layak menjadi username, atau diizinkan untuk pengguna saat ini. Pemeriksaan ini juga tidak dapat mendeteksi setiap kasus mojibake: teks yang pernah keliru di-decode lalu di-encode menjadi UTF-8 yang valid dapat lolos pemeriksaan sambil tetap menampilkan karakter yang salah.
Karena itu, konversi tanpa dasar berisiko. Pemanggilan fungsi yang "mengonversi dari encoding apa pun yang mungkin dipakai" dapat mengubah ketidakpastian menjadi kehilangan data permanen. Jika sebuah impor memang menggunakan encoding legacy, identifikasi encoding tersebut dari sumber yang terdokumentasi, konversi sekali di batas impor, dan pertahankan bukti yang cukup agar proses dapat direproduksi atau dibalik. Jangan berulang kali menjalankan konversi pada data yang riwayatnya tidak diketahui.
Tentukan charset koneksi database saat membuat koneksi
Kolom utf8mb4 saja tidak memberi tahu MariaDB cara menafsirkan byte yang datang dari client. Koneksi client-server memiliki variabel character set sendiri. Dokumentasi DSN PDO_MYSQL menyediakan elemen charset, sehingga intent dapat ditetapkan ketika PDO terhubung:
$pdo = new PDO(
'mysql:host=127.0.0.1;dbname=appdb;charset=utf8mb4',
$username,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
Menentukan charset=utf8mb4 lebih baik daripada berharap server, distribusi, atau instalasi yang baru di-upgrade memiliki default yang sesuai. Cara ini juga menempatkan negosiasi koneksi sebelum query aplikasi. Prepared statement tetap diperlukan untuk memisahkan struktur SQL dari value; opsi character set tidak menggantikan parameterization.
Saat mendiagnosis environment, periksa session yang aktif, bukan hanya configuration file:
SHOW SESSION VARIABLES LIKE 'character_set_%';
SHOW SESSION VARIABLES LIKE 'collation_connection';
Value yang relevan seharusnya menceritakan satu kisah yang selaras untuk client, connection, dan results. Pengamatan ini adalah bukti untuk koneksi tersebut, bukan bukti bahwa setiap worker atau impor command-line memakai setting yang sama.
Audit schema, bukan hanya default database
Dokumentasi character set dan collation MariaDB menjelaskan setting pada level server, database, table, column, dan connection. Default memang mengalir ke bawah, tetapi table lama atau column yang dikonfigurasi secara eksplisit dapat berbeda dari database-nya. Default juga berbeda antarversi dan distribusi MariaDB. Karena itu, value di level server bukanlah audit schema.
SHOW CREATE TABLE memperlihatkan definisi efektif sebuah table. Information Schema dapat menginventarisasi semua textual column:
SELECT
TABLE_NAME,
COLUMN_NAME,
CHARACTER_SET_NAME,
COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'appdb'
AND CHARACTER_SET_NAME IS NOT NULL
ORDER BY TABLE_NAME, ORDINAL_POSITION;
Untuk schema baru, tulis utf8mb4 secara eksplisit, bukan nama historis utf8. Dokumentasi Unicode MariaDB menjelaskan bahwa utf8 adalah alias yang maknanya dapat bergantung pada konfigurasi, sedangkan utf8mb4 menyimpan supplementary character. Setelah itu, pilih collation berdasarkan kebutuhan perbandingan yang nyata dan versi MariaDB yang harus saling beroperasi. Tidak ada satu collation universal untuk semua bahasa dan kebijakan identifier. Pencarian judul yang case-insensitive, token yang case-sensitive, dan username unik dapat membutuhkan pertimbangan berbeda.
Mengubah collation bukan sekadar perubahan kosmetik. Perubahan itu dapat mengubah value mana yang dianggap sama, cara hasil diurutkan, dan apakah value lama masih memenuhi unique index. Mengubah character set bisa lebih berbahaya: MariaDB memperingatkan bahwa konversi dapat menghilangkan data jika deklarasi lama tidak sesuai dengan konten sebenarnya. Buat backup terlebih dahulu, periksa byte yang mewakili data, lakukan rehearsal pada salinan, dan verifikasi perilaku aplikasi sebelum mengubah production column.
Pertahankan output escaping sebagai kontrol terpisah
UTF-8 yang benar menjaga teks tetap utuh; hal itu tidak membuat teks tidak tepercaya menjadi aman di HTML. Karakter seperti <, >, &, dan tanda kutip tetap memiliki makna pada level syntax. Untuk konteks plain HTML text atau quoted attribute, dokumentasi htmlspecialchars PHP mendukung encoding dan substitution policy yang dinyatakan eksplisit:
echo htmlspecialchars(
$value,
ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5,
'UTF-8'
);
Konteks lain, termasuk JavaScript, CSS, URL, dan HTTP header, memiliki aturan encoding berbeda. Demikian pula, HTML escaping biasanya dilakukan saat data memasuki konteks output HTML, bukan sebelum disimpan. Menyimpan teks yang sudah di-escape mencampur presentation dengan data dan membuka peluang double encoding ketika value yang sama dipakai di tempat lain.
Uji round trip nyata dengan teks yang menantang
Kalimat ASCII tidak dapat membuktikan bahwa jalur UTF-8 bekerja karena byte ASCII memiliki representasi dasar yang sama di dalam UTF-8. Corpus pengujian kecil sebaiknya melintasi beberapa script dan panjang byte, menyertakan supplementary character seperti emoji, serta memuat rangkaian yang canonically equivalent:
$samples = [
'Semarang',
'Grüße',
'日本語',
'مرحبا',
'emoji: 🧭',
"composed: é",
"decomposed: e\u{0301}",
];
Test perlu mengirim value tersebut melalui request parser yang sama dengan aplikasi, melakukan binding melalui jalur PDO yang sama, membacanya dari target column, melakukan serialize melalui response normal, lalu membandingkan hasil dengan ekspektasi awal. Periksa HTTP Content-Type, variabel session database, definisi column, value tersimpan, dan byte yang dikembalikan ketika perbandingan gagal. Insert langsung ke database menguji lebih sedikit bagian sistem dan dapat melewatkan batas web yang rusak.
Kesamaan byte secara tepat cocok bila aplikasi berjanji mempertahankan input apa adanya. Search key dan identifier mungkin membutuhkan kebijakan normalization yang terdokumentasi. Perbedaannya perlu dibuat eksplisit, bukan baru ditemukan setelah string yang terlihat sama berubah menjadi akun atau tag yang terpisah.
Validitas UTF-8 bukan Unicode normalization
Karakter yang terlihat sebagai é dapat direpresentasikan sebagai satu precomposed code point atau sebagai e yang diikuti combining acute accent. Kedua rangkaian dapat menjadi UTF-8 yang valid dan terlihat sama, tetapi gagal dalam perbandingan byte-for-byte. Character Model for String Matching dari W3C, yang saat ini berstatus First Public Working Draft, menjelaskan canonical equivalence dan juga memperingatkan bahwa normalization tidak dapat membuat setiap rangkaian karakter yang tampak sama menjadi equivalent.
Karena itu, normalization adalah keputusan product dan data model. Menormalisasi identifier ke NFC pada batas yang terdokumentasi dengan jelas dapat membuat matching lebih mudah diprediksi. Menerapkan compatibility normalization tanpa pertimbangan dapat menghapus perbedaan yang penting. Database collation mungkin menangani sebagian equivalence untuk perbandingan, tetapi perilakunya perlu diuji untuk versi dan collation yang dipilih; jangan menganggapnya menulis ulang teks tersimpan ke satu normalization form.
Dekati kerusakan legacy dengan bukti, bukan tebakan
Ketika teks lama sudah salah, mengganti response header atau deklarasi column mungkin hanya mengubah cara byte yang sama ditafsirkan pada kesempatan berikutnya. Sebuah string juga bisa pernah salah di-decode dan di-encode ulang lebih dari sekali. Tidak ada SQL statement universal yang aman untuk merekonstruksi riwayat yang tidak diketahui.
Perbaikan yang hati-hati dimulai dengan menghentikan write yang tidak diperlukan, membuat backup terverifikasi, mengambil sampel raw byte bersama deklarasi saat ini, mengidentifikasi jalur ingestion dan kemungkinan source encoding, lalu mereproduksi transformasi yang diusulkan pada salinan. Acceptance test perlu menyertakan teks asli yang diketahui dari sumber tepercaya. Jika bukti tersebut tidak tersedia, ketidakpastian sebaiknya dicatat, bukan disembunyikan di balik konversi yang hanya membuat beberapa contoh terlihat lebih baik.
Kesimpulan
Teks Unicode yang andal dalam aplikasi PHP/MariaDB adalah rangkaian kesepakatan eksplisit. Response menyatakan UTF-8, input divalidasi terhadap kontrak yang diketahui, PDO menegosiasikan utf8mb4, schema column memakai character set dan collation yang disengaja, output di-escape sesuai konteksnya, dan pengujian menjalankan round trip lengkap dengan data yang lebih beragam daripada ASCII.
Rangkaian itu pun memiliki batas. Ia tidak memilih kebijakan normalization atau collation yang tepat untuk suatu product, tidak menjamin setiap font memiliki semua glyph, dan tidak memperbaiki byte yang riwayatnya tidak diketahui. Yang diberikan adalah sesuatu yang lebih praktis: batas-batas yang dapat diamati. Saat teks berubah, investigasi dapat menemukan kontrak mana yang tidak sepakat, alih-alih menambah konversi lain dan berharap gejalanya menghilang.
References
- WHATWG: Encoding Standard
- WHATWG: HTML Standard - Specifying the Document's Character Encoding
- PHP Manual: PDO_MYSQL DSN
- PHP Manual: mb_check_encoding
- MariaDB Documentation: Setting Character Sets and Collations
- MariaDB Documentation: Unicode
- W3C: Character Model for the World Wide Web - String Matching (First Public Working Draft, 16 July 2026)
- PHP Manual: htmlspecialchars
