Slide 1

Slide 1 text

責務を分離するための 例外設計 梶川 琢馬 @kajitack PHPカンファレンス2024

Slide 2

Slide 2 text

梶川 琢馬 (Takuma Kajikawa) バックエンドとフロントエンド半々ぐらい。 新卒の頃からPHPにはお世話になってます! 趣味はトライスロン。 来年オーストラリアのIRONMANレースに出る予定! 株式会社 TechBowl プロダクトエンジニア @kajitack

Slide 3

Slide 3 text

業界最強のメンター陣が
 ありとあらゆる お悩みにお答えします。 大手上場企業やベンチャー企業の最前線で活躍している 60社140名以上のトップエンジニアが在籍。 キャリアのことも、技術のことも、まとめて相談できます。 あなたのロールモデルがきっと見つかります。

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

複数のサービス、無数のクラスの ルールがある A 面談予約を作るときは引数は〇〇とXXが必H A 面談予約をキャンセルする場合は開始前である必要 こういったルールを違反した場合に例外処理を行いたい。 ドメインモデル図 オブジェクトに対する制約

Slide 6

Slide 6 text

例外処理の方針は 決まったものが特にないので、 チームで話し合いました...! ※例外の使い方は各プロジェクトやライブラリによって違うのであくまでも参考に

Slide 7

Slide 7 text

21 PHPの例外の型を分類してみ 1 カスタム例外の設Æ Ç1 例外でコードの責務を表現する 今日話すこと

Slide 8

Slide 8 text

PHPの例外の型を分類してみる

Slide 9

Slide 9 text

) 実装や技術的な問題で起きる例外(技術的な例外 #) ユースケースで想定できる例外(ドメイン例外) エラーの種類

Slide 10

Slide 10 text

実装や技術的な問題で起きる例外 ユーザーが通常のユースケースとして回避できないエラー。HTTPステータスでいう500系 起きる原 p バr p DBや外部APIなどの接続エラy p データの不整合 対処方) p 個別でcatchせずにFWや共通のハンドラーでcatchするd p プロトコルに対応したエラーコードを返すd p ユーザーに詳細なメッセージは見せないd p 「運営に連絡してください」「アクセスが集中しています。しばらくお待ち下さい」

Slide 11

Slide 11 text

