本セッションでの対象者・話すこと

並里氏:では、PHPのエラーから学ぶ例外処理、エラーハンドリングのLTを始めたいと思います。よろしくお願いします。

まず1点、このLTの資料自体がPHPerKaigi 2022で話した20分の内容なので、少し駆け足になっていると思います。ご了承ください。よろしくお願いします。

目次です。「今回の対象者と話す内容」「エラーについて」「エラーハンドリングについて」「PHPの例外について」「エラーハンドリングを学ぶ」「振り返り」でやっていきたいと思います。

対象者は「そもそも、エラーハンドリングという言葉を知っているけど、何をするの?」という人だったり、「PHPのエラー? とりあえずいつもググってるよ!」という人。

(スライドを示して)話す内容は、エラーについて、(スライドに記載している)以下を話していきます。

一応、本内容での言葉を統一しておきます。「エラー=エラー処理」「エラーハンドリング=例外処理」「例外=Exception」というところで話していきます。

PHPのエラーは大きく2種類ある

まずエラーについて。エラーは実行中のプログラムが正常に処理をできなくなるような問題・事態のことです。

PHPのエラーにはどういうものがあるかというと、大きく分類して「致命的なエラー」と「警告・注意」が存在します。致命的なエラーというのが「現在の処理を中断する=早く直せ」というような、実装者に向けたエラーですね。警告・注意は「現在の処理は中断しないけど、直したほうがいいよ」というレベルのエラーになります。

致命的なエラーの代表は、Fatal errorやParse errorがあります。

WarningやNoticeなどはphp.iniの設定次第で無視はできるのですが、バグの発生要因になるので、あまり推奨はしません。

PHPのエラーハンドリング

次にエラーハンドリングについて話します。エラーハンドリングとはそもそもなにかというと、プログラムがエラーを起こした時に、すぐに実行を終了せずに、あらかじめ用意しておいた処理を行うこと。ユーザー操作によって解決できない問題があった場合に対処するための処理です。

PHPのエラーハンドリングについてですが、5系から導入されたtry-catchで記述していきます。例外が発生し得る箇所をtry {}で囲み、例外が発生した時点で後続する処理は中断され、catch {}で受け取り、例外が発生した時の対応(ハンドリング)を行います。例外を明示的に発生させたい場合は、該当する箇所でthrowを用いて例外を投げます。

PHPのエラーハンドリングの書き方です。(スライドを示して)こういうふうにtry {}で囲みます。try {}の中のif文ですが、ここでは例として、メールが重複していたらRuntimeException、拡張した例外を投げるという処理を書いています。

PHPの例外について

PHPの例外について少し話します。実は「PHPのエラー」で紹介した致命的なエラーなどの一部は、try-catchでは補足できません。方法はあるにはあります。ですがたいていの場合、それらは捕捉する必要自体がありません。次のページで簡単にPHPの例外について触れながら説明していきます。

(スライドを示して)ここも図が見づらくて申し訳ないのですが、5系まではすべての致命的エラーはエラーハンドリングができず、処理がそこで中断されてしまいます。しかし7系からは、Throwableインターフェースが実装され、それを継承したExceptionとErrorが導入されました。そのおかげで致命的エラーの多くがErrorをthrowするようになり、捕捉が可能になりました。

しかし、致命的なエラーのすべてがErrorに実装されたわけではないのが現状です。ごめんなさい。(スライドを示して)これは7系の時点なので、8系がどうなっているかは置いておきます。ですが先述したとおり、すべてを捕捉する必要はありません。

たいていのエラーはプログラムやFW(Frame Work)側が検知してくれているので、実装者は開発中に起きたエラーを修正するだけで済みます。そのためエラーハンドリングをする必要はありませんという意味です。

エラーハンドリングを使わないとどうなるのか?

では、実際にエラーハンドリングを学んでいきます。「エラーハンドリングというものについて少しは理解できたけど、必要性は?」「どういう時に使えばいいの?」などの疑問が残っていると思います。なので、一つひとつQ&A方式で説明します。

まず「エラーハンドリングを使わないとどうなるの?」。1、エラーの発生箇所や原因の特定が難しくなる。2、エラー発生後も後続の処理が実行されてしまう。3、エラーが起きていることをユーザーが認知できない。

ここで、アンサーです。「エラーハンドリングはどんな時に用いるのか?」。繰り返しになりますが、1、エラーの発生箇所や原因の特定をしたい時。2、エラー発生後に後続の処理を中断させて、別の対応をしたい時。3、エラーが起きていることをユーザーに通知したい時です。

