Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPでResult型を なるべく使いやすい形で実装する

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for higaki higaki
November 07, 2025
340

PHPでResult型を なるべく使いやすい形で実装する

Avatar for higaki

higaki

November 07, 2025
Tweet

Transcript

  1. fn divide(a: f64, b: f64) -> Result<f64, String> { if

    b == 0.0 { Err("ゼロで割ることはできません ".to_string()) } else { Ok(a / b) } } // 呼び出し側 let result = divide(10.0, 0.0); if result.is_ok() { println!("結果: {:?}", result.unwrap()); } else { println!("エラー: {:?}", result.unwrap_err()); } /** * @return Result<float, string> */ function divide(float $a, float $b): Result { if ($b == 0.0) { return new Err("ゼロで割ることはできません "); } return new Ok($a / $b); } // 呼び出し側 $result = divide(10.0, 0.0); if ($result->isOk()) { echo "結果: " . $result->unwrap() . "\n"; } else { echo "エラー: " . $result->unwrapErr() . "\n"; } Rust PHP(僕の実装)
  2. fn divide(a: f64, b: f64) -> Result<f64, String> { if

    b == 0.0 { Err("ゼロで割ることはできません ".to_string()) } else { Ok(a / b) } } // 呼び出し側 let result = divide(10.0, 0.0); if result.is_ok() { println!("結果: {:?}", result.unwrap()); } else { println!("エラー: {:?}", result.unwrap_err()); } /** * @return Result<float, string> */ function divide(float $a, float $b): Result { if ($b == 0.0) { return new Err("ゼロで割ることはできません "); } return new Ok($a / $b); } // 呼び出し側 $result = divide(10.0, 0.0); if ($result->isOk()) { echo "結果: " . $result->unwrap() . "\n"; } else { echo "エラー: " . $result->unwrapErr() . "\n"; } Rust PHP(僕の実装)
  3. fn divide(a: f64, b: f64) -> Result<f64, String> { if

    b == 0.0 { Err("ゼロで割ることはできません ".to_string()) } else { Ok(a / b) } } // 呼び出し側 let result = divide(10.0, 0.0); if result.is_ok() { println!("結果: {:?}", result.unwrap()); } else { println!("エラー: {:?}", result.unwrap_err()); } /** * @return Result<float, string> */ function divide(float $a, float $b): Result { if ($b == 0.0) { return new Err("ゼロで割ることはできません "); } return new Ok($a / $b); } // 呼び出し側 $result = divide(10.0, 0.0); if ($result->isOk()) { echo "結果: " . $result->unwrap() . "\n"; } else { echo "エラー: " . $result->unwrapErr() . "\n"; } Rust PHP(僕の実装)
  4. function completePayment(ProcessingPayment $payment): CompletedPayment { if ($payment->amount <= 0) {

    throw new PaymentAmountRuleError("Payment amount must be positive"); } return new CompletedPayment($payment->amount); } // 呼び出し側 try { $result = completePayment($payment); // ... 支払い完了時の処理 } catch (PaymentAmountRuleError $e) { // ... 支払金エラー時の処理 }
  5. function completePayment(ProcessingPayment $payment): CompletedPayment { if ($payment->amount <= 0) {

    throw new PaymentAmountRuleError("Payment amount must be positive"); } return new CompletedPayment($payment->amount); } // 呼び出し側 try { $result = completePayment($payment); // ... 支払い完了時の処理 } catch (PaymentAmountRuleError $e) { // ... 支払金エラー時の処理 }
  6. function completePayment(ProcessingPayment $payment): ?CompletedPayment { if ($payment->amount <= 0) {

    return null; } return new CompletedPayment($payment->amount); } // 呼び出し側 $result = completePayment($payment); if ($result === null) { // ... 支払金エラー時の処理 } // ... 支払い完了時の処理
  7. function completePayment(ProcessingPayment $payment): ?CompletedPayment { if ($payment->amount <= 0) {

    return null; } return new CompletedPayment($payment->amount); } // 呼び出し側 $result = completePayment($payment); if ($result === null) { // ... 支払金エラー時の処理 } // ... 支払い完了時の処理
  8. function completePayment(ProcessingPayment $payment): CompletedPayment|PaymentAmountRuleError { if ($payment->amount <= 0) {

    return new PaymentAmountRuleError("Payment amount must be positive"); } return new CompletedPayment($payment->amount); } // 呼び出し側 $result = completePayment($payment); if ($result instanceof PaymentAmountRuleError) { // ... 支払金エラー時の処理 } // ... 支払い完了時の処理
  9. function completePayment(ProcessingPayment $payment): CompletedPayment|PaymentAmountRuleError { if ($payment->amount <= 0) {

    return new PaymentAmountRuleError("Payment amount must be positive"); } return new CompletedPayment($payment->amount); } // 呼び出し側 $result = completePayment($payment); if ($result instanceof PaymentAmountRuleError) { // ... 支払金エラー時の処理 } // ... 支払い完了時の処理
  10. /** * @return Result<CompletedPayment, PaymentAmountRuleError> */ function completePayment(ProcessingPayment $payment): Result

    { if ($payment->amount <= 0) { return new Err(new PaymentAmountRuleError("Payment amount must be positive")); } return new Ok(new CompletedPayment($payment->amount)); } // 呼び出し側 $result = completePayment($payment); if ($result->isErr()) { return match (true) { $result->unwrapErr() instanceof PaymentAmountRuleError::class => // ... 支払金エラー時の処理 } } // ... 支払い完了時の処理
  11. /** * @return Result<CompletedPayment, PaymentAmountRuleError> */ function completePayment(ProcessingPayment $payment): Result

    { if ($payment->amount <= 0) { return new Err(new PaymentAmountRuleError("Payment amount must be positive")); } return new Ok(new CompletedPayment($payment->amount)); } // 呼び出し側 $result = completePayment($payment); if ($result->isErr()) { return match (true) { $result->unwrapErr() instanceof PaymentAmountRuleError::class => // ... 支払金エラー時の処理 } } // ... 支払い完了時の処理
  12. • PHPの静的解析ツール ◦ 未使用の変数や型の不一致などを発見してくれる ◦ @templeteでジェネリクスも使用できる • CLIで実行できる ◦ CIでチェックできる

    • レベルがある(0~10) ◦ 10になるほど型が厳密になっていく • baselineで既存のエラーを一旦無視もできる PHPStan
  13. /** * @template T * @template E */ interface Result

    { // 各関数 } /** * @template E * @implements Result<never, E> */ final readonly class Err implements Result { /** * @param E $value */ public function __construct( private mixed $value, ) {} // エラー時の関数 } /** * @template T * @implements Result<T, never> */ final readonly class Ok implements Result { /** * @param T $value */ public function __construct( private mixed $value, ) {} // 成功時の関数 }
  14. /** * @template E * @implements Result<never, E> */ final

    readonly class Err implements Result { // … public function isOk(): false { return false; } } /** * @template T * @implements Result<T, never> */ final readonly class Ok implements Result { // … public function isOk(): true { return true; } } /** * @template T * @template E */ interface Result { public function isOk(): bool; }
  15. /** * @template E * @implements Result<never, E> */ final

    readonly class Err implements Result { // … public function isErr(): true { return true; } } /** * @template T * @implements Result<T, never> */ final readonly class Ok implements Result { // … public function isErr(): false { return false; } } /** * @template T * @template E */ interface Result { // … public function isErr(): bool; }
  16. /** * @template E * @implements Result<never, E> */ final

    readonly class Err implements Result { // … } /** * @template T * @implements Result<T, never> */ final readonly class Ok implements Result { // … /** * @return T */ public function unwrap() { return $this->value; } } /** * @template T * @template E */ interface Result { // … }
  17. /** * @template E * @implements Result<never, E> */ final

    readonly class Err implements Result { // ... /** * @return E */ public function unwrapErr() { return $this->value; } } /** * @template T * @implements Result<T, never> */ final readonly class Ok implements Result { // … } /** * @template T * @template E */ interface Result { // … }
  18. /** * @template E * @implements Result<never, E> */ final

    readonly class Err implements Result { // ... /** * @template D * @param D $default * @return D */ public function unwrapOr(mixed $default) { return $default; } } /** * @template T * @implements Result<T, never> */ final readonly class Ok implements Result { // ... /** * @template D * @param D $default * @return T */ public function unwrapOr(mixed $default) { return $this->value; } } /** * @template T * @template E */ interface Result { // ... /** * @template D * @param D $default * @return ($this is Result<T, E> ? T|D : ($this is Result<never, E> ? D : T)) */ public function unwrapOr(mixed $default); }
  19. /** * @return Result<CompletedPayment, PaymentAmountRuleError> */ function completePayment(ProcessingPayment $payment): Result

    { if ($payment->amount <= 0) { return new Err(new PaymentAmountRuleError("Payment amount must be positive")); } return new Ok(new CompletedPayment($payment->amount)); } // 呼び出し側 $result = completePayment($payment); if ($result->isErr()) { return match (true) { $result->unwrapErr() instanceof PaymentAmountRuleError::class => // ... 支払金エラー時の処理 } } // ... 支払い完了時の処理 使い方
  20. /** * @return Result<CompletedPayment, PaymentAmountRuleError> */ function completePayment(ProcessingPayment $payment): Result

    { if ($payment->amount <= 0) { return new Err(new PaymentAmountRuleError("Payment amount must be positive")); } return new Ok(new CompletedPayment($payment->amount)); } // 呼び出し側 $result = completePayment($payment); if ($result->isErr()) { return match (true) { $result->unwrapErr() instanceof PaymentAmountRuleError::class => // ... 支払金エラー時の処理 } } // ... 支払い完了時の処理 使い方
  21. エラーが一つしか返せない ❌ 一つのエラーしか返却できない 失敗時のジェネリクスで @templateを使用 ⭕ 複数のエラーを返却できる 失敗時のジェネリクスで @template-covariantを使用 /**

    * @template E * @implements Result<never, E> */ final readonly class Err implements Result /** * @template-covariant E * @implements Result<never, E> */ final readonly class Err implements Result
  22. エラーが一つしか返せない @template 非変 @template-contravariant 反変 引用:PHPDocを使ったPHPのジェネリクス[https://www.phper.ninja/entry/2020/03/01/054833] 引数の型は反変(contravariant)でなくてはならない 戻り値は共変(covariant)でなくてはならない @template-covariant 共変

    引用: PHPマニュアル[https://www.php.net/manual/ja/language.oop5.variance.php] 共変性とは、子クラスのメソッドが、親クラスの戻り値よりも、より特定の、狭い型を返すことを許すことです。 反変性とは、親クラスのものよりも、より抽象的な、広い型を引数に指定することを許すものです。
  23. 型のNarrowingできてなかった ❌ 型のNarrowingできない ⭕ 型のNarrowingできる interface Result { public function

    isOk(): bool; } interface Result { /** * @phpstan-assert-if-true Ok<T> $this * @phpstan-assert-if-false Err<E> $this */ public function isOk(): bool; }
  24. IDEがメソッドを補完してくれない 推論してくれない interface Result { public function isOk(): bool; public

    function isErr(): bool; public function unwrapOr(mixed $default); } PHPDocは 省略してます
  25. IDEがメソッドを補完してくれない 推論してくれない interface Result { public function isOk(): bool; public

    function isErr(): bool; public function unwrapOr(mixed $default); } PHPDocは 省略してます
  26. IDEがメソッドを補完してくれない 推論してくれない 推論してくれる interface Result { public function isOk(): bool;

    public function isErr(): bool; public function unwrapOr(mixed $default); } interface Result { public function isOk(): bool; public function isErr(): bool; public function unwrap(); public function unwrapErr(); public function unwrapOr(mixed $default); } PHPDocは 省略してます PHPDocは 省略してます
  27. /** * @template E * @implements Result<never, E> */ final

    readonly class Err implements Result { // … } /** * @template T * @implements Result<T, never> */ final readonly class Ok implements Result { // … /** * @return T */ public function unwrap() { return $this->value; } } /** * @template T * @template E */ interface Result { // … }
  28. /** * @template E * @implements Result<never, E> */ final

    readonly class Err implements Result { // … /** * @return never */ public function unwrap() { throw new LogicException('called Result->unwrap() on an err value'); } } /** * @template T * @implements Result<T, never> */ final readonly class Ok implements Result { // … /** * @return T */ public function unwrap() { return $this->value; } } /** * @template T * @template E */ interface Result { // … public function unwrap() }
  29. /** * @template E * @implements Result<never, E> */ final

    readonly class Err implements Result { // ... /** * @return E */ public function unwrapErr() { return $this->value; } } /** * @template T * @implements Result<T, never> */ final readonly class Ok implements Result { // … } /** * @template T * @template E */ interface Result { // … }
  30. /** * @template E * @implements Result<never, E> */ final

    readonly class Err implements Result { // ... /** * @return E */ public function unwrapErr() { return $this->value; } } /** * @template T * @implements Result<T, never> */ final readonly class Ok implements Result { // … /** * @return never */ public function unwrapErr() { throw new LogicException('called Result->unwrapErr() on an ok value'); } } /** * @template T * @template E */ interface Result { // … public function unwrapErr() }
  31. IDEがメソッドを補完してくれない いい感じにしたい! interface Result { // … /** * @return

    ($this is Result<T, never> ? T : never) */ public function unwrap(); } Okの時 Tが返却される
  32. IDEがメソッドを補完してくれない いい感じにしたい! interface Result { // … /** * @return

    ($this is Result<T, never> ? T : never) */ public function unwrap(); } Ok以外の時 neverが返却される (throwされる)
  33. IDEがメソッドを補完してくれない interface Result { // … /** * @return ($this

    is Result<T, never> ? T : never) */ public function unwrap(); }
  34. IDEがメソッドを補完してくれない interface Result { // … /** * @return ($this

    is Result<T, never> ? T : LogicException&never) */ public function unwrap(); }
  35. 自己紹介 ひがき 愛媛 → 兵庫 → 福岡 → 東京 福岡は約2年住んでました!!

    大好きな街!!! カンファレンス初参加は 「PHPカンファレンス福岡2023」 人生変わりました(本当に!!)