PHP最佳實踐(譯)

 

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

譯者:youngsterxyfhtml

最後修訂日期&維護者

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

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

若是你認爲我在某些事情上可以幫到你,或者對本文檔有點建議或糾正存在的錯誤,請給我寫封郵件程序員

簡介

PHP是一門複雜的語言,通過多年折騰,使其不一樣版本之間高度不一致,有時還有些bug。 每一個版本都有本身獨有的特性、多餘和怪異之處,也很難跟蹤哪一個版本有哪些問題。這也就 很好理解爲何有時它會遭到那麼多的厭惡。github

儘管如此,現在它仍是Web開發方面最流行的語言。因其悠久的歷史,對於實現密碼哈希和 數據庫訪問諸如此類的基本任務你可以找到不少教程。但問題在於,5個教程,你就頗有可能 找到5種徹底不一樣的完成任務的方式,那麼哪一種是「正確」的方式呢?其餘方式有難以捉摸的bug 或者陷阱?確實很難搞明白,因此你常常要在互聯網上反覆查找嘗試確認正確的答案。web

這也是PHP編程新手頻繁地由於醜陋、過期、或不安全的代碼而遭到責備的緣由之一。若是 Google搜索的第一個結果是一篇4年前的文章,講述一種5年前的方法,那麼PHP新手們也就 很難改變常常遭受責備的現狀。正則表達式

本文檔經過爲PHP中常見的使人困惑的問題和任務編輯組織一系列被認爲最佳實踐的基本作法, 來嘗試解決上述問題。若一個低層次的任務在PHP中有多種使人困惑的實現方式,本文也會涵蓋。算法

是什麼

這是一份指南,在PHP程序員遇到一些常見低層次任務但不明確最佳作法(因爲PHP可能提供 了多種解決方案)之時,爲其建議最佳實踐。例如:鏈接數據庫是一個常見任務,PHP中提供了 大量可行的方案,但並非全部的都是好的作法,所以,本文也會包含該問題。sql

本文包含的是一系列簡短的、入門性質的方案。涉及的示例在基本設定下就可以運行起來, 你研究一下應該就能把它們變爲對你有用的東西。

本文將指出一些咱們認爲是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(),始終使用===!==,不然 你就會碰到問題。

進一步閱讀

相關文章
相關標籤/搜索