Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
精通 PHP 錯誤處理
讓除錯更自在
PHP 也有 Day #35
2018.05.29
Simon Asika (飛鳥)
認識 PHP 的 Error
PHP 的常見錯誤種類
• E_ERROR 執行期的 Fatal Error,無法進行錯誤修復,程式會直接停止。
• E_WARNING 警告,但不會停止程式。
• E_NOTICE 不屬於錯誤,但可能會發生錯誤,因此提示你。
• E_STRI...
E_ERROR
• 產生以下錯誤
Fatal error: Uncaught Error: Call to undefined function bar() in
D:wwwslimpublicindex.php:7
• 因為 function...
E_WARNING
• 產生以下錯誤畫面:
Warning: A non-numeric value encountered in D:wwwslimpublicindex.php on line 3
123
• 不正確的資料操作與計算,可能造...
E_NOTICE
• 產生以下畫面
Notice: Undefined variable: b in D:wwwslimpublicindex.php on line 6
A
• $b 沒有被預先宣告,這種寫法在 PHP 中是允許的,但是這樣子...
E_STRICT
• 產生以下錯誤訊息
Warning: Declaration of B::foo() should be compatible with A::foo($a = 123)
in D:wwwslimpublicindex.ph...
E_DEPRECATED
• 產生以下畫面
Deprecated: Function mcrypt_create_iv() is deprecated
in D:wwwslimpublicindex.php on line 6
�Ի��eD��...
你也可以產生屬於自己的錯誤訊息
• 用 trigger_error() 來立即觸發使用者定義的錯誤訊息
• 預設值是 E_USER_NOTICE,所以程式會繼續執行下去,只是跳 Notice 訊息。
Notice: $a is not A in...
改用 E_USER_ERROR
• 這次用的是 ERROR type,程式就會終止執行了:
Fatal error: $a is not A in D:wwwslimpublicindex.php on line 6
• 可用的種類有:
 E...
看不到 Notice 或 Deprecated 怎麼辦
• 可以在 php.ini 修改 error_reporting 直接用 E_ALL
 error_reporting = E_ALL
