Skip to content

PHP ここで差がつくエラー処理 完全版

公開日:

東京都大田区蒲田大田区産業プラザ PiOで開催された『PHPカンファレンス2017』でレギュラーセッション(25分)として発表しました。

Download PDF

スライドテキスト

Page 1

ここで差がつくエラー処理

By Error Handling your application will be completed

ެ։༻׬શ൛

PHP Conference Japan 2017, 8th Oct

Kamata Tokyo, PiO #phpcon2017

Page 2

2017年10月8日に開催されたPHPカンファレンスで発表した「ここで差がつくエラー処理」の発表資料を補訂した完全版です

Page 3

お前誰よ

  • うさみけんた (@tadsan) / Zonu.EXE
  • GitHub/Packagistでは id: zonuexe
  • ピクシブ株式会社技術基盤チーム (pixiv.net)
  • Emacs Lisper, PHPer
  • 入社前は自宅警備をしながらRuby書いてた
  • Emacs PHP Modeのメンテナ引き継ぎました
  • Qiitaに記事を書いたり変なコメントしてるよ

Page 4

Page 5

We are hiring!

Page 6

はじめに

Page 7

みなさん困ってませんか

Page 8

Page 9

こんな画面を見せてしまってませんか

Page 10

Page 11

本番サービスで見せちゃってませんか

Page 12

Page 13

HTML 出力途中で落ちたりしませんか

Page 14

Page 15

エラーになってもWebサイトの体裁はなんとか保ちたい……ですよね?

Page 16

画面はpixivの実在のバグではなく故意に例外を発生させたものです

Page 17

アジェンダ

Page 18

1. 例外・エラーの種類2. 本番環境でのエラー処理3. 開発環境でのエラー処理

Page 19

具体的にはこんな話をします

エラーと例外の基本概念

それらを制御するやりかた

開発環境でうんざりしない方法

Page 20

参考資料

だいたい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

Page 21

やらない話

エラー監視

例外設計

そもそもエラーって何だ

Page 22

PHPのコードの試しかた

php -r 'var_dump(1 + []);'

REPL (PsySH または php -a)

https://3v4l.org/

多くのバージョンで串刺し実行

Page 23

エラー・例外とは

プログラムが正常ではないときに、それを通知する仕組み

PHPではエラーと例外がある

PHP5では処理系は基本的にエラー

PHP7で例外を投げることが増えた

Page 24

1. 例外・エラーの種類2. 本番環境でのエラー処理3. 開発環境でのエラー処理

Page 25

エラー

Page 26

エラー

従来のPHPで異常状態を通知するための仕組み

関数呼び出し、未定義変数の参照結構いろんなところで発生する

PHP7で種類が整理された

Page 27

主要なエラー

Notice よくない処理の通知

Warning, Error

実行時の警告・エラー

Deprecated, Strict

非互換/廃止予定の機能

Fatal Error 致命的エラー

Page 28

Notice

配列の未定義位置から値を取り出そうとしたときなどに発生する

信号無視みたいな軽犯罪

ただしバグの温床になるので、無視せずきちんと対処を強く推奨

Page 29

Noticeの例 (一部)

echo $i; // 存在しない変数

echo $_GET['id']; // 存在しないインデクス

echo $o->abc; // 存在しないプロパティ

Page 30

Warning, Error

何かが「間違ってる」ときに発生

デフォルトの挙動ではWarningでは停止せずに進む

一貫性がないが、どちらも異常状態なので無視せず直した方がよい

Page 31

Warning, Errorの例(一部)

// 存在しないファイルを開く

$f = fopen("/hoge.txt", "r");

foo($i); // 引数が足りない、PHP5のみ

// PHP7ではArgumentErrorに変更

function foo($i, $j) { echo $i, $j; }

Page 32

Deprecated, Strict

廃止予定や互換性の怪しい機能

例: split() 関数 (explode()とは別物)

5.3でdeprecated、7.0で削除

Strictは7.0で廃止(再分類)された

https://wiki.php.net/rfc/reclassify_e_strict

Page 33

Deprecated, Strictの例(一部)

class Hoge {/** 古いスタイルのコンストラクタ */

function Hoge() {}}

Page 34

Deprecatedの例(一部)

//** PHP5.3でDeprecatedエラー、7で削除

$a = split(',', '1,2,3');// エラー抑制演算子、通称なると

// エラーを握り潰せるべんりで危険なやつ

$a = @split(',', '1,2,3');

Page 35

Fatal Error (致命的エラー)

これ以上は続行できないエラー

同名のクラスや関数を多重定義しようとしたとき

ファイルをrequireできなかったとき

明らかに不正な式

Page 36

Fatal Errorの例(一部)

// PHP5系とPHP7系でエラーになる

// タイミングが異なるコード

$i = mt_rand(1, 2);echo $i;if ($i === 1) {// 数値と配列は加算できない

$x = 1 + [];}

Page 37

Fatal Errorの例(一部)

// PHP5系とPHP7系でエラーになる

// タイミングが異なるコード

PHP7: 何も出力されずFatal

それ以外: 「1」とFatalまたは「2」のみ

$i = mt_rand(1, 2);echo $i;if ($i === 1) {$x = 1 + [];}

Page 38

PHP7は賢いので、実際に実行されることがなくても(絶対実行されないifの中だろうと)コンパイル時に検出してくれる

Page 39

例外

Page 40

例外 (Exception, Throwable)

PHP5で追加された言語機能

異常状態のときにthrowで投げる

たいいき(Non-local)

大域脱出と呼ばれる機能

catch文で捕捉できる

Page 41

例外オブジェクト

PHP5ではExceptionまたは、Exceptionを継承したクラスPHP7ではThrowableインターフェイスと、Exceptionを継承しないErrorとその派生クラスが追加便宜上まとめて「例外」と呼ぶ

Page 42

ErrorとエラーとE_ERROR

PHP5系でエラーが発生したものが、7系では例外(Errorクラス)をthrowするように徐々に移行すべての移行されたわけではなく、まだエラーと例外の概念を両方知っておく必要がある

E_ERRORレベルのエラー…ややこしい

Page 43

エラーハンドリング

Page 44

伝統的なエラー処理手法

// MySQLに接続失敗したら異常終了$db = mysql_connect() or die("接続に失敗しました");

die() は exit() と同じ意味

ここでいきなり強制終了する

そのあと処理はほとんどできない

Page 45

せめて例外に

if (do_something() === false) {throw new FooException('do_something() が失敗した');}

現在実行中の処理を中断して、try…catch まで遡る

catchできなかったら、そこでおとなしく野垂れ死ぬしかない…?

ただちに終了されないので、まだ挽回の余地がある

Page 46

エラーハンドリング

発生したエラーをなんとかする

Page 47

エラーハンドリング

発生したエラーをなんとかする

A: どうにか続行を試みる

B: どうにもならないので諦める

Page 48

エラーハンドリング

発生したエラーをなんとかする

A: どうにか続行を試みる

B: どうにもならないので諦める

せめてログだけでも…

なんとか共通ナビゲーションとロゴだけは表示してほしい…

Page 49

我々に三回のチャンスがある

set_error_handler()

set_exception_handler()

register_shutdown_function()

Page 50

error_reporting()

「出力するエラーの種類を設定する」

どのエラーレベルに対応するかビットフラグで設定する

引数なしで呼ぶと現在の値を返す

error_reporting(E_ALL & ~E_NOTICE)

Page 51

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

Page 52

set_error_handler()

エラーが発生したときに、set_error_hanlder() に登録した関数が呼ばれる

エラーに応じて処理を続行させることもできるし終了させてもいい

Fatal errorは捕捉できない!

Page 53

set_exception_handler()

例外がtry-catchでも捕捉されなかったときに、set_exception_hanlder() に登録した関数が呼ばれる

関数が実行された後は停止する

登録できる関数はひとつだけ

Page 54

register_shutdown_function()

PHPスクリプト終了時に必ず呼ばれる関数を登録する

set_error_handler()に来ないFatal Error(致命的エラー)を捕捉してロギングできるチャンス

最後のエメラルドスプラッシュ

Page 55

定石

Page 56

エラー処理の定石

デフォルトのエラー出力を切る

エラーは例外に変換して、未捕捉の例外とまとめて処理する

処理すべきエラーかどうかの判定にerror_reporting()を利用する

Page 57

デフォルトのエラー出力

Page 58

デフォルトのエラー出力

// 必ず読まれるPHPファイルで設定する

// 少なくとも本番環境では絶対offにする

ini_set('display_errors', 'off');

Page 59

set_exception_handler() 設定方法

// 例外をハンドリング

set_exception_handler(function ($e){// 例外をログに記録

YourApp::logException($e);// エラー時の画面を表示

YourApp::displayErrorPage($e);});

Page 60

エラー抑制とerror_reporting

@(エラー抑制演算子)は一時的にerror_reporting(0)に設定して処理を実行するだけ独自設定したエラーハンドラがerror_reporting()を考慮してくれないと、エラーが握り潰されない

Page 61

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

Page 62

error_reporting()

error_reporting()で設定したエラーレベルはデフォルトのエラーハンドラで利用される自作のエラーハンドラを設定するときは、error_reporting()を考慮して実装しなければならない

実際グローバル変数みたいなもの

Page 63

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

});

Page 64

例外のロギング

PHPの標準機能error_log()は、ちょっと弱い

rollbar, faultlineなどに送るpixivの場合は例外情報をバックトレースも含めてJSONエンコードしてファイルに追記、Fluentdで収集

Page 65

http://k1low.hatenablog.com/entry/2017/06/13/083000

Page 66

開発時の原則

Page 67

error_reporting()

可能な限り無視しない

例: 本番ではDeprecatedを無視

ただしプロジェクト全体にNotice発生箇所が多い場合は無理しない

一気にリファクタリングしようとしてエンバグする方がずっと怖い

Page 68

エラー設定は初期化処理で実行する

error_reportingはphp.iniでも設定可能だが、PHPで明示的に設定すれば環境依存がなくなる

深いところで仕方なく変更する必要に迫られたときは、小さなスコープで変更してすぐに戻す

Page 69

段階的にエラーレベルを上げる

pixivの開発時にもまだNoticeが発生する箇所が残ってる…

やむを得ず変更する際は浅い位置(例:コントローラ)で設定する

Deprecatedはユニットテストで追い詰めていく

Page 70

pixivの設定

// 全体設定

error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);

// ユニットテスト

error_reporting(E_ALL);

// 新規ファイルや動作確認できたページ

// (コントローラー)単位でレベルを上げる

setErrorReportingAll();

Page 71

べんりヘルパー関数

/*** 開発時のみエラーレベルを最大に上げる

** 本番(運用環境)には影響しない*/

function setErrorReportingAll() {if (is_development()) {error_reporting(E_ALL);}}

Page 72

深いところで一時的にエラーレベルを変更

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

Page 73

手を出せないライブラリ/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;}

Page 74

whoops!

Page 75

whoops!“PHP errors for cool kids”

エラー処理用フレームワーク

set_exception_handler()はひとつしかハンドラを登録できないが、whoopsは複数登録できる

Page 76

Page 77

whoops! ハンドラーハンドラー(関数)は複数登録できるスタック (FIFO, 後入れ先出し)

ロギング・デバッグだけでなくエラー画面の表示にも利用できる

WEB+DB Press Vol.96サンプルにwhoopsを使った設定例書きました

http://gihyo.jp/magazine/wdpress/archive/2017/vol96

Page 78

whoops! 注意点PrettyPageHandlerはべんりだがもし本番環境で有効化されてしまうと脆弱性や情報漏洩の原因にすらなりかねない

設定に自信がなければ、単に開発環境用のカッコイイエラー画面として割り切るのが安全

Page 79

whoopsの有効化

