展開文件目錄

錯誤與例外

錯誤

在許多「重例外」(exception-heavy) 程式語言中,一旦發生錯誤,就會丟出例外。這的確是可行的運作方式,不過 PHP 卻是「輕例外」(exception-light) 語言。當然它具備例外,在和物件作用時,核心也開始採用這個機制來處理,只是PHP 會盡其可能保持運作,無視發生的事情,除非是嚴重錯誤。

舉例而言:

$ php -a
php > echo $foo;
Notice: Undefined variable: foo in php shell code on line 1

這裡只是 notice 層級的錯誤, PHP 仍然會愉快地繼續執行。這對來自「重例外」語言的人可能會帶來困擾,例如在 Python 中,存取一個不存在的變數會丟出例外:

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

本質上的差異在於任何小事都會驚嚇到 Python,因此開發人員可以確信任何潛在的問題或邊緣案例都能捕捉到,相同的情況 PHP 仍然會保持運作,除非極端的問題發生,這時才會丟出錯誤並回報。

錯誤嚴重性

PHP 有幾個錯誤嚴重性等級。三個最常見的訊息類型是錯誤(error)、通知(notice)和警告(warning)。它們有不同的嚴重性:「E_ERROR」、「E_NOTICE」和「E_WARNING」。錯誤是執行期的嚴重問題,通常是因為程式碼出錯而造成,而且必須修正它,因為它會使 PHP 停止運作。警告是非嚴重性的錯誤,程式執行不會因此而中止。通知是建議性質的訊息,是因為程式碼在執行期有潛在機會造成問題,程式不會因此停止。

另一個在編譯時期回報錯誤的訊息類型是「E_STRICT」。這個訊息用來建議修改程式碼,才能維持最佳的運作性並能與日後的 PHP 版本相容。

改變 PHP 的錯誤回報行為

錯誤回報可以藉由 PHP 設定及/或函式呼叫變更。使用內建的 PHP 函式 「error_reporting()」,可以設定程式執行期間的錯誤等級,方法是傳遞預先定義的錯誤等級常數,意謂如果你只想看到警告和錯誤(而非通知),那你可以這樣設定:

error_reporting(E_ERROR | E_WARNING);

你也可以決定錯誤是否在螢幕上顯示(開發時好用)或隱藏後記錄下來(適用於正式環境)。想知道更多細節,可以查看 錯誤報告 章節。

行內錯誤抑制

你可以讓 PHP 利用錯誤控制運算子「@」來抑制特定的錯誤。將這個運算子放置在表達式之前,其後的任何錯誤都不會出現。

echo @$foo['bar'];

如果「$foo['bar']」存在,程式會將結果輸出,不過如果變數「$foo」或「bar」鍵值不存在,將會回傳空值或不輸出任何東西。假如不使用錯誤控制運算子,這個運算式會建立一個錯誤訊息「PHP Notice: Undefined variable: foo」或「PHP Notice: Undefined index: bar」。

這看起來像是個好主意,不過卻也有些討厭的代價。PHP 處理使用「@」的運算式,比起不用時會犧牲一些效能。過早最佳化在所有程式中也許都是爭論點,不過效能在你的應用程式/函式中佔有重要地位,那麼了解錯誤控制運算子的效能影響就很重要。

其次,錯誤控制運算子 完全 吃掉錯誤。不但沒有顯示,而且也不會記錄在錯誤記錄中。此外,在正式環境 PHP 沒有辦法關閉錯誤控制運算子。也許你沒錯,那些錯誤是無害的,不過有些較具傷害性的錯誤同時也被隱藏。

如果有方法可以避免錯誤抑制運算子,你應該考慮使用,舉例來說,上面的程式碼可以這樣重寫:

echo isset($foo['bar']) ? $foo['bar'] : '';

