PHP最佳實踐(譯)
簡介
PHP是一門複雜的語言,經過多年折騰,使其不同版本之間高度不一致,有時還有些bug。 每個版本都有自己獨有的特性、多餘和怪異之處,也很難跟蹤哪個版本有哪些問題。這也就 很好理解為什麼有時它會遭到那麼多的厭惡。
儘管如此,如今它還是Web開發方面最流行的語言。因其悠久的歷史,對於實現密碼哈希和 資料庫訪問諸如此類的基本任務你能夠找到很多教程。但問題在於,5個教程,你就很有可能 找到5種完全不同的完成任務的方式,那麼哪種是「正確」的方式呢?其他方式有難以捉摸的bug 或者陷阱?確實很難搞明白,所以你經常要在互聯網上反覆查找嘗試確認正確的答案。
這也是PHP編程新手頻繁地因為醜陋、過時、或不安全的代碼而遭到責備的原因之一。如果 Google搜索的第一個結果是一篇4年前的文章,講述一種5年前的方法,那麼PHP新手們也就 很難改變經常遭受責備的現狀。
本文檔通過為PHP中常見的令人困惑的問題和任務編輯組織一系列被認為最佳實踐的基本做法, 來嘗試解決上述問題。若一個低層次的任務在PHP中有多種令人困惑的實現方式,本文也會涵蓋。
是什麼
這是一份指南,在PHP程序員遇到一些常見低層次任務但不明確最佳做法(由於PHP可能提供 了多種解決方案)之時,為其建議最佳實踐。例如:連接資料庫是一個常見任務,PHP中提供了 大量可行的方案,但並不是所有的都是好的做法,因此,本文也會包含該問題。
本文包含的是一系列簡短的、入門性質的方案。涉及的示例在基本設定下就能夠運行起來, 你研究一下應該就能把它們變為對你有用的東西。
本文將指出一些我們認為是PHP中最新最好的東西。然而,這意味如果你在使用老版本的PHP, 一些用來實現這些解決方案的特性對你並不可用。
這份文檔會一直更新,我會盡我最大努力保持該文檔與PHP的發展同步。
不是什麼
本文檔不是一份PHP教程。你應該在別處學習語言基礎和語法。
它也不是一份針對web應用常見問題,如cookie存儲、緩存、編程風格、文檔等的指南。
它也不是一個安全指南。當本文檔觸碰到一些安全相關的問題時,也是希望你自己做些研究來 確保你的PHP應用的安全問題。你的代碼造成的問題應該都是自己的過錯。
該文檔也並不是在主張一種特定的編程風格、模式或者框架。
也不是在主張一種特定的方式來完成高層次任務如用戶註冊、登錄系統等。本文檔只限於 PHP的悠久歷史所造成的一些易混淆或不明確的低層次任務。
它不是一個一勞永逸的解決方案,也不是一個唯一的方案。下面要講述的一些方法對於你的 特定場景來說也許並不是最好的,存在很多不同的方式來達到同樣的目的。特別是,高負載web 應用也許能從更加難懂的方案中獲益更多。
我們在使用哪個版本的PHP?
帶Suhosin-Patch的PHP 5.3.10-1ubuntu3.6,安裝在Ubuntu 12.04 LTS上。
PHP是Web世界裡的百年老龜,它的殼上銘刻著一段豐富、複雜、而粗糙的歷史。在一個共享 主機的環境里,它的配置可能會限制你能做的事情。
為了保持清晰地敘述,我們將僅針對一個版本的PHP進行講述。在2013年4月30日時,該版本 為PHP 5.3.10-1ubuntu3.6 with Suhosin-Patch。若你在Ubuntu 12.04 LTS伺服器 上使用apt-get進行安裝的就是該版本的PHP。
你也許發現這些方案中的一些在其他或者更老版本的PHP上也能工作。如果是這樣的話,就由 你來研究在這些更老版本上潛在的難以捉摸的bug或安全問題。
存儲密碼
使用phpass庫來哈希和比較密碼
經phpass 0.3測試
在存入資料庫之前進行哈希保護用戶密碼的標準方式。許多常用的哈希演算法如md5,甚至是sha1 對於密碼存儲都是不安全的,因為駭客能夠使用那些演算法輕而易舉地破解密碼。
對密碼進行哈希最安全的方法是使用bcrypt演算法。開源的phpass庫以一個易於使用的類來提供 該功能。
示例
HashPassword('my super cool password');
// You can now safely store the contents of $hashedPassword in your database!
// Check if a user has provided the correct password by comparing what they
// typed with our hash
$hasher->CheckPassword('the wrong password', $hashedPassword); // false
$hasher->CheckPassword('my super cool password', $hashedPassword); // true
?>
陷阱
- 許多資源可能推薦你在哈希之前對你的密碼「加鹽」。想法很好,但phpass在HashPassword()函數中已經對你的密碼「加鹽」了,這意味著你不需要自己「加鹽」。
進一步閱讀
連接並查詢MySQL資料庫
使用PDO及其預處理語句功能。
在PHP中,有很多方式來連接到一個MySQL資料庫。PDO(PHP數據對象)是其中最新且最健壯的一種。PDO跨多種不同類型資料庫有一個一致的介面,使用面向對象的方式,支持更多的新資料庫支持的特性。
你應該使用PDO的預處理語句函數來幫助防範SQL注入攻擊。使用函數bindValue來確保你的SQL免於一級SQL注入攻擊。(雖然並不是100%安全的,查看進一步閱讀獲取更多細節。)在以前,這必須使用一些「魔術引號(magic quotes)」函數的組合來實現。PDO使得那堆東西不再需要。
示例
PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => false,
PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8mb4'
)
);
$handle = $link->prepare('select Username from Users where
UserId = ? or Username = ? limit ?');
// PHP bug: if you don't specify PDO::PARAM_INT, PDO may enclose
// the argument in quotes.
// This can mess up some MySQL queries that don't expect integers
// to be quoted.
// See: https://bugs.php.net/bug.php?id=44639
// If you're not sure whether the value you're passing is an integer,
// use the is_int() function.
$handle->bindValue(1, 100, PDO::PARAM_INT);
$handle->bindValue(2, 'Bilbo Baggins');
$handle->bindValue(3, 5, PDO::PARAM_INT);
$handle->execute();
// Using the fetchAll() method might be too resource-heavy if you're
// selecting a truly massive amount of rows.
// If that's the case, you can use the fetch() method and loop through
// each result row one by one.
// You can also return arrays and other things instead of objects. See
// the PDO documentation for details.
$result = $handle->fetchAll(PDO::FETCH_OBJ);
foreach($result as $row){
print($row->Username);
}
}
catch(PDOException $ex){
print($ex->getMessage());
}
?>
陷阱
- 當綁定整型變數時,如果不傳遞PDO::PARAM_INT參數有事可能會導致PDO對數據加引號。這會 搞壞特定的MySQL查詢。查看該bug報告。
- 未使用
set names utf8mb4
作為首個查詢,可能會導致Unicode數據錯誤地存儲進資料庫,這依賴於你的配置。如果你 絕對有把握你的Unicode編碼數據不會出問題,那你可以不管這個。 - 啟用持久連接可能會導致怪異的並發相關的問題。這不是一個PHP的問題,而是一個應用層面 的問題。只要你仔細考慮了後果,持久連接一般會是安全的。查看Stack Overfilow這個問題。
- 即使你使用了
set names utf8mb4
,你也得確認實際的資料庫表使用的是utf8mb4字符集! - 可以在單個execute()調用中執行多條SQL語句。只需使用分號分隔語句,但注意這個bug,在該文檔所針對的PHP版本中還沒修復。
進一步閱讀
- PHP手冊:PDO
- 為什麼你應該使用PHP的PDO訪問資料庫
- Stack Overflow: PHP PDO vs 普通的mysql_connect
- Stack Overflow: PDO預處理語句足以防範SQL注入嗎?
- Stack Overflow: 在MySQL中使用SET NAMES utf8?
PHP標籤
使用 。
有幾種不同的方式用來區分PHP程序塊:, , , 以及。對於打字來說,更短的標籤更方便些,但唯一一種在所有PHP伺服器上都一定能工作的標籤 是。若你計劃將你的PHP應用部署到一台上面的PHP配置你無法控制的伺服器上,那麼你應始終使用 。
若你僅僅是為自己編碼,也能控制你將使用的PHP配置,你可能覺得短標籤更方便些。但記住 可能會和XML聲明衝突,並且實際上是ASP的風格。
無論你選擇哪一種,確保一致。
陷阱
- 在一個純PHP文件(例如,僅包含一個類定義的文件)中包含一個關閉?>標籤時,確保其後 不會跟著任何換行。當PHP解析器安全地吃進跟在關閉標籤之後的單個換行符時,任何其他的換行 都可能被輸出到瀏覽器,如果之後要輸出某些HTTP頭,那麼可能會造成混淆。
- 編寫Web應用時,確保在關閉?>標籤與html的標籤之間不會留下換行。正確的HTML 文件中,標籤必須是文件中的第一樣東西—在其之前的任何空格或換行都會使其 無效。
進一步閱讀
自動載入類
使用spl_autoload_register()來註冊你的自動載入函數。
PHP提供了若干方式來自動載入包含還未載入的類的文件。老的方法是使用名為__autoload()魔術全局函數。然而你一次僅能定義一個autoload()函數,因此如果你的程序 包含一個也使用了autoload()函數的庫,就會發生衝突。
處理這個問題的正確方法是唯一地命名你的自動載入函數,然後使用spl_autoload_register()函數 來註冊它。該函數允許定義多個autoload()這樣的函數,因此你不必擔心其他代碼的autoload()函數。
示例
進一步閱讀
從性能角度來看單引號和雙引號
其實並不重要。
已有很多人花費很多筆墨來討論是使用單引號(')還是雙引號(")來定義字元串。 單引號字元串不會被解析,因此放入字元串的任何東西都會以原樣顯示。雙引號字元串會被解析, 字元串中的任何PHP變數都會被求值。另外,轉義字元如換行符n和製表符t在單引號字元串中 不會被求值,但在雙引號字元串中會被求值。
由於雙引號字元串在程序運行時要求值,從而理論上使用單引號字元串能提高性能,因為PHP 不會對單引號字元串求值。這對於一定規模的應用來說也許確實如此,但對於現實中一般的應用來說, 區別非常小以至於根本不用在意。因此對於普通應用,你選擇哪種字元串並不重要。對於負載 極其高的應用來說,是有點作用的。根據你的應用的需要來做選擇,但無論你選擇什麼,請保持一致。
進一步閱讀
- PHP手冊:字元串
- PHP基準(向下滾動到引號類型(Quote Types))
- Stack Overflow: PHP中單引號字元串相比雙引號字元串有性能優勢么?
define() vs. const
使用define(),除非考慮到可讀性、類常量、或關注微優化
習慣上,在PHP中是使用define()函數來定義常量。但從某個時候開始,PHP中也能夠使用const 關鍵字來聲明常量了。那麼當定義常量時,該使用哪種方式呢?
答案在於這兩種方法之間的區別。
- define()在執行期定義常量,而const在編譯期定義常量。這樣const就有輕微的速度優勢, 但不值得考慮這個問題,除非你在構建大規模的軟體。
- define()將常量放入全局作用域,雖然你可以在常量名中包含命名空間。這意味著你不能 使用define()定義類常量。
- define()允許你在常量名和常量值中使用表達式,而const則都不允許。這使得define() 更加靈活。
- define()可以在if()代碼塊中調用,但const不行。
示例
因為define()更加靈活,你應該使用它以避免一些令人頭疼的事情,除非你明確地需要類 常量。使用const通常會產生更加可讀的代碼,但是以犧牲靈活性為代價的。
無論你選擇哪一種,請保持一致。
進一步閱讀
緩存PHP opcode
使用APC
在一個標準的PHP環境中,每次訪問PHP腳本時,腳本都會被編譯然後執行。一次又一次地花費 時間編譯相同的腳本對於大型站點會造成性能問題。
解決方案是採用一個opcode緩存。opcode緩存是一個能夠記下每個腳本經過編譯的版本,這樣 伺服器就不需要浪費時間一次又一次地編譯了。通常這些opcode緩存系統也能智能地檢測到 一個腳本是否發生改變,因此當你升級PHP源碼時,並不需要手動清空緩存。
有幾個PHP opcode緩存可用,其中值得關注的有eaccelerator, xcache,以及APC。 APC是PHP項目官方支持的,最為活躍,也最容易安裝。它也提供一個可選的類memcached 的持久化鍵-值對存儲,因此你應使用它。
安裝APC
在Ubuntu 12.04上你可以通過在終端中執行以下命令來安裝APC:
user@localhost: sudo apt-get install php-apc
除此之外,不需要進一步的配置。
將APC作為一個持久化鍵-值存儲系統來使用
APC也提供了對於你的腳本透明的類似於memcached的功能。與使用memcached相比一個大的優勢是 APC是集成到PHP核心的,因此你不需要在伺服器上維護另一個運行的部件,並且PHP開發者在APC 上的工作很活躍。但從另一方面來說,APC並不是一個分散式緩存,如果你需要這個特性,你就 必須使用memcached了。
示例
陷阱
- 如果你使用的不是PHP-FPM(例如你在 使用mod_php 或mod_fastcgi),那麼 每個PHP進程都會有自己獨有的APC實例,包括鍵-值存儲。若你不注意,這可能會在你的應用 代碼中造成同步問題。
進一步閱讀
PHP與Memcached
若你需要一個分散式緩存,那就使用Memcached客戶端庫。否則,使用APC。
緩存系統通常能夠提升應用的性能。Memcached是一個受歡迎的選擇,它能配合許多語言使用, 包括PHP。
然而,從一個PHP腳本中訪問一個Memcached伺服器,你有兩個不同且命名很愚蠢的客戶端庫選擇項:Memcache和Memcached。 它們是兩個名字幾乎相同的不同庫,兩者都可用於訪問一個Memcached實例。
事實證明,Memcached庫對於Memcached協議的實現最好,包含了一些Mmecache庫沒有的有用的特性, 並且看起來Memcached庫的開發也最為活躍。
然而,如果不需要訪問來自一組分散式伺服器的一個Memcached實例,那就使用APC。 APC得到PHP項目的支持,具備很多和Memcached相同的功能,並且能夠用作opcode緩存,這能提高PHP腳本的性能。
安裝Memcached客戶端庫
在安裝Memcached伺服器之後,需要安裝Memcached客戶端庫。沒有該庫,PHP腳本就沒法與 Memcached伺服器通信。
在Ubuntu 12.04上,你可以使用如下命令來安裝Memcached客戶端庫:
user@localhost: sudo apt-get install php5-memcached
使用APC作為替代
查看opcode緩存一節閱讀更多與使用APC作為 Memcached替代方案相關的信息。
進一步閱讀
- PHP手冊:Memcached
- PHP手冊:APC
- Stack Overflow: PHP中使用Memcache vs. Memcached
- Stack Overflow: Memcached vs APC,我該選擇哪一個?
PHP與正則表達式
使用PCRE(preg_*)家族函數
PHP有兩種使用不同的方式來使用正則表達式:PCRE(Perl兼容表示法,preg*)函數 和POSIX(POSIX擴展表示法,ereg*) 函數。
每個函數家族各自使用一種風格稍微不同的正則表達式。幸運的是,POSIX家族函數從PHP 5.3.0開始就被棄用了。因此,你絕不應該使用POSIX家族函數編寫新的代碼。始終使用 PRCE家族函數,即preg_*函數。
進一步閱讀
配置Web伺服器提供PHP服務
使用PHP-FPM
有多種方式來配置一個web伺服器以提供PHP服務。傳統(並且糟糕的)的方式是使用Apache的 mod_php。Mod_php將PHP 綁定到Apache自身,但是Apache對於該模塊功能的管理工作非常糟糕。一旦遇到較大的流量, 就會遭受嚴重的內存問題。
後來兩個新的可選項很快流行起來:mod_fastcgi 和mod_fcgid。兩者均保持一定數量的PHP執行進程, Apache將請求發送到這些埠來處理PHP的執行。由於這些庫限制了存活的PHP進程的數量, 從而大大減少了內存使用而沒有影響性能。
一些聰明的人創建一個fastcgi的實現,專門為真正與PHP工作良好而設計,他們稱之為 PHP-FPM。PHP 5.3.0之前,為安裝它, 你得跨越許多障礙,但幸運的是,PHP 5.3.3的核心包含了PHP-FPM,因此在Ubuntu 12.04上安裝它非常方便。
如下示例是針對Apache 2.2.22的,但PHP-FPM也能用於其他web伺服器如Nginx。
安裝PHP-FPM和Apache
在Ubuntu 12.04上你可以使用如下命令安裝PHP-FPM和Apache:
user@localhost: sudo apt-get install apache2-mpm-worker
libapache2-mod-fastcgi php5-fpm
user@localhost: sudo a2enmod actions alias fastcgi
注意我們必須使用apache2-mpm-worker,而不是apache2-mpm-prefork或apache2-mpm-threaded。
接下來配置Aapache虛擬主機將PHP請求路由到PHP-FPM進程。將如下配置語句放入Apache 配置文件(在Ubuntu 12.04上默認配置文件是/etc/apache2/sites-available/default)。
AddHandler php5-fcgi .php
Action php5-fcgi /php5-fcgi
Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi
FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -host 127.0.0.1:9000 -idle-timeout 120 -pass-header Authorization
最後,重啟Apache和FPM進程:
user@localhost: sudo service apache2 restart && sudo service php5-fpm
restart
進一步閱讀
發送郵件
使用PHPMailer
經PHPMailer 5.1測試
PHP提供了一個mail()函數,看起來很簡單易用。 不幸的是,與PHP中的很多東西一樣,它的簡單性是個幻象,因其虛假的表面使用它會導致 嚴重的安全問題。
Email是一組網路協議,比PHP的歷史還曲折。完全可以說發送郵件中的陷阱與PHP的mail() 函數一樣多,這個可能會令你有點「不寒而慄」吧。
PHPMailer是一個流行而 成熟的開源庫,為安全地發送郵件提供一個易用的介面。它關注可能陷阱,這樣你可以專註 於更重要的事情。
示例
Sender = 'bbaggins@example.com';
$mailer->AddReplyTo('bbaggins@example.com', 'Bilbo Baggins');
$mailer->SetFrom('bbaggins@example.com', 'Bilbo Baggins');
$mailer->AddAddress('gandalf@example.com');
$mailer->Subject = 'The finest weed in the South Farthing';
$mailer->MsgHTML('
You really must try it, Gandalf!
-Bilbo
');
// Set up our connection information.
$mailer->IsSMTP();
$mailer->SMTPAuth = true;
$mailer->SMTPSecure = 'ssl';
$mailer->Port = 465;
$mailer->Host = 'my smpt host';
$mailer->Username = 'my smtp username';
$mailer->Password = 'my smtp password';
// All done!
$mailer->Send();
?>
驗證郵件地址
使用filter_var()函數
Web應用可能需要做的一件常見任務是檢測用戶是否輸入了一個有效的郵件地址。毫無疑問 你可以在網上找到一些聲稱可以解決該問題的複雜的正則表達式,但是最簡單的方法是使用 PHP的內建filter_val()函數。
示例
進一步閱讀
凈化HTML輸入和輸出
對於簡單的數據凈化,使用htmlentities()函數, 複雜的數據凈化則使用HTML Purifier庫
經HTML Purifier 4.4.0測試
在任何wbe應用中展示用戶輸出時,首先對其進行「凈化」去除任何潛在危險的HTML是非常必要的。 一個惡意的用戶可以製作某些HTML,若被你的web應用直接輸出,對查看它的人來說會很危險。
雖然可以嘗試使用正則表達式來凈化HTML,但不要這樣做。HTML是一種複雜的語言,試圖 使用正則表達式來凈化HTML幾乎總是失敗的。
你可能會找到建議你使用strip_tags() 函數的觀點。雖然strip_tags()從技術上來說是安全的,但如果輸入的不合法的HTML(比如, 沒有結束標籤),它就成了一個「愚蠢」的函數,可能會去除比你期望的更多的內容。由於非技術用戶 在通信中經常使用<和>字元,strip_tags()也就不是一個好的選擇了。
如果閱讀了驗證郵件地址一節, 你也許也會考慮使用filter_var() 函數。然而filter_var()函數在遇到斷行時會出現問題, 並且需要不直觀的配置以接近htmlentities()函數的效果, 因此也不是一個好的選擇。
對於簡單需求的凈化
如果你的web應用僅需要完全地轉義(因此可以無害地呈現,但不是完全去除)HTML,則使用 PHP的內建htmlentities()函數。 這個函數要比HTML Purifier快得多,因此它不對HTML做任何驗證—僅轉義所有東西。
htmlentities()不同於類似功能的函數htmlspecialchars(), 它會編碼所有適用的HTML實體,而不僅僅是一個小的子集。
示例
Mua-ha-ha! Twiddling my evil mustache...
'; // Use the ENT_QUOTES flag to make sure both single and double // quotes are escaped. // Use the UTF-8 character encoding if you've stored the text as // UTF-8 (as you should have). // See the UTF-8 section in this document for more details. $safeHtml = htmlentities($evilHtml, ENT_QUOTES, 'UTF-8'); // $safeHtml is now fully escaped HTML. You can output $safeHtml // to your users without fear! ?>
對於複雜需求的凈化
對於很多web應用來說,簡單地轉義HTML是不夠的。你可能想完全去除任何HTML,或者允許 一小部分子集的HTML存在。若是如此,則使用HTML Purifier 庫。
HTML Purifier是一個經過充分測試但效率比較低的庫。這就是為什麼如果你的需求並不複雜 就應使用htmlentities(),因為 它的效率要快得多。
HTML Purifier相比strip_tags() 是有優勢的,因為它在凈化HTML之前會對其校驗。這意味著如果用戶輸入無效HTML,HTML Purifier相比strip_tags()更能保留HTML的原意。HTML Purifier高度可定製,允許你為HTML的一個子集建立白名單來允許這個HTML子集的實體存在 輸出中。
但其缺點就是相當的慢,它要求一些設置,在一個共享主機的環境里可能是不可行的。其文檔 通常也複雜而不易理解。以下示例是一個基本的使用配置。查看文檔 閱讀HTML Purifier提供的更多更高級的特性。
示例
Mua-ha-ha! Twiddling my evil mustache...';
// Set up the HTML Purifier object with the default configuration.
$purifier = new HTMLPurifier(HTMLPurifier_Config::createDefault());
$safeHtml = $purifier->purify($evilHtml);
// $safeHtml is now sanitized. You can output $safeHtml to your
// users without fear!
?>
陷阱
- 以錯誤的字元編碼使用htmlentities()會造成意想不到的輸出。在調用該函數時始終確認 指定了一種字元編碼,並且該編碼與將被凈化的字元串的編碼相匹配。更多細節請查看 UTF-8一節。
- 使用htmlentities()時,始終包含ENT_QUOTES和字元編碼參數。默認情況下,htmlentities() 不會對單引號編碼。多愚蠢的默認做法!
- HTML Purifier對於複雜的HTML效率極其的低。可以考慮設置一個緩存方案如APC來保存經過凈化的結果 以備後用。
進一步閱讀
- PHP HTML凈化工具對比
- Stack Overflow: 使用strip_tags()來防止XSS?
- Stack Overflow: PHP中凈化用戶輸入的最佳方法是什麼?
- Stack Overflow: 斷行時的FILTER_SANITIZE_SPECIAL_CHARS問題
PHP與UTF-8
沒有一行式解決方案。小心、注意細節,以及一致性。
PHP中的UTF-8糟透了。原諒我的用詞。
目前PHP在低層次上還不支持Unicode。有幾種方式可以確保UTF-8字元串能夠被正確處理, 但並不容易,需要深入到web應用的所有層面,從HTML,到SQL,到PHP。我們旨在提供一個簡潔、 實用的概述。
PHP層面的UTF-8
基本的字元串操作,如串接 兩個字元串、將字元串賦給變數,並不需要任何針對UTF-8的特殊東西。然而,多數 字元串函數,如strpos() 和strlen,就需要特殊的考慮。這些 函數都有一個對應的mb_*函數:例如,mb_strpos()和mb_strlen()。這些對應的函數 統稱為多位元組字元串函數。這些多位元組字元串 函數是專門為操作Unicode字元串而設計的。
當你操作Unicode字元串時,必須使用mb_*函數。例如,如果你使用substr() 操作一個UTF-8字元串,其結果就很可能包含一些亂碼。正確的函數應該是對應的多位元組函數, mb_substr()。
難的是始終記得使用mb_*函數。即使你僅一次忘了,你的Unicode字元串在接下來的處理中 就可能產生亂碼。
並不是所有的字元串函數都有一個對應的mb_*。如果不存在你想要的那一個,那你就只能 自認倒霉了。
此外,在每個PHP腳本的頂部(或者在全局包含腳本的頂部)你都應使用 mb_internal_encoding 函數,如果你的腳本會輸出到瀏覽器,那麼還得緊跟其後加個mb_http_output() 函數。在每個腳本中顯式地定義字元串的編碼在以後能為你減少很多令人頭疼的事情。
最後,許多操作字元串的PHP函數都有一個可選參數讓你指定字元編碼。若有該選項, 你應 始終顯式地指明UTF-8編碼。例如,htmlentities() 就有一個字元編碼方式選項,在處理這樣的字元串時應始終指定UTF-8。
MySQL層面的UTF-8
如果你的PHP腳本會訪問MySQL,即使你遵從了前述的注意事項,你的字元串也有可能在資料庫 中存儲為非UTF-8字元串。
確保從PHP到MySQL的字元串為UTF-8編碼的,確保你的資料庫以及數據表均設置為utf8mb4字符集, 並且在你的資料庫中執行任何其他查詢之前先執行MySQL查詢set names utf8mb4
。這是至關重要的。示例 請查看連接並查詢MySQL資料庫一節內容。
注意你必須使用utf8mb4
字符集來獲得完整的UTF-8支持,而不是utf8
字符集!原因 請查看進一步閱讀。
瀏覽器層面的UTF-8
使用mb_http_output()函數 來確保你的PHP腳本輸出UTF-8字元串到瀏覽器。並且在HTML頁面的標籤塊中包含字符集標籤塊。
示例
PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => false,
PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8mb4'
)
);
// Store our transformed string as UTF-8 in our database
// Assume our DB and tables are in the utf8mb4 character set and collation
$handle = $link->prepare('insert into Sentences (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 Sentences 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);
?>UTF-8 test pageBody);
// This should correctly output our transformed UTF-8 string to the browser
}
?>
進一步閱讀
- PHP手冊:多位元組字元串函數
- PHP UTF-8備忘單
- Stack Overflow: 什麼因素致使PHP不兼容Unicode?
- Stack Overflow: PHP與MySQL之間國際化字元串的最佳實踐
- 怎樣在MySQL資料庫中完整支持Unicode
處理日期和時間
使用DateTime類。
在PHP糟糕的老時光里,我們必須使用date(), gmdate(), date_timezone_set(), strtotime()等等令人迷惑的 組合來處理日期和時間。悲哀的是現在你仍舊會找到很多在線教程在講述這些不易使用的老式函數。
幸運的是,我們正在討論的PHP版本包含友好得多的DateTime類。 該類封裝了老式日期函數所有功能,甚至更多,在一個易於使用的類中,並且使得時區轉換更加容易。 在PHP中始終使用DateTime類來創建,比較,改變以及展示日期。
示例
add(new DateInterval('P10D'));
echo($date->format('Y-m-d h:i:s')); // 2011-05-14 05:00:00
// Sadly we don't have a Middle Earth timezone
// Convert our UTC date to the PST (or PDT, depending) time zone
$date->setTimezone(new DateTimeZone('America/Los_Angeles'));
// Note that if you run this line yourself, it might differ by an
// hour depending on daylight savings
echo($date->format('Y-m-d h:i:s')); // 2011-05-13 10:00:00
$later = new DateTime('2012-05-20', new DateTimeZone('UTC'));
// Compare two dates
if($date < $later)
echo('Yup, you can compare dates using these easy operators!');
// Find the difference between two dates
$difference = $date->diff($later);
echo('The 2nd date is ' . $difference['days'] . ' later than 1st date.');
?>
陷阱
- 如果你不指定一個時區,DateTime::__construct() 就會將生成日期的時區設置為正在運行的計算機的時區。之後,這會導致大量令人頭疼的事情。 在創建新日期時始終指定UTC時區,除非你確實清楚自己在做的事情。
- 如果你在DateTime::__construct()中使用Unix時間戳,那麼時區將始終設置為UTC而不管 第二個參數你指定了什麼。
- 向DateTime::__construct()傳遞零值日期(如:「0000-00-00」,常見MySQL生成該值作為 DateTime類型數據列的默認值)會產生一個無意義的日期,而不是「0000-00-00」。
- 在32位系統上使用DateTime::getTimestamp() 不會產生代表2038年之後日期的時間戳。64位系統則沒有問題。
進一步閱讀
檢測一個值是否為null或false
使用===操作符來檢測null和布爾false值。
PHP寬鬆的類型系統提供了許多不同的方法來檢測一個變數的值。然而這也造成了很多問題。 使用==來檢測一個值是否為null或false,如果該值實際上是一個空字元串或0,也會誤報 為false。isset是檢測一個變數是否有值, 而不是檢測該值是否為null或false,因此在這裡使用是不恰當的。
is_null()函數能準確地檢測一個值 是否為null,is_bool可以檢測一個值 是否是布爾值(比如false),但存在一個更好的選擇:===操作符。===檢測兩個值是否同一, 這不同於PHP寬鬆類型世界裡的相等。它也比is_null()和is_bool()要快一些,並且有些人 認為這比使用函數來做比較更乾淨些。
示例
陷阱
- 測試一個返回0或布爾false的函數的返回值時,如strpos(),始終使用===和!==,否則 你就會碰到問題。
進一步閱讀
建議與指正
感謝閱讀!如果你有些地方還不太理解,很正常,PHP是複雜的,並且充斥著陷阱。因為我也 只是一個人,所以本文檔中難免存在錯誤。
如果你想為本文檔貢獻建議或糾正錯誤之處,請使用最後修訂日期&維護者 一節中的信息聯繫我。
原文: PHP Best Practices-A short, practical guide for common and confusing PHP tasks
譯者:youngsterxyf
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive