Đối với một lập trình viên PHP hay MySQL, mọi thứ có vẻ khá dễ dàng và thoải mái khi làm việc với dữ liệu chỉ gồm các ký tự tiếng Anh. Cho đến khi bạn vướng vào sự rắc rối của mã hóa UTF-8 trong xử lý ngôn ngữ không phải tiếng Anh.
Nói sơ lược về mã hóa UTF-8 thì đây là một kiểu mã hóa ký tự của tập ký tự Unicode. Nó được thiết kế để tương thích ngược với mã hóa ASCII và tránh những nhược điểm của UTF-16 và UTF-32. UTF-8 trở thành kiểu mã hóa phổ biến và chiếm ưu thế khi hơn một nửa số trang web hiện tại sử dụng kiểu mã hóa này.
Trong bài viết này, sẽ có 3 hướng dẫn chính được trình bày để có thể sử dụng mã hóa UTF-8 trong PHP và MySQL:

  • Thay đổi trong file config php.ini và PHP code.
  • Thay đổi trong file config my.ini và những vấn đề liên quan khác đến MySQL.
  • Thay đổi dữ liệu trong CSDL MySQL sử dụng mã hóa latin1 thành mã hóa UTF-8.

PHP và mã hóa UTF-8

Để sử dụng UTF-8 là kiểu mã hóa ký tự mặc định, thay đổi ở file config php.ini như sau:
default_charset = "utf-8";
Thay đổi trong PHP code:

Ở header:

header('Content-Type: text/html; charset=utf-8');

Trong file XML:

<?xml version="1.0" encoding="UTF-8"?>
Tuy nhiên, không phải tất cả ký tự UTF-8 đều được chấp nhận ở trong XML, vì vậy cần loại bỏ những ký tự này. Một hàm hữu dụng sau đây có thể giải quyết vấn đề trên:

 function utf8_for_xml($string)
  {
    return preg_replace('/[^x{0009}x{000a}x{000d}x{0020}-x{D7FF}x{E000}-x{FFFD}]+/u',
                        ' ', $string);
  }

Trong HTML:

  • HTML content:
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  • HTML form:
    <form accept-charset="utf-8">

Chỉ định UTF-8 trong hàm hmlspecialchars:

htmlspecialchars($str, ENT_NOQUOTES, "UTF-8")

Khi kết nối với MySQL:

Sử dụng hàm mysql_set_charset:

 $link = mysql_connect('localhost', 'user', 'password');
 mysql_set_charset('utf8', $link);

Từ PHP 5.5.0, hàm mysql_set_charset được thay thế bởi mysql::set_charset

$mysqli = new mysqli("localhost", "my_user", "my_password", "test");
    
  /* check connection */
  if (mysqli_connect_errno()) {
      printf("Connect failed: %sn", mysqli_connect_error());
      exit();
  }
    
  /* change character set to utf8 */
  if (!$mysqli->set_charset("utf8")) {
      printf("Error loading character set utf8: %sn", $mysqli->error);
  } else {
      printf("Current character set: %sn", $mysqli->character_set_name());
  }
    
  $mysqli->close();

Sử dụng hàm xử lý chuỗi tương thích với UTF-8:

