Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

こんな経験ありませんか? どこで try-catch を書くべきか分からない エラーメッセージがユーザーにとって分かりにくい 同じようなエラー処理をあちこちに書いている 2/63

Slide 3

Slide 3 text

今日のゴール 例外処理の役割を理解する ☑ 例外を分類する ☑ 例外をモデリングして設計に組み込む ☑ 3/63

Slide 4

Slide 4 text

梶川 琢馬 / 𝕏 @kajitack 株式会社 TechBowl の VPoT TechTrain の開発、メンターをやってます。 初PHPカンファレンス福岡! 4/63

Slide 5

Slide 5 text

例外の役割 5/63

Slide 6

Slide 6 text

例: 注文処理 /** * @param $productId 商品ID * @param $quantity 数量 */ function createOrder(int $productId, int $quantity): Order { // 永続化 Order::create(['productId' => $productId, 'quantity' => $quantity]); } 6/63

Slide 7

Slide 7 text

妥当性のチェックとエラー対応 以下のケースで正しく注文ができないのでエラーとして扱う 存在しない商品を指定された 数量が 0 以下を指定された 在庫以上の数量を指定された DB に保存できなかった 7/63

Slide 8

Slide 8 text

エラー対応を注文処理にそのまま記述した function createOrder(int $productId, int $quantity): Order { // 妥当性チェック if (!productExists($productId)) { // エラー対応 echo "エラー: 商品が見つかりません"; return; } if ($quantity <= 0) { echo "エラー: 数量は1以上を指定してください"; return; } $stock = getStock($productId); if ($stock < $quantity) { echo "エラー: 在庫が不足しています"; return; } try { return Order::create(['productId' => $productId, 'quantity' => $quantity]); } catch (Exception $e) { error_log("Order creation failed: " . $e->getMessage()); echo "エラー: 注文処理に失敗しました"; } } 8/63

Slide 9

Slide 9 text

エラー対応と通常の処理の責務が混在 エラーをユーザーに伝えるという UI の関心事を扱っている 通常の処理の見通しが悪くなり、 リトライや中断、監視ツールへの通知など柔軟に変更できない 9/63

Slide 10

Slide 10 text

例外で責務を分離させる 妥当性チェックと例外を投げることに集中する 問題なければ、通常の処理を行う メソッド内で解決できるエラーのみハンドリング その他は、呼び出し側にハンドリングを任せる 10/63

Slide 11

Slide 11 text

妥当性チェックと例外を投げることに集中する function createOrder(int $productId, int $quantity): Order { if ($quantity <= 0) { throw new LogicException('Quantity must be greater than 0'); } if (!productExists($productId)) { throw new NotFoundException($productId); } $stock = getStock($productId); if ($stock < $quantity) { throw new InsufficientStockException($productId, $quantity, $stock); } // DBへの保存に発生する例外はcatchしない return Order::create(['productId' => $productId, 'quantity' => $quantity]); } 11/63

Slide 12

Slide 12 text

状況に応じた柔軟なエラー対応が可能 try { $order = createOrder( $request->input('productId'), $request->input('quantity') ); return response()->json($order, 201); } catch (NotFoundException $e) { return response()->json(['error' => $e->getMessage()], 404); } catch (InsufficientStockException $e) { return response()->json(['error' => $e->getMessage()], 400); } // 他の例外はさらに呼び出し元のエラーハンドラで共通の対応 12/63

Slide 13

Slide 13 text

例外の役割 1. 処理を中断する 2. 通常の制御フローとエラー対応を分離する 3. エラー情報を詳細に保持し、調査に役立てる 13/63

Slide 14

Slide 14 text

エラーを「見つけやすく」 「起こりにくく」する 14/63

Slide 15

Slide 15 text

デプロイ前にエラーをテストすることが重要 設計段階でビジネスロジックとして例外を定義する 定義した例外が発生するテストを行う 15/63

Slide 16

Slide 16 text

設計段階でビジネスロジックと して例外を定義する 16/63

Slide 17

Slide 17 text

ビジネス例外 預金額を超える引き出し、在庫切れの注文など ビジネスロジックの一部として失敗が想定できるケース 技術的例外 システムの異常状態 ネットワーク障害、DB コネクション切断など 実装ミスやインフラ側で対処が必要なエラー プログラマが知るべき97のこと 技術的例外とビジネス例外を明確に区別する - Dan Bergh Johnsson 17/63

Slide 18

Slide 18 text

PHPの例外に当てはめる 18/63

Slide 19

Slide 19 text

Error 直接 throw 文を書くことがないが、PHP 内部で投げるオブジェクト 投げられた場合、実装ミスだと判断できる = 技術的例外 19/63

Slide 20

Slide 20 text

function addNumbers(int $a, int $b): int { return $a + $b; } 呼び出し側の実装ミス = 技術的例外 引数に文字列が渡される 加算した結果が PHP がサポートする整数型の最大値を超える 20/63

Slide 21

Slide 21 text

LogicException コードのロジックに問題があるときに投げる例外 投げられた場合、実装ミスだと判断できる = 技術的例外 21/63

Slide 22

Slide 22 text

呼び出し側の実装ミス = 技術的例外 function inverse(int $x) { if ($x === 0) { throw new LogicException('Division by zero'); } return 1/$x; } 22/63

Slide 23

Slide 23 text

RuntimeException 実行時に予期せぬエラーが発生した場合に投げる例外 投げられた場合、一時的な障害やデータの不整合が発生したと判断できる = 技術的例外 23/63

Slide 24

Slide 24 text

実行時の環境やデータに依存するエラー = 技術的例外 function readConfig(string $path): array { if (!file_exists($path)) { throw new RuntimeException('Config file not found'); } $content = file_get_contents($path); if ($content === false) { throw new RuntimeException('Failed to read config file'); } return json_decode($content, true); } 24/63

Slide 25

Slide 25 text

定義されてる例外は技術的な例外として分類し、 ビジネス例外は独自に定義する 25/63

Slide 26

Slide 26 text

ビジネス例外を カスタム例外として設計 26/63

Slide 27

Slide 27 text

エラーもドメインの 一部としてモデル化する ドメインをモデル化する際には、文字列などのプリミティブ型を 使わず、ドメインに特化した型を作成します。 さて、エラーも同じように扱われるべきです。 ドメインに 関する議論で特定の種類のエラーが挙がった場合、ドメイン内の 他の要素と同様にモデル化するべきです。 特別な対応が必要なエラーの種類ごとに個別のケースを用意する 関数型ドメインモデリング ドメイン駆動設計とF#でソフトウェアの複雑さに立ち向かおう Scott Wlaschin (著), 猪股 健太郎 (著) 27/63

Slide 28

Slide 28 text

チーム内の 会話で洗い出す 例: 注文作成の条件 開発者と ドメインエキスパートとの会話の 中で想定できる → ビジネス例外 28/63

Slide 29

Slide 29 text

チーム内の 会話で洗い出す 例: データベースの接続中断 開発者の技術的な 関心にのみ現れる → 技術的例外 29/63

Slide 30

Slide 30 text

モデル毎に制約を 図にまとめる 図にまとめることによって、 ドメインロジックについて 会話しやすくなる TechBowl流ドメインモデルでの情報設計 https://speakerdeck.com/hiroki_nakamura/techbowlliu-domeinmoderingu-2025-02-28 DDD × Whimsicalで快適モデリングライフ! https://zenn.dev/techtrain_blog/articles/334ac36e79d946 30/63

Slide 31

Slide 31 text

例外にも関心の分離と依存のルールを適用する 31/63

Slide 32

Slide 32 text

各レイヤーで投げる例外 ドメイン層で投げる例外を独自例外として定義 32/63

Slide 33

Slide 33 text

例外の使い分けとハンドリング 33/63

Slide 34

Slide 34 text

例: 注文処理フロー 1. 入力値として商品 ID と数量を受け取る 2. 在庫確認と注文作成 3. 注文確定 34/63

Slide 35

Slide 35 text

処理の流れ 1. 入力値として商品 ID と数量を受け取る 2. 商品 ID と数量 ValueObject を作成 3. 注文 Entity を作成 4. DB に保存 35/63

Slide 36

Slide 36 text

例外実装の流れ 1. ビジネスルールの実装 2. 入力値の妥当性確認 3. リクエストのバリデーション 4. ハッピーパスの実装 36/63

Slide 37

Slide 37 text

ビジネスルールの実装 Entity 作成前に商品の存在と在庫をチェック 商品が見つからない場合や在庫不足の場合はドメイン例外を投げる function createOrder(ProductId $productId, Quantity $quantity): Order { if (!$this->product_repository->exists($productId)) { throw new NotFoundException($productId); } $stock = $this->inventory_repository->getStock($productId); if ($stock < $quantity->value()) { throw new InsufficientStockException($productId, $quantity, $stock); } // 問題ない状態で Entity が作成される return Order::create($productId, $quantity); } 37/63

Slide 38

Slide 38 text

入力値の妥当性確認 妥当性確認には 2 種類ある 構文的(syntactical)な妥当性確認 ドメインモデルの状態に依存しない形式チェック → ValueObject やリクエストバリデーションで実施 意味的(semantical)な妥当性確認 ドメインモデルの現在の状態を踏まえたビジネスルール → ドメイン層で実施 手を動かしてわかるクリーンアーキテクチャ ヘキサゴナルアーキテクチャによるクリーンなアプリケーション開発 38/63

Slide 39

Slide 39 text

構文的な妥当性確認の実装 数量 ValueObject を作成 コンストラクタが呼ばれた段階で処理を中断できる ドメインモデルの状態に依存しない構文的なチェック 呼び出し側の実装ミスなので、 InvalidArgumentException (LogicException) を投げる class Quantity { public function __construct(int $value) { if ($value <= 0) { throw new InvalidArgumentException('数量は1以上を指定してください quantity: '.$value); } $this->value = $value; } } 39/63

Slide 40

Slide 40 text

リクエストのバリデーションを実装 例外処理は最初のエラーで中断するため、すべてのエラーを一度に 検証できる validator を作成する。 // Laravelのバリデーションを使用した例 class CreateOrderRequest extends FormRequest { public function rules(): array { return [ 'productId' => ['required', 'integer'], 'quantity' => ['required', 'integer', 'min:1'] ]; } } 40/63

Slide 41

Slide 41 text

ハッピーパスの実装 ビジネスルールを満たした Entity を DAO を通じて永続化 ここで投げられる例外は、DB 接続エラーやテーブル定義との データ型ミスマッチなどの技術的な例外 $this->dao->create( $order->productId()->value(), $order->quantity()->value() ); 41/63

Slide 42

Slide 42 text

ドメイン例外のハンドリング Application と Presentation 側で 個別に例外を HTTP レスポンスに変換し、詳細な エラーメッセージを返す 42/63

Slide 43

Slide 43 text

技術的例外のハンドリング すべて 500 エラーの HTTP レスポンスに変換する 監視ツールへのレポート ログに出力 43/63

Slide 44

Slide 44 text

例外を投げる方はハンドリングの 仕方を知らなくてもいい CLI やバッチ処理、gRPC などに変わってもドメイン例外を投げる箇所は気にせずに済む。 44/63

Slide 45

Slide 45 text

テスト段階で見つけて、防ぐ 45/63

Slide 46

Slide 46 text

例外は仕様として定義する 例外の型によって理由を区別し、 @throws で仕様として宣言する /** * @throws MyException */ public function myLogic() { if (// 違反) { throw new MyException(); } } 46/63

Slide 47

Slide 47 text

静的解析で @throws のチェック PHPStan で指定した例外クラスをハンドリングしているかチェックできる ドメイン例外の基底クラスを指定 parameters: exceptions: checkedExceptionClasses: # ここで指定した例外クラス(とそのサブクラス)を「Checked Exception」として扱う # throw する場合は必ずメソッドに @throws を明記させる対象になる - Package\Domain\Exception\DomainException check: # Checked Exception を投げているのに、メソッドに @throws が書かれていない場合にエラーにする missingCheckedExceptionInThrows: true # @throws の型が抽象的すぎる場合にエラーにする(例:@throws \Exception はNG) tooWideThrowType: true 47/63

Slide 48

Slide 48 text

PHPStormの例外解析 ハンドリングしていない例外があると 警告を出す。 デフォルトで技術的例外は 除外されている。 そのまま使えばドメイン例外の みチェックできる。 48/63

Slide 49

Slide 49 text

期待した例外が発生することをテストする 戻り値のチェック等と同様にテストコードを記述 #[Test] public function myLogicのルールを違反した場合_例外を投げる(): void { $this->expectException(MyException::class); myLogic(); } 49/63

Slide 50

Slide 50 text

投げるべきところで投げることをテストする テストしておくことで、他の箇所ではエラーが起きにくくなる 予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント https://speakerdeck.com/twada/growing-reliable-code-phperkaigi-2022 50/63

Slide 51

Slide 51 text

例外を投げる ことをデバッグ PHPStorm で例外クラスを指定して、 ブレークポイントを設定して デバッグできる 51/63

Slide 52

Slide 52 text

例外設計を行うことで、テスト段階で 見つけて、防ぐことができる 運用では技術的例外の対応に注力できる 52/63

Slide 53

Slide 53 text

例外を使わないパターン 53/63

Slide 54

Slide 54 text

他言語のアプローチ: エラーを値として扱う Go 例外を制御構造と結びつけることは複雑な コードにつながる → エラーを値(error 型)として戻り値で返す Rust 例外が存在せず、Result型で成功/失敗を表現 → 処理の失敗も「結果」の一部としてモデリング Why does Go not have exceptions? https://go.dev/doc/faq Errors are values https://go.dev/blog/errors-are-values Error Handling The Rust Programming Language https://doc.rust-lang.org/book/ch09-00-error-handling.html 54/63

Slide 55

Slide 55 text

失敗は例外ではない? 「例外」という名前が示す通り、本来は「例外的な」 状況に使うべき 失敗は想定できる結果であるため、その失敗を例外として モデリングすることは概念的におかしい 処理の失敗は「例外」ではなく、成功と同様に 考慮すべき「結果」 セキュア・バイ・デザイン 安全なソフトウェア設計 Dan Bergh Johnsson (著), Daniel Deogun (著), Daniel Sawano (著), 須田智之 (翻訳) 55/63

Slide 56

Slide 56 text

PHPで例外を使わない実装 56/63

Slide 57

Slide 57 text

戻り値として表現する手法 方法 構造 問題点 / 特徴 Union User|Exception|null 成功・失敗・欠損が混在 ・呼び出し側が instanceof チェック乱立 ・網羅保証がない 配列 [$user, $err] 両方null/非nullもあり得る ・排他性を型で保証できない ・ $err 無視しても動く Result どちらか一方のみ ・成功/失敗を型で明示 ・呼び出し側に分岐を強制(lint) 57/63

Slide 58

Slide 58 text

Result 型 try-catch とは別のエラーハンドリングのための型 戻り値として「 成功 」または「 失敗 」を持つ 成功 (Ok): 正常な値を返す 失敗 (Err): エラー情報を返す 成功した場合、失敗した場合など処理を分岐できる 関数のシグネチャから「この関数は失敗する可能性が ある」ことが明示的 呼び出し側は戻り値を取り出すためにチェックする ことが強制される Result 型を返す処理 if ($input === null) { // 失敗の場合 return Result::err('Input cannot be null'); } else { // 成功の場合 return Result::ok($input); } 結果の検証と値の取得 if ($result->isOk()) { $value = $result->unwrap(); } else { $error = $result->unwrapErr(); } 58/63

Slide 59

Slide 59 text

とはいえ、PHP では 例外処理が一般的 一切例外を使わなくてもライブラリが投げてくる フレームワークの例外ハンドリングも積極的に使いたい トランザクションのロールバック ログ 監視ツール 59/63

Slide 60

Slide 60 text

ビジネス例外のみResult型を使う ビジネス例外 Result 型でビジネスプロセスの結果を表現 ビジネスロジックの純粋性と整合性が保証される。 条件分岐に try-catch を使わないことで、コードの可読性と保守性が向上する 技術的例外 システムの異常状態なので、まさに「例外」 例外のメリットである大域脱出を活かして即座に処理を中断し、 今まで通り、フレームワークのエラーハンドラに任せることができる。 60/63

Slide 61

Slide 61 text

詳しい実装方法はこちらを参考に! Result型で“失敗”を型にするPHPコードの書き方 https://speakerdeck.com/kajitack/result-type-in-php 61/63

Slide 62

Slide 62 text

まとめ 62/63

Slide 63

Slide 63 text

例外の使い方 妥当性チェックと例外を投げることに集中する メソッド内で解決できるエラーのみハンドリングし、 呼び出し側にハンドリングを任せる ビジネス例外と 技術的例外を使い分ける ビジネス例外はカスタム例外か Result 型でモデリング ビジネス例外はテスト段階で見つけて、防ぐ 運用では技術的例外の対応に注力できる 63/63