2017の2日目です。ここ最近、本業のほうが死ぬほど忙しく、すっかりブログを書いていなかった@Hirakuです。前のことですが、今年のPHPカンファレンス福岡で例外の話をしてきました。この時の発表では例外に関する概要が主だったので、例外クラスそのものの設計について補足を書いてみることにします。例外のある世界観PHP7ではいくつかのエラーが例外と同じ挙動を示すようになり、エラーではなく例外機構を使う言語に様変わりしました。例外というのは、かなり侵略性の高い概念であり、例外のある世界では以下の前提に立つことが暗黙のうちに強要されます。3行目}正解は1~3全てです。発生する可能性は消えません。PHPは言語自体が例外を投げることがある世界になっています。シグナルや、ユーザーによるリクエスト中断など。今の仕様では例外ではなくても、いつ例外になってもおかしくないものがあります。それに、ある日突然、Structクラスが他のクラス/ライブラリに依存するようなコードに変更されたとすると、自動的に依存先のクラス/ライブラリがthrowする例外が継承されるようになります。こんな世界では、「try~catchで全例外を捉えて起こさないようにするぞ!」というのは厳しすぎます。なく、以下を前提にするべきです。どこでも例外が発生する可能性がある例外によって中断されても、ソフトウェアが不適切な状態にならないように保証するのは各プログラマの責任(例外安全)例外安全に関しては福岡の資料で書いたのでそちらを参照して下さい。PHPでthrowできるもの「どこでも例外が発生する可能性がある」そんな不確かな場所で、コードなんて書いてられるか!という気持ちにもなりますが、一方でPHPの場合、throwされてくるものに強い制限があるのでした。PHPではthrow句の後ろに書けるものはThrowableインターフェースを継承したインスタンスだけに制限されています。(PHP5系ではExceptionクラスか、それを継承したクラスのインスタンスのみ。)なので、例外として飛んできたものは必ず以下のメソッドが生えています。Throwable個人的にはこの制約は便利だと思っていて、最低限のインターフェースが割と豊富なので、このフォーマットに皆が合わせれば色々と生きやすくなります。PHPUnitの例外ハンドリングいくつかのフレームワークでは、どんな例外が起きたとしても必ずこれらのメソッドが生えていることを利用して、フォーマット化して例外を表示してくれます。テストフレームワークであるPHPUnitで、テスト実行中に例外が起きると、以下のような表示になります。symfony/consoleの例外ハンドリングコマンドラインツールを作るときによく使われる、symfony/consoleだと、処理中に例外が発生すると以下のような表示を自動で作ってくれます。これも例外クラス名とgetMessageの中身を採用していますね。messageやcodeは何を書くべきか例外のコンストラクタは3つ引数を取ります。$previous今までに述べたような使われ方を想定するに、$messageは何が起きたのかをなるべく詳細に書くべきでしょう。日本語か英語かは特に規定されていないのでプロジェクトの方針次第ですが、固定文言だけ、とかではなく状況を詳しく説明するidや時刻なども埋め込むと良いでしょう。$codeに関しては、PHP内でも特にこれと言った標準はないように思います。キャッチ側と握って特定のコードを入れてもいいですし、使わなければ省略か0でかまわないでしょう。例外はあくまで詳細なmessageを作る隠したいなら一度キャッチしてハンドリングするという方針であるべきです。そもそも、例外を投げる方は、誰がこの例外を受け止めるかを制御できないのですから。場合によっては、依存先のライブラリが投げてきた例外をキャッチして、別の例外として投げ直すことがあります。素直に例外を作り直して投げてしまうと、実質$eが握りつぶされ、真の原因がわからなくなってしまいます。$e);}こうすると、真の原因を辿れるようにしつつ、表面上の例外クラスを差し替えることができます。symfony/consoleとかでも、ちゃんとpreviousには対応されてて、こんな風に表示してくれます。複雑な例外を作るたとえば、HTMLフォームのバリデーションエラーを考えてみます。family_namefirst_namefamily_name_kanafirst_name_kanagenderemailこれらのうち、first_name_kanaとemailに入力エラーがあったとします。messageは、適当に改行で複数メッセージを詰めても良いでしょう。first_name_kanaにカタカナで入力してください。emailは100文字以内で入力してください。実際の場面を考えると、以下のような処理を作りたくなるのではないでしょうか。やりくりするのは厳しいものがあります。例外クラスに他の値も蓄えてしまうと良いでしょう。}}で、例外を投げる場面でこんな風にします。'first_name_kanaにカタカナで入力してください。'emailは100文字以内で入力してください。->setErrors($errors);これなら、catchしたValidationExceptionから$paramsや$errorsを取り出せて、後処理もしやすいでしょう。例外にsetterを生やす技PHPの例外は、newした箇所で自動的にスタックトレースを収集し、蓄えるという特徴があります。この機能を活かすならば、例外クラスのnewは極力ベタ打ちして、関数化なども避けたほうが便利です。一方で、例外クラスのコンストラクタは標準が決まっており、コンストラクタ引数を変更するのはあまり行儀がいいとも言えません。$thisするsetterを生やすようにすると、その場で拡張パラメータを増やすことができて使い勝手が良くなります。何を書くべきかの方が重要なポイントだと思います。うまく例外の示す契約に乗ると、PHPUnitなどの各種フレームワークとも過ごしやすくなりますので、一度例外設計を見直してみてはいかがでしょうか。NAKANOSeesaaブログ×この広告は180日以上新しい記事の投稿がないブログに表示されております。