Page 1
ここで差がつくエラー処理
By Error Handling your application will be completed
ެ։༻શ൛
PHP Conference Japan 2017, 8th Oct
Kamata Tokyo, PiO #phpcon2017
公開日:
by USAMI Kenta@tadsan
に東京都大田区蒲田の大田区産業プラザ PiOで開催された『PHPカンファレンス2017』でレギュラーセッション(25分)として発表しました。
By Error Handling your application will be completed
PHP Conference Japan 2017, 8th Oct
Kamata Tokyo, PiO #phpcon2017
2017年10月8日に開催されたPHPカンファレンスで発表した「ここで差がつくエラー処理」の発表資料を補訂した完全版です
We are hiring!
はじめに
みなさん困ってませんか
こんな画面を見せてしまってませんか
本番サービスで見せちゃってませんか
HTML 出力途中で落ちたりしませんか
エラーになってもWebサイトの体裁はなんとか保ちたい……ですよね?
画面はpixivの実在のバグではなく故意に例外を発生させたものです
アジェンダ
1. 例外・エラーの種類2. 本番環境でのエラー処理3. 開発環境でのエラー処理
具体的にはこんな話をします
エラーと例外の基本概念
それらを制御するやりかた
開発環境でうんざりしない方法
http://php.net/language.operators.errorcontrol
http://php.net/set_error_handler
http://php.net/set_exception_handler
http://php.net/register_shutdown_function
http://php.net/ErrorException
http://php.net/display-errors
やらない話
エラー監視
例外設計
そもそもエラーって何だ
PHPのコードの試しかた
php -r 'var_dump(1 + []);'
REPL (PsySH または php -a)
https://3v4l.org/
多くのバージョンで串刺し実行
エラー・例外とは
プログラムが正常ではないときに、それを通知する仕組み
PHPではエラーと例外がある
PHP5では処理系は基本的にエラー
PHP7で例外を投げることが増えた
1. 例外・エラーの種類2. 本番環境でのエラー処理3. 開発環境でのエラー処理
エラー
エラー
従来のPHPで異常状態を通知するための仕組み
関数呼び出し、未定義変数の参照結構いろんなところで発生する
PHP7で種類が整理された
主要なエラー
Notice よくない処理の通知
Warning, Error
実行時の警告・エラー
Deprecated, Strict
非互換/廃止予定の機能
Fatal Error 致命的エラー
Notice
配列の未定義位置から値を取り出そうとしたときなどに発生する
信号無視みたいな軽犯罪
ただしバグの温床になるので、無視せずきちんと対処を強く推奨
echo $i; // 存在しない変数
echo $_GET['id']; // 存在しないインデクス
echo $o->abc; // 存在しないプロパティ
Warning, Error
何かが「間違ってる」ときに発生
デフォルトの挙動ではWarningでは停止せずに進む
一貫性がないが、どちらも異常状態なので無視せず直した方がよい
// 存在しないファイルを開く
$f = fopen("/hoge.txt", "r");
foo($i); // 引数が足りない、PHP5のみ
// PHP7ではArgumentErrorに変更
function foo($i, $j) { echo $i, $j; }
Deprecated, Strict
廃止予定や互換性の怪しい機能
例: split() 関数 (explode()とは別物)
5.3でdeprecated、7.0で削除
Strictは7.0で廃止(再分類)された
https://wiki.php.net/rfc/reclassify_e_strict
class Hoge {/** 古いスタイルのコンストラクタ */
function Hoge() {}}
//** PHP5.3でDeprecatedエラー、7で削除
$a = split(',', '1,2,3');// エラー抑制演算子、通称なると
// エラーを握り潰せるべんりで危険なやつ
$a = @split(',', '1,2,3');
Fatal Error (致命的エラー)
これ以上は続行できないエラー
同名のクラスや関数を多重定義しようとしたとき
ファイルをrequireできなかったとき
明らかに不正な式
// PHP5系とPHP7系でエラーになる
// タイミングが異なるコード
$i = mt_rand(1, 2);echo $i;if ($i === 1) {// 数値と配列は加算できない
$x = 1 + [];}
// PHP5系とPHP7系でエラーになる
// タイミングが異なるコード
PHP7: 何も出力されずFatal
それ以外: 「1」とFatalまたは「2」のみ
$i = mt_rand(1, 2);echo $i;if ($i === 1) {$x = 1 + [];}
PHP7は賢いので、実際に実行されることがなくても(絶対実行されないifの中だろうと)コンパイル時に検出してくれる
例外
例外 (Exception, Throwable)
PHP5で追加された言語機能
異常状態のときにthrowで投げる
たいいき(Non-local)
大域脱出と呼ばれる機能
catch文で捕捉できる
例外オブジェクト
PHP5ではExceptionまたは、Exceptionを継承したクラスPHP7ではThrowableインターフェイスと、Exceptionを継承しないErrorとその派生クラスが追加便宜上まとめて「例外」と呼ぶ
ErrorとエラーとE_ERROR
PHP5系でエラーが発生したものが、7系では例外(Errorクラス)をthrowするように徐々に移行すべての移行されたわけではなく、まだエラーと例外の概念を両方知っておく必要がある
E_ERRORレベルのエラー…ややこしい
エラーハンドリング
// MySQLに接続失敗したら異常終了$db = mysql_connect() or die("接続に失敗しました");
せめて例外に
if (do_something() === false) {throw new FooException('do_something() が失敗した');}
現在実行中の処理を中断して、try…catch まで遡る
catchできなかったら、そこでおとなしく野垂れ死ぬしかない…?
ただちに終了されないので、まだ挽回の余地がある
エラーハンドリング
発生したエラーをなんとかする
エラーハンドリング
発生したエラーをなんとかする
A: どうにか続行を試みる
B: どうにもならないので諦める
エラーハンドリング
発生したエラーをなんとかする
A: どうにか続行を試みる
B: どうにもならないので諦める
せめてログだけでも…
なんとか共通ナビゲーションとロゴだけは表示してほしい…
我々に三回のチャンスがある
set_error_handler()
set_exception_handler()
register_shutdown_function()
error_reporting()
「出力するエラーの種類を設定する」
どのエラーレベルに対応するかビットフラグで設定する
引数なしで呼ぶと現在の値を返す
error_reporting(E_ALL & ~E_NOTICE)
// 無視できるもの全部無視error_reporting(0);// 全部のエラーを捕捉する
error_reporting(E_ALL);error_reporting(E_ALL | E_STRICT); // PHP 5.3以下// Noticeのみ無視
error_reporting(E_ALL & ~E_NOTICE);// NoticeとDeprecatedを無視error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
set_error_handler()
エラーが発生したときに、set_error_hanlder() に登録した関数が呼ばれる
エラーに応じて処理を続行させることもできるし終了させてもいい
Fatal errorは捕捉できない!
set_exception_handler()
例外がtry-catchでも捕捉されなかったときに、set_exception_hanlder() に登録した関数が呼ばれる
関数が実行された後は停止する
登録できる関数はひとつだけ
register_shutdown_function()
PHPスクリプト終了時に必ず呼ばれる関数を登録する
set_error_handler()に来ないFatal Error(致命的エラー)を捕捉してロギングできるチャンス
最後のエメラルドスプラッシュ
定石
エラー処理の定石
デフォルトのエラー出力を切る
エラーは例外に変換して、未捕捉の例外とまとめて処理する
処理すべきエラーかどうかの判定にerror_reporting()を利用する
デフォルトのエラー出力
// 必ず読まれるPHPファイルで設定する
// 少なくとも本番環境では絶対offにする
ini_set('display_errors', 'off');
set_exception_handler() 設定方法
// 例外をハンドリング
set_exception_handler(function ($e){// 例外をログに記録
YourApp::logException($e);// エラー時の画面を表示
YourApp::displayErrorPage($e);});
エラー抑制とerror_reporting
@(エラー抑制演算子)は一時的にerror_reporting(0)に設定して処理を実行するだけ独自設定したエラーハンドラがerror_reporting()を考慮してくれないと、エラーが握り潰されない
set_error_handler('my_error_handler');function my_error_handler ($level, $message, $file = null, $line = null) {// エラーレベルのAND(ビット積)をとる
if (error_reporting() & $level > 0) {throw new ErrorException($message, 0, $level, $file, $line);}}
error_reporting()
error_reporting()で設定したエラーレベルはデフォルトのエラーハンドラで利用される自作のエラーハンドラを設定するときは、error_reporting()を考慮して実装しなければならない
実際グローバル変数みたいなもの
register_shutdown_function(function(){if (empty($error = error_get_last())) return;// エラーハンドラーで捕捉できないエラーを判定
$fatal = E_ERROR | E_PARSE | E_COMPILE_WARNING
| E_CORE_ERROR|E_CORE_WARNING|E_COMPILE_ERROR;if (($error['level'] & $fatal_errors) > 0) {$e = new ErrorException($error['message'], 0,$error['type'], $error['file'], $error['line']);my_error_handler($e);}
});
例外のロギング
PHPの標準機能error_log()は、ちょっと弱い
rollbar, faultlineなどに送るpixivの場合は例外情報をバックトレースも含めてJSONエンコードしてファイルに追記、Fluentdで収集
http://k1low.hatenablog.com/entry/2017/06/13/083000
開発時の原則
error_reporting()
可能な限り無視しない
例: 本番ではDeprecatedを無視
ただしプロジェクト全体にNotice発生箇所が多い場合は無理しない
一気にリファクタリングしようとしてエンバグする方がずっと怖い
エラー設定は初期化処理で実行する
error_reportingはphp.iniでも設定可能だが、PHPで明示的に設定すれば環境依存がなくなる
深いところで仕方なく変更する必要に迫られたときは、小さなスコープで変更してすぐに戻す
段階的にエラーレベルを上げる
pixivの開発時にもまだNoticeが発生する箇所が残ってる…
やむを得ず変更する際は浅い位置(例:コントローラ)で設定する
Deprecatedはユニットテストで追い詰めていく
// 全体設定
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
// ユニットテスト
error_reporting(E_ALL);
// 新規ファイルや動作確認できたページ
// (コントローラー)単位でレベルを上げる
setErrorReportingAll();
/*** 開発時のみエラーレベルを最大に上げる
** 本番(運用環境)には影響しない*/
function setErrorReportingAll() {if (is_development()) {error_reporting(E_ALL);}}
/** mcryptは7.1で非推奨、7.2で削除予定 */function mcrypt_wrapper($text) {$err = error_reporting();error_reporting(E_ALL & ~E_DEPRECATED);try {// mcryptを使った処理
} finally {error_reporting($err);}return $result;}
/** なんかエラー吐きまくる古いSDKのラッパー */
function hoge_legacy_wrapper($text) {$err = error_reporting();error_reporting(E_ALL &~E_NOTICE & ~E_DEPRECATED);try {$result = \Hoge\Legacy\SDK::proc();} finally {error_reporting($err);}return $result;}
whoops!
whoops!“PHP errors for cool kids”
エラー処理用フレームワーク
set_exception_handler()はひとつしかハンドラを登録できないが、whoopsは複数登録できる
whoops! ハンドラーハンドラー(関数)は複数登録できるスタック (FIFO, 後入れ先出し)
ロギング・デバッグだけでなくエラー画面の表示にも利用できる
WEB+DB Press Vol.96サンプルにwhoopsを使った設定例書きました
http://gihyo.jp/magazine/wdpress/archive/2017/vol96
whoops! 注意点PrettyPageHandlerはべんりだがもし本番環境で有効化されてしまうと脆弱性や情報漏洩の原因にすらなりかねない
設定に自信がなければ、単に開発環境用のカッコイイエラー画面として割り切るのが安全
// 開発時のみWhoopsを有効化する場合
if (is_development()) {$handler = (PHP_SAPI === 'cli')? new \Whoops\Handler\PlainTextHandler: new \Whoops\Handler\PrettyPageHandler;$whoops = new \Whoops\Run;$whoops->register();}
落穂
エラーレベルとビットフラグ
エラー発生時のレベルとerror_reporting()はビットフラグ
E_ALLは全種類のエラーのフラグを立てた状態
E_ALL & ~E_NOTICEのように反転してビット積をとるのが簡単
printf('%015b', E_ALL);// => "111111111111111"printf('%015b', E_DEPRECATED);// => "010000000000000"printf('%015b', E_ALL & ~E_DEPRECATED);// => "101111111111111"
trigger_error()
エラーを意図的に発生させる
ただし全てのエラーを発生させられるわけではなくE_USER_系のみ
今日エラーはあまり考慮されてないので、役立たないかも…
USERの有無は別のエラーなので
// 関数をリネームしたいとき/*** @deprecated {@see grant()} を使ってください*/function credential(array $data){$msg = 'credential() is obsolete function. Use grant() instead.';trigger_error($msg, E_USER_DEPRECATED);return grant($data);}
もうひとつのエラーハンドリング
set_exception_handler()を使ったハンドリングは…実際(説明)難しい細かなエラーハンドリングはtry-catchを使った方がやりやすい
エラーの仕組み(このセッションで説明した内容!)よりは把握しやすい
https://gist.github.com/zonuexe/577d089815e241d5da26
pixivの場合 (AppRunnerパターン)
アプリケーション(PC版, スマートフォン版,etc...)ごとにAppRunner(社内用語)と呼ばれるクラスを定義する例外の種類ごとにcatchして、適切な処理(ロギング・エラー表示)などをする
http://inside.pixiv.net/entry/2015/12/18/210721
pixivの場合 (なぜこのパターンか)
例外にも種類がある
アプリケーションのバグ
クエリパラメータの異常入力
それらのアプリケーションの事情に沿ってロギングする
参考資料
http://php.net/language.operators.errorcontrol
http://php.net/set_error_handler
http://php.net/set_exception_handler
http://php.net/register_shutdown_function
http://php.net/ErrorException
http://php.net/display-errors
PHPのエラーと例外再入門by Hiraku NAKANO
https://speakerdeck.com/hirak/php-error-and-exception
faultline/faultline-phpによるサーバ管理不要なエラートラッキングとその効果 by Ken'ichiro Oyama
http://k1low.hatenablog.com/entry/2017/06/13/083000
参考資料 (pixiv AppRunner)
(ピクシブ株式会社 Advent Calendar 2015)
健全なpixivは健康なPHPに宿る〜モダンPHPを保つ7つの鍵
http://inside.pixiv.net/entry/2015/12/18/210721
ソースコード (サンプル)
https://gist.github.com/zonuexe/577d089815e241d5da26
最後に
メンバー全員が知るべき?
知っておくとためにはなる…かも
でも完全に理解してないといけないわけではないとは言っても社内で誰かが知っておかないと困ることではある
「Noticeを潰せ」だけ周知したい
Webサービスの開発基盤を設計したいひとも
創作文化のための開発に興味があるひとも
今回の内容がいまいちぴんと来なくても大丈夫
PHP, JavaScript, Ruby,Scala, Go, データ分析,インフラ, その他…
まずは一度オフィスに遊びに来ませんか
ヾ(〃><)ノ゙☆
Twitter @tadsan宛またはrecruit.pixiv.net
_\ヽ, ,、`''|/ノ.|_ |\`ヽ、|\, V`L,,_|ヽ、).|/ ,、/ ヽYノ.| r''ヽ、.|| `ー-ヽ|ヮ| `|ヽ, ,r .|ヽ,r'''ヽ!'-‐'''''ヽ、ノ,,,..---r'",r, , 、`ヽ、 ヾヽ、__/ ./ハレハ i`ヽ、 `''r`ミ_.レ//r,,,、 レ'レハヾ, L,,_ `ヽ、"レ, l;;;l l;;;l`i.リレ' リ ̄~~ヽ、 ワ `"/-'`'`'`''''''''" ┼ヽ -|r‐、. レ |d⌒) ./| _ノ __ノ
