🙆‍♀️

PHPでメソッドの失敗を表現する

2025/02/06に公開

phpでメソッドの失敗を表現する方法について(PHPに限らず)、いろいろやり方は考えられるが、どれがいいのかしっくりこなかったので、記事にしてまとめてみました。
おすすめのやり方や考え方があれば是非コメントで教えていただければ嬉しいです。

今回は認証を行うメソッドを想定します。
認証処理が失敗する主な原因としては以下が考えられます。
・IDに該当するユーザ存在しない
・パスワードが間違っている
これらの失敗原因をうまく表現する方法について考えていきます。

特別な型(falseやnull)で表現する

まず考えられるのは、取得したい型以外の、失敗を表現する特別な型を使用することです。

class AuthService
{
    public function authenticate(string $id, string $password): User | false // ?User
    {
        $user_id = new UserId($id);
        $password = new Password($password);
            
        $user = $this->user_repository->find($user_id);
        // ユーザが存在するか?
        if (is_null($user)) return false;

        // パスワードが正しいか?
        return $user->isPasswordEqualTo($password) ? $user : false;
    }
}

シンプルですが、失敗の原因を表現することができません。
失敗の原因をフロントエンド等に伝える必要がある場合には適していません。(ユーザに認証失敗の原因を伝えたい場合とか)
?Userのようにして、nullで失敗を表現してもいいかもしれません。

例外を使う

次はカスタム例外クラスを使う方法です。

class AuthService
{
    public function authenticate(string $id, string $password): User
    {
        $user_id = new UserId($id);
        $password = new Password($password);
            
        $user = $this->user_repository->find($user_id);
        if (is_null($user)) throw new UserNotFoundException();

        return $user->isPasswordEqualTo($password)
            ? $user
            : throw new InvalidPasswordException();
    }
}

この場合、カスタム例外クラスの型等を使って認証の失敗理由を表現することができます。
ただ、例外を認証の失敗という普通に起こり得るケースに使用するのは、本来の例外の使い方から外れていると考えることもできます。
(ユーザが入力したIDやパスワードが間違うという状況は容易に起こり得える)
また、メソッドがどのような例外を投げるかはメソッド内部を見るしかない(PHPDocを使ってもいいが、コードと乖離する可能性をはらむ)ので、メソッドの挙動がわかりづらい。

Enumを使う

最後はEnumを使う方法です。

class AuthService
{
    public function authenticate(string $id, string $password): AuthenticationResult
    {
        $user_id = new UserId($id);
        $password = new Password($password);
            
        $user = $this->user_repository->find($user_id);
        if (is_null($user)) return AuthenticateResult::UserNotFound;

        return $user->isPasswordEqualTo($password)
            ? AuthenticationResult::Success
            : AuthenticationResult::IncorrectPassword;
    }
}

enum AuthenticationResult
{
    case Success;
    case UserNotFound;
    case IncorrectPassword;

    public ?User $user;

	public function __construct(
		public readonly ?User $user
	) {}

    public function isSuccessful(): bool
    {
        return $this === AuthenticationResult::Success;
    }

    public function getUser(): User
    {
        if (is_null($this->user)) throw new LogicException('you get user only if authentication is success.');

        return $this->user;
    }
}

このようにEnumを使うことで、認証の結果をシグネチャで表現することができ、明示的になりました。
また、Enumにはメソッドを定義することもできるので、成功時に必要な情報を持たせることもできます。

まとめ

失敗の原因を表明する必要がある場合にはEnumを、そうでないシンプルなケースではfalseやnullを使えばいいのではないでしょうか。
多言語だと、ResultとかEitherといった型が用意されていることもあるので、今度そちらについても調べて何かあれば追記します。

Discussion