PHP Conference Japan 2024

例外

目錄

PHP 擁有與其他程式語言類似的例外模型。例外可以被拋出 (throw),並且在 PHP 中被捕獲 (亦稱為「捕捉 (catch)」)。程式碼可以被包含在 try 區塊中,以便於捕捉潛在的例外。每個 try 必須至少有一個對應的 catchfinally 區塊。

如果拋出一個例外,並且其目前的函式作用域中沒有 catch 區塊,則例外會沿著呼叫堆疊「向上冒泡」到呼叫函式,直到找到相符的 catch 區塊。它沿途遇到的所有 finally 區塊都會被執行。如果呼叫堆疊一路回溯到全域作用域,卻沒有遇到相符的 catch 區塊,程式將會以致命錯誤終止,除非已設定全域例外處理程式。

拋出的物件必須是 Throwable 的一個 instanceof。嘗試拋出非此類型的物件將導致 PHP 致命錯誤。

從 PHP 8.0.0 開始,throw 關鍵字是一個表達式,可以在任何表達式上下文中使用。在先前的版本中,它是一個陳述式,必須獨立一行。

catch

catch 區塊定義了如何回應拋出的例外。一個 catch 區塊定義了一種或多種它可以處理的例外或錯誤類型,以及一個用於指派例外的變數(PHP 8.0.0 之前此變數為必需)。拋出的例外或錯誤遇到的第一個與拋出物件類型相符的 catch 區塊將會處理該物件。

可以使用多個 catch 區塊來捕捉不同類別的例外。正常執行(在 try 區塊內沒有拋出例外時)將在依序定義的最後一個 catch 區塊之後繼續。例外可以在 catch 區塊內被 throw(或重新拋出)。如果沒有,執行將在觸發的 catch 區塊之後繼續。

當拋出例外時,陳述式後面的程式碼將不會被執行,PHP 將會嘗試尋找第一個相符的 catch 區塊。如果例外未被捕捉,將會發出一個带有「Uncaught Exception ...」訊息的 PHP 致命錯誤,除非已使用 set_exception_handler() 定義了處理程式。

從 PHP 7.1.0 開始,catch 區塊可以使用管道符號 (|) 指定多個例外。這在不同類別階層的不同例外以相同方式處理時很有用。

從 PHP 8.0.0 開始,捕捉到的例外的變數名稱是可選的。如果未指定,catch 區塊仍將執行,但無法存取拋出的物件。

finally

除了 catch 區塊之外,或者取代 catch 區塊,也可以指定一個 finally 區塊。finally 區塊中的程式碼總會在 trycatch 區塊之後執行,無論是否拋出例外,並且在恢復正常執行之前。

一個值得注意的互動是在 finally 區塊和 return 陳述式之間。如果在 trycatch 區塊中遇到 return 陳述式,finally 區塊仍然會被執行。此外,return 陳述式會在遇到時被評估,但結果會在 finally 區塊執行後才返回。另外,如果 finally 區塊也包含 return 陳述式,則會返回 finally 區塊中的值。

全域例外處理器

如果允許例外向上傳遞到全域範圍,則可以由設定的全域例外處理器攔截。如果沒有其他區塊被呼叫,set_exception_handler() 函式可以設定一個函式,該函式將取代 catch 區塊被呼叫。效果基本上與將整個程式包裝在一個 try-catch 區塊中,並以該函式作為 catch 相同。

注意事項

注意:

內部 PHP 函式主要使用錯誤回報,只有現代的物件導向擴充功能使用例外。然而,錯誤可以很容易地透過 ErrorException 轉換為例外。然而,這種技術只適用於非致命錯誤。

範例 #1 將錯誤回報轉換為例外

<?php
function exceptions_error_handler($severity, $message, $filename, $lineno) {
throw new
ErrorException($message, 0, $severity, $filename, $lineno);
}

set_error_handler('exceptions_error_handler');
?>

提示

標準 PHP 函式庫 (SPL) 提供了許多內建例外

範例

範例 #2 拋出例外

<?php
function inverse($x) {
if (!
$x) {
throw new
Exception('Division by zero.');
}
return
1/$x;
}

try {
echo
inverse(5) . "\n";
echo
inverse(0) . "\n";
} catch (
Exception $e) {
echo
'Caught exception: ', $e->getMessage(), "\n";
}

// 繼續執行
echo "Hello World\n";
?>

上述範例將輸出:

0.2
Caught exception: Division by zero.
Hello World

範例 #3 使用 finally 區塊的例外處理

<?php
function inverse($x) {
if (!
$x) {
throw new
Exception('Division by zero.');
}
return
1/$x;
}

try {
echo
inverse(5) . "\n";
} catch (
Exception $e) {
echo
'Caught exception: ', $e->getMessage(), "\n";
} finally {
echo
"First finally.\n";
}

try {
echo
inverse(0) . "\n";
} catch (
Exception $e) {
echo
'Caught exception: ', $e->getMessage(), "\n";
} finally {
echo
"Second finally.\n";
}

// 繼續執行
echo "Hello World\n";
?>

上述範例將輸出:

