Page 1
ここで差がつくエラー処理
By Error Handling your application will be completed
公開用完全版
, 8th Oct
PHP Conference Japan 2017
#phpcon2017
Kamata Tokyo, PiO
公開日:
by USAMI Kenta @tadsan
ここで差がつくエラー処理
By Error Handling your application will be completed
公開用完全版
, 8th Oct
PHP Conference Japan 2017
#phpcon2017
Kamata Tokyo, PiO
2017年10月8日に開催された
PHPカンファレンスで発表した
「ここで差がつくエラー処理」の
完全版
発表資料を補訂した です
お前誰よ
うさみけんた (@tadsan) / Zonu.EXE
We are hiring!
はじめに
みなさん困ってませんか
こんな画面を見せて
しまってませんか
本番サービスで
見せちゃってませんか
HTML 出力途中で
落ちたりしませんか
エラーになっても
Webサイトの体裁は
なんとか保ちたい
……ですよね?
画面はpixivの実在のバグではなく 故意に例外を発生させたものです
アジェンダ
1. 例外・エラーの種類
2. 本番環境でのエラー処理 3. 開発環境でのエラー処理
具体的にはこんな話をします
エラーと例外の基本概念
それらを制御するやりかた
開発環境でうんざりしない方法
参考資料
だいたいPHPマニュアルに載ってる
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
whoops!
https://github.com/filp/whoops
やらない話
エラー監視
例外設計
そもそもエラーって何だ
PHPのコードの試しかた
php -r 'var_dump(1 + []);'
php -a
REPL (PsySH または )
https://3v4l.org/
多くのバージョンで串刺し実行
エラー・例外とは
プログラムが正常ではないときに、
それを通知する仕組み
PHPではエラーと例外がある
PHP5では処理系は基本的にエラー
PHP7で例外を投げることが増えた
1. 例外・エラーの種類
2. 本番環境でのエラー処理 3. 開発環境でのエラー処理
エラー
エラー
従来のPHPで異常状態を通知する
ための仕組み
関数呼び出し 未定義変数の参照
、
結構いろんなところで発生する
PHP で種類が整理された
主要なエラー
Notice
よくない処理の通知
Warning, Error
実行時の警告・エラー
Deprecated, Strict
非互換/廃止予定の機能
Fatal Error
致命的エラー
Notice
配列の未定義位置から値を取り出 そうとしたときなどに発生する
信号無視みたいな軽犯罪
ただしバグの温床になるので、 無視せずきちんと対処を強く推奨
Noticeの例 (一部)
echo $i; //
存在しない変数
echo $_GET['id']; //
存在しないインデクス
echo $o->abc; //
存在しないプロパティ
Warning, Error
何かが「間違ってる」ときに発生
デフォルトの挙動ではWarningで
は停止せずに進む
一貫性がないが、どちらも異常状 態なので無視せず直した方がよい
Warning, Errorの例(一部)
//
存在しないファイルを開く
$f = fopen("/hoge.txt", "r");
foo($i); //
引数が足りない、PHP5のみ
// ArgumentError
PHP7では に変更
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
Deprecated, Strictの例(一部)
class Hoge {
/** */
古いスタイルのコンストラクタ
function Hoge() {
}
}
Deprecatedの例(一部)
//**
PHP5.3でDeprecatedエラー、7で削除
$a = split(',', '1,2,3');
//
エラー抑制演算子、通称なると
// エラーを握り潰せるべんりで危険なやつ
$a = @split(',', '1,2,3');
Fatal Error (致命的エラー)
これ以上は続行できないエラー
同名のクラスや関数を
多重定義しようとしたとき
require
ファイルを できなかったとき
明らかに不正な式
Fatal Errorの例(一部)
//
PHP5系とPHP7系でエラーになる
//
タイミングが異なるコード
$i = mt_rand(1, 2);
echo $i;
if ($i === 1) {
//
数値と配列は加算できない
$x = 1 + [];
}
Fatal Errorの例(一部)
//
PHP5系とPHP7系でエラーになる
//
タイミングが異なるコード
$i = mt_rand(1, 2);
echo $i;
: 何も出力されずFatal
PHP7
if ($i === 1) {
: 「1」とFatal
それ以外
または「2」のみ
$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("接続に失敗しまし
た");
die() は exit() と同じ意味
ここでいきなり強制終了する
そのあと処理はほとんどできない
せめて例外に
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() 設定方法
//
無視できるもの全部無視
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() 設定方法
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() 設定方法
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はユニットテストで
追い詰めていく
pixivの設定
//
全体設定
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
/** */
なんかエラー吐きまくる古い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の有効化
//
開発時のみ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の有無は別のエラーなので
trigger_error利用例
//
関数をリネームしたいとき
/**
* @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版, スマートフォン版,
ごとにAppRunner と
etc...) (社内用語)
呼ばれるクラスを定義する
例外の種類ごとにcatchして、
適切な処理(ロギング・エラー表示)
などをする
http://inside.pixiv.net/entry/2015/12/18/210721
pixivの場合 (なぜこのパターンか)
例外にも種類がある
アプリケーションのバグ
クエリパラメータの異常入力
それらのアプリケーションの 事情に沿ってロギングする
参考資料
参考資料
だいたいPHPマニュアルに載ってる
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
whoops!
https://github.com/filp/whoops
参考 資料
(にすると良い)
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
使用フォント
_
\ヽ, ,、
セプテンバーM・L
`''|/ノ .| _ |
Migu 1M
\`ヽ、| \, V
Rounded M+ 2p
`L,,_ |ヽ、)
.|
/ ,、
/ ヽYノ .| r''ヽ、.| | `ー-ヽ|ヮ | `| ヽ, ,r .| ヽ,r'''ヽ!'-‐'''''ヽ、ノ ,,,..---r'",r, , 、`ヽ、 ヾ
ヽ、__/ ./ハレハ i`ヽ、 `''r`ミ_
.レ//r,,,、 レ'レハヾ, L,,_ `ヽ、 "レ, l;;;l l;;;l`i.リレ' リ ̄~~
ヽ、 ワ `"/-'`'`'
`''''''''" ┼ヽ -|r‐、. レ | d⌒) ./| _ノ __ノ