本章是由 [Alex Cabal](https://alexcabal.com/) 最初撰寫在 [PHP Best Practices](https://phpbestpractices.org/#utf-8) 中的,我們使用它作為進行建議的基礎。
**這不是在開玩笑。請小心、仔細并且前后一致地處理它。**
目前,PHP 仍未在底層實現對 Unicode 的支持。雖然有很多途徑可以確保 UTF-8 字符串能夠被正確地處理,但這并不是很簡單的事情,通常需要對 Web 應用進行全方面的檢查,從 HTML 到 SQL 再到 PHP。我們將爭取進行一個簡潔實用的總結。
### PHP 層面的 UTF-8
最基本的字符串操作,像是連結兩個字符串或將字符串賦值給變量,并不需要對 UTF-8 做特別的處理。然而大多數字符串的函數,像 `strpos()` 和 `strlen()`,確實需要特別的處理。這些函數名中通常包含 mb_*:比如,`mb_strpos()` 和 `mb_strlen()`。這些 mb_* 字符串是由 [Multibyte String Extension](http://php.net/book.mbstring) 提供支持的,它專門為操作 Unicode 字符串而特別進行了設計。
在操作 Unicode 字符串時,請你務必使用 mb_* 函數。例如,如果你對一個 UTF-8 字符串使用 substr(),那返回的結果中有很大可能會包含一些亂碼。正確的方式是使用 mb_substr()。
最難的地方在于每次都要記得使用 mb_* 函數。如果你哪怕只有一次忘記了使用,你的 Unicode 字符串就有在接下來的過程中變成亂碼的風險。
不是所有的字符串函數都有一個對應的 mb_* 函數。如果你想要的功能沒有對應的 mb_* 函數的話,那只能說你運氣不佳了。
你應該在你所有的 PHP 腳本(或全局包含的腳本)的開頭使用 mb_internal_encoding() 函數,然后緊接著在會對瀏覽器進行輸出的腳本中使用 mb_http_output()。在每一個腳本當中明確聲明字符串的編碼可以免去很多日后的煩惱。
另外,許多對字符串進行操作的函數都有一個可選的參數用來指定字符串編碼。當可以設定這類參數時,你應該始終明確指定使用 UTF-8。例如,htmlentities() 有一個字符編碼的選項,你應該始終將其設為 UTF-8。從 PHP 5.4.0 開始, htmlentities() 和 htmlspecialchars() 的編碼都已經被默認設為了 UTF-8。
最后,如果你所編寫的是分布式的應用程序并且不能確定 mbstring 擴展一定開啟的話,可以考慮使用 [patchwork/utf8](https://packagist.org/packages/patchwork/utf8) Composer 包。它會在 mbstring 可用時自動使用,否則自動切換回非 UTF-8 函數。
### 數據庫層面的 UTF-8
如果你使用 PHP 來操作到 MySQL,有些時候即使你做到了上面的每一點,你的字符串仍可能面臨在數據庫中以非 UTF-8 的格式進行存儲的問題。
為了確保你的字符串從 PHP 到 MySQL都使用 UTF-8,請檢查確認你的數據庫和數據表都設定為 `utf8mb4 `字符集和整理,并且確保你的 PDO 連接請求也使用了 `utf8mb4` 字符集。請看下方的示例代碼,這是 非常重要 的。
請注意為了完整的 UTF-8 支持,你必須使用 `utf8mb4` 而不是 `utf8`!你會在進一步閱讀中找到原因。
### 瀏覽器層面的 UTF-8
使用 `mb_http_output()` 函數來確保 PHP 向瀏覽器輸出 UTF-8 格式的字符串。
隨后瀏覽器需要接收 HTTP 應答來指定頁面是由 UTF-8 進行編碼的。以前這一步是通過在頁面 [<head> 標簽下包含字符集 <meta>](http://htmlpurifier.org/docs/enduser-utf8.html) 標簽實現的,這是一種可行的方式。但更好的做法是在 Content-Type 響應頭中進行設置,因為這樣做的速度會更快。
~~~
<?php
// Tell PHP that we're using UTF-8 strings until the end of the script
mb_internal_encoding('UTF-8');
// Tell PHP that we'll be outputting UTF-8 to the browser
mb_http_output('UTF-8');
// Our UTF-8 test string
$string = 'êl síla erin l? e-govaned v?n.';
// Transform the string in some way with a multibyte function
// Note how we cut the string at a non-Ascii character for demonstration purposes
$string = mb_substr($string, 0, 15);
// Connect to a database to store the transformed string
// See the PDO example in this document for more information
// Note the `charset=utf8mb4` in the Data Source Name (DSN)
$link = new PDO(
'mysql:host=your-hostname;dbname=your-db;charset=utf8mb4',
'your-username',
'your-password',
array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => false
)
);
// Store our transformed string as UTF-8 in our database
// Your DB and tables are in the utf8mb4 character set and collation, right?
$handle = $link->prepare('insert into ElvishSentences (Id, Body) values (?, ?)');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->bindValue(2, $string);
$handle->execute();
// Retrieve the string we just stored to prove it was stored correctly
$handle = $link->prepare('select * from ElvishSentences where Id = ?');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->execute();
// Store the result into an object that we'll output later in our HTML
$result = $handle->fetchAll(\PDO::FETCH_OBJ);
header('Content-Type: text/html; charset=UTF-8');
?><!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>UTF-8 test page</title>
</head>
<body>
<?php
foreach($result as $row){
print($row->Body); // This should correctly output our transformed UTF-8 string to the browser
}
?>
</body>
</html>
~~~
### 延伸閱讀
* [PHP 手冊:字符串運算符](http://php.net/language.operators.string)
* [PHP 手冊:字符串函數](http://php.net/ref.strings)
* [strpos()](http://php.net/function.strpos)
* [strlen()](http://php.net/function.strlen)
* [substr()](http://php.net/function.substr)
* [PHP 手冊:多字節字符串函數](http://php.net/ref.mbstring)
* [mb_strpos()](http://php.net/function.mb-strpos)
* [mb_strlen()](http://php.net/function.mb-strlen)
* [mb_substr()](http://php.net/function.mb-substr)
* [mb_internal_encoding()](http://php.net/function.mb-internal-encoding)
* [mb_http_output()](http://php.net/function.mb-http-output)
* [htmlentities()](http://php.net/function.htmlentities)
* [htmlspecialchars()](http://php.net/function.htmlspecialchars)
* [PHP UTF-8 Cheatsheet](http://blog.loftdigital.com/blog/php-utf-8-cheatsheet)
* [Handling UTF-8 with PHP](http://www.phpwact.org/php/i18n/utf-8)
* [Stack Overflow: What factors make PHP Unicode-incompatible?](http://stackoverflow.com/questions/571694/what-factors-make-php-unicode-incompatible)
* [Stack Overflow: Best practices in PHP and MySQL with international strings](http://stackoverflow.com/questions/140728/best-practices-in-php-and-mysql-with-international-strings)
* [How to support full Unicode in MySQL databases](http://mathiasbynens.be/notes/mysql-utf8mb4)
* [Bringing Unicode to PHP with Portable UTF-8](http://www.sitepoint.com/bringing-unicode-to-php-with-portable-utf8/)
* [Stack Overflow: DOMDocument loadHTML does not encode UTF-8 correctly](http://stackoverflow.com/questions/8218230/php-domdocument-loadhtml-not-encoding-utf-8-correctly)
- 歡迎
- 入門指南
- 使用當前穩定版本
- 內置的WEB服務器
- Mac安裝
- Windows安裝
- 代碼風格指南
- 語言亮點
- 編程范式
- 命名空間
- PHP標準庫
- 命令行接口
- Xdebug
- 依賴管理
- Composer 與 Packagist
- PEAR
- 開發實踐
- 基礎知識
- 日期和時間
- 設計模式
- 使用UTF8編碼
- 依賴注入
- 基本概念
- 復雜的問題
- 容器
- 延伸閱讀
- 數據庫
- MYSQL 擴展
- PDO 擴展
- 數據庫交互
- 數據庫抽象層
- 使用模板
- 好處
- 原生PHP模板
- 編譯模板
- 延伸閱讀
- 錯誤與異常
- 錯誤
- 異常
- 安全
- Web應用程序安全
- 密碼哈希
- 數據過濾
- 配置文件
- 注冊全局變量
- 錯誤報告
- 測試
- 測試驅動開發
- 行為驅動開發
- 其他測試工具
- 服務器與部署
- Platform as a Service (PaaS)
- 虛擬或專用服務器
- 共享服務器
- 構建及部署應用
- 虛擬化
- Vagrant
- Docker
- 緩存
- Opcode緩存
- 對象緩存
- 文檔撰寫
- 資源
- 社區
- Credits