エラーハンドリングはどんな時に用いるのか?

まず1の「エラーの発生箇所や原因の特定をしたい時」から説明します。

例外が発生した内容をcatch句で受け取り、メッセージなどをロギングしておけば、発生箇所や発生日時、原因の特定などが容易になります。そうすることで、エラーが起きた時にエラーのthrow の内容を見て即座に対応がしやすくなったりが可能になります。

2番「エラー発生後に後続の処理を中断させて別の対応をしたい時」。例外を受け取ったら処理を中断させて別の対応をしたい時は、ロギングの時と同様に、catch句の中で行いたい処理を書きます。その際、returnをしないと後続してしまうので、注意をお願いします。

では、3「エラーが起きていることをユーザーに認知したい時」。こちらも先ほどと同様に、catch句で例外を受け取ったら、呼び出し元やユーザーにreturnでエラーを通知するという内容を実装しています。この対応が抜けていると、ユーザーは処理が問題なく対処されたと認識されてしまいます。

エラーハンドリングを考える上で大事な3つのこと

ではちょっと別の質問です。「エラーハンドリングを考える上で大事なことは?」

1、例外を握り潰さない。2、エラーハンドリングのスコープは狭く。3、捕捉すべき例外とそうでない例外を理解しましょう。

ではまず1つ目の「例外を握り潰さない」。せっかく例外をthrowしても、catch句でなにもしなければエラーを無視しているも同然です。catch句ではロギングのほかに、エラーの発生を呼び出し元に伝える、後続の処理を行わないためにreturnするなど、要件に応じた対応をしましょう。

throwをせっかく投げてもcatch句でなにも書いてない状態を、“エラーを握り潰す”とか“例外を握り潰す”みたいに言うのですが、しっかり例外が発生したら呼び出し元に伝えたり、後続処理を行わないようにするなど、要件に応じて対応をお願いします。

2つ目「エラーハンドリングのスコープは小さく」。(スライドを示して)この図を見てもらうと、try {}の中にIfがいっぱいあったり、例外が書いてあると想定しています。さまざまな例外を全部1つのtry {}の中に収めて(しまい)、catchをすべてExceptionで拾ってしまっています。

tryの範囲が広いと、例外が発生した時の特定が難しくなります。catch句はすべてExceptionで受け取るのではなく、それぞれ適切な例外クラスを利用しましょう。

3つ目「捕捉すべき例外とそうでない例外」。捕捉すべき例外というのを3つ挙げています。「例外発生以後も処理が続けられてしまうと、データ不整合など障害につながってしまう時」「例外の発生後に呼び出し元やユーザーへの通知、ロギングなどを行いたい時」「例外発生前に行っている処理や状態をロールバックしたい時」が捕捉すべき例外と挙げています。

逆に捕捉すべきではない例外。開発時に対処できるタイプエラーなどの例外や、例外が発生してもその後の回復処理などが行えない時などは、また別の対応が必要になってくるかなと思います。

PHPのエラーハンドリングの実装例

だいぶ駆け足になってきたので理解が追いついていない方もいると思うのですが、少し振り返りをします。

ここまで学んだエラーハンドリングを振り返ります。(スライドを示して)ここに例のコードを書いています。user -> insertPoint。ユーザーにまず1ポイントを付与するというところで、プログラムがあって、基幹システムにAPIでユーザーとポイント情報を渡す。

基幹システムにユーザーとポイント情報をAPIでpostしているが、なにかのエラーになって、ユーザーにはポイントが付与されたままになってしまう。この状態のままだと、なにかエラーがあっても、ユーザーにはポイントが付与されたことになってしまっています。

基幹システムへのAPIで成功したレスポンスが返ってこない場合に、例外をthrowして、ユーザーに対して付与していたポイントをロールバック。その後、開発者向けにロギングと通知、リダイレクトしてユーザーにエラーがあって、ポイントを付与できなかったことを伝える。

ここに書いてあるコードが、先ほど上で見た例外処理を実装したパターンになっています。

try {}の中でinsertPoint、ユーザーに1ポイント与える。その後、API通信が成功したかどうかを例外でcatchします。例外が発生した場合は、catch句でデータベースにまずロールバックを行って、ロギングしてSlackにエラー通知をする。ここではSlackを例に挙げています。最終的にユーザーにエラーが発生したことを伝えるため、エラーを表示といったところを簡単に実装した例です。

少し駆け足になってしまいましたが、私の発表は以上になります。ご清聴ありがとうございました。