5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

laravelミューテータ/カスタムキャストを利用した定数型の値オブジェクトの実装例

Last updated at Posted at 2023-06-27

初めての投稿となります

ドメイン駆動設計の概念にある値オブジェクトですが
たとえば、値段と個数という値が存在した時に、間違えて個数を値段と勘違いをして
値段+個数と足し算してしまうミスが・・・
何てことあり得ませんよね?
それを防ぐために値オブジェクトクラスを作成するとか、ナンセンスだなぁなんて思った事があります
たいていの場合の値オブジェクトクラスはバリデーション専用のクラスみたいな感じになってしまっていて、上記の例だとどちらも0以上の整数なので、このバリデーションっている?と疑問に思う事もしばしば

そんな中で今までconstで切って定義していたステータス値などの定数を
値オブジェクトクラスとして使用すると使い勝手が良かったのでそれを紹介したいと思います
使用しているlaravelのバージョンは10となり、8以前のバージョンですとミューテータの書き方が少し異なります

たとえばこのような実装方法

    const STATUS_WAIT = 0;
    const STATUS_DOING = 1;
    const STATUS_END = 2;

    --- 略 ---

    // $objectはEloquentモデル
    $object = $repository->find($id); // DBから1件取得するような処理
    if($object->status === self::STATUS_WAIT) {
        // 何か処理
    }

これは単純に待機中、実行中、終了の定数が存在して、DBにその状態を表すstatusカラムが含まれるレコードが保存されており、DBから取得したレコードからstatusを見て何か処理を行うといったよくあるやつです
こういった実装方法を今回紹介する値オブジェクトクラスとカスタムキャストで作成すると管理が簡単になります

適用した書き方は以下となります

    // $objectはEloquentモデル
    $object = $repository->find($id); // DBから1件取得するような処理
    if($object->status->isWait()) {
        // 何か処理
    }    

実際に作成してみる

まずは定数型の値オブジェクトのベースとなるクラスを作成します
作成場所は任意で良いです
自分はapp/Domain/ValueObjectsという場所に保存しています
このベースとなるクラスは初回の1度だけ作成するクラスとなります

<?php

namespace App\Domain\ValueObjects;

abstract class AbstractValueObject
{
    const TYPES = [];

    protected int $type;

    /**
     * コンストラクタ
     * @param int $type
     */
    public function __construct(int $type)
    {
        if (!static::isValidValue($type)) {
            throw new \InvalidArgumentException;
        }

        $this->type = $type;
    }

    /**
     * インスタンス生成
     * @param $name
     * @param $arguments
     * @return static
     */
    public static function __callStatic($name, $arguments): static
    {
        return new static(static::TYPES[$name] ?? '');
    }

    /**
     * 値を取得
     * @return int
     */
    public function getValue(): int
    {
        return $this->type;
    }

    /**
     * 値からキーを取得
     */
    public function getKey(): bool|int|string
    {
        return array_search($this->type, static::TYPES);
    }

    /**
     * 値から小文字のキーを取得
     */
    public function getKeyLowerCase(): string
    {
        return strtolower(array_search($this->type, static::TYPES));
    }

    /**
     * 値を検証
     * @param int $type
     * @return bool
     */
    public static function isValidValue(int $type): bool
    {
        return in_array($type, static::TYPES);
    }

    /**
     * インスタンス化
     * @param int $type
     * @return static
     */
    public static function of(int $type): static
    {
        return new static($type);
    }

    /**
     * 文字列として出力を行う
     * @return string
     */
    public function __toString()
    {
        return (string)$this->type;
    }

    /**
     * TYPESの一覧を取得する
     * @return array
     */
    public static function getTypesArray(): array
    {
        return static::TYPES;
    }

}

次に値オブジェクトのクラス本体の実装です
保存場所ですが自分はapp/Domain/ValueObjects/[機能ごとのディレクトリ]に保存しています

<?php

namespace App\Domain\ValueObjects\Fuga;

use App\Domain\ValueObjects\AbstractValueObject;

/**
 * 使用例のステータスを扱うクラス
 * Class HogeStatus
 * @package App\Domain\ValueObjects
 * @method static self WAIT()
 * @method static self DOING()
 * @method static self END()
 */
class HogeStatus extends AbstractValueObject
{
    const TYPES = [
        'WAIT'             =>  0, // 待機状態
        'DOING'            =>  1, // 実行状態
        'END'              =>  2, // 終了状態
    ];

    
    public function isWait()
    {
        return $this->type === self::TYPES['WAIT'];
    }
    
    public function isDoing()
    {
        return $this->type === self::TYPES['DOING'];
    }
    
    public function isEnd()
    {
        return $this->type === self::TYPES['END'];
    }
}

これで値オブジェクト部分は完了です
PHPDocにて設定名をメソッドとして呼び出せるように書いておくと利便性が上がります

従来は初期化処理を以下のような実装を行っていたかと思います

    const STATUS_WAIT = 0;

    --- 略 ---
    
    $object->status = self::STATUS_WAIT;

上記クラスを使用すると以下のように何に使用する値なのか識別しやすく初期化ができるようになります
あれ?このステータスってどんな値だっけ?とか設定用のconstってどのファイルで定義していたっけ?みたいな混乱から免れる事ができるようになるかと思います

    $object->status = HogeStatus::WAIT();

さて、最後にlaravelのミューテータ/カスタムキャスト機能を使用して
DBから取得したEloquentオブジェクトが自動で変数をこの型に変換するようにします
laravelのドキュメントに従って今回はapp/Castsに保存しておきます

<?php

namespace App\Casts;

use App\Domain\ValueObjects\Fuga\HogeStatus;
use http\Exception\InvalidArgumentException;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class HogeStatusAttribute implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes) : HogeStatus
    {
        return HogeStatus::of($value);
    }

    public function set(Model $model, string $key, mixed $value, array $attributes)
    {
        if(! $value instanceof HogeStatus) {
            throw new InvalidArgumentException('given value is not ValueObject: '.__CLASS__);
        }
        return $value->getValue();
    }

    public function serialize(Model $model, string $key, mixed $value, array $attributes): string
    {
        return $value->getValue();
    }
}

基本的に他のAttributeを作成する場合でも、これをコピペしてHogeStatusの部分を自分で実装した定数型の値オブジェクトクラス名に変えるだけでOKです

最後にモデル本体側にキャスト設定を入れたら完了です

class Hoges extend Model
{
    protected $casts = [
        'status' => HogeStatusAttribute::class,
    ];
}

いかがでしたでしょうか?
ただ、PHP8.1からは列挙型を利用しても良いかもしれないですね

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?