PHPでResult型やってみる
2025/07/20追記
続編となる記事を書きました!
ご参考までに
はじめに
こんにちは。ひがきです。
PHPカンファレンス関西2025の「PHPでResult型(クラス)やってみよう」で時間の関係で省略せざるを得ない部分の補足資料となります。
どうやってPHPでResult型を実装するかの部分についての説明をこの記事に記載しておきます。
Special Thanks
PHPでResult型を実装する際にtadsanに相談に乗っていただきました。本当にありがとうございました。
どうやってPHPでResult型を実現するか
先に完成系のコードを載せておきます。
最終的なResult Interface
/**
* @template T
* @template E
*/
interface Result
{
/**
* @phpstan-assert-if-true Ok<T> $this
* @phpstan-assert-if-false Err<E> $this
*/
public function isOk(): bool;
/**
* @phpstan-assert-if-true Err<E> $this
* @phpstan-assert-if-false Ok<T> $this
*/
public function isErr(): bool;
/**
* @return ($this is Result<T, never> ? T : never)
*/
public function unwrap(): mixed;
/**
* @return ($this is Result<never,E> ? E : never)
*/
public function unwrapErr(): mixed;
/**
* @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): mixed;
}
最終的なOkクラス
/**
* @template T
* @implements Result<T, never>
*/
final readonly class Ok implements Result
{
/**
* @param T $value
*/
public function __construct(
private mixed $value,
) {
}
public function isOk(): true
{
return true;
}
public function isErr(): false
{
return false;
}
/**
* @return T
*/
public function unwrap(): mixed
{
return $this->value;
}
public function unwrapErr(): never
{
throw new LogicException('called Result->unwrapErr() on an ok value');
}
/**
* @template D
* @param D $default
* @return T
*/
public function unwrapOr(mixed $default): mixed
{
return $this->value;
}
}
最終的なErrクラス
/**
* @template E
* @implements Result<never, E>
*/
final readonly class Err implements Result
{
/**
* @param E $value
*/
public function __construct(
private mixed $value,
) {
}
public function isOk(): false
{
return false;
}
public function isErr(): true
{
return true;
}
public function unwrap(): never
{
throw new LogicException('called Result->unwrap() on an err value');
}
/**
* @return E
*/
public function unwrapErr(): mixed
{
return $this->value;
}
/**
* @template D
* @param D $default
* @return D
*/
public function unwrapOr(mixed $default): mixed
{
return $default;
}
}
基本方針
基本方針として以下としました。(抽象クラスで実装しても同じことができるはずです)
-
Result
のinterfaceを作成 -
Ok
クラスでResult
のinterfaceを実装する -
Err
クラスでResult
のinterfaceを実装する -
Result
でOk
とErr
の具体は@template
で受け取る
/**
* @template T
* @template E
*/
interface Result {
// 各関数を定義
}
/**
* @template T
* @implements Result<T, never>
*/
final readonly class Ok implements Result {
/**
* @param T $value
*/
public function __construct(
private mixed $value,
) {}
// 各関数を実装
}
/**
* @template E
* @implements Result<never, E>
*/
final readonly class Err implements Result {
/**
* @param E $value
*/
public function __construct(
private mixed $value,
) {}
// 各関数を実装
}
isOkの実装
isOk
はResult
の中身がOk
かチェックする関数です。
言い換えれば、ある処理の結果が成功したことを確認する関数です。
Result interface
interface Result
{
/**
* @phpstan-assert-if-true Ok<T> $this
* @phpstan-assert-if-false Err<E> $this
*/
public function isOk(): bool;
}
@phpstan-assert-if-true
と@phpstan-assert-if-false
で型のnarrowingを行なっています。
Ok
final readonly class Ok implements Result
{
// ...
public function isOk(): true
{
return true;
}
}
Err
final readonly class Err implements Result
{
// ...
public function isOk(): false
{
return false;
}
}
isErrの実装
isErr
はResult
の中身がErr
かチェックする関数です。
Result interface
interface Result
{
// ...
/**
* @phpstan-assert-if-true Err<E> $this
* @phpstan-assert-if-false Ok<T> $this
*/
public function isErr(): bool;
@phpstan-assert-if-true
と@phpstan-assert-if-false
で型のnarrowingを行なっています。
Ok
final readonly class Ok implements Result
{
// ...
public function isErr(): false
{
return false;
}
}
Err
final readonly class Err implements Result
{
// ...
public function isErr(): true
{
return true;
}
}
unwrapの実装
unwrap
はOk
の値を返却する関数です。
Result
の中身がErr
の時にunwrap
を実行するとLogicException
を投げてnever型を返すようにしました。
これにより、後続の処理を記載した際にPHPStanがエラーを吐いてくれるので、ある処理が失敗した時に成功時の値を取得しようしていることに気づけます。
Result interface
interface Result
{
// ...
/**
* @return ($this is Result<T, never> ? T : never)
*/
public function unwrap(): mixed;
}
interfaceでnever型か成功時の値が返ってくることを明示しました。
Ok
final readonly class Ok implements Result
{
/**
* @return T
*/
public function unwrap(): mixed
{
return $this->value;
}
}
Err
final readonly class Err implements Result
{
// ...
public function unwrap(): never
{
throw new LogicException('called Result->unwrap() on an err value');
}
}
unwrapErrの実装
unwrapErr
はErr
の値を返却する関数です。
Result
の中身がOk
の時にunwrapErr
を実行するとLogicException
を投げてnever型を返すようにしました。
これにより、後続の処理を記載した際にPHPStanがエラーを吐いてくれるので、ある処理が成功した時に失敗時の値を取得しようしていることに気づけます。
Result interface
interface Result
{
// ...
/**
* @return ($this is Result<never,E> ? E : never)
*/
public function unwrapErr(): mixed;
}
interfaceでnever型か失敗時の値が返ってくることを明示しました。
Ok
final readonly class Ok implements Result
{
// ...
public function unwrapErr(): never
{
throw new LogicException('called Result->unwrapErr() on an ok value');
}
}
Err
final readonly class Err implements Result
{
// ...
/**
* @return E
*/
public function unwrapErr(): mixed
{
return $this->value;
}
}
unwrapOrの実装
unwrapOr
はResult
の型がOk
の場合はOk
の値を返却し、Err
の場合は、引数に指定したものを返却します。
Result interface
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): mixed;
}
@return T|D
で充分だと思うんですが、 型のnarrowingも行うようにしました。
Ok
final readonly class Ok implements Result
{
// ...
/**
* @template D
* @param D $default
* @return T
*/
public function unwrapOr(mixed $default): mixed
{
return $this->value;
}
}
Err
final readonly class Err implements Result
{
// ...
/**
* @template D
* @param D $default
* @return D
*/
public function unwrapOr(mixed $default): mixed
{
return $default;
}
}
補足
PHPStanのAllowedSubtypesを使用することでPHPStanにResult
のinterfaceを実装するクラスはOk
とErr
の2つだけであることを伝えることができます。
あと、phpstan-sealedがリリースされると、AllowedSubTypesClassReflectionExtensionの代わりに、以下の記載だけで済むみたいです!
/**
* @phpstan-sealed Ok|Err
*/
interface Result {
// ...
}
Result型で嬉しいところ
複数のエラーの可能性がある場合、match
文などを使用することで、失敗時の対応の網羅チェックできます。
(PHPStanレベル5以上の場合)
/**
* @return Result<CompletedPayment, PaymentAmountRuleError|PaymentMethodNotRegistedError>
*/
function completePayment(ProcessingPayment $payment): Result {
if ($payment->amount <= 0) {
return new Err(new PaymentAmountRuleError("Payment amount must be positive"));
}
if ($payment->method == PaymentMethod::NotRegisterd) {
return new Err(new PaymentMethodNotRegistedError("Payment amount must be positive"));
}
return new Ok(new CompletedPayment($payment->amount));
}
上記のコードの場合、completePayment
関数はPaymentAmountRuleError
とPaymentMethodNotRegistedError
の失敗の可能性があります。
// 呼び出し側
$result = completePayment($payment);
if ($result->isErr()) {
match (true) {
$result->unwrapErr() instanceof PaymentAmountRuleError::class => // ... 支払金エラー時の処理
}
}
// ... 支払い完了時の処理
しかし、PaymentMethodNotRegistedError
がmatch文でチェックできていないので、PHPStanがエラーを出力してくれます。
※ Union型でも網羅チェックはできますが、失敗時と成功時を同列でチェックを行う必要があります。
まとめ
PHPでResult型を実装する方法について説明しました。
この記事がPHPでResult型を実装する際の参考になれば幸いです。
Discussion