💪

PHP8.1で表現する「ぼくがかんがえたさいきょうのじょうたいせんい」

2022/10/08に公開

お前誰よ?

こんにちは!
tyamahoriです。

普段は、PHP/Laravelを利用してAPIアプリケーションを開発している人です。アプリケーション設計に興味があり、DDDだ!、クリーンアーキテクチャだ!、責務だ!、見通しだ!、ドメイン知識だ!と普段から騒いでいる人です。

で、何が言いたいのよ?

  • PHP8.1において、matchとenumを使うと何かと便利な表現ができるよ!
  • 状態遷移をいい感じに表現してみるよ!

状態遷移を表してみる

皆さんは、状態遷移の管理はどうされてますか?悩みどころ多くないですか?今回、ぼくがかんがえたさいきょうのじょうたいせんいをPHP8.1を利用して表現してみます。

コードは以下の通りです

<?php

declare(strict_types=1);

namespace Package\SomeSpecificApplication\CreateApp\Domain\Entity;

use DomainException;

enum StatusEnum: string
{
    case ONE = 'one';

    case TWO = 'two';

    case THREE = 'three';

    case FOUR = 'four';

    case FIVE = 'five';

    case SIX = 'six';

    case SEVEN = 'seven';

    /**
     * 外部向けに表示するラベルを定義する
     * @return string
     */
    private function displayLabel(): string
    {
        return match ($this) {
            self::ONE => 'その1',
            self::TWO => 'その2',
            self::THREE => 'その3',
            self::FOUR => 'その4',
            self::FIVE => 'その5',
            self::SIX => 'その6',
            self::SEVEN => 'その7',
        };
    }

    /**
     * 次に取れるステータスを取得する
     * @return StatusEnum[]
     */
    private function nextStatuses(): array
    {
        return match ($this) {
            self::ONE => [
                self::TWO,
                self::THREE,
            ],
            self::TWO => [
                self::THREE,
                self::FOUR,
            ],
            self::THREE => [
                self::FOUR,
                self::FIVE,
            ],
            self::FOUR => [
                self::FIVE,
                self::SIX,
            ],
            self::FIVE => [
                self::SIX,
                self::SEVEN,
            ],
            self::SIX => [
                self::SEVEN,
                self::ONE,
            ],
            self::SEVEN => [
                self::ONE,
                self::TWO,
            ],
        };
    }

    /**
     * @param StatusEnum $nextStatus
     * @return $this
     */
    public function to(self $nextStatus): self
    {
        if (!in_array($nextStatus, $this->nextStatuses(), true)) {
            throw new DomainException(
                "Cannot change status from '$this->value' to '$nextStatus->value'"
            );
        }

        return self::from($nextStatus->value);
    }

    /**
     * StatusEnumで定義しているすべてのvalueとそのラベルの配列を返す
     * @return array
     */
    public static function selectList(): array
    {
        $array = [];
        foreach (self::cases() as $item) {
            $array[$item->value] = $item->displayLabel();
        }

        return $array;
    }

    /**
     * そのStatusEnumの次に取れるvalueとラベルの配列を返す
     * @return array
     */
    public function nextStatusList(): array
    {
        $array = [];
        foreach ($this->nextStatuses() as $item) {
            $array[$item->value] = $item->displayLabel();
        }

        return $array;
    }
}

サンプルのテストコードはこちら

<?php

namespace Tests\Unit;

use DomainException;
use Package\SomeSpecificApplication\CreateApp\Domain\Entity\StatusEnum;
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    /**
     * @test
     * @return void
     */
    public function 次に行けないステータスの場合はDomainExceptionが発生する(): void
    {
        $this->expectException(DomainException::class);
        StatusEnum::FOUR->to(StatusEnum::ONE);
    }

    /**
     * @test
     * @return void
     */
    public function 次のステータスに行ける(): void
    {
        $currentStatus = StatusEnum::FOUR;
        $nextStatus = StatusEnum::FIVE;

        $status = $currentStatus->to($nextStatus);
        $this->assertSame($nextStatus, $status);
    }

    /**
     * @test
     * @return void
     */
    public function ステータス一覧が配列で帰ってくる(): void
    {
        $actual = StatusEnum::selectList();

        $this->assertIsArray($actual);
        $this->assertCount(7, $actual);
    }

    /**
     * @test
     * @return void
     */
    public function ステータスoneからtwoかthreeに遷移できる(): void
    {
        $enum1 = StatusEnum::ONE;

        $enum2 = $enum1->to(StatusEnum::TWO);
        $enum3 = $enum1->to(StatusEnum::THREE);

        $this->assertIsArray($enum1->nextStatusList());
        $this->assertCount(2, $enum1->nextStatusList());
        $this->assertSame($enum2, StatusEnum::TWO);
        $this->assertSame($enum3, StatusEnum::THREE);
    }
}

コードの注意点とか

namespaceはサンプルですので、手元で動かす際は、書き直してください。また、テストコードは最小限なので、テストケースを増やすなり、dataproviderを使うなりして、きっちりと書いていく必要はありそうです。

考えたこととか

displayLabel()

Enumに対応するラベルを定義しています。システムの利用者に対してどんな常態なのかを伝えるために使うメソッドとして想定しています。

nextStatuses()

現在のステータスから次に取れるステータスを定義しています。matchと組み合わせて、次に取れるステータスの配列を取得します。

to()

ステータスを遷移させるメソッドです。次のステータスEnumを引数で私、問題がなければ次のステータスEnumが返り、問題があれば例外が発生するようになっています。

selectList()

配列を取得します。配列のkeyはEnumで定義したvalueの値、配列のvalueはEnumのdisplayLabel()で定義したラベルになったものです。用途としては管理画面のselectboxなどで、ステータスを表示する場合を想定しています。

nextStatusList()

selectList()とロジックはほぼ同じです。違うのは、次に取ることがステータスの配列が返されることです。

テスト

StatusEnumの最低限の使い方を表してはいます。必要に応じて書き足したりしていきます。

まとめ

enumとmatchをうまく組み合わせることで、状態遷移を表せました。実際の現場では更にロジックが複雑にはなるかとは思います。。ただ、実装の取っ掛かりになると嬉しいです!

Discussion