ユースケースで想定できる例外 クライアント側の問題。HTTPステータスでいう400系 起きる原s s ライブラリ特有の制約違t s アプリケーション特有の制約違t s (例) 予約する際の件数制限を超えていた 対処方B s 例外をcatchする場合もある“ s catchしない場合はFWや共通のハンドラーでcatchする。(技術的な例外と同じ扱いT s プロトコルに対応したエラーコードや独自のエラーコードを返す“ s ユーザーに詳細なメッセージを返す“ s (例) 「予約可能な件数を超えています」

Slide 12

Slide 12 text

PHPの例外の型 Throwable RuntimeException 独自例外 LogicException Error Exception 他にもこれらを継承したクラスがたくさん定義されている。 throwできるクラスのinterface

Slide 13

Slide 13 text

PHP内部で投げるErrorと開発者が投げるException Throwable RuntimeException 独自例外 LogicException Error PHPが内部で投げるエラー throwできるクラスのinterface アプリケーションで投げる例外 Exception

Slide 14

Slide 14 text

技術的な例外と想定可能な例外 Throwable RuntimeException 独自例外 LogicException Error throwできるクラスのinterface 技術的な例外 Exception 想定可能な例外

Slide 15

Slide 15 text

Error 戻り値がintではなくfloatのときにPHPが投げる function int int : int return + ( $a, $b) { $a $b; } ( , ); add add PHP_INT_MAX 1 TypeError 0除算のときにPHPが投げる function int int : int return / ( $a, $b) { $a $b; } ( , ); divide divide 10 0 DivisionByZeroError 直接throw文を書くことがないが、PHP内部で投げるオブジェクト。 「PHPの使い方を間違っている実装ミス」を表す。

Slide 16

Slide 16 text

LogicException コードのロジックに問題があるときに投げる例外。 与えられた引数の条件を満たしてない場合など。 投げられた場合、ロジックの修正が必要だと判断できる。 RutimeException 実行時に予期せぬエラーが発生した場合に投げる例外。 実装が正しくても起きてしまう。 投げられた場合、一時的な障害やデータの不整合が発生し たと判断できる。 ( $reservationId) { ($reservationId ) { ( ); } $reservation :: ($reservationId); ($reservation ) { ( ); } public function int : void if < throw new = if === throw new if === throw new cancelReservation find 1 LogicException Reservation null RuntimeException RuntimeException "IDは1以上である必要があります" "予約が見つかりませんでした。" "すでにキャンセルされています。" // DBからfetchする ($reservation->status 'canceled') { ( ); }

Slide 17

Slide 17 text

技術的な例外の分類まとめ Error 直接throw文を書くことがないが、PHP内部で投げるオブジェクト。 投げられた場合、ロジックの修正が必要だと判断できる。 LogicException コードのロジックに問題があるときに投げる例外。 投げられた場合、ロジックの修正が必要だと判断できる。 RutimeException 実行時に予期せぬエラーが発生した場合に投げる例外。 投げられた場合、一時的な障害やデータの不整合が発生したと判断できる。

Slide 18

Slide 18 text

GF 例外はcatchしたいものとそうでないものがあ3 F Error/LogicException/RuntimeException はcatchしな ÇF catchしたい場合はカスタム例外を使う PHPの例外の型を分類してみる

Slide 19

Slide 19 text

カスタム例外の設計

Slide 20

Slide 20 text

カスタム例外 通常のユースケースにおいて想定される例外。 呼び出し側で必ず処理するかどうかを判断してほしい ( $reservationId) { $reservation :: ($reservationId); ($reservation ) { ( ); } public function int : void = if === throw new if === throw new cancelReservation find // DBからfetchする Reservation null CouldNotFoundReservation InvalidReservationStatus "予約が見つかりませんでした。" "すでにキャンセルされています。" ($reservation['status'] 'canceled') { ( ); }

Slide 21

Slide 21 text

カスタム例外の設計 S Exceptionを継承す7 S 名前はどのようなケースで起きるかを表A S エラーのメッセージを内部で組み立てるようにす7 S factoryメソッドで生成する final class extends private function string parent:: public static function : return new self -> { ( $message, ) { ($message); } ( $reservationId ) { ( $reservationId ); } } CouldNotFindReservation __construct withId value Exception __construct ReservationId CouldNotFindReservation "予約が見つかりませんでした。ID: { ()}"

Slide 22

Slide 22 text

なぜカスタム例外はExceptionを継承するのか? B 呼び出され方によって変わるので想定内の例外なのか判断できなV B PHPStormのInspectionでRuntimeExceptionとLogicExceptionを無€ B 無視したいライブリラリの例外も追加できるので便利

Slide 23

Slide 23 text

必要なカスタム例外は事業ごとに違うので、 チームで議論しながら決める ドメインモデル図 オブジェクトに対する制約

Slide 24

Slide 24 text

カスタム例外のハンドリング ‰ ビジネスロジックを扱う場所でカスタム例外を投げU ‰ 呼び出したユースケース側で表示の仕方を決めU ‰ 対象のオブジェクトが存在しない場t ‰ ルーティングのIDが間違っている→ステータスコードを404のエラーにして返 ‰ データの不整合→ロジックのエラーなので処理しない

Slide 25

Slide 25 text

ライブラリで定義されてるカスタム例外の扱い 3 想定可能であれば、カスタム例外で投げ直すF 3 技術的な例外であればLogicException、RuntimeExceptionなどで投げ( 3 必要な例外をより明確にできる

Slide 26

Slide 26 text

例外で責務を分離する

Slide 27

Slide 27 text

I 問題がある場合は早く例外を投げて処理を中断すE I メソッド内で解決できない例外は呼び出し側に任せE I 呼び出し側に処理してもらいたい例外は仕様として定義する どこまで責任を持つべきかを例外で表す

Slide 28

Slide 28 text

( $reservationId) { ($reservationId ) { ( ); } $reservation :: ($reservationId); ($reservation ) { ( ); } public function int : void if < throw new = if === throw new if === throw new cancelReservation find 1 LogicException Reservation null RuntimeException RuntimeException "IDは1以上である必要があります" "予約が見つかりませんでした。" "すでにキャンセルされています。" // DBからfetchする ($reservation->status 'canceled') { ( ); } 問題がある場合は早く中断する Î DBに問い合わせる前にIDのチェックしておµ Î 取得したオブジェクトを操作する前にチェックする

Slide 29

Slide 29 text

引数自身が例外を投げることでより早く中断できる Before After public function int     if < throw new ( $reservationId) { ($reservationId ) {     ( ); } ... cancelReservation 1 LogicException "IDは1以上である必要があります" class public function int if <= throw new -> = { ( $value) { ($value ) { ( ); } value $value; } ReservationId __construct 0 InvalidArgumentException $this "IDは1以上である必要があります。" public function ( $reservationId) { } cancelReservation ReservationId     // IDのチェックが不要になった W IDがロジック違反かどうかはID自身でチェックすI W IDのコンストラクタが呼ばれた段階で処理を中断できる

Slide 30

Slide 30 text

IDが入力値やルーティングから与えられた場合、 クライアントエラー(想定内の例外)では? class public function int if <= throw new -> = { ( $value) { ($value ) { ( ); } value $value; } ReservationId __construct 0 InvalidArgumentException $this "IDは1以上である必要があります。" 入力値のチェックはバリデーターで行う。 バリデーション実装のミス(バグ)なので、LogicException(InvalidArgumentException)を投げる。

Slide 31

Slide 31 text

解決できない例外は呼び出し側に任せる S 呼び出し側で処理してほしい例外はPHPDocの @throws で仕様として宣言する /** * */ // 違反) { @throws public function if throw new MyException MyException () { ( (); } } myLogic try catch { (); } ( $e) { } myLogic MyException // 何かしらの処理 /** * */ @throws public function MyException myLogic () { } myLogicCaller 投げる 処理できる場合はcatchする 処理できない場合は更に呼び出し側に任せる

Slide 32

Slide 32 text

定義した例外を投げることをテストする ' 戻り値のチェック等と同様に例外が投げられるかをテストすA ' デバッグ時に例外のBreakPointを貼ることもできる #[ ] () { ( ); (); } Test $this MyException public function : void -> ::class XXの場合_例外を発生させる expectException myLogic

Slide 33

Slide 33 text

S 例外の型を使い分けて必要なときだけハンドリングす# S カスタム例外を投げることを明示して、どういうルールがあるかを伝え# S 責務を分離し、必要な処理に集中することができる まとめ

Slide 34

Slide 34 text

皆さんのプロジェクトではどうしてるか?ぜひ聞かせてください! ご清聴ありがとうございました!