phpのenumで状態遷移の管理をする

## 今回試す状態遷移
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348060/9b723af3-2245-e7e0-e9c6-f8bdd20000ab.png)


- 必ず図の流れで遷移する
(例: 
販売中→承認済みなどスキップして遷移できない。
申請 or 承認済みからキャンセルになる場合もある。
)

- phpバージョン: 8.1.7

## コード

```php
<?php

declare(strict_types=1);

enum State
{
    case 販売中;
    case キャンセル;
    case 申請;
    case 承認済み;
    case 契約済み;

    public function allowedStatuses(): array
    {
        return [
            State::販売中->name => [State::申請],
            State::申請->name => [State::承認済み, State::キャンセル],
            State::承認済み->name => [State::契約済み, State::キャンセル],
        ];
    }

    public function setStatus(State $from, State $to): State
    {
        $fromIndex = $from->name;
        $allowedStatuses = $this->allowedStatuses();
        if (!array_key_exists($fromIndex, $allowedStatuses)) {
            throw new Exception('遷移元の値がおかしい');
        }

        if (!in_array($to, $allowedStatuses[$fromIndex], true)) {
            throw new Exception('不正な遷移だ');
        }

        return $to;
    }
}

final class Example {

    public function __get(string $name): mixed
    {
        if (!property_exists($this, $name)) {
            return null;
        }
        return $this->$name;
    }

    public function __construct(
        private State $state
    ) {
    }

    public function setState(State $from, State $to): void
    {
        $this->state = $this->state->setStatus($from, $to);
    }
}

```

## 実行: 正常系と異常系をいくつか試し

#### 販売中→申請(正常)
<details><summary>サンプルコード</summary>

```php
$example = new Example(State::販売中);
echo $example->state->name;//販売中
try {
    $example->setState($example->state, State::申請);
} catch (Exception $e) {
    echo $e->getMessage();
}
echo $example->state->name;//申請
```
</details>

#### 申請→キャンセル(正常)
<details><summary>サンプルコード</summary>

```php
$example = new Example(State::申請);
echo $example->state->name;//申請
try {
    $example->setState($example->state, State::キャンセル);
} catch (Exception $e) {
    echo $e->getMessage();
}
echo $example->state->name;//キャンセル

```
</details>

#### 承認済み→申請(異常)

<details><summary>サンプルコード</summary>

```php
$example = new Example(State::承認済み);
echo $example->state->name;//承認済み
try {
    $example->setState($example->state, State::申請);
} catch (Exception $e) {
    echo $e->getMessage();exit;//不正な遷移だ
}
echo $example->state->name;// (出力なし)
```
</details>


#### 契約済み→キャンセル(異常)
<details><summary>サンプルコード</summary>

```php
$example = new Example(State::契約済み);
echo $example->state->name;//契約済み
try {
    $example->setState($example->state, State::キャンセル);
} catch (Exception $e) {
    echo $e->getMessage();exit;//遷移元の値がおかしい
}
echo $example->state->name;// (出力なし)
```
</details>


## 説明
- 列挙型の値を日本語で書いてみました。(allowedStatuses()部分に関してはかなりわかりやすくなった)
- fromからtoへ遷移可能かをチェックするための配列
fromがkeyに、toがarray[key]の配列中身にあれば遷移可能
```php
public function allowedStatuses(): array
{
    return [
        State::販売中->name => [State::申請],
        State::申請->name => [State::承認済み, State::キャンセル],
        State::承認済み->name => [State::契約済み, State::キャンセル],
    ];
}
```


```php
public function setStatus(State $from, State $to): State
{
    $fromIndex = $from->name;
    $allowedStatuses = $this->allowedStatuses();
    if (!array_key_exists($fromIndex, $allowedStatuses)) {
        throw new Exception('遷移元の値がおかしい');
    }

    if (!in_array($to, $allowedStatuses[$fromIndex], true)) {
        throw new Exception('不正な遷移だ');
    }

    return $to;
}
```

## 感想
- 今回ドメインモデルのvalueObjectとして扱い、使いやすいと思った。
- 公式で状態を持つのは禁止と書かれているが、今回は状態の遷移管理で状態を持っているわけでは無いので大丈夫だと思う(そもそも継承することもされることもないのが仕様ないのでできないと思われる。)
>列挙型では状態を持つことが禁止されています
https://www.php.net/manual/ja/language.enumerations.object-differences.php

- 調べてみるとstateパターンというのもあった。もし間違った使い方やこうやった方が良いよなことがありましたらコメント頂けますと幸いです。