🐘

PHPでResult型やってみる

に公開

2025/07/20追記

続編となる記事を書きました!
ご参考までに

https://zenn.dev/higaki/articles/my-php-result-type-more

はじめに

こんにちは。ひがきです。

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を実装する
  • ResultOkErrの具体は@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の実装

isOkResultの中身が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の実装

isErrResultの中身が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の実装

unwrapOkの値を返却する関数です。

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の実装

unwrapErrErrの値を返却する関数です。

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の実装

unwrapOrResultの型が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を実装するクラスはOkErrの2つだけであることを伝えることができます。

https://phpstan.org/developing-extensions/allowed-subtypes

あと、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関数はPaymentAmountRuleErrorPaymentMethodNotRegistedErrorの失敗の可能性があります。

呼び出し側
// 呼び出し側
$result = completePayment($payment);
if ($result->isErr()) {
    match (true) {
       $result->unwrapErr() instanceof PaymentAmountRuleError::class => // ... 支払金エラー時の処理
    }
}
// ... 支払い完了時の処理

しかし、PaymentMethodNotRegistedErrorがmatch文でチェックできていないので、PHPStanがエラーを出力してくれます。

※ Union型でも網羅チェックはできますが、失敗時と成功時を同列でチェックを行う必要があります。

まとめ

PHPでResult型を実装する方法について説明しました。

この記事がPHPでResult型を実装する際の参考になれば幸いです。

Discussion