📓

【PHP】IDクラスをどう実装するか?

2024/01/21に公開

はじめに

DDD(ドメイン駆動設計)に限りませんが、
IDを扱うときにint型やstring型といったプリミティブ型を使うと、
制約の遵守が難しくなtたり判定処理がコード内の様々な箇所に散らばってしいます。
これは、煩雑なコードとバグの温床につながる可能性があります。

そこで、本記事ではPHP8.2を用いたIDクラスの私なりの実装パターンを紹介します。

説明すること

  • IDクラスの私なりの実装パターン

説明しないこと

  • DDD(ドメイン駆動設計)全般

実装方針

DDDにおいて、IDはエンティティの一意性を保証します。

エンティティを新規作成する際にIDを新規採番してセットする必要があります。

もし、DBでIDとしてAuto Incrementの値を採用していた場合、
エンティティを新規作成する前に一度DBから新規IDを取得し、そのIDでエンティティを作成するような手順になってしまいます。

そのため、DBに聞かずに採番出来るようにULIDを採用しました。
ULIDはランダム性が高く、理論上、重複の可能性が非常に低いです。
万が一重複が生じた場合でも、DBの挿入時に主キー重複エラーにより適切に処理されます。
さらに、時系列に基づくソートが可能なため、データの管理において便利です。

また、Laravelには時系列でソートできるorderedUUIDというものも別途存在します。
(UUIDのv4とかv7とかの仕様とは少しズレたLaravel独自実装のもの?)
ULIDとUUIDを比較すると、下記のようにULIDの方が短いので好みです。
ここら辺はチーム方針に応じて選択すれば良いと思います。

ULIDとUUIDの例

ULID: 01HMKRTG4JJ1EEY8CPXG0W5ZNH
UUID: 1b46550c-53bb-493f-ad60-efa5e99b9754

PHP8.2での実装パターン

PHP8.2を用いたIDクラスの実装では、継承を採用しました。

最近のオブジェクト指向プログラミングでは、継承よりもコンポジションを推奨する意見があります。
継承を使用すると、基底クラスに加えられた変更が全ての派生クラスに影響を及ぼす可能性があるためです。
私もそれに同意しています。
しかし、今回のケースでは基底クラスの変更は考えにくく、
もし修正することがあっても、それは全てのIDクラスに対応が必要なものであると考えて継承を採用しました。

以下が実装したコードになります。

基底クラスのコード

引数に値がセットされていればその値を使用し、そうでなければ新しいULIDを生成します。

abstract readonly class Ulid
{
    public string $value;

    public function __construct(
        string $value = null
    ) {
        // 引数に値がセットされていたらその値を採用。未設定ならULIDを新規採番
        $this->value = $value ?? (string)Str::ulid();

        // ULIDのフォーマットチェック
        if (!Str::isUlid($this->value)) {
            throw new IllegalArgumentException('Invalid ULID format. value=' . $this->value);
        }
    }

    /**
     * 値が等しいか判定する
     */
    public function equals(Ulid $ulid): bool
    {
        return $this->value === $ulid->value;
    }
}

実装クラスのコード

実装はこのようにシンプルです。

readonly class UserId extends Ulid
{
}

使い方

このように使用できます。

$userIdA = new UserId();
$userIdB = new UserId($userIdA->value);

dump($userIdA->equals($userId2));  // true

懸念点

本実装では継承という形を採用したため、以下のような状況が発生する可能性があります。

①引数を基底クラスの型で宣言して、どのIDでも入れられるエンティティが出来る

readonly class User
{
    public function __construct(
        public UUID $id,
        public string $name,
        ...
    ) {
    }
}

// UserId以外のIDクラスも入れられてしまう
$user = new User(
    new TaskId(),
    'hoge'
);

②別のIDクラスと比較できてしまう。

$userId = new UserId();
$taskId = new TaskId();

dump($userId->equals($taskId));  // <-エラーにならない

これらの問題は、チームの規約として制限することで回避しています。

まとめ

以上、ULIDを適用するIDクラスの実装パターンについて紹介しました。

Discussion