鍍金池/ 教程/ PHP/ 錯誤與異常
依賴管理
安全
測試
使用模板
開發(fā)實踐
入門指南
服務(wù)器與部署
社區(qū)
語言亮點
錯誤與異常
虛擬化技術(shù)
資源
文檔撰寫
數(shù)據(jù)庫
依賴注入
緩存
代碼風(fēng)格指南

錯誤與異常

錯誤

在許多「重異常」(exception-heavy) 的編程語言中,一旦發(fā)生錯誤,就會拋出異常。這確實是一個可行的方式。不過 PHP 卻是一個 「輕異?!?exception-light) 的語言。當(dāng)然它確實有異常機制,在處理對象時,核心也開始采用這個機制來處理,只是 PHP 會盡可能的執(zhí)行而無視發(fā)生的事情,除非是一個嚴(yán)重錯誤。

舉例來說:

{% highlight console %}
$ php -a
php > echo $foo;
Notice: Undefined variable: foo in php shell code on line 1
{% endhighlight %}

這里只是一個 notice 級別的錯誤,PHP 仍然會愉快的繼續(xù)執(zhí)行。這對有「重異?!咕幊探?jīng)驗的人來說會帶來困惑,例如在 Python 中,引用一個不存在的變量會拋出異常:

{% highlight console %}
$ python
>>> print foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'foo' is not defined
{% endhighlight %}

本質(zhì)上的差異在于 Python 會對任何小錯誤進(jìn)行拋錯,因此開發(fā)人員可以確信任何潛在的問題或者邊緣的案例都可以被捕捉到,與此同時 PHP 仍然會保持執(zhí)行,除非極端的問題發(fā)生才會拋出異常。

錯誤嚴(yán)重性

PHP 有幾個錯誤嚴(yán)重性等級。三個最常見的的信息類型是錯誤(error)、通知(notice)和警告(warning)。它們有不同的嚴(yán)重性: E_ERROR 、E_NOTICEE_WARNING。錯誤是運行期間的嚴(yán)重問題,通常是因為代碼出錯而造成,必須要修正它,否則會使 PHP 停止執(zhí)行。通知是建議性質(zhì)的信息,是因為程序代碼在執(zhí)行期有可能造成問題,但程序不會停止。 警告是非致命錯誤,程序執(zhí)行也不會因此而中止。

另一個在編譯期間會報錯的信息類型是「E_STRICT」。這個信息用來建議修改程序代碼以維持最佳的互通性并能與今后的 PHP 版本兼容。

更改 PHP 錯誤報告行為

錯誤報告可以由 PHP 配置及函數(shù)調(diào)用改變。使用 PHP 內(nèi)置的函數(shù) error_reporting(),可以設(shè)定程序執(zhí)行期間的錯誤等級,方法是傳入預(yù)定義的錯誤等級常量,意味著如果你只想看到警告和錯誤(而非通知),你可以這樣設(shè)定:

{% highlight php %}
<?php
error_reporting(E_ERROR | E_WARNING);
{% endhighlight %}

你也可以控制錯誤是否在屏幕上顯示 (開發(fā)時比較有用)或隱藏后記錄日志 (適用于正式環(huán)境)。如果想知道更多細(xì)節(jié),可以查看 錯誤報告 章節(jié)。

行內(nèi)錯誤抑制

你可以讓 PHP 利用錯誤控制操作符 @ 來抑制特定的錯誤。將這個操作符放置在表達(dá)式之前,其后的任何錯誤都不會出現(xiàn)。

{% highlight php %}
<?php
echo @$foo['bar'];
{% endhighlight %}

如果 $foo['bar'] 存在,程序會將結(jié)果輸出,如果變量 $foo 或是 'bar' 鍵值不存在,則會返回 null 并且不輸出任何東西。如果不使用錯誤控制操作符,這個表達(dá)式會產(chǎn)生一個錯誤信息 PHP Notice: Undefined variable: fooPHP Notice: Undefined index: bar 。

這看起來像是個好主意,不過也有一些討厭的代價。PHP 處理使用 @ 的表達(dá)式比起不用時效率會低一些。過早的性能優(yōu)化在所有程序語言中也許都是爭論點,不過如果性能在你的應(yīng)用程序 / 類庫中占有重要地位,那么了解錯誤控制操作符的性能影響就比較重要。

其次,錯誤控制操作符會 完全 吃掉錯誤。不但沒有顯示,而且也不會記錄在錯誤日志中。此外,在正式環(huán)境中 PHP 也沒有辦法關(guān)閉錯誤控制操作符。也許你認(rèn)為那些錯誤時無害的,不過那些較具傷害性的錯誤同時也會被隱藏。

如果有方法可以避免錯誤抑制符,你應(yīng)該考慮使用,舉例來說,上面的程序代碼可以這樣重寫:

{% highlight php %}
<?php
echo isset($foo['bar']) ? $foo['bar'] : '';
{% endhighlight %}

