《現代 PHP》學習筆記(十九):錯誤及例外

前言

本文為《現代 PHP》一書的學習筆記。

環境

  • Windows 10
  • Wnmp 3.1.0

錯誤及例外

錯誤及例外是個絕佳的工具,可以幫助開發者預防那些不可測的意外。錯誤及例外常常被搞混,它們都是在某些事情出錯時出現的。雖然現今開發者大量地依賴例外而非錯誤,仍需要對於錯誤保有基本的應對方式。

前置的 @ 記號放在 PHP 函式前可以避免錯誤的產生,但這是種反模式的行為,應該避免。

例外是物件導向版本的 PHP 錯誤處理系統,可以被實例化、被拋出、被接住,兼具了激進和保守的用途。

例外

例外是 Exception 類別的物件,當遇到一個不可修復的問題(例如遠端 API 沒有回應、資料庫查詢失敗或前置條件未滿足等),例外會被拋出。

使用 new 關鍵字來實例化 Exception 物件,它有兩個主要的屬性,分別是一個訊息和一個數値碼。

1
$exception = new Exception('Danger!', 100);

可以使用 getCode()getMessage() 方法來檢查 Exception 物件。

1
2
$code = $exception->getCode();
$message = $exception->getMessage();

拋出例外

當程式碼遇到例外情況或在當前情況無法運作時拋出例外。一旦例外被拋出,接下來的 PHP 程式碼不會被執行。

可以使用 throw 關鍵字接著一個 Exception 實體來拋出例外。

1
throw new Exception('Something went wrong!')

PHP 提供了內建的 Exception 子類別:

  • Exception
  • ErrorException

除此之外,Standard PHP Library 還補充了數個額外的 PHP 例外。

接住例外

被拋出的例外應該被接住並且妥善地處理。沒被接住的例外會導致致命錯誤,並且終止應用程式,甚至暴露敏感訊息。

使用 trycatch 將可能拋出例外的程式碼包覆起來。

範例 5-38:接住被拋出的例外

1
2
3
4
5
6
7
8
9
10
11
try {
$pdo = new PDO('mysql://host=wrong_host;dbname=wrong_name');
} catch (PDOException $e) {
// 檢查例外
$code = $e->getCode();
$message = $e->getMessage();

// 顯示一個友善的訊息
echo 'Something went wrong. Check back soon, please.';
exit;
}

使用 finally 在無論有沒有接住任何例外時,都執行其中的程式碼。

範例 5-39:接住數個被拋出的例外

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
throw new Exception('Not a PDO exception');
$pdo = new PDO('mysql://host=wrong_host;dbname=wrong_name');
} catch (PDOException $e) {
// 處理 PDO 例外
echo "Caught PDO exception";
} catch (Exception $e) {
// 處理其他例外
echo "Caught generic exception";
} finally {
// 總是執行
echo "Always do this";
}

例外處理器

永遠設立一個全域的例外處理器,例外處理器是最終的安全網。當在開發階段時,使用例外處理器顯示除錯資訊;而在產品階段時,使用人性化的訊息。

例外處利器需要接收一個 Exception 類別作為參數,可以使用 set_exception_handler() 函式來註冊。

1
2
3
set_exception_handler(function (Exception $e) {
// 處理並記錄例外
})

如果以自製的例外處理器替換掉現存的處理器,可以在程式碼結束後使用 restore_exception_handler() 函式復原。

範例 5-40:設置全域例外處理器

1
2
3
4
5
6
7
8
9
10
11
// 註冊例外處利器
set_exception_handler(function (Exception $e) {
// 處理並記錄例外
echo "Handling exception: " . $e->getMessage();
});

// 程式碼
throw new \Exception("Someting went wrong!");

// 回復原本的例外處理器
restore_exception_handler();

錯誤

錯誤經常在 PHP 腳本根本性地無法執行時(例如語法錯誤)被觸發,PHP 開發者必須同時預測並處理 PHP 錯誤及例外。

可以使用 error_reporting() 函式或是在 php.ini 檔中使用 error_reporting() 告知 PHP 哪些錯誤要被回報,以及哪些錯誤要被忽略,兩者都接受名為 E_* 的常數。

開發者應該遵循以下四條規則:

  • 永遠開啟錯誤回報。
  • 在開發階段時顯示錯誤。
  • 在產品階段不顯示錯誤。
  • 永遠記錄錯誤。

錯誤處理器

錯誤處理器就像是例外處理器,可以妥善地處理錯誤,開發者需要適時地呼叫 die()exit() 方法。

使用 set_error_handler() 方法註冊全域的錯誤處理器。

1
2
3
set_error_handler($errno, $errstr, $errfile, $errline) {
// 處理錯誤
}

set_error_handler() 會接收五個參數:

  • $errno,錯誤等級(對應到 PHP 的 E_* 常數)。
  • $errstr,錯誤訊息。
  • $errfile,錯誤發生的檔案名稱。
  • $errline,錯誤發生的程式碼行數。
  • $errcontext,一個陣列指向錯誤發生時的符號表格,這是非必要的。

有的 PHP 錯誤可以被錯誤處理器函式轉變成 ErrorException 物件,並且將它拋出到現存的例外處理系統。

範例 5-41:設立全域錯誤處理器

1
2
3
4
5
6
7
8
9
10
11
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
if (!(error_reporting() & $errno)) {
// 錯誤沒有在 error_reporting 設定中被指定,因此忽略
return;
}

throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
});

// 回復原本的例外處理器
restore_exception_handler();

轉變 PHP 錯誤並不是非常地直覺,必須審慎地選擇檔案,符合 php.ini 檔中的 error_reporting 設定。

開發階段的錯誤及例外

Whoops 是一個現代的 PHP 元件,提供設計良好、容易閱讀的錯誤及例外診斷頁面,由 Filipe Dobreira 和 Denis Sokolov 建立和維護。

1
composer require filp/whoops

範例 5-42:註冊 Whoops 處理器

1
2
3
4
5
6
7
require 'vendor/autoload.php';

$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();

throw new \Exception('This is an exception!');

產品階段的錯誤及例外

Monolog 是個非常棒的 PHP 元件,可以用來記錄錯誤。

1
composer require monolog/monolog

範例 5-43:使用 Monolog 作為開發階段記錄

1
2
3
4
5
6
7
8
9
require 'vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('my-app-name');
$log->pushHandler(new StreamHandler('logs/development.log', Logger::WARNING));

$log->warning('This is a warning!');

搭配 SwiftMailer 元件,可以在嚴重或緊急錯誤發生時發送電子郵件給管理者。

1
composer require swiftmailer/swiftmailer

範例 5-44:使用 Monolog 作為產品階段記錄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require 'vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SwiftMailerHandler;

date_default_timezone_set('America/New_York');

// Setup Monolog and basic handler
$log = new Logger('my-app-name');
$log->pushHandler(new StreamHandler('logs/production.log', Logger::WARNING));

$transport = \Swift_SmtpTransport::newInstance('smtp.example.com', 587)
->setUsername('USERNAME')
->setPassword('PASSWORD');
$mailer = \Swift_Mailer::newInstance($transport);
$message = \Swift_Message::newInstance()
->setSubject('Website error!')
->setFrom(array('daemon@example.com' => 'John Doe'))
->setTo(array('admin@example.com'));
$log->pushHandler(new SwiftMailerHandler($mailer, $message, Logger::CRITICAL));

$log->critical('The server is on fire!');

參考資料

  • Josh Lockhart(2015)。現代 PHP。台北市:碁峯資訊。