Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

Asika

May 29, 2018
Tweet

More Decks by Asika

Other Decks in Programming

Transcript

  1. 精通 PHP 錯誤處理
    讓除錯更自在
    PHP 也有 Day #35
    2018.05.29
    Simon Asika (飛鳥)

    View full-size slide

  2. 認識 PHP 的 Error

    View full-size slide

  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

    View full-size slide

  4. 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

    View full-size slide

  5. 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('%s', $result);

    View full-size slide

  6. 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;

    View full-size slide

  7. 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()
    {
    }
    }

    View full-size slide

  8. 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;

    View full-size slide

  9. 你也可以產生屬於自己的錯誤訊息
    • 用 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;

    View full-size slide

  10. 改用 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);
    }

    View full-size slide

  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);

    View full-size slide

  12. 用 @ 隱藏錯誤訊息
    • 可放在一行的最開頭,或是 function call 前面。通常用在極度不確定外部輸入格式時。
    • 將這一行的錯誤訊息隱藏,這個案例中原本會產生下面的訊息,但實際上被屏蔽了:
    Warning: substr() expects parameter 1 to be string, array given
    • 其原理是在這一行開始執行時,背景將 error_reporting 改成 0,然後下一行執行前再改
    回來。
    • 所以如果有特別寫錯誤處理器的話,依然抓的到此錯誤。
    @$a = substr([], 0, 5);
    var_dump($a);

    View full-size slide

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

    View full-size slide

  14. 不養成良好的編寫習慣,其它開發者拿到你的 code 就
    變成這樣

    View full-size slide

  15. 寫程式時要保持這種心態:就好像將來要維護你這些代碼的人是一位
    殘暴的精神病患者,而且他知道你住在哪。
    --- John Woods (1991)

    View full-size slide

  16. 自行捕獲錯誤

    View full-size slide

  17. 使用 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);

    View full-size slide

  18. • 加上一點改變,現在我們可以根據錯誤碼加上不同的提示 (這裡只抓前四個示範)
    • 本範例會印出
    警告: 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);

    View full-size slide

  19. • 但是你會發現,現在加上 @ 或關閉錯誤訊息都沒用。為內建的錯誤處理已經被我
    們強制覆蓋了。
    • 照樣印出錯誤訊息
    警告: 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); // 加上 @ 也沒用了

    View full-size slide

  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);

    View full-size slide

  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);

    View full-size slide

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

    View full-size slide

  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;
    });

    View full-size slide

  24. log 紀錄結果
    • 這只是一個簡單的範例,實際的網站開發請另外使用 Monolog 之類的套件來處理
    log檔,並記得做 rotating 免得log塞爆。
    • 有 DevOps 人員或採用 microservice 的團隊,可以考慮把 log 服務拉出去成為一台
    獨立伺服器,所有訊息都往遠端打出去,就不用擔心 log 爆量問題。
    • 主流框架大多都幫你處理好這些工作了,感謝上天,感謝 Opensource。

    View full-size slide

  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;
    });

    View full-size slide

  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;
    }

    View full-size slide

  27. 認識 Exception

    View full-size slide

  28. 如何使用 Exception
    • 任何開發者自己認為是錯誤的地方,都可以丟出 Exception 中斷程式流程。
    • 丟出 Exception 後,下方的程式就不會再執行,但此時整個程序沒有終止 (不像 Fatal Error 會直接終
    止)
    • 我們可以 catch 丟出去的 Exception,然後轉而執行其它程式或流程。
    $a = 'B';
    if ($a !== 'A') {
    throw new RuntimeException('$a is not A', 500); // 從這裡中斷執行
    }
    echo $a; // 這裡不會再執行了...

    View full-size slide

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

    View full-size slide

  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 '這裡會繼續執行';

    View full-size slide

  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 {
    // 其它錯誤
    }
    }

    View full-size slide

  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);
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. 沒有捕獲 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');

    View full-size slide

  36. 但我們一樣可以捕獲最上層 Exception
    • 還記得前面的 set_error_handler() 嗎?
    • 我們也可以用 set_exception_handler(); 來抓取拋到最外層的 Exception。
    set_exception_handler(function (Throwable $e) {
    http_response_code($e->getCode());
    echo <<

    View full-size slide

  37. 抓取到 Exception 後的結果

    View full-size slide

  38. 抓取到 Exception 後的結果
    看!很簡單吧?
    我們完成了自製的錯誤處理器

    View full-size slide

  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 <<

    View full-size slide

  40. PHP7 的
    Exceptions
    See http://asika.windspeaker.co/post/3503-php-exceptions

    View full-size slide

  41. Exception的使用觀念

    View full-size slide

  42. 例外不是錯誤
    Exception 不代表 Error,他可以是流程控制的一部份
    但必須被認定為【異常狀況】

    View full-size slide

  43. 異常狀況的處理
    • 在一個大量迴圈的任務中,我們希望即便少數的 job 失敗了,還是要繼續跑完後面
    的 jobs
    • 且我們希望每一個 Job 失敗時,會 mail 通知管理員
    • 因此在這邊,Exception 做為異常狀況處理器,會控制流程去寄送通知信,但又不
    中斷程序,使得迴圈繼續跑下去。
    foreach ($jobs as $job) {
    try {
    Queue::process($job);
    } catch (QueueException $e) {
    Mailer::send('A queue job error', $message);
    }
    }

    View full-size slide

  44. 但Exception也不是流程控制
    可以用 if else 解決的問題,就不要用 Exception

    View full-size slide

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

    View full-size slide

  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);
    }

    View full-size slide

  47. 或是這樣用
    • 將多個 function 呼叫的 Exception 做集中處理,可以讓異常處理流程更乾淨易讀。
    try {
    $obj->methodA();
    $obj->methodB();
    $obj->methodC();
    } catch (BadRouteException $e) {
    } catch (PDOException $e) {
    } catch (RuntimeException $e) {
    } catch (Throwable $e) {
    }

    View full-size slide

  48. Exception 一定要處理
    不要隱蔽錯誤
    try {
    foo();
    } catch (RuntimeException $e) {
    // No action
    }

    View full-size slide

  49. 使用 Exception 的情境
    • 通常比較少在同一個空間內同時 try ... catch 又同時 throw Exception。
    • 開發 function 的人可以根據異常狀況拋出各種 Exception,幫助使用 function 的人
    處理例外流程。
    • 使用 function 的人可以用 try ... catch 捕獲 function 的異常,然後處理可能的錯誤
    修復。
    • 是否是【異常】非常重要,既定的可預期流程,都應該用 if else 處理。但是異常
    的處理可以放心用 Exception 作流程跳轉。

    View full-size slide

  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
    }

    View full-size slide

  51. 願各位的程式都能自動修復錯誤
    --- Thank You

    View full-size slide