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

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

60ade929e3d8ffc6cebf8c584c7e478c?s=128

Asika

May 29, 2018
Tweet

Transcript

  1. 2.
  2. 4.

    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
  3. 5.

    E_ERROR • 產生以下錯誤 Fatal error: Uncaught Error: Call to undefined

    function bar() in D:\www\slim\public\index.php:7 • 因為 function 根本不存在,系統無從猜測可能的行為,也無法修復錯誤,故為 Fatal Error,強制停止程式運作。 function foo() { echo 'foo'; } bar(); // 呼叫了不存在的 function
  4. 6.

    E_WARNING • 產生以下錯誤畫面: Warning: A non-numeric value encountered in D:\www\slim\public\index.php

    on line 3 123 • 不正確的資料操作與計算,可能造成程式 BUG,但是系統可以修復其行為,所以 程式不會中斷,可以被隱藏。 $result = 123 + 'ABC'; // 數字加字串 echo sprintf('<span style="color: red;">%s</span>', $result);
  5. 7.

    E_NOTICE • 產生以下畫面 Notice: Undefined variable: b in D:\www\slim\public\index.php on

    line 6 A • $b 沒有被預先宣告,這種寫法在 PHP 中是允許的,但是這樣子有很大機率出現 BUG ,故 PHP 會用 Notice 提示你最好預先宣告。 $a = 'A'; $ab = $a . $b; // $b 不存在 echo $ab;
  6. 8.

    E_STRICT • 產生以下錯誤訊息 Warning: Declaration of B::foo() should be compatible

    with A::foo($a = 123) in D:\www\slim\public\index.php on line 17 對PHP開發過程更嚴謹的要求。 class A { public function foo($a = 123) { } } class B extends A { // 與 parent class 介面不一樣 public function foo() { } }
  7. 9.

    E_DEPRECATED • 產生以下畫面 Deprecated: Function mcrypt_create_iv() is deprecated in D:\www\slim\public\index.php

    on line 6 �Ի��eD��IN� • 若使用了已被棄用的語言功能就會發出此提示,提醒你趕快改用新功能。 error_reporting(E_ALL); // PHP 7.2 deprecated mcrypt $v = mcrypt_create_iv(16); echo $v;
  8. 10.

    你也可以產生屬於自己的錯誤訊息 • 用 trigger_error() 來立即觸發使用者定義的錯誤訊息 • 預設值是 E_USER_NOTICE,所以程式會繼續執行下去,只是跳 Notice 訊息。

    Notice: $a is not A in D:\www\slim\public\index.php on line 6 B $a = 'B'; if ($a !== 'A') { trigger_error('$a is not A', E_USER_NOTICE); } echo $a;
  9. 11.

    改用 E_USER_ERROR • 這次用的是 ERROR type,程式就會終止執行了: Fatal error: $a is

    not A in D:\www\slim\public\index.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); }
  10. 12.

    看不到 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);
  11. 13.

    用 @ 隱藏錯誤訊息 • 可放在一行的最開頭,或是 function call 前面。通常用在極度不確定外部輸入格式時。 • 將這一行的錯誤訊息隱藏,這個案例中原本會產生下面的訊息,但實際上被屏蔽了:

    Warning: substr() expects parameter 1 to be string, array given • 其原理是在這一行開始執行時,背景將 error_reporting 改成 0,然後下一行執行前再改 回來。 • 所以如果有特別寫錯誤處理器的話,依然抓的到此錯誤。 @$a = substr([], 0, 5); var_dump($a);
  12. 14.

    建議的設定 • PHP 的預設設定是 E_ALL & ~E_NOTICE ,意思是顯示所有錯誤,Notice 除外。 •

    開發過程,建議強制設成 E_ALL 或 -1,開發者應該要清空所有可能的 Warning & Notice 不要讓任何可能的 BUG 有機會出現。 • 老舊系統運作時,考慮設成 0,不要讓使用者看見大量的 Notice 訊息。 • 網站正式運作時,也可以設定成 0。但最好確保重要的錯誤有被 log 記錄下來。 • 從每個字敲出來就用最高標準對待自己的程式碼,未來的錯誤才會少。
  13. 18.

    使用 set_error_handler() 捕獲錯誤 • 會印出: 錯誤[2]: substr() expects parameter 1

    to be string, array given - 位置: D:\www\slim\public\index.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);
  14. 19.

    • 加上一點改變,現在我們可以根據錯誤碼加上不同的提示 (這裡只抓前四個示範) • 本範例會印出 警告: substr() expects parameter 1

    to be string, array given - 位置: D:\www\slim\public\index.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);
  15. 20.

    • 但是你會發現,現在加上 @ 或關閉錯誤訊息都沒用。為內建的錯誤處理已經被我 們強制覆蓋了。 • 照樣印出錯誤訊息 警告: substr() expects

    parameter 1 to be string, array given - 位置: D:\www\slim\public\index.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); // 加上 @ 也沒用了
  16. 21.

    • 之前有說過,@ 的作用就是當下即時把 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);
  17. 22.

    • 如果你希望錯誤的顯示與否與之前設定 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);
  18. 23.

    搭配 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'); // ... });
  19. 24.

    • 左邊是完整的範例 • 先組好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; });
  20. 25.

    log 紀錄結果 • 這只是一個簡單的範例,實際的網站開發請另外使用 Monolog 之類的套件來處理 log檔,並記得做 rotating 免得log塞爆。 •

    有 DevOps 人員或採用 microservice 的團隊,可以考慮把 log 服務拉出去成為一台 獨立伺服器,所有訊息都往遠端打出去,就不用擔心 log 爆量問題。 • 主流框架大多都幫你處理好這些工作了,感謝上天,感謝 Opensource。
  21. 26.

    別忘了做個美美的錯誤畫面 • 送出 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; });
  22. 27.

    可以把錯誤當作 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; }
  23. 29.

    如何使用 Exception • 任何開發者自己認為是錯誤的地方,都可以丟出 Exception 中斷程式流程。 • 丟出 Exception 後,下方的程式就不會再執行,但此時整個程序沒有終止

    (不像 Fatal Error 會直接終 止) • 我們可以 catch 丟出去的 Exception,然後轉而執行其它程式或流程。 $a = 'B'; if ($a !== 'A') { throw new RuntimeException('$a is not A', 500); // 從這裡中斷執行 } echo $a; // 這裡不會再執行了...
  24. 30.

    捕獲 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; // 這裡又可以繼續執行了
  25. 31.

    更複雜的案例 • 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 '這裡會繼續執行';
  26. 32.

    善用 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 { // 其它錯誤 } }
  27. 33.

    自定義 Exception • 你可以繼承 Exception 或 RuntimeException 建立自己的 Exception •

    拋出之後,用 catch (IDontFeelSoGood $e) 就可以針對這個 Exception 做自訂 義錯誤處理。 class IDontFeelSoGoodException extends Exception { } if (count($infinityGems) === 6) { throw new IDontFeelSoGoodException('Tony I\'m sorry', 404); }
  28. 34.

    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'); }
  29. 35.

    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(); }
  30. 36.

    沒有捕獲 Exception 的結果 • 會顯示 Uncaught Exception 然後終止程序 Fatal error:

    Uncaught RuntimeException: $a is not A in D:\www\slim\public\index.php:5 Stack trace: #0 D:\www\slim\public\index.php(11): foo('B') #1 {main} thrown in D:\www\slim\public\index.php on line 5 function foo($a) { if ($a !== 'A') { throw new RuntimeException('$a is not A'); } echo $a; } foo('B');
  31. 37.

    但我們一樣可以捕獲最上層 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);
  32. 40.

    把雙劍客合起來用 • 前面提到 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; });
  33. 44.

    異常狀況的處理 • 在一個大量迴圈的任務中,我們希望即便少數的 job 失敗了,還是要繼續跑完後面 的 jobs • 且我們希望每一個 Job

    失敗時,會 mail 通知管理員 • 因此在這邊,Exception 做為異常狀況處理器,會控制流程去寄送通知信,但又不 中斷程序,使得迴圈繼續跑下去。 foreach ($jobs as $job) { try { Queue::process($job); } catch (QueueException $e) { Mailer::send('A queue job error', $message); } }
  34. 46.

    不要這樣用 • 如果你只是想讓未登入 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'); }
  35. 47.

    但可以這樣用 • 這個案例中,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); }
  36. 48.

    或是這樣用 • 將多個 function 呼叫的 Exception 做集中處理,可以讓異常處理流程更乾淨易讀。 try { $obj->methodA();

    $obj->methodB(); $obj->methodC(); } catch (BadRouteException $e) { } catch (PDOException $e) { } catch (RuntimeException $e) { } catch (Throwable $e) { }
  37. 50.

    使用 Exception 的情境 • 通常比較少在同一個空間內同時 try ... catch 又同時 throw

    Exception。 • 開發 function 的人可以根據異常狀況拋出各種 Exception,幫助使用 function 的人 處理例外流程。 • 使用 function 的人可以用 try ... catch 捕獲 function 的異常,然後處理可能的錯誤 修復。 • 是否是【異常】非常重要,既定的可預期流程,都應該用 if else 處理。但是異常 的處理可以放心用 Exception 作流程跳轉。
  38. 51.

    防禦型程式設計 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 }