• 或是直接 runtime 時用 functio...
用 @ 隱藏錯誤訊息
• 可放在一行的最開頭,或是 function call 前面。通常用在極度不確定外部輸入格式時。
• 將這一行的錯誤訊息隱藏,這個案例中原本會產生下面的訊息,但實際上被屏蔽了:
Warning: substr() exp...
建議的設定
• PHP 的預設設定是 E_ALL & ~E_NOTICE ,意思是顯示所有錯誤,Notice 除外。
• 開發過程,建議強制設成 E_ALL 或 -1,開發者應該要清空所有可能的 Warning &
Notice 不要讓任何可能...
不養成良好的編寫習慣,其它開發者拿到你的 code 就
變成這樣
寫程式時要保持這種心態:就好像將來要維護你這些代碼的人是一位
殘暴的精神病患者,而且他知道你住在哪。
--- John Woods (1991)
自行捕獲錯誤
使用 set_error_handler() 捕獲錯誤
• 會印出:
錯誤[2]: substr() expects parameter 1 to be string, array given - 位置:
D:wwwslimpublicinde...
• 加上一點改變,現在我們可以根據錯誤碼加上不同的提示 (這裡只抓前四個示範)
• 本範例會印出
警告: substr() expects parameter 1 to be string, array given - 位置: D:wwwsli...
• 但是你會發現,現在加上 @ 或關閉錯誤訊息都沒用。為內建的錯誤處理已經被我
們強制覆蓋了。
• 照樣印出錯誤訊息
警告: substr() expects parameter 1 to be string, array given - 位置...
• 之前有說過,@ 的作用就是當下即時把 error_reporting 改成 0,所以我們加上
error_reporting 的判斷,如果是 0,就直接略過。
• 現在不會再出現錯誤訊息了。
set_error_handler(functi...
• 如果你希望錯誤的顯示與否與之前設定 error_reporting 的內容相同,則可以用位
元運算子的 & 符號來做比對
• 詳情請見
 http://bit.ly/2LyEcbC
 https://stackoverflow.com/...
搭配 log 紀錄錯誤訊息
• 隱蔽錯誤是有風險的,我們可以嘗試把錯誤都記錄在 log 內,至少發生不明錯誤時
還有紀錄可以查詢
• error_log() 的參數說明請見 http://php.net/manual/en/function.e...
• 左邊是完整的範例
• 先組好messsage,然後立即 log
• 接著判斷 error_reporting 有必要才
echo 錯誤訊息
• error_log() 不會幫你換行,記得加上
換行符號。
set_error_handler(...
log 紀錄結果
• 這只是一個簡單的範例,實際的網站開發請另外使用 Monolog 之類的套件來處理
log檔,並記得做 rotating 免得log塞爆。
• 有 DevOps 人員或採用 microservice 的團隊,可以考慮把 lo...
別忘了做個美美的錯誤畫面
• 送出 500 HTTP 錯誤碼,然後 render 錯誤畫面,畫上可愛的插圖,大功告成。
set_error_handler(function ($code, $msg, $file, $line) {
// .....
可以把錯誤當作 Exception 丟出喔
• Warning 可以當作 Exception 一樣 catch 到,很神奇吧。這樣就可以把所有錯誤一致
性的交給 exception handler 處理了。
• 注意: Fatal Error ...
認識 Exception
如何使用 Exception
• 任何開發者自己認為是錯誤的地方,都可以丟出 Exception 中斷程式流程。
• 丟出 Exception 後,下方的程式就不會再執行,但此時整個程序沒有終止 (不像 Fatal Error 會直接終
止)
...
捕獲 Exception
• 用 try ... catch 包住 Exception,就能自訂中斷流程,做額外的錯誤處理
• 在 try 區塊裡面,只要丟出 Exception 的話,後方程式就不會再執行,但會跳到 catch 的
區塊,所以...
更複雜的案例
• Exception 不一定是自己丟出的,也可能是
核心獲第三方函市庫丟出來的,範例中丟
出的 PDOExcception 通常是 SQL 有誤時會丟
出。
• Exception 可以分多個種類,用不同的 catch
來補獲。...
善用 Code 判斷錯誤種類
• 就算是相同的 Exception 類型,也可以用 code 來區隔其錯誤狀態。
try {
if (!$user->isLogin()) {
throw new RuntimeException('Access...
自定義 Exception
• 你可以繼承 Exception 或 RuntimeException 建立自己的 Exception
• 拋出之後,用 catch (IDontFeelSoGood $e) 就可以針對這個 Exception 做...
Catch 到 Exception 之後怎麼辦
• 範例中根據捕獲的 Exception 種類有不
同的操作。
• 有的做 redirect,有的直接 404 或 403。
• 善用自訂義 Exceptions 搭配多層 catch
可以做到很...
Exception 可以無視層次跳躍
• 範例中 Exception 是在 function 內拋出,但是外部的 try ... catch 可以抓到。
• Exception 是無視層次的,會向上一直跳到有 try ... catch 的地方...
沒有捕獲 Exception 的結果
• 會顯示 Uncaught Exception 然後終止程序
Fatal error: Uncaught RuntimeException: $a is not A in D:wwwslimpublici...
但我們一樣可以捕獲最上層 Exception
• 還記得前面的 set_error_handler() 嗎?
• 我們也可以用 set_exception_handler(); 來抓取拋到最外層的 Exception。
set_exceptio...
抓取到 Exception 後的結果
抓取到 Exception 後的結果
看!很簡單吧?
我們完成了自製的錯誤處理器
把雙劍客合起來用
• 前面提到 error handler 可以把錯
誤當做 Exception 丟出去。
• 所以搭配 exception handler 就可
以集中處理所有可能的錯誤訊息。
• 這裡開始就複雜多了,沒關係,
框架們都幫你搞...
PHP7 的
Exceptions
See http://asika.windspeaker.co/post/3503-php-exceptions
Exception的使用觀念
例外不是錯誤
Exception 不代表 Error,他可以是流程控制的一部份
但必須被認定為【異常狀況】
異常狀況的處理
• 在一個大量迴圈的任務中,我們希望即便少數的 job 失敗了,還是要繼續跑完後面
的 jobs
• 且我們希望每一個 Job 失敗時,會 mail 通知管理員
• 因此在這邊,Exception 做為異常狀況處理器,會控制流程...
但Exception也不是流程控制
可以用 if else 解決的問題,就不要用 Exception
不要這樣用
• 如果你只是想讓未登入 user 轉過去登入頁面,在這個案例中,用 if 就能處理了
try {
if ($user->group === 'guest') {
throw new UnauthorisedException('P...
但可以這樣用
• 這個案例中,user group 如果出現預定義的格式以外的值,肯定屬於異常狀況,就
直接拋出 Exception 吧。
switch ($user->group) {
case 'guest':
// Please logi...
或是這樣用
• 將多個 function 呼叫的 Exception 做集中處理,可以讓異常處理流程更乾淨易讀。
try {
$obj->methodA();
$obj->methodB();
$obj->methodC();
} catch ...
Exception 一定要處理
不要隱蔽錯誤
try {
foo();
} catch (RuntimeException $e) {
// No action
}
使用 Exception 的情境
• 通常比較少在同一個空間內同時 try ... catch 又同時 throw Exception。
• 開發 function 的人可以根據異常狀況拋出各種 Exception,幫助使用 function ...
防禦型程式設計
public function __construct($string, $int, $array)
{
// 最基本的檢查,型別不對就丟錯
if (!is_string($string))
{
throw new Invali...
願各位的程式都能自動修復錯誤
--- Thank You
PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在
Upcoming SlideShare
Loading in …5
×

PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在

3,657 views

Published on

開發 PHP 的過程,你一定會常常遇到各種錯誤,除了讓框架來告訴你錯誤訊息以外,我們還能用什麼方法更自在的控制錯誤訊息呢?

本次演講將整理出許多 php 常用的除錯功能,適合想要跳脫框架、開發函式庫,並持續精進的工程師們。

Published in: Software
  • Be the first to comment

PHP 也有 Day #35 - 精通 PHP 錯誤處理,讓除錯更自在

  1. 1. 精通 PHP 錯誤處理 讓除錯更自在 PHP 也有 Day #35 2018.05.29 Simon Asika (飛鳥)
  2. 2. 認識 PHP 的 Error
  3. 3. PHP 的常見錯誤種類 • E_ERROR 執行期的 Fatal Error,無法進行錯誤修復,程式會直接停止。 • E_WARNING 警告,但不會停止程式。 • E_NOTICE 不屬於錯誤,但可能會發生錯誤,因此提示你。 • E_STRICT 更嚴格的 PHP 規範提示。 • E_DEPRECATED 被棄用的 function 等,提示你趕快換掉成新的用法。 • 其它  E_PARCE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_WARNING, E_COMPILE_ERROR, E_RECOVERABLE_ERROR, E_ALL 等  See http://php.net/manual/en/errorfunc.constants.php
  4. 4. E_ERROR • 產生以下錯誤 Fatal error: Uncaught Error: Call to undefined function bar() in D:wwwslimpublicindex.php:7 • 因為 function 根本不存在,系統無從猜測可能的行為,也無法修復錯誤,故為 Fatal Error,強制停止程式運作。 function foo() { echo 'foo'; } bar(); // 呼叫了不存在的 function
  5. 5. E_WARNING • 產生以下錯誤畫面: Warning: A non-numeric value encountered in D:wwwslimpublicindex.php on line 3 123 • 不正確的資料操作與計算,可能造成程式 BUG,但是系統可以修復其行為,所以 程式不會中斷,可以被隱藏。 $result = 123 + 'ABC'; // 數字加字串 echo sprintf('<span style="color: red;">%s</span>', $result);
  6. 6. E_NOTICE • 產生以下畫面 Notice: Undefined variable: b in D:wwwslimpublicindex.php on line 6 A • $b 沒有被預先宣告,這種寫法在 PHP 中是允許的,但是這樣子有很大機率出現 BUG ,故 PHP 會用 Notice 提示你最好預先宣告。 $a = 'A'; $ab = $a . $b; // $b 不存在 echo $ab;
  7. 7. E_STRICT • 產生以下錯誤訊息 Warning: Declaration of B::foo() should be compatible with A::foo($a = 123) in D:wwwslimpublicindex.php on line 17 對PHP開發過程更嚴謹的要求。 class A { public function foo($a = 123) { } } class B extends A { // 與 parent class 介面不一樣 public function foo() { } }
  8. 8. E_DEPRECATED • 產生以下畫面 Deprecated: Function mcrypt_create_iv() is deprecated in D:wwwslimpublicindex.php on line 6 �Ի��eD��IN� • 若使用了已被棄用的語言功能就會發出此提示,提醒你趕快改用新功能。 error_reporting(E_ALL); // PHP 7.2 deprecated mcrypt $v = mcrypt_create_iv(16); echo $v;
  9. 9. 你也可以產生屬於自己的錯誤訊息 • 用 trigger_error() 來立即觸發使用者定義的錯誤訊息 • 預設值是 E_USER_NOTICE,所以程式會繼續執行下去,只是跳 Notice 訊息。 Notice: $a is not A in D:wwwslimpublicindex.php on line 6 B $a = 'B'; if ($a !== 'A') { trigger_error('$a is not A', E_USER_NOTICE); } echo $a;
  10. 10. 改用 E_USER_ERROR • 這次用的是 ERROR type,程式就會終止執行了: Fatal error: $a is not A in D:wwwslimpublicindex.php on line 6 • 可用的種類有:  E_USER_ERROR, E_USER_NOTICE, E_USER_WARNING, E_USER_DEPRECATED 等  大多數 E_* 的錯誤訊息,都有 E_USER_* 的對應 if ($a !== 'A') { trigger_error('$a is not A', E_USER_ERROR); }
  11. 11. 看不到 Notice 或 Deprecated 怎麼辦 • 可以在 php.ini 修改 error_reporting 直接用 E_ALL  error_reporting = E_ALL • 或是直接 runtime 時用 function 設定: // 回報所有錯誤, php 5.4 以後 E_STRICT 包含在內 error_reporting(E_ALL); // 回報特定錯誤 error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE); // 回報所有錯誤,Notice 除外 error_reporting(E_ALL & ~E_NOTICE); // 設為 -1 是最大值,所有可能的錯誤全部顯示,無關版本 error_reporting(-1); // 關閉錯誤訊息,不顯示。 error_reporting(0);
  12. 12. 用 @ 隱藏錯誤訊息 • 可放在一行的最開頭,或是 function call 前面。通常用在極度不確定外部輸入格式時。 • 將這一行的錯誤訊息隱藏,這個案例中原本會產生下面的訊息,但實際上被屏蔽了: Warning: substr() expects parameter 1 to be string, array given • 其原理是在這一行開始執行時,背景將 error_reporting 改成 0,然後下一行執行前再改 回來。 • 所以如果有特別寫錯誤處理器的話,依然抓的到此錯誤。 @$a = substr([], 0, 5); var_dump($a);
  13. 13. 建議的設定 • PHP 的預設設定是 E_ALL & ~E_NOTICE ,意思是顯示所有錯誤,Notice 除外。 • 開發過程,建議強制設成 E_ALL 或 -1,開發者應該要清空所有可能的 Warning & Notice 不要讓任何可能的 BUG 有機會出現。 • 老舊系統運作時,考慮設成 0,不要讓使用者看見大量的 Notice 訊息。 • 網站正式運作時,也可以設定成 0。但最好確保重要的錯誤有被 log 記錄下來。 • 從每個字敲出來就用最高標準對待自己的程式碼,未來的錯誤才會少。
  14. 14. 不養成良好的編寫習慣,其它開發者拿到你的 code 就 變成這樣
  15. 15. 寫程式時要保持這種心態:就好像將來要維護你這些代碼的人是一位 殘暴的精神病患者,而且他知道你住在哪。 --- John Woods (1991)
  16. 16. 自行捕獲錯誤
  17. 17. 使用 set_error_handler() 捕獲錯誤 • 會印出: 錯誤[2]: substr() expects parameter 1 to be string, array given - 位置: D:wwwslimpublicindex.php (8) • 變數說明  $code 錯誤碼,例如 E_WARNING 就是 2  $msg 錯誤訊息  $file/$line 錯誤發生的檔案與行  $context 錯誤當下的環境相關資訊 (php7.2 deprecated) set_error_handler(function ($code, $msg, $file, $line, $context) { echo sprintf('錯誤[%d]: %s - 位置: %s (%s)', $code, $msg, $file, $line); die; }); $a = substr([], 0, 5);
  18. 18. • 加上一點改變,現在我們可以根據錯誤碼加上不同的提示 (這裡只抓前四個示範) • 本範例會印出 警告: substr() expects parameter 1 to be string, array given - 位置: D:wwwslimpublicindex.php (23) set_error_handler(function ($code, $msg, $file, $line) { $errormaps = [ E_ERROR => '錯誤', // 1 E_WARNING => '警告', // 2 E_PARSE => '語法錯誤', // 4 E_NOTICE => '提醒', // 8 ]; echo sprintf( '%s: %s - 位置: %s (%s)', $errormaps[$code] ?? '錯誤', // 用 code 取出錯誤說明 $msg, $file, $line ); die; }); $a = substr([], 0, 5);
  19. 19. • 但是你會發現,現在加上 @ 或關閉錯誤訊息都沒用。為內建的錯誤處理已經被我 們強制覆蓋了。 • 照樣印出錯誤訊息 警告: substr() expects parameter 1 to be string, array given - 位置: D:wwwslimpublicindex.php (23) error_reporting(0); // 這個沒用了 set_error_handler(function ($code, $msg, $file, $line) { $errormaps = [ // ... 略 ]; echo sprintf( '%s: %s - 位置: %s (%s)', $errormaps[$code] ?? '錯誤', // 用 code 取出錯誤說明 $msg, $file, $line ); die; }); @$a = substr([], 0, 5); // 加上 @ 也沒用了
  20. 20. • 之前有說過,@ 的作用就是當下即時把 error_reporting 改成 0,所以我們加上 error_reporting 的判斷,如果是 0,就直接略過。 • 現在不會再出現錯誤訊息了。 set_error_handler(function ($code, $msg, $file, $line) { if (error_reporting() === 0) { return; } $errormaps = [ // ... 略 ]; echo sprintf( '%s: %s - 位置: %s (%s)', $errormaps[$code] ?? '錯誤', $msg, $file, $line ); die; }); @$a = substr([], 0, 5);
  21. 21. • 如果你希望錯誤的顯示與否與之前設定 error_reporting 的內容相同,則可以用位 元運算子的 & 符號來做比對 • 詳情請見  http://bit.ly/2LyEcbC  https://stackoverflow.com/questions/4705838/when-should-i-use-a-bitwise-operator  http://php.net/manual/en/language.operators.bitwise.php set_error_handler(function ($code, $msg, $file, $line) { if ((error_reporting() & $code) === 0) { return; } // ... }); $a = substr([], 0, 5);
  22. 22. 搭配 log 紀錄錯誤訊息 • 隱蔽錯誤是有風險的,我們可以嘗試把錯誤都記錄在 log 內,至少發生不明錯誤時 還有紀錄可以查詢 • error_log() 的參數說明請見 http://php.net/manual/en/function.error-log.php set_error_handler(function ($code, $msg, $file, $line) { // ... error_log($msg . PHP_EOL, 3, __DIR__ . '/logs/error.log'); // ... });
  23. 23. • 左邊是完整的範例 • 先組好messsage,然後立即 log • 接著判斷 error_reporting 有必要才 echo 錯誤訊息 • error_log() 不會幫你換行,記得加上 換行符號。 set_error_handler(function ($code, $msg, $file, $line) { $errormaps = [ E_ERROR => '錯誤', // 1 E_WARNING => '警告', // 2 E_PARSE => '語法錯誤', // 4 E_NOTICE => '提醒', // 8 ]; $msg = sprintf( '%s: %s - 位置: %s (%s)', $errormaps[$code] ?? '錯誤', $msg, $file, $line ); error_log($msg . PHP_EOL, 3, __DIR__ . '/logs/error.log'); if ((error_reporting() & $code) === 0) { return; } echo $msg; die; });
  24. 24. log 紀錄結果 • 這只是一個簡單的範例,實際的網站開發請另外使用 Monolog 之類的套件來處理 log檔,並記得做 rotating 免得log塞爆。 • 有 DevOps 人員或採用 microservice 的團隊,可以考慮把 log 服務拉出去成為一台 獨立伺服器,所有訊息都往遠端打出去,就不用擔心 log 爆量問題。 • 主流框架大多都幫你處理好這些工作了,感謝上天,感謝 Opensource。
  25. 25. 別忘了做個美美的錯誤畫面 • 送出 500 HTTP 錯誤碼,然後 render 錯誤畫面,畫上可愛的插圖,大功告成。 set_error_handler(function ($code, $msg, $file, $line) { // ...略 if ((error_reporting() & $code) === 0) { return; } http_response_code(500); echo view('error.default', compact(['msg', 'code', 'file', 'line'])); die; });
  26. 26. 可以把錯誤當作 Exception 丟出喔 • Warning 可以當作 Exception 一樣 catch 到,很神奇吧。這樣就可以把所有錯誤一致 性的交給 exception handler 處理了。 • 注意: Fatal Error 不能 catch set_error_handler(function ($code, $msg, $file, $line) { // ...略 if ((error_reporting() & $code) === 0) { return; } throw new ErrorException($msg, 500, $code, $file, $line); }); try { $a = substr([], 0, 5); } catch (ErrorException $e) { echo $e; }
  27. 27. 認識 Exception
  28. 28. 如何使用 Exception • 任何開發者自己認為是錯誤的地方,都可以丟出 Exception 中斷程式流程。 • 丟出 Exception 後,下方的程式就不會再執行,但此時整個程序沒有終止 (不像 Fatal Error 會直接終 止) • 我們可以 catch 丟出去的 Exception,然後轉而執行其它程式或流程。 $a = 'B'; if ($a !== 'A') { throw new RuntimeException('$a is not A', 500); // 從這裡中斷執行 } echo $a; // 這裡不會再執行了...
  29. 29. 捕獲 Exception • 用 try ... catch 包住 Exception,就能自訂中斷流程,做額外的錯誤處理 • 在 try 區塊裡面,只要丟出 Exception 的話,後方程式就不會再執行,但會跳到 catch 的 區塊,所以可以另外執行除錯工作。 • 如果在 catch 內沒有 die 掉程式的話,try ... catch 後面的程式可以繼續執行,不會終止 程序。 $a = 'B'; try { if ($a !== 'A') { throw new RuntimeException('$a is not A'); // 直接跳出 } echo $a; // 這裡不會執行 } catch (Exception $e) { error_log($e->getMessage(), 3, 'logs/error.log'); // '$a is not A’ } echo 123; // 這裡又可以繼續執行了
  30. 30. 更複雜的案例 • Exception 不一定是自己丟出的,也可能是 核心獲第三方函市庫丟出來的,範例中丟 出的 PDOExcception 通常是 SQL 有誤時會丟 出。 • Exception 可以分多個種類,用不同的 catch 來補獲。 • 越下面的 catch 包含範圍越廣大。 • 最後可以用一個 finally 來執行出現錯誤後一 定要做的任何處理。 $pdo = new PDO('mysql:...'); try { $pdo->prepare($sql)->execute(); } catch (PDOException $e) { // 處理 PDO 本身的錯誤 } catch (Exception $e) { // 處理其它可能的錯誤 } catch (Throwable $e) { // 處理 php7 error } finally { unset($pdo); // 終止連線 } echo '這裡會繼續執行';
  31. 31. 善用 Code 判斷錯誤種類 • 就算是相同的 Exception 類型,也可以用 code 來區隔其錯誤狀態。 try { if (!$user->isLogin()) { throw new RuntimeException('Access denied.', 401); } // ...略 } catch (RuntimeException $e) { if ($e->getCode() === 401) { // 未登入 } elseif ($e->getCode() === 404) { // 找不到頁面 } else { // 其它錯誤 } }
  32. 32. 自定義 Exception • 你可以繼承 Exception 或 RuntimeException 建立自己的 Exception • 拋出之後,用 catch (IDontFeelSoGood $e) 就可以針對這個 Exception 做自訂 義錯誤處理。 class IDontFeelSoGoodException extends Exception { } if (count($infinityGems) === 6) { throw new IDontFeelSoGoodException('Tony I'm sorry', 404); }
  33. 33. Catch 到 Exception 之後怎麼辦 • 範例中根據捕獲的 Exception 種類有不 同的操作。 • 有的做 redirect,有的直接 404 或 403。 • 善用自訂義 Exceptions 搭配多層 catch 可以做到很靈活的錯誤處理。 try { User::save($userData); } catch (UserNotLoginException $e) { header('Location: /login'); } catch (UserNotFoundException $e) { http_response_code(404); die('Sorry, this user not found.'); } catch (UnauthorisedExceotion $e) { http_response_code(403); die('Forbidden'); }
  34. 34. Exception 可以無視層次跳躍 • 範例中 Exception 是在 function 內拋出,但是外部的 try ... catch 可以抓到。 • Exception 是無視層次的,會向上一直跳到有 try ... catch 的地方才停止。 • 如果沒有 try ... catch,則會跳到最上層,成為 Fatal Error 終止程序。 function foo($a) { if ($a !== 'A') { throw new RuntimeException('$a is not A'); } echo $a; } try { foo('B'); } catch (RuntimeException $e) { echo $e->getMessage(); }
  35. 35. 沒有捕獲 Exception 的結果 • 會顯示 Uncaught Exception 然後終止程序 Fatal error: Uncaught RuntimeException: $a is not A in D:wwwslimpublicindex.php:5 Stack trace: #0 D:wwwslimpublicindex.php(11): foo('B') #1 {main} thrown in D:wwwslimpublicindex.php on line 5 function foo($a) { if ($a !== 'A') { throw new RuntimeException('$a is not A'); } echo $a; } foo('B');
  36. 36. 但我們一樣可以捕獲最上層 Exception • 還記得前面的 set_error_handler() 嗎? • 我們也可以用 set_exception_handler(); 來抓取拋到最外層的 Exception。 set_exception_handler(function (Throwable $e) { http_response_code($e->getCode()); echo <<<HTML <h1>{$e->getMessage()}</h1> <strong>Code:</strong> {$e->getCode()} <br/> <strong>File:</strong> <code>{$e->getFile()} ({$e->getLine()})</code> <h3>Call Stack</h3> <pre>{$e->getTraceAsString()}</pre> HTML; die; }); throw new RuntimeException('Oops, something went wrong.', 403);
  37. 37. 抓取到 Exception 後的結果
  38. 38. 抓取到 Exception 後的結果 看!很簡單吧? 我們完成了自製的錯誤處理器
  39. 39. 把雙劍客合起來用 • 前面提到 error handler 可以把錯 誤當做 Exception 丟出去。 • 所以搭配 exception handler 就可 以集中處理所有可能的錯誤訊息。 • 這裡開始就複雜多了,沒關係, 框架們都幫你搞定了。 • 還有很多靈活用法,請參考:  http://www.w3school.com.cn/php/p hp_exception.asp  https://code.tutsplus.com/tutorials/p hp-exceptions--net-22274 // 把所有 Error 也當作 Exception 丟出去 set_error_handler(function ($code, $msg, $file, $line) { // ... throw new ErrorException($msg, 500, $code, $file, $line); }); // 所有的 Error, Warning, Notice & Exceptions 通通集中在這邊處理 set_exception_handler(function (Throwable $e) { http_response_code($e->getCode()); echo <<<HTML <h1>{$e->getMessage()}</h1> <strong>Code:</strong> {$e->getCode()} <br/> <strong>File:</strong> <code>{$e->getFile()} ({$e- >getLine()})</code> <h3>Call Stack</h3> <pre>{$e->getTraceAsString()}</pre> HTML; die; });
  40. 40. PHP7 的 Exceptions See http://asika.windspeaker.co/post/3503-php-exceptions
  41. 41. Exception的使用觀念
  42. 42. 例外不是錯誤 Exception 不代表 Error,他可以是流程控制的一部份 但必須被認定為【異常狀況】
  43. 43. 異常狀況的處理 • 在一個大量迴圈的任務中,我們希望即便少數的 job 失敗了,還是要繼續跑完後面 的 jobs • 且我們希望每一個 Job 失敗時,會 mail 通知管理員 • 因此在這邊,Exception 做為異常狀況處理器,會控制流程去寄送通知信,但又不 中斷程序,使得迴圈繼續跑下去。 foreach ($jobs as $job) { try { Queue::process($job); } catch (QueueException $e) { Mailer::send('A queue job error', $message); } }
  44. 44. 但Exception也不是流程控制 可以用 if else 解決的問題,就不要用 Exception
  45. 45. 不要這樣用 • 如果你只是想讓未登入 user 轉過去登入頁面,在這個案例中,用 if 就能處理了 try { if ($user->group === 'guest') { throw new UnauthorisedException('Please login', 401); } elseif (...) { } elseif (...) { } else { } } catch (UnauthorisedException $e) { header('Location: /login'); } if ($user->group === 'guest') { header('Location: /login'); }
  46. 46. 但可以這樣用 • 這個案例中,user group 如果出現預定義的格式以外的值,肯定屬於異常狀況,就 直接拋出 Exception 吧。 switch ($user->group) { case 'guest': // Please login first. case 'member': // Welcome Back case 'manager': // Sir, yes sir. default: throw new UnauthorisedException('Uh... Who are you?', 403); }
  47. 47. 或是這樣用 • 將多個 function 呼叫的 Exception 做集中處理,可以讓異常處理流程更乾淨易讀。 try { $obj->methodA(); $obj->methodB(); $obj->methodC(); } catch (BadRouteException $e) { } catch (PDOException $e) { } catch (RuntimeException $e) { } catch (Throwable $e) { }
  48. 48. Exception 一定要處理 不要隱蔽錯誤 try { foo(); } catch (RuntimeException $e) { // No action }
  49. 49. 使用 Exception 的情境 • 通常比較少在同一個空間內同時 try ... catch 又同時 throw Exception。 • 開發 function 的人可以根據異常狀況拋出各種 Exception,幫助使用 function 的人 處理例外流程。 • 使用 function 的人可以用 try ... catch 捕獲 function 的異常,然後處理可能的錯誤 修復。 • 是否是【異常】非常重要,既定的可預期流程,都應該用 if else 處理。但是異常 的處理可以放心用 Exception 作流程跳轉。
  50. 50. 防禦型程式設計 public function __construct($string, $int, $array) { // 最基本的檢查,型別不對就丟錯 if (!is_string($string)) { throw new InvalidArgumentException('Argument 1 should be string.'); } // 這個檢查比較鬆一點,只要是數字都可以過,不一定要 int 型態 if (!is_numeric($int)) { throw new InvalidArgumentException('Argument 2 should be a number.'); } // 這個檢查比較特別,如果是 Iterator 物件也能夠接受,因為同樣可以 foreach if (!is_array($array) && !($array instanceof Traversable)) { throw new InvalidArgumentException('Argument 3 should be Traversable.'); } // Do some stuff }
  51. 51. 願各位的程式都能自動修復錯誤 --- Thank You

×