PHP: 振る舞いを持つEnumの作り方

本稿ではPHPで振る舞いを持つEnumの実装方法を順を追って説明していく。

Enumを定数で実装する方法も考えられるが、本稿ではクラスを使う。クラスでEnumを実装すると、それぞれの値に振る舞い(メソッド)を持たせられる利点がある。加えて、クラスで表現されたEnumはそれを利用するコードでクラス名を型宣言に使えるので、型安全なコードを書ける長所もある。

本稿では「支払い状況」を例にEnumを作っていく。まず、支払い状況は4つの状態があるものと考えよう。

1. 保留(pending)
2. 承認済み(approved)
3. 却下(rejected)
4. 支払い済み(paid)

これらを実装していく過程を見ていこう。

なお、本稿で実装したコードは[GitHubで公開している](https://github.com/suin/php-playground/tree/master/HowToImplementEnumWithClass)ので、完成版はそちらをご覧いただきたい。

まず、「支払い状況」のEnumクラスを作る。このクラスは継承する必要がないため`final`クラスにする。

```php
/**
 * 支払状況
 */
final class PaymentStatus
{
}
```

入れ物ができたので、ステートごとに一つ定数を作り、`PaymentStatus`クラスのメンバに含める。

```php
final class PaymentStatus
{
    public const PENDING = 0;

    public const APPROVED = 1;

    public const REJECTED = 2;

    public const PAID = 3;
}
```

`PaymentStatus`オブジェクトが状態を持てるように`$status`属性を追加し、コンストラクタで受け取るようにする。

```php
final class PaymentStatus
{
    // ...

    /**
     * @var int
     */
    private $status;

    public function __construct(int $status)
    {
        $this->status = $status;
    }
}
```

最後に支払い状況から、支払い処理が「完了」しているのかを判定する振る舞いをEnumに持たせる。

```php
final class PaymentStatus
{
    // ...

    /**
     * 支払い済みもしくは支払い却下になったか
     */
    public function isCompleted(): bool
    {
        return $this->status === self::REJECTED
            || $this->status === self::PAID;
    }
}
```

これで、クライアントコードは、次のように定義した振る舞いの`isCompleted`メソッドを利用できるようになる。

```php
function display_completion_status(PaymentStatus $paymentStatus): void
{
    echo $paymentStatus->isCompleted() ? '完了' : '未完了';
}
```

## Enumオブジェクトの使い方に制約を加える

この`PaymentStatus`クラスの実装にはひとつ問題がある。このクラスのコンストラクタは`int`であれば何でも受け付けてしまうので、4つのステータス(`PENDING`, `APPROVED`, `REJECTED`, `PAID`)以外でも予期せず動いてしまう。

```php
new PaymentStatus(9999); // 予期しないコード
```

また、`PaymentStatus`クラスの定数が`public`なので、`PaymentStatus::PENDING`などが直接参照されて使われてしまう恐れもある。これができてしまうと、せっかくカプセル化した「支払い処理が完了しているのかを判定する振る舞い」のロジックがクライアントコードに流れ出たりする。

```php:クライアントコード
// 望まない定数の使われ方
$paymentStatus = PaymentStatus::PENDING;

if ($paymentStatus === PaymentStatus::REJECTED 
 || $paymentStatus === PaymentStatus::PAID) {
    // ...
}
```

これらを防ぐために、コンストラクタと定数は`private`にし、クライアントコードが触れないようにする。

```php
final class PaymentStatus
{
    private const PENDING = 0;

    private const APPROVED = 1;

    private const REJECTED = 2;

    private const PAID = 3;

    // ...

    private function __construct(int $status)
    {
        $this->status = $status;
    }

    // ...
}
```

このままだとクライアントコードは`PaymentStatus`クラスのインスタンスが作れないので、クライアントコード向けのインスタンス作成手段を静的メソッドで提供する。

```php
final class PaymentStatus
{
    // ...

    public static function pending(): self
    {
        return new self(self::PENDING);
    }

    public static function approved(): self
    {
        return new self(self::APPROVED);
    }

    public static function rejected(): self
    {
        return new self(self::REJECTED);
    }

    public static function paid(): self
    {
        return new self(self::PAID);
    }
}
```

クライアントコードはEnumを使うとき次のように呼び出すようになる:

```php
$pending = PaymentStatus::pending();
```

## `PaymentStatus`クラスの完成形

ここまでの実装過程を経ればEnumクラスは完成だ。`PaymentStatus`クラスの完成形は次のようになる:

```php
/**
 * 支払状況
 */
final class PaymentStatus
{
    private const PENDING = 0;

    private const APPROVED = 1;

    private const REJECTED = 2;

    private const PAID = 3;

    /**
     * @var int
     */
    private $status;

    /**
     * Enumオブジェクトは、このクラスの静的メソッドからのみ作られることを保証するために、コン
     * ストラクタの可視性はprivateにする。
     */
    private function __construct(int $status)
    {
        $this->status = $status;
    }

    /**
     * 「保留」の状況を表すオブジェクトを返す
     */
    public static function pending(): self
    {
        return new self(self::PENDING);
    }

    /**
     * 「承認済み」の状況を表すオブジェクトを返す
     */
    public static function approved(): self
    {
        return new self(self::APPROVED);
    }

    /**
     * 「支払い却下」の状況を表すオブジェクトを返す
     */
    public static function rejected(): self
    {
        return new self(self::REJECTED);
    }

    /**
     * 「支払い済み」の状況を表すオブジェクトを返す
     */
    public static function paid(): self
    {
        return new self(self::PAID);
    }

    /**
     * 支払い済みもしくは支払い却下になったか
     */
    public function isCompleted(): bool
    {
        return $this->status === self::REJECTED
            || $this->status === self::PAID;
    }
}
```

## テストコードを書く

最後にPHPUnitでテストコードを書いてEnumの振る舞いをチェックしておく。

```php:PaymentStatusTest.php
use PHPUnit\Framework\TestCase;

/**
 * 支払状況のテストコード
 */
final class PaymentStatusTest extends TestCase
{
    public function test_pending_should_be_incompleted_state(): void
    {
        self::assertFalse(PaymentStatus::pending()->isCompleted());
    }

    public function test_approved_should_be_incompleted_state(): void
    {
        self::assertFalse(PaymentStatus::approved()->isCompleted());
    }

    public function test_rejected_should_be_completed_state(): void
    {
        self::assertTrue(PaymentStatus::rejected()->isCompleted());
    }

    public function test_paid_should_be_completed_state(): void
    {
        self::assertTrue(PaymentStatus::paid()->isCompleted());
    }
}
```

テスト結果

```console
$ phpunit --testdox --color --verbose PaymentStatusTest.php
PHPUnit 7.3.5 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.1.25 with Xdebug 2.6.1

PaymentStatus
 ✔ Pending should be incompleted state [2.75 ms]
 ✔ Approved should be incompleted state [0.31 ms]
 ✔ Rejected should be completed state [0.33 ms]
 ✔ Paid should be completed state [0.20 ms]

Time: 37 ms, Memory: 4.00MB

OK (4 tests, 4 assertions)
```

以上で、PHPで振る舞いを持つEnumの実装は完了になる。