Slide 1

Slide 1 text

例外で責務を表現しよう phperkaigi2024 @kajitack 1

Slide 2

Slide 2 text

自己紹介 梶川 琢馬 (かじかわ たくま) X: @kajitack 所属: 株式会社TechBowl PHPerkaigiでの登壇は初めて 2

Slide 3

Slide 3 text

3

Slide 4

Slide 4 text

今日話すこと webのAPI開発の中で取り入れている例外設計の考え方を紹介 こんな方にオススメ 例外をなんとなくcatchしている 例外の分類がよく分からない カスタム例外をいつ使って良いか分からない PHPDocの @throws にどの例外を指定すれば良いか分からない 4

Slide 5

Slide 5 text

Agenda 1. エラーハンドリングは避けられない 2. メソッドは1つの責務に集中したい 3. 例外でコードの責務を表現する 4. 例外の型を使い分ける 5

Slide 6

Slide 6 text

1. エラーハンドリングは避けられない 6

Slide 7

Slide 7 text

一見、エラーが起きなさそうなシンプルなコード function addNumbers(int $a, int $b): int { return $a + $b; } 引数に文字列が渡される 加算した結果がPHPがサポートする整数型の最大値を超える 実行環境のマシンでメモリーが足りなくなった 7

Slide 8

Slide 8 text

API開発ではもっと... リクエストの形式が想定外 DBや外部APIに接続失敗した 関連するエンティティが存在しなかった ビジネスロジックに違反した 変更の永続化に失敗した レスポンスへの変換に失敗した 8

Slide 9

Slide 9 text

エラーによって処理を変えたい リトライ 代替手段を使う ログに出力する 何もせず、処理を中断する 9

Slide 10

Slide 10 text

2. メソッドは一つの責務に集中したい 10

Slide 11

Slide 11 text

起きそうなエラーのハンドリングをひたすらやる? try { $this->db->create(...$args); } catch (InvalidArgumentException) { // 引数の形式が間違っている時の処理 } catch (PDOException) { // DB エラーに対する処理 // 省略 } catch (Throwable) { // その他のエラーが起きた時の処理 } 11

Slide 12

Slide 12 text

やりたいこと メソッド内で解決できないエラーはハンドリングしない 呼び出し側に状況に応じた任意のエラーハンドリングをさせる 12

Slide 13

Slide 13 text

3. 例外でコードの責務を表現する 13

Slide 14

Slide 14 text

例外の機能 処理を中断する = 責務の分離 処理を中断した理由を表す = 責務の表現 14

Slide 15

Slide 15 text

例外で処理を中断する = 責務の分離 throwすると、catchするまで処理が中断される class Id { public function __construct(int $value) { if ($value <= 0) { throw new InvalidArgmentException(); } $this->value = $value; } $id = new Id(0); // 以降は実行されない $this->repository->findById($id); 15

Slide 16

Slide 16 text

例外で中断した理由を表す = 責務の表現 型によってエラーの理由を区別する @throws で仕様として宣言する /** * @throws MyException */ public function myLogic() { if (// 違反) { throw new MyException(); } } 16

Slide 17

Slide 17 text

@throwsで宣言された例外がある場合のハンドリング try { myLogic(); } catch (MyException $e) { // 何かしらの処理 } 処理しない場合はさらに呼び出し元へ任せる事ができる。 /** * @throws MyException */ public function myLogicCaller() { myLogic(); } 17

Slide 18

Slide 18 text

期待した例外が発生することをテストする 戻り値のチェック等と同様に、明示的に宣言されている例外をテストする /** * @test */ public function myLogic のルールを違反した場合_ 例外を発生させる(): void { $this->expectException(MyException::class); myLogic(); } 18

Slide 19

Slide 19 text

4. 例外の型を使い分ける 19

Slide 20

Slide 20 text

例外の型はツリー構造になっている Throwable Error Exception LogicException RuntimeException カスタム例外 20

Slide 21

Slide 21 text

LogicException 呼び出し側の使い方が間違っている時に発生する例外 例:引数の形式が正しくない場合、InvalidArgumentExceptionを発生させる class Id { public function __construct(int $value) { if ($value <= 0) { throw new InvalidArgmentException(); } $this->value = $value; } 21

Slide 22

Slide 22 text

RuntimeException 実行時に起きる例外 例:有効に見えるIDだが、DBや外部APIへの問い合わせに失敗する 22

Slide 23

Slide 23 text

Exceptionを継承したカスタム例外 実行時に発生する例外 ビジネスロジックの違反を表す エンドユーザーに分かりやすいメッセージで表す必要がある場合 23

Slide 24

Slide 24 text

カスタム例外の定義例 final class CouldNotFindMeetup extends Exception { private function __construct( string $message, ) { parent::__construct($message); } public static function withId( MeetupId $meetupId ): CouldNotFindMeetup { return new self( "Could not find a meetup with ID {$meeupId->value()}" ); } } 24

Slide 25

Slide 25 text

カスタム例外を投げるメソッドの定義 1. InvalidArgumentExceptionを投げる 2. RuntimeExceptionなどを投げる 3. ビジネスロジックで使う可能性のあるカスタム例外を投げる /** * @trows CouldNotFindMeetup // ...3 */ public function findById(int $id): MeetUp if ($id <= 0) { throw new InvalidArgmentException(); //...1 } $meetup = $this->dao->find($id); // ...2 if ($meetup === null) { throw CouldNotFindMeetup::withId($id); // ...3 } return $meetup; } 25

Slide 26

Slide 26 text

明示的に宣言しない例外をハンドリングしたいとき ライブラリで @throws が宣言されていない例外を扱いたいとき トランザクションのロールバックやリソースの開放などを行うとき set_exception_handlerでまとめてエラーのレスポンスにしたいとき 26

Slide 27

Slide 27 text

まとめ エラーが起きたときは例外を投げて処理を中断する 呼び出し側に対してビジネスロジック上起きうるエラーを例外で明示する 明示的に宣言された例外のみcatchする catchしない場合はthrowすることを宣言する 結果、コードの見通しが良くなりテストもしやすくなる 27

Slide 28

Slide 28 text

Thanks! PHPをはじめとしたwebやモバイル開発 の教材、メンターが沢山居るので、興味 がある方はぜひ覗いてみてください! 副業でメンターやりたい方も募集中で す! 28