Pengguna aplikasi Anda tidak selalu menulis teks ASCII murni: ada nama dengan aksen seperti José, simbol mata uang, hingga emoji pada kolom komentar. Unicode adalah standar yang memberi nomor unik (code point) pada setiap karakter tersebut, sedangkan UTF-8 adalah encoding yang menyandikan nomor itu menjadi deretan byte. PHP tidak menyimpan informasi encoding di dalam string, sehingga kebenaran penanganan teks bergantung sepenuhnya pada fungsi yang kita pilih. Pada Chapter ini kita akan merapikan model mental Unicode dan UTF-8, memetakan keluarga fungsi mb_*, lalu memastikan keluaran HTML dan JSON tetap ramah terhadap karakter apa pun.
UTF-8 sebagai Encoding Bawaan #
PHP modern menjadikan UTF-8 sebagai encoding default di hampir semua perbatasan sistem. Konstanta konfigurasi default_charset bernilai UTF-8, ekstensi JSON menyandikan keluarannya sebagai UTF-8, dan htmlspecialchars() memakai charset yang sama ketika parameter encoding tidak ditulis. Dua nilai berikut menegaskan posisi tersebut:
<?php
echo ini_get("default_charset") . "\n";
echo mb_internal_encoding() . "\n";Output yang Dihasilkan:
UTF-8
UTF-8Nilai default_charset dipakai oleh fungsi-fungsi yang bergantung pada charset bawaan, sedangkan mb_internal_encoding() melaporkan encoding yang dipakai keluarga fungsi mb_* ketika parameter encoding tidak diberikan. Nilai default ini selaras dengan kenyataan bahwa halaman HTML modern menyatakan <meta charset="UTF-8"> dan format JSON memang didefinisikan sebagai UTF-8. Meski demikian, PHP tidak pernah menolak string berisi byte rusak — memastikan data yang masuk memang UTF-8 tetap menjadi tanggung jawab kita.
String PHP Tetap Deretan Byte #
Chapter String Length dan Encoding
telah menunjukkan bahwa string PHP hanyalah deretan byte tanpa label encoding: strlen("ações") mengembalikan 7 karena menghitung byte, sedangkan mb_strlen() mengembalikan 5 karena mendekode byte tersebut menurut UTF-8 terlebih dahulu. Konsekuensinya, operasi level byte berpotensi merusak teks kapan pun satu karakter menempati lebih dari satu byte. Huruf é disandikan dua byte dan emoji umumnya empat byte, tanpa penanda apa pun di dalam string yang membedakan keduanya. Satu-satunya pertahanan adalah memilih fungsi yang bekerja pada level karakter.
Tabel berikut memetakan operasi teks yang paling sering kita lakukan beserta versi level byte dan level karakternya:
| Operasi | Level byte | Level karakter | Bahasan detail |
|---|---|---|---|
| Mengukur panjang | strlen() | mb_strlen() | String Length dan Encoding |
| Mencari posisi | strpos() | mb_strpos() | Searching dalam String |
| Mengambil potongan | substr() | mb_substr() | String Length dan Encoding |
| Memecah string | str_split() | mb_str_split() | Splitting dan Joining |
| Memangkas tepi | trim() | mb_trim() (PHP 8.4) | Trimming dan Padding |
| Mengubah kapital | strtoupper() / strtolower() | mb_strtoupper() / mb_strtolower() | Chapter ini dan Text Normalization |
Ketika kedua level itu tertukar tanpa sengaja, kerusakannya langsung terlihat. Contoh klasiknya adalah pengubahan kapital:
<?php
$kota = "São Paulo";
echo strtoupper($kota) . "\n";
echo mb_strtoupper($kota, "UTF-8") . "\n";Output yang Dihasilkan:
SãO PAULO
SÃO PAULOstrtoupper() memeriksa byte satu per satu terhadap tabel huruf kecil ASCII. Huruf a dan o memang dinaikkan menjadi A dan O, tetapi ã tersusun dari byte c3 a3 yang tidak dikenali sebagai huruf kecil ASCII sehingga dibiarkan apa adanya — hasilnya SãO PAULO. Sebaliknya, mb_strtoupper() mendekode byte menurut UTF-8 terlebih dahulu, mengenali ã sebagai huruf kecil, lalu menyandikan kembali hasilnya menjadi SÃO PAULO. Parameter kedua "UTF-8" membuat niat tersebut eksplisit dan mudah diaudit pembaca kode.
Tidak semua fungsi byte memiliki padanan mb_*. Fungsi pembalik string strrev() adalah contohnya:
<?php
$kata = "café";
echo bin2hex(strrev($kata)) . "\n";Output yang Dihasilkan:
a9c3666163Keluaran heksadesimal a9c3666163 memperlihatkan bencana level byte: huruf é (c3 a9) terbelah dan urutannya terbalik menjadi a9 c3 di awal hasil, disusul f, a, c. Bila byte rusak itu dicetak langsung ke terminal atau browser, layar menampilkan simbol aneh pengganti. Kami memakai bin2hex() di sini justru agar Anda dapat melihat byte mentahnya tanpa bergantung pada perilaku tampilan terminal.
Emoji dan Grapheme Cluster #
Emoji menambahkan lapisan menarik pada model mental kita. Emoji komposit seperti 👨👩👧 sebenarnya adalah rangkaian beberapa code point yang dijahit oleh karakter Zero-Width Joiner (ZWJ), bukan satu code point tunggal:
<?php
$keluarga = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
echo "strlen: " . strlen($keluarga) . "\n";
echo "mb_strlen: " . mb_strlen($keluarga) . "\n";Output yang Dihasilkan:
strlen: 18
mb_strlen: 5Mari kita hitung bersama: tiga emoji masing-masing menempati empat byte (12 byte) dan dua ZWJ masing-masing tiga byte (6 byte), sehingga strlen() melaporkan 18 byte. mb_strlen() mendekode deretan itu menjadi lima code point — tiga emoji ditambah dua ZWJ — dan mengembalikan 5. Namun mata pengguna melihat satu lambang keluarga saja. Satu unit yang dilihat pengguna tersebut disebut grapheme cluster, dan lima bukan angka yang cocok untuk batas input “satu emoji per pesan”.
<?php
// Membutuhkan ekstensi intl; tidak tersedia di lingkungan kursus ini
$keluarga = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
echo grapheme_strlen($keluarga);Pada instalasi PHP dengan ekstensi intl aktif, potongan di atas mencetak 1 karena seluruh rangkaian dihitung sebagai satu grapheme cluster utuh. Tanpa intl, sebagian besar proyek memilih pola pustaka seperti symfony/polyfill yang menyediakan fungsi yang sama melalui Composer — topik yang akan kita bahas di Section Composer dan Dependency Management.
Menyusun Keluaran HTML dengan htmlspecialchars #
Sebelum teks yang berasal dari pengguna dicetak ke halaman web, karakter khusus HTML wajib diubah menjadi entitasnya. Fungsi htmlspecialchars() menggantikan <, >, &, serta kedua jenis tanda kutip menjadi bentuk yang aman:
<?php
$profil = '<a href="halaman-profil">Budi & Siti</a>';
echo htmlspecialchars($profil, ENT_QUOTES, "UTF-8") . "\n";Output yang Dihasilkan:
<a href="halaman-profil">Budi & Siti</a>Flag ENT_QUOTES meminta kedua jenis tanda kutip ikut diubah menjadi " dan ', sedangkan parameter ketiga "UTF-8" memberi tahu PHP bahwa input disandikan UTF-8 — informasi yang dipakai untuk membuang byte tak valid. Keluarannya amal ditempel ke HTML: tanda < sudah menjadi < sehingga browser menampilkan tautan sebagai teks, bukan merendernya sebagai elemen nyata. Perlu dicatat bahwa meng-escape hanya satu lapis pertahanan; strategi lengkap melawan serangan XSS kita bedah tuntas di Section Web Security.
JSON dan UTF-8 #
Ekstensi JSON bekerja sepenuhnya di atas UTF-8, tetapi secara default json_encode() menyandikan ulang semua karakter non-ASCII menjadi urutan escape seperti \u00e9:
<?php
$namaProduk = "Kopi Toraja café";
echo json_encode($namaProduk) . "\n";
echo json_encode($namaProduk, JSON_UNESCAPED_UNICODE) . "\n";Output yang Dihasilkan:
"Kopi Toraja caf\u00e9"
"Kopi Toraja café"Baris pertama menunjukkan perilaku default: é menjadi \u00e9, menghasilkan JSON yang 100 persen valid tetapi sulit dibaca manusia. Baris kedua memakai flag JSON_UNESCAPED_UNICODE sehingga karakter UTF-8 dibiarkan apa adanya. Parser JSON membaca kedua bentuk itu secara identik, jadi pilihan ini murni soal keterbacaan berkas konfigurasi dan log. Kombinasikan dengan JSON_UNESCAPED_SLASHES ketika keluaran berisi banyak tanda / dan Anda ingin menghemat ukuran.
Ringkasan #
String PHP adalah deretan byte, dan UTF-8 hanyalah kesepakatan penyandian — kesepakatan yang kebetulan menjadi default di seluruh ekosistem PHP modern, dari default_charset sampai JSON. Keluarga fungsi mb_* adalah alat utama untuk bekerja pada level karakter, sementara fungsi level byte seperti strrev() atau strtoupper() akan merusak teks non-ASCII. Dengan modal ini, kita siap menghadapi alat pencarian teks yang paling kuat sekaligus paling sering disalahpahami: pola regular expression.