PHP最佳實踐(譯)

jopen 11年前發布 | 44K 次閱讀 PHP PHP開發

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

譯者:youngsterxyf

</h1>

最后修訂日期&維護者

本文檔最后審閱于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緩存可用,其中值得關注的有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 // 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_php 或mod_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_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)。

<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 = '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('<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('sgamgee@example.com', FILTER_VALIDATE_EMAIL); //Returns "sgamgee@example.com". 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 onclick="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 onclick="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(),始終使用===!==,否則 你就會碰到問題。

進一步閱讀

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!