PHP最佳實踐

本文轉載自:http://youngsterxyf.github.io/2013/06/01/php-best-practices/

以下是原文,整理個目錄出來如下

 

PHP最佳實踐(譯)Jun 01, 2013

原文: PHP Best Practices-A short, practical guide for common and confusing PHP tasks

譯者:youngsterxyf

最後修訂日期&維護者

本文檔最後審閱於2013年3月8日。最後修改於2013年5月8日。

由我,Alex Cabal,維護該文檔。我編寫PHP程序已有很長一段時間了,當前我 經營着Scribophile,由認真作家組成的一個在線寫作團體,Writerfolio,爲自由職業者提供的一個易用寫作工具集,以及 Standard Ebooks,一個圖文並茂、無數字版權管理的公共領域電子書出版商。 有時我是個爲吸引我的項目或客戶而工作的自由職業者。

如果你認爲我在某些事情上能夠幫到你,或者對本文檔有點建議或糾正存在的錯誤,請給我寫封郵件

簡介

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庫以一個易於使用的類來提供 該功能。

示例

<?php
// Include the phpass library
require_once('phpass-03/PasswordHash.php')

// Initialize the hasher without portable hashes (this is more secure)
$hasher = new PasswordHash(8, false);

// Hash the password. $hashedPassword will be a 60-character string.
$hashedPassword = $hasher->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使得那堆東西不再需要。

示例