0.2
First finally.
Caught exception: Division by zero.
Second finally.
Hello World

範例 #4 finally 區塊和 return 之間的互動

<?php

function test() {
try {
throw new
Exception('foo');
} catch (
Exception $e) {
return
'catch';
} finally {
return
'finally';
}
}

echo
test();
?>

上述範例將輸出:

finally

範例 #5 巢狀例外

<?php

class MyException extends Exception { }

class
Test {
public function
testing() {
try {
try {
throw new
MyException('foo!');
} catch (
MyException $e) {
// 重新拋出例外
throw $e;
}
} catch (
Exception $e) {
var_dump($e->getMessage());
}
}
}

$foo = new Test;
$foo->testing();

?>

上述範例將輸出:

string(4) "foo!"

範例 #6 多重捕捉例外處理

<?php

class MyException extends Exception { }

class
MyOtherException extends Exception { }

class
Test {
public function
testing() {
try {
throw new
MyException();
} catch (
MyException | MyOtherException $e) {
var_dump(get_class($e));
}
}
}

$foo = new Test;
$foo->testing();

?>

上述範例將輸出:

string(11) "MyException"

範例 #7 省略捕捉的變數

僅在 PHP 8.0.0 及之後的版本允許。

<?php

class SpecificException extends Exception {}

function
test() {
throw new
SpecificException('Oopsie');
}

try {
test();
} catch (
SpecificException) {
print
"拋出了 SpecificException,但我們不在乎細節。";
}
?>

範例 #8 將 throw 作為表達式

僅在 PHP 8.0.0 及之後的版本允許。

<?php

函式 test() {
do_something_risky() or throw new Exception('執行失敗');
}

try {
test();
} catch (
Exception $e) {
print
$e->getMessage();
}
?>
新增註釋

使用者貢獻的註釋 13 則註釋

ask at nilpo dot com
15 年前
如果您打算建立許多自訂例外,您可能會發現這段程式碼很有用。我建立了一個介面和一個抽象例外類別,以確保內建 Exception 類別的所有部分都保留在子類別中。它還會正確地將所有資訊推送回父建構函式,確保不會遺失任何資訊。這讓您可以快速建立新的例外。它還覆寫了預設的 __toString 方法,使其更加完善。

<?php
介面 IException
{
/* 從 Exception 類別繼承的受保護方法 */
public function getMessage(); // 例外訊息
public function getCode(); // 使用者定義的例外代碼
public function getFile(); // 來源檔案名稱
public function getLine(); // 來源行數
public function getTrace(); // backtrace() 的陣列
public function getTraceAsString(); // 格式化的追蹤字串

/* 從 Exception 類別繼承的可覆寫方法 */
public function __toString(); // 格式化的顯示字串
public function __construct($message = null, $code = 0);
}

abstract class CustomException extends Exception implements IException
{
protected
$message = 'Unknown exception'; // 例外訊息
private $string; // 未知
protected $code = 0; // 使用者定義的例外代碼
protected $file; // 例外發生的來源檔案名稱
protected $line; // 例外發生的來源行數
private $trace; // 未知

public function __construct($message = null, $code = 0)
{
if (!
$message) {
throw new
$this('Unknown '.get_class($this));
}
parent::__construct($message, $code);
}

public function
__toString()
{
return
get_class($this) . " '{$this->message}' in {$this->file}({$this->line})\n"
. "{$this->getTraceAsString()}";
}
}
?>

現在您可以一行建立新的例外

<?php
class TestException extends CustomException {}
?>

以下測試顯示所有資訊在整個回溯過程中都得到了正確保留。

<?php
function exceptionTest()
{
try {
throw new
TestException();
}
catch (
TestException $e) {
echo
"Caught TestException ('{$e->getMessage()}')\n{$e}\n";
}
catch (
Exception $e) {
echo
"Caught Exception ('{$e->getMessage()}')\n{$e}\n";
}
}

echo
'<pre>' . exceptionTest() . '</pre>';
?>

範例輸出:

Caught TestException ('Unknown TestException')
TestException 'Unknown TestException' in C:\xampp\htdocs\CustomException\CustomException.php(31)
#0 C:\xampp\htdocs\CustomException\ExceptionTest.php(19): CustomException->__construct()
#1 C:\xampp\htdocs\CustomException\ExceptionTest.php(43): exceptionTest()
#2 {main}
tianyiw at vip dot qq dot com
1 年前
淺顯易懂的 `finally`。
<?php
try {
try {
echo
"before\n";
1 / 0;
echo
"after\n";
} finally {
echo
"finally\n";
}
} catch (
\Throwable) {
echo
"exception\n";
}
?>
# 輸出
before
finally
exception
Johan
13 年前
在整個頁面上使用自訂錯誤處理可以避免使用者看到呈現不完整的頁面。

<?php
ob_start
();
try {
/*包含所有頁面邏輯
並在需要時拋出錯誤*/
...
} catch (
Exception $e) {
ob_end_clean();
displayErrorPage($e->getMessage());
}
?>
jlherren
10 個月前
如同其他地方所提到的,從 `finally` 區塊拋出例外會取代先前拋出的例外。但原始例外可以神奇地從新例外的 `getPrevious()` 方法取得。