當(dāng) fopen() 載入文件失敗時,也許是一個使用錯誤抑制符的合理例子。你可以在嘗試載入文件前檢查是否存在,但是如果這個文件在檢查后才被刪除,而此時 fopen() 還未執(zhí)行 (聽起來有點不太可能,但是確實會發(fā)生),這時 fopen() 會返回 false 并且 拋出操作。這也許應(yīng)該由 PHP 本身來解決,但這時一個錯誤抑制符才能有效解決的例子。

前面我們提到在正式的 PHP 環(huán)境中沒有辦法關(guān)閉錯誤控制操作符。但是 Xdebug 有一個 xdebug.scream 的 ini 配置項,可以關(guān)閉錯誤控制操作符。你可以按照下面的方式修改 php.ini。

{% highlight ini %}
xdebug.scream = On
{% endhighlight %}

你也可以在執(zhí)行期間通過 ini_set 函數(shù)來設(shè)置這個值:

{% highlight php %}
<?php
ini_set('xdebug.scream', '1')
{% endhighlight %}

「Scream」這個 PHP 擴展提供了和 xDebug 類似的功能,只是 Scream 的 ini 設(shè)置項叫做 scream.enabled 。

當(dāng)你在調(diào)試代碼而錯誤信息被隱藏時,這是最有用的方法。請務(wù)必小心使用 scream ,而是把它當(dāng)時暫時性的調(diào)試工具。有許多的 PHP 函數(shù)類庫代碼也許無法在錯誤抑制操作符停用時正常使用。

錯誤異常類

PHP 可以完美化身為「重異?!沟某绦蛘Z言,只需要幾行代碼就能切換過去?;旧夏憧梢岳?ErrorException 類拋出「錯誤」來當(dāng)做「異?!?,這個類是繼承自 Exception 類。

這在大量的現(xiàn)代框架中是一個常見的做法,比如 Symfony 和 Laravel。Laravel 默認(rèn)使用 Whoops! 擴展包來處理錯誤,如果 app.debug 啟動的話,會將錯誤當(dāng)成異常顯示出來,而關(guān)閉則會隱藏。

在開發(fā)過程中將錯誤當(dāng)作異常拋出可以更好的處理它,如果在開發(fā)時發(fā)生異常,你可以將它包在一個 catch 語句中具體說明這種情況如何處理。每捕捉一個異常,都會使你的應(yīng)用程序越來越健壯。

更多關(guān)于如何使用 ErrorException 來處理錯誤的細(xì)節(jié),可以參考 ErrorException Class。

異常

異常是許多流行編程語言的標(biāo)配,但它們往往被 PHP 開發(fā)人員所忽視。像 Ruby 就是一個極度重視異常的語言,無論有什么錯誤發(fā)生,像是 HTTP 請求失敗,或者數(shù)據(jù)庫查詢有問題,甚至找不到一個圖片資源,Ruby (或是所使用的 gems),將會拋出異常,你可以通過屏幕立刻知道所發(fā)生的問題。

PHP 處理這個問題則比較隨意,調(diào)用 file_get_contents() 函數(shù)通常只會給出 FALSE 值和警告。許多較早的 PHP 框架比如 CodeIgniter 只是返回 false,將信息寫入專有的日志,或者讓你使用類似 $this->upload->get_error() 的方法來查看錯誤原因。這里的問題在于你必須找出錯誤所在,并且通過翻閱文檔來查看這個類使用了什么樣的錯誤的方法,而不是明確的暴露錯誤。

另一個問題發(fā)生在當(dāng)類自動拋出錯誤到屏幕時會結(jié)束程序。這樣做會阻擋其他開發(fā)者動態(tài)處理錯誤的機會。應(yīng)該拋出異常讓開發(fā)人員意識到錯誤的存在,讓他們可以選擇處理的方式,例如:

{% highlight php %}
<?php
$email = new Fuel\Email;
$email->subject('My Subject');
$email->body('How the heck are you?');
$email->to('guy@example.com', 'Some Guy');

try
{
    $email->send();
}
catch(Fuel\Email\ValidationFailedException $e)
{
    // 驗證失敗
}
catch(Fuel\Email\SendingFailedException $e)
{
    // 這個驅(qū)動無法發(fā)送 email
}
finally
{
    // 無論拋出什么樣的異常都會執(zhí)行,并且在正常程序繼續(xù)之前執(zhí)行
}
{% endhighlight %}

SPL 異常

原生的 Exception 類并沒有提供太多的調(diào)試情境給開發(fā)人員,不過可以通過建立一個特殊的 Exception 來彌補它,方式就是建立一個繼承自原生 Exception 類的一個子類:

{% highlight php %}
<?php
class ValidationException extends Exception {}
{% endhighlight %}

如此一來,可以加入多個 catch 區(qū)塊,并且根據(jù)不同的異常分別處理。通過這樣可以建立 許多自定義異常,其中有些已經(jīng)在 SPL 擴展 提供的 SPL 異常中定義了。

舉例來說,如果你使用了 __call() 魔術(shù)方法去調(diào)用一個無效的方法,而不是拋出一個模糊的標(biāo)準(zhǔn) Exception 或是建立自定義的異常處理,你可以直接拋出 throw new BadMethodCallException;。