// 開発時のみWhoopsを有効化する場合

if (is_development()) {$handler = (PHP_SAPI === 'cli')? new \Whoops\Handler\PlainTextHandler: new \Whoops\Handler\PrettyPageHandler;$whoops = new \Whoops\Run;$whoops->register();}

Page 80

落穂

Page 81

エラーレベルとビットフラグ

エラー発生時のレベルとerror_reporting()はビットフラグ

E_ALLは全種類のエラーのフラグを立てた状態

E_ALL & ~E_NOTICEのように反転してビット積をとるのが簡単

Page 82

ビットフラグの確認方法

printf('%015b', E_ALL);// => "111111111111111"printf('%015b', E_DEPRECATED);// => "010000000000000"printf('%015b', E_ALL & ~E_DEPRECATED);// => "101111111111111"

Page 83

trigger_error()

エラーを意図的に発生させる

ただし全てのエラーを発生させられるわけではなくE_USER_系のみ

今日エラーはあまり考慮されてないので、役立たないかも…

USERの有無は別のエラーなので

Page 84

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

Page 85

もうひとつのエラーハンドリング

set_exception_handler()を使ったハンドリングは…実際(説明)難しい細かなエラーハンドリングはtry-catchを使った方がやりやすい

エラーの仕組み(このセッションで説明した内容!)よりは把握しやすい

https://gist.github.com/zonuexe/577d089815e241d5da26

Page 86

pixivの場合 (AppRunnerパターン)

アプリケーション(PC版, スマートフォン版,etc...)ごとにAppRunner(社内用語)と呼ばれるクラスを定義する例外の種類ごとにcatchして、適切な処理(ロギング・エラー表示)などをする

http://inside.pixiv.net/entry/2015/12/18/210721

Page 87

pixivの場合 (なぜこのパターンか)

例外にも種類がある

アプリケーションのバグ

クエリパラメータの異常入力

それらのアプリケーションの事情に沿ってロギングする

Page 88

参考資料

Page 89

参考資料

だいたい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

Page 90

参考(にすると良い)資料

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

Page 91

参考資料 (pixiv AppRunner)

(ピクシブ株式会社 Advent Calendar 2015)

健全なpixivは健康なPHPに宿る〜モダンPHPを保つ7つの鍵

http://inside.pixiv.net/entry/2015/12/18/210721

ソースコード (サンプル)

https://gist.github.com/zonuexe/577d089815e241d5da26

Page 92

最後に

Page 93

メンバー全員が知るべき?

知っておくとためにはなる…かも

でも完全に理解してないといけないわけではないとは言っても社内で誰かが知っておかないと困ることではある

「Noticeを潰せ」だけ周知したい

Page 94

Page 95

Webサービスの開発基盤を設計したいひとも

Page 96

創作文化のための開発に興味があるひとも

Page 97

今回の内容がいまいちぴんと来なくても大丈夫

Page 98

PHP, JavaScript, Ruby,Scala, Go, データ分析,インフラ, その他…

Page 99

まずは一度オフィスに遊びに来ませんか

ヾ(〃><)ノ゙☆

Page 100

Twitter @tadsan宛またはrecruit.pixiv.net

Page 101

使用フォント

セプテンバーM・L

Migu 1M

Rounded M+ 2p

_\ヽ, ,、`''|/ノ.|_ |\`ヽ、|\, V`L,,_|ヽ、).|/ ,、/ ヽYノ.| r''ヽ、.|| `ー-ヽ|ヮ| `|ヽ, ,r .|ヽ,r'''ヽ!'-‐'''''ヽ、ノ,,,..---r'",r, , 、`ヽ、 ヾヽ、__/ ./ハレハ i`ヽ、 `''r`ミ_.レ//r,,,、 レ'レハヾ, L,,_ `ヽ、"レ, l;;;l l;;;l`i.リレ' リ ̄~~ヽ、 ワ `"/-'`'`'`''''''''" ┼ヽ -|r‐、. レ |d⌒) ./| _ノ __ノ