「fopen()」載入檔案失敗時,也許是一個使用錯誤抑制的合理例子。你可以在嘗試載入檔案前檢查是否存在,但是這個檔案如果在檢查後才被刪除,而「fopen()」還沒執行(聽起來有點不太可能,但就是會發生),這時「fopen()」將會回傳false 並且 丟出錯誤。這也許該由 PHP 本身來解決,但這就是一個錯誤抑制才能有效解決的例子。

前面我們提到在正式 PHP 環境沒有辦法關閉錯誤控制運算子。但是 xDebug 有「xdebug.scream」的 ini 設定,可以關閉錯誤控制運算子。你可以依照下面的方式修改「php.ini」:

xdebug.scream = On

你也可以在執行期間透過「ini_set」函式設定這個值:

ini_set('xdebug.scream', '1')

Scream」這個 PHP 擴充套件提供和 xDebug 類似的功能,只是 Scream 的 ini 設定叫做「scream.enabled」。

當你在除錯而懷疑錯誤資訊被隱藏時,這是最有用的方法。Scream 務必小心使用,而且只拿來當作暫時性的除錯工具。有許多 PHP 函式庫也許無法在錯誤運算子停用時正常運作。

ErrorException 類別

PHP 可以完美化身為「重例外」程式語言,只需要幾行程式碼就能切換過去。基本上你可以利用「ErrorException」類別丟出「錯誤」來當作「例外」,這個類別是擴充「Exception」類別而來。

這在許多現今框架中是常見的實務作法,像是 Symfony 和 Laravel 都是。Laravel 預設會透過 Whoops! 套件,將錯誤當作例外顯示出來,如果「app.debug」啟用的話;關閉則會隱藏。

丟出錯誤作為例外,在開發過程中可以更好地處理它,如果在開發時看到例外,你可以將它包在 catch 敘述中,再寫下如何處理的程式。捕捉每一個例外,都會使應用程式越來越穩固。

更多關於如何使用「ErrorException」來處理錯誤的細節,可以參考 ErrorException Class

例外

例外是許多流行語言的標準配備,但它們往往被 PHP 開發人員忽視。像 Ruby 就是一個極端重視例外的語言,無論有什麼不對勁發生,像是 HTTP 請求失敗,或是資料庫的查詢有問題,甚至像找不到圖片,Ruby (或是所使用的gem),將會丟出例外到畫面上,讓你立刻了解問題發生了。

PHP 看待這事則是相當隨意,呼叫「file_get_contents()」通常只會給出「FALSE」值和警告。許多較早的 PHP 框架像是 CodeIgniter 只是回傳 false、將訊息寫入記錄,頂多讓你使用像「$this->upload->get_error()」來檢視錯誤原因。這裡的問題出在你必須找出錯誤所在,並且檢視文件來查看這個類別使用什麼樣的錯誤方法,而不是明顯曝露錯誤。

另一個問題是發生在類別自動丟出錯誤到畫面並跳出程序。當你這樣做時,其他開發者動態處理錯誤的機會也被擋下了。例外應該丟出能讓開發人員意識到錯誤的存在,讓他們可以選擇處理的方式,例如:

<?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)
{
    // 無法寄出信件
}
finally
{
    // 無論丟出什麼樣的例外都會執行,並且在正常程序繼續之前執行
}

SPL 例外

原生的「例外」類別沒有提供太多除錯脈絡給開發人員,要修正的解法是建立一個特殊的「Exception」類型,方式是從原生「Exception」類別建立一個子類別:

<?php
class ValidationException extends Exception {}

如此一來,可以加入多重 catch 區塊,並且依不同的例外分別處理。這也可以建立許多客製例外,其中有些已經避免使用在 SPL 擴充套件 提供的 SPL 例外。

舉例來說,如果你使用「__call()」魔術方法去呼叫了無效的方法,而不是丟出模糊的標準 Exception,或是建立客製例外專門處理,你可能已經幹了「throw new BadMethodCallException;」。