<?php
try {
try {
throw new
RuntimeException('Exception A');
} finally {
throw new
RuntimeException('Exception B');
}
}
catch (
Throwable $exception) {
echo
$exception->getMessage(), "\n";
// 'previous' 神奇地可用了!
echo $exception->getPrevious()->getMessage(), "\n";
}
?>

將會印出

Exception B
Exception A
Shot (Piotr Szotkowski)
16 年前
「正常的執行流程(當 try 區塊中沒有拋出任何例外,*或者當沒有與拋出例外類別相符的 catch 區塊時*)將會在最後一個定義的 catch 區塊之後繼續執行。」

「如果一個例外沒有被捕捉到,PHP 將會發出一個致命錯誤(Fatal Error),並顯示「未捕捉到的例外…」訊息,除非已經使用 set_exception_handler() 定義了一個處理程式。」

這兩句話關於「當沒有與拋出例外類別相符的 catch 區塊時」會發生什麼的情況,似乎有點矛盾(而第二句話實際上是正確的)。
christof+php[AT]insypro.com
7 年前
如果你的 E_WARNING 類型的錯誤無法用 try/catch 捕捉,你可以將它們更改為其他類型的錯誤,如下所示

<?php
set_error_handler
(function($errno, $errstr, $errfile, $errline){
if(
$errno === E_WARNING){
// 將其設為比警告更嚴重的錯誤,以便可以被捕捉
trigger_error($errstr, E_ERROR);
return
true;
} else {
// 使用預設的 php 錯誤處理程式
return false;
}
});

try {
// 可能導致 E_WARNING 的程式碼
} catch(Exception $e){
// 處理 E_WARNING 的程式碼(此時它實際上已更改為 E_ERROR)
} finally {
restore_error_handler();
}
?>
daviddlowe dot flimm at gmail dot com
7 年前
從 PHP 7 開始,Exception 和 Error 類別都實作了 Throwable 介面。這表示,如果您想同時捕捉 Error 實例和 Exception 實例,您應該捕捉 Throwable 物件,如下所示

<?php

try {
throw new
Error( "foobar" );
// 或:
// throw new Exception( "foobar" );
}
catch (
Throwable $e) {
var_export( $e );
}

?>
Edu
11 年前
「finally」區塊可以更改 catch 區塊拋出的例外。

<?php
try{
try {
throw new
\Exception("Hello");
} catch(
\Exception $e) {
echo
$e->getMessage()." catch in\n";
throw
$e;
} finally {
echo
$e->getMessage()." finally \n";
throw new
\Exception("Bye");
}
} catch (
\Exception $e) {
echo
$e->getMessage()." catch out\n";
}
?>

輸出結果為:

Hello catch in
Hello finally
Bye catch out
Simo
9 年前
第三個例子不太好。 inverse("0a") 不會被捕捉到,因為 (bool) "0a" 會返回 true,然而 1/"0a" 會將字串強制轉換為整數零,並嘗試執行計算。
telefoontoestel at nospam dot org
10 年前
使用 finally 區塊時請記住,如果在 catch 區塊中使用了 exit/die 陳述式,將不會執行 finally 區塊。

<?php
try {
echo
"try block<br />";
throw new
Exception("test");
} catch (
Exception $ex) {
echo
"catch block<br />";
} finally {
echo
"finally block<br />";
}

// try block
// catch block
// finally block
?>

<?php
try {
echo
"try block<br />";
throw new
Exception("test");
} catch (
Exception $ex) {
echo
"catch block<br />";
exit(
1);
} finally {
echo
"finally block<br />";
}

// try block
// catch block
?>
mlaopane at gmail dot com
6 年前
<?php

/**
* 你可以捕捉到在深層函式中拋出的例外
*/

function employee()
{
throw new
\Exception("我只是個員工!");
}

function
manager()
{
employee();
}

function
boss()
{
try {
manager();
} catch (
\Exception $e) {
echo
$e->getMessage();
}
}

boss(); // 輸出: "我只是個員工!"
Tom Polomsk
10 年前
與文件說明相反的是,在 PHP 5.5 及更高版本中,可以只使用 try-finally 區塊,而不需要任何 catch 區塊。
Sawsan
13 年前
以下是一個重新拋出異常和使用 getPrevious 函數的例子

<?php

$name
= "Name";

//檢查名稱是否只包含字母,並且不包含單詞 name

try
{
try
{
if (
preg_match('/[^a-z]/i', $name))
{
throw new
Exception("$name 包含 a-z A-Z 以外的字元");
}
if(
strpos(strtolower($name), 'name') !== FALSE)
{
throw new
Exception("$name 包含單詞 name");
}
echo
"名稱有效";
}
catch(
Exception $e)
{
throw new
Exception("請重新輸入名稱", 0, $e);
}
}

catch (
Exception $e)
{
if (
$e->getPrevious())
{
echo
"先前的異常是:".$e->getPrevious()->getMessage()."<br/>";
}
echo
"異常是:".$e->getMessage()."<br/>";
}

?>
To Top