<?php
try{
    // Create a new connection.
    // You'll probably want to replace hostname with localhost in the first parameter.
    // The PDO options we pass do the following:
    // \PDO::ATTR_ERRMODE enables exceptions for errors.  This is optional but can be handy.
    // \PDO::ATTR_PERSISTENT disables persistent connections, which can cause concurrency issues in certain cases.  See "Gotchas".
    // \PDO::MYSQL_ATTR_INIT_COMMAND alerts the connection that we'll be passing UTF-8 data.
    // This may not be required depending on your configuration, but it'll save you headaches down the road
    // if you're trying to store Unicode strings in your database.  See "Gotchas".
    $link = new \PDO(   'mysql:host=your-hostname;dbname=your-db', 
                        'your-username', 
                        'your-password', 
                        array(
                            \PDO::ATTR_ERRMODE => \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標籤

使用 <?php ?>

有幾種不同的方式用來區分PHP程序塊:<?php ?>, <?= ?>, <? ?>, 以及<% %>。對於打字來說,更短的標籤更方便些,但唯一一種在所有PHP服務器上都一定能工作的標籤 是<?php ?>。若你計劃將你的PHP應用部署到一臺上面的PHP配置你無法控制的服務器上,那麼你應始終使用<?php ?>

若你僅僅是爲自己編碼,也能控制你將使用的PHP配置,你可能覺得短標籤更方便些。但記住 <? ?>可能會和XML聲明衝突,並且<? ?>實際上是ASP的風格。

無論你選擇哪一種,確保一致。

陷阱

  • 在一個純PHP文件(例如,僅包含一個類定義的文件)中包含一個關閉?>標籤時,確保其後 不會跟着任何換行。當PHP解析器安全地吃進跟在關閉標籤之後的單個換行符時,任何其他的換行 都可能被輸出到瀏覽器,如果之後要輸出某些HTTP頭,那麼可能會造成混淆。
  • 編寫Web應用時,確保在關閉?>標籤與html的<!doctype>標籤之間不會留下換行。正確的HTML 文件中,<!doctype>標籤必須是文件中的第一樣東西—在其之前的任何空格或換行都會使其 無效。

進一步閱讀

自動加載類

使用spl_autoload_register()來註冊你的自動加載函數。

PHP提供了若干方式來自動加載包含還未加載的類的文件。老的方法是使用名爲__autoload()魔術全局函數。然而你一次僅能定義一個__autoload()函數,因此如果你的程序 包含一個也使用了__autoload()函數的庫,就會發生衝突。

處理這個問題的正確方法是唯一地命名你的自動加載函數,然後使用spl_autoload_register()函數 來註冊它。該函數允許定義多個__autoload()這樣的函數,因此你不必擔心其他代碼的__autoload()函數。

示例

<?php
// First, define your auto-load function
function MyAutoload($className){
    include_once($className . '.php');
}

// Next, register it with PHP
spl_autoload_register('MyAutoload');

// Try it out!
// Since we haven't included a file defining the MyClass object, our
// auto-loader will kick in and include MyClass.php.
// For this example, assume the MyClass class is defined in the MyClass.php
// file.
$var = new MyClass();
?>

進一步閱讀

從性能角度來看單引號和雙引號

其實並不重要。

已有很多人花費很多筆墨來討論是使用單引號(')還是雙引號(")來定義字符串。 單引號字符串不會被解析,因此放入字符串的任何東西都會以原樣顯示。雙引號字符串會被解析, 字符串中的任何PHP變量都會被求值。另外,轉義字符如換行符\n和製表符\t在單引號字符串中 不會被求值,但在雙引號字符串中會被求值。

由於雙引號字符串在程序運行時要求值,從而理論上使用單引號字符串能提高性能,因爲PHP 不會對單引號字符串求值。這對於一定規模的應用來說也許確實如此,但對於現實中一般的應用來說, 區別非常小以至於根本不用在意。因此對於普通應用,你選擇哪種字符串並不重要。對於負載 極其高的應用來說,是有點作用的。根據你的應用的需要來做選擇,但無論你選擇什麼,請保持一致。

進一步閱讀

define() vs. const

使用define(),除非考慮到可讀性、類常量、或關注微優化

習慣上,在PHP中是使用define()函數來定義常量。但從某個時候開始,PHP中也能夠使用const 關鍵字來聲明常量了。那麼當定義常量時,該使用哪種方式呢?

答案在於這兩種方法之間的區別。

  1. define()在執行期定義常量,而const在編譯期定義常量。這樣const就有輕微的速度優勢, 但不值得考慮這個問題,除非你在構建大規模的軟件。
  2. define()將常量放入全局作用域,雖然你可以在常量名中包含命名空間。這意味着你不能 使用define()定義類常量。
  3. define()允許你在常量名和常量值中使用表達式,而const則都不允許。這使得define() 更加靈活。
  4. define()可以在if()代碼塊中調用,但const不行。

示例

<?php
// Let's see how the two methods treat namespaces
namespace MiddleEarth\Creatures\Dwarves;
const GIMLI_ID = 1;
define('MiddleEarth\Creatures\Elves\LEGOLAS_ID', 2);

echo(\MiddleEarth\Creatures\Dwarves\GIMLI_ID);  // 1
echo(\MiddleEarth\Creatures\Elves\LEGOLAS_ID);  // 2; note that we used define()

// Now let's declare some bit-shifted constants representing ways to enter Mordor.
define('TRANSPORT_METHOD_SNEAKING', 1 << 0); // OK!
const TRANSPORT_METHOD_WALKING = 1 << 1; //Compile error! const can't use expressions as values
 
// Next, conditional constants.
define('HOBBITS_FRODO_ID', 1);
 
if($isGoingToMordor){
    define('TRANSPORT_METHOD', TRANSPORT_METHOD_SNEAKING); // OK!
    const PARTY_LEADER_ID = HOBBITS_FRODO_ID // Compile error: const can't be used in an if block
}
 
// Finally, class constants
class OneRing{
    const MELTING_POINT_DEGREES = 1000000; // OK!
    define('SHOW_ELVISH_DEGREES', 200); // Compile error: can't use define() within a class
}
?>

因爲define()更加靈活,你應該使用它以避免一些令人頭疼的事情,除非你明確地需要類 常量。使用const通常會產生更加可讀的代碼,但是以犧牲靈活性爲代價的。

無論你選擇哪一種,請保持一致。

進一步閱讀

緩存PHP opcode

使用APC

在一個標準的PHP環境中,每次訪問PHP腳本時,腳本都會被編譯然後執行。一次又一次地花費 時間編譯相同的腳本對於大型站點會造成性能問題。

解決方案是採用一個opcode緩存。opcode緩存是一個能夠記下每個腳本經過編譯的版本,這樣 服務器就不需要浪費時間一次又一次地編譯了。通常這些opcode緩存系統也能智能地檢測到 一個腳本是否發生改變,因此當你升級PHP源碼時,並不需要手動清空緩存。

有幾個PHP opcode緩存可用,其中值得關注的有eacceleratorxcache,以及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
// Store some values in the APC cache.  We can optionally pass a time-to-live, 
// but in this example the values will live forever until they're garbage-collected by APC.
apc_store('username-1532', 'Frodo Baggins');
apc_store('username-958', 'Aragorn');
apc_store('username-6389', 'Gandalf');
 
// After storing these values, any PHP script can access them, no matter when it's run!
$value = apc_fetch('username-958', $success);
if($success === true)
    print($value); // Aragorn
 
$value = apc_fetch('username-1', $success); // $success will be set to boolean false, because this key doesn't exist.
if($success !== true) // Note the !==, this checks for true boolean false, not "falsey" values like 0 or empty string.
    print('Key not found');
 
apc_delete('username-958'); // This key will no longer be available.
?>

陷阱

  • 如果你使用的不是PHP-FPM(例如你在 使用mod_phpmod_fastcgi),那麼 每個PHP進程都會有自己獨有的APC實例,包括鍵-值存儲。若你不注意,這可能會在你的應用 代碼中造成同步問題。

進一步閱讀

PHP與Memcached

若你需要一個分佈式緩存,那就使用Memcached客戶端庫。否則,使用APC。

緩存系統通常能夠提升應用的性能。Memcached是一個受歡迎的選擇,它能配合許多語言使用, 包括PHP。

然而,從一個PHP腳本中訪問一個Memcached服務器,你有兩個不同且命名很愚蠢的客戶端庫選擇項: MemcacheMemcached。 它們是兩個名字幾乎相同的不同庫,兩者都可用於訪問一個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與正則表達式

使用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_fastcgimod_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)。

<VirtualHost *:80>
    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
</VirtualHost>

最後,重啓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是一個流行而 成熟的開源庫,爲安全地發送郵件提供一個易用的接口。它關注可能陷阱,這樣你可以專注 於更重要的事情。

示例

<?php
// Include the PHPMailer library
require_once('phpmailer-5.1/class.phpmailer.php');
 
// Passing 'true' enables exceptions.  This is optional and defaults to false.
$mailer = new PHPMailer(true);
 
// Send a mail from Bilbo Baggins to Gandalf the Grey
 
// Set up to, from, and the message body.  The body doesn't have to be HTML;
// check the PHPMailer documentation for details.
$mailer->Sender = '[email protected]';
$mailer->AddReplyTo('[email protected]', 'Bilbo Baggins');
$mailer->SetFrom('[email protected]', 'Bilbo Baggins');
$mailer->AddAddress('[email protected]');
$mailer->Subject = 'The finest weed in the South Farthing';
$mailer->MsgHTML('<p>You really must try it, Gandalf!</p><p>-Bilbo</p>');
 
// 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()函數。

示例

<?php
filter_var('[email protected]', FILTER_VALIDATE_EMAIL);
//Returns "[email protected]". This is a valid email address.

filter_var('sauron@mordor', FILTER_VALIDATE_EMAIL);
// Returns boolean false! This is *not* a valid email address.
?>

進一步閱讀

淨化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實體,而不僅僅是一個小的子集。

示例

<?php
// Oh no!  The user has submitted malicious HTML, and we have to display it in our web app!
$evilHtml = '<div οnclick="xss();">Mua-ha-ha!  Twiddling my evil mustache...</div>';
 
// 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提供的更多更高級的特性。

示例

<?php
// Include the HTML Purifier library
require_once('htmlpurifier-4.4.0/HTMLPurifier.auto.php');
 
// Oh no!  The user has submitted malicious HTML, and we have to display it in our web app!
$evilHtml = '<div οnclick="xss();">Mua-ha-ha!  Twiddling my evil mustache...</div>';
 
// 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與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頁面的<head>標籤塊中包含字符集<meta>標籤塊

示例

<?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 = 'Aš galiu valgyti stiklą ir jis manęs nežeidžia';
 
// Transform the string in some way with a multibyte function
$string = mb_substr($string, 0, 10);
 
// Connect to a database to store the transformed string
// See the PDO example in this document for more information
// Note the `set names utf8mb4` commmand!
$link = new \PDO(   'mysql:host=your-hostname;dbname=your-db',
                    'your-username',
                    'your-password',
                    array(
                        \PDO::ATTR_ERRMODE => \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);
?><!doctype html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; 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>

進一步閱讀

處理日期和時間

使用DateTime類

在PHP糟糕的老時光裏,我們必須使用date()gmdate()date_timezone_set()strtotime()等等令人迷惑的 組合來處理日期和時間。悲哀的是現在你仍舊會找到很多在線教程在講述這些不易使用的老式函數。

幸運的是,我們正在討論的PHP版本包含友好得多的DateTime類。 該類封裝了老式日期函數所有功能,甚至更多,在一個易於使用的類中,並且使得時區轉換更加容易。 在PHP中始終使用DateTime類來創建,比較,改變以及展示日期。

示例

<?php
// Construct a new UTC date.  Always specify UTC unless you really know what you're doing!
$date = new DateTime('2011-05-04 05:00:00', new DateTimeZone('UTC'));
 
// Add ten days to our initial date
$date->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()要快一些,並且有些人 認爲這比使用函數來做比較更乾淨些。

示例

<?php
$x = 0;
$y = null;
 
// Is $x null?
if($x == null)
    print('Oops! $x is 0, not null!');
 
// Is $y null?
if(is_null($y))
    print('Great, but could be faster.');
 
if($y === null)
    print('Perfect!');
 
// Does the string abc contain the character a?
if(strpos('abc', 'a'))
    // GOTCHA!  strpos returns 0, indicating it wishes to return the position of the first character.
    // But PHP interpretes 0 as false, so we never reach this print statement!
    print('Found it!'); 
 
//Solution: use !== (the opposite of ===) to see if strpos() returns 0, or boolean false.   
if(strpos('abc', 'a') !== false)
    print('Found it for real this time!');
?>

陷阱

  • 測試一個返回0或布爾false的函數的返回值時,如strpos(),始終使用===!==,否則 你就會碰到問題。

進一步閱讀

建議與指正

感謝閱讀!如果你有些地方還不太理解,很正常,PHP是複雜的,並且充斥着陷阱。因爲我也 只是一個人,所以本文檔中難免存在錯誤。

如果你想爲本文檔貢獻建議或糾正錯誤之處,請使用最後修訂日期&維護者 一節中的信息聯繫我。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章