PHP
DDD
enum
マジックメソッド
ValueObject

「PHPで列挙型(enum)を作る」を参考にValueObjectを作ったので一つ一つのメソッドの役割や意味をまとめてみた

前提

PHPでEnum型を作りたくて以下の記事を参考にした。
(記事の方ではなく、コメントに追記されている方のコードを利用)
https://qiita.com/Hiraku/items/71e385b56dcaa37629fe

ただ、コピペ芸人になりたくなかったので、
Enumクラスで使われているメソッドが何をしているのか、
一つ一つ自分なりに解釈してみたのでここにメモ。

コード

上記記事にあるコードを参考に自分が使う形に修正した。
予約のステータスを表すクラス。
このStatusクラス(ValueObject)は別途定義したReservationクラス(Entity)のプロパティとして定義している前提。

//Status.php
final class Status
{
    const ENUM = [
        'Contact' => 'Contact',
        'Confirmation' => 'Confirmation',
        'BeforePayment' => 'BeforePayment',
        'AfterPayment' => 'AfterPayment',
        'BeforeUse' => 'BeforeUse',
        'Used' => 'Used',
    ];

    private $status;

    public function __construct($key)
    {
        if (!static::isValidValue($key)) {
            throw new \InvalidArgumentException;
        }

        $this->status = self::ENUM[$key];
    }

    public static function isValidValue($key)
    {
        return array_key_exists($key, self::ENUM);
    }

    public function __toString()
    {
        return $this->status;
    }

    public function __callStatic($method, array $args)
    {
        return new self($method);
    }

    public function __set($key, $value)
    {
        throw new \BadMethodCallException('All setter is forbbiden');
    }
}

一つ一つのメソッド等の意味

1.Status型で使える値を限定するための定数の配列
ここに定義した値以外が使われた場合は例外を返す(コンストラクタで行う)

    const ENUM = [
        'Contact' => 'Contact',
        'Confirmation' => 'Confirmation',
        'BeforePayment' => 'BeforePayment',
        'AfterPayment' => 'AfterPayment',
        'BeforeUse' => 'BeforeUse',
        'Used' => 'Used',
    ];

2.コンストラクタ
定数ENUMで定義した値以外が引数に入った場合に例外を返す。
ENUMに定義した値だった場合はプロパティにその値をセットする。
isValidValue()を実行する時に遅延静的束縛で使うstatic::が使われているが、今回継承元のクラスがある訳ではないし、クラスにfinalがついていて継承もしない前提であればself::でもいい?)

    private $status;

    public function __construct($key)
    {
        //isValidValueは別途定義
        if (!static::isValidValue($key)) {
            throw new \InvalidArgumentException('定義されていないステータス');
        }

        $this->status = self::ENUM[$key];
    }

(例)

$status = new Status('Out of define');
// 結果:InvalidArgumentException: 定義されていないステータス

3.定義された値かどうかの判定
array_key_existsでnewする時に渡されたキーが定数ENUMの配列内に存在するか検証している。
これをコンストラクタで呼び出す時に実行することで予め定義した値以外を弾くようにしている。

    public static function isValidValue($key)
    {
        return array_key_exists($key, self::ENUM);
    }

4.インスタンスが文字列として呼び出された時に文字列の値を返す
__toStringはPHPで予め定義されているマジックメソッドで、インスタンスが文字列として呼び出された時の動作を指定する。
ここではnewした時にプロパティに設定された値(文字列)をそのまま返している。

多分ステータスを表示したい!と思った場合、
例えばecho $status;と文字列のように扱えた方が直観的、ということだと思う。このメソッドは無くてもクラス自体は使える。

    public function __toString()
    {
        return $this->status;
    }

(例)

$status = new Status('Confirmation');
echo $status;
// 'Confirmation'

5.静的メソッドの形でも呼び出せるようにする
__callStatic()もマジックメソッド。
定義されていない静的メソッドが呼び出された時の動作を指定する。

指定されたメソッド(::Contactや::Confirmation等)のメソッド名の文字列を引数にして自分自身のインスタンスを返している。

    public static function __callStatic($method, array $args)
    {
        return new self($method);
    }

(例)

$status = Status::Contact();

6.$statusへの書き込み時の動作を設定

予約の状態を適切に管理したいため、好き勝手に$statusが書き換えられないように
private $statusとしている。
privateのプロパティのようにアクセス不能プロパティへの書き込みが行われようとした際の動作を指定したい場合は__setマジックメソッドを使う。

    public function __set($key, $value)
    {
        throw new \BadMethodCallException('All setter is forbbiden');
    }

(例)

$status = new Status('Contact');
$status->status = 'Confirmation';
// BadMethodCallException: All setter is forbbiden

おまけ ステータスを変更する

このStatusクラス(ValueObject)はReservationクラス(Entity)のプロパティとして定義している。Reservationクラスのstatusを変更したい場合に以下の変更用のメソッドを使っている。

なお、ステータスを変更する場合は、
・変更用のメソッドを使う(直接Statusクラスのプロパティを書き換えない)
・変更に制限をかける(ContactからいきなりUsedにならない、等)
・現在のStatusインスタンス自体は変化せず、新しいインスタンスを作成して返す
ようにした。

以下は、現在がContactステータスの場合のみConfirmationステータスに変更出来るメソッド。

//Status.php
    public function toConfirmation()
    {
        if($this->status !== 'Contact'){
            throw new \InvalidArgumentException('このステータスには変更出来ません');
        }
        //このインスタンスの値を変更するのでは無く新しいインスタンスを作成して返す
        return new self('Confirmation');
    }

以上。