Có nhiều hàm sẽ lỗi hoặc xử lý sai khi một ký tự cần nhiều hơn 1 byte (như UTF-8). Ví dụ hàm strlen sẽ trả về số lượng byte chứ không phải số lượng ký tự.
Để giải quyết việc này có 2 tùy chọn:

  1. Sử dụng các hàm iconv (ví dụ: `iconv_strlen,…). Dù vậy, bản thân chuỗi ký tự vẫn cần phải đảm bảo được mã hóa đúng.
  2. Extension mbstring: extension cung cấp một tập đầy đủ các hàm cho việc xử lý mã hóa sử dụng nhiều byte (multibyte)

MySQL với mã hóa UTF-8

Thay đổi trong file config my.ini:

 [client]
  default-character-set=UTF-8
    
  [mysql]
  default-character-set=UTF-8
    
  [mysqld]
  character-set-client-handshake = false #force encoding to uft8
  character-set-server=UTF-8
  collation-server=UTF-8_general_ci
    
  [mysqld_safe]
  default-character-set=UTF-8
  • MySQL UTF-8 thực sự chỉ là một phần của tập ký tự UTF-8 khi chỉ sử dụng tối đa 3 bytes trong khi mã hóa UTF-8 yêu cầu 4 bytes. Do đó, một số ký tự không được hỗ trợ trong MySQL UTF-8. Kể từ MySQL 5.5.3, điều này có thể được giải quyết với tập ký tự utf8mb4.
  • Trong trường hợp không cài đặt mã hóa khi kết nối với MySQL, thì sau khi kết nối xong, có thể sử dụng lệnh/truy vấn sau:
    set names UTF-8;
  • Khi xác định kích cỡ của các trường kiểu varchar, đừng quên UTF-8 yêu cầu 4 bytes trên mỗi ký tự.

Chuyển đổi dữ liệu sử dụng mã hóa latin1 sang UTF-8

  1. Thiết lập cài đặt ở file config my.ini như hướng dẫn thứ 2.
  2. Thực hiện câu lệnh sau:
    ALTER SCHEMAyour-db-nameDEFAULT CHARACTER SET UTF-8;
  3. Xác nhận mọi thứ đã được cài đặt về UTF-8
    mysql> show variables like 'char%';
  4. Tạo file dump cho việc chuyển đổi:
    mysqldump -u USERNAME -pDB_PASSWORD --opt --skip-set-charset --default-character-set=latin1 --skip-extended-insert DATABASENAME --tables TABLENAME > DUMP_FILE_TABLE.sql
  5. Thay thế charset trong file dump từ latin1 sang UTF-8
    Ví dụ sử dụng Perl:
    perl -i -pe 's/DEFAULT CHARSET=latin1/DEFAULT CHARSET=UTF-8/' DUMP_FILE_TABLE.sql
    Đối với người dùng Windows: Việc chuyển đổi có thể thực hiện bằng WordPad sử dụng chức năng find-and-replace.
  6. Kể từ giờ, việc thay đổi sẽ được thực hiện trên CSDL nên đảm bảo backup dữ liệu trước khi thực hiện
    mysql> source "DUMP_FILE_TABLE.sql";
  7. Tìm kiếm những bản ghi có thể bị chuyển đổi sai và sửa chúng (ví dụ những ký tự được mã hóa 2 lần). Vì những ký tự không phải ASCII là multi-byte (sử dụng nhiều hơn 1 byte/ký tự) nên có thể tìm chúng bằng cách so sánh số byte và số lượng ký tự.
  • Kiểm tra có ký tự multi-byte hay không (nếu truy vấn sau trả về 0 thì không có ký tự multi-byte và có thể bỏ qua các bước tiếp theo)
    mysql> select count(*) from MY_TABLE where LENGTH(MY_FIELD) != CHAR_LENGTH(MY_FIELD);
  • Copy những hàng có ký tự multi-byte sang bảng tạm:
 create table temptable (
        select * from MY_TABLE where
        LENGTH(MY_FIELD) != CHAR_LENGTH(MY_FIELD));
  • Chuyển đổi ký tự bị mã hóa UTF-8 2 lần về dạng UTF-8 chuẩn.
    Vì MySQL đã tự động chuyển đổi dữ liệu sang UTF-8 một lần nữa trên dữ liệu UTF-8 nên cần một “thủ thuật” nhỏ để tránh việc chuyển đổi này.
    Đầu tiên, chuyển đổi kiểu mã hóa về latin1, do đó mã hóa 2 lần sẽ bị loại bỏ :
    alter table temptable modify temptable.ArtistName varchar(128) character set latin1;
    Lưu ý: Cần đặt đúng kiểu dữ liệu cũ. Như ở ví dụ trên là vachar(128)
    Vấn đề bây giờ là nếu cài đặt cột hiện tại về mã hóa UTF-8, MySQL sẽ chuyển đổi từ latin1 về UTF-8. Để tránh việc này, chúng ta thay đổi kiểu dữ liệu của cột về blob rồi sau đó mới cài đặt UTF-8. Điều này lợi dụng từ việc MySQL không chuyển đổi kiểu mã hóa của một blob.
alter table temptable modify temptable.ArtistName blob;
alter table temptable modify temptable.ArtistName varchar(128) character set UTF-8;
  • Loại bỏ hàng chỉ có ký tự single-byte (sử dụng một byte) ra khỏi bảng tạm:
    delete from temptable where LENGTH(MY_FIELD) = CHAR_LENGTH(MY_FIELD);
  • Thay thế dữ liệu vào bảng chính
    replace into MY_TABLE (select * from temptable);
  1. Kiểm tra dữ liệu còn lại, nếu cần thì thực hiện lại từ bước 7.

Lời kết

Mã hóa dữ liệu đòi hỏi sự cẩn trọng và tỉ mỉ. Nếu hiểu rõ về nó, lập trình viên có thể tiết kiệm một khoảng thời gian và công sức đáng kể khi đối mặt với những vấn đề về dữ liệu. Bài viết này như một lần nữa khẳng định tầm quan trọng của việc cân nhắc khi định nghĩa charset trong dự án cũng như môi trường phát triển ngay từ ban đầu.

Referrence:
https://www.toptal.com/php/a-utf-8-primer-for-php-and-mysql