CakePHP と DDD ~インフラ層について②~
― CakePHP とドメインモデルをつなぐ“境界線”の設計 ―
前回の記事では、CakePHP の Model(Table / Entity)と View はすべて インフラ層に閉じ込める という方針を整理しました。
では次に課題となるのが、
「ドメイン層の Entity と、CakePHP の Table / Entity をどう橋渡しするか?」
「リポジトリ層をどんなルールで設計するか?」
この2点です。
DDD の観点から見ると、この橋渡しをどう作るかで、
フレームワーク依存度・保守性・テスト容易性 に大きな差が出てきますよね。
1. リポジトリは “ドメインにとっての DB の抽象化”
DDD では Repository は 永続化を抽象化する役割 を持ちます。
つまり、
- 「データがどこにあるか?」
- 「どうやって取得するか?」
を ドメイン層には一切考えさせない ことが重要です。
ドメイン層が永続化の詳細を意識し始めると、
フレームワーク変更やテスト戦略の自由度が大きく損なわれてしまいます。
2. ドメイン層に書くもの:Repository Interface
まずドメイン層には 抽象的なインターフェースだけ を置きます。
例:UserRepositoryInterface
interface UserRepositoryInterface
{
public function find(UserId $id): ?User;
public function save(User $user): void;
}
ここには CakePHP のための型は一切出さない のが鉄則。
- Table
- ORM の Entity
- Query オブジェクト
などはすべて排除します。
3. インフラ層に置くもの:Repository の実装(=CakePHP 依存 OK)
CakePHP の Table を利用しつつ、
ドメイン Entity と Cake Entity の変換 をこの層に閉じ込めます。
class UserRepository implements UserRepositoryInterface
{
public function __construct(
private UsersTable $table,
private UserMapper $mapper,
) {}
public function find(UserId $id): ?User
{
$record = $this->table->find()
->where(['id' => $id->value()])
->first();
return $record
? $this->mapper->toDomain($record)
: null;
}
public function save(User $user): void
{
$entity = $this->mapper->toPersistence($user);
$this->table->saveOrFail($entity);
}
}
CakePHP の依存はすべてここに閉じ込められます。
4. Mapper の導入が必須になる理由
ドメイン Entity と CakePHP Entity は 思想が違う ため、直接混在させるのは良くないと思います。
| 層 | 意図 |
|---|---|
| ドメイン Entity | 不変条件/値オブジェクト/ルールを保持 |
| CakePHP Entity | 入力値のバリデーション/DB マッピングが主目的 |
CakePHP の Entity は「入力値の入れ物」として mutable に扱われることも多い一方、
ドメイン Entity は不変条件を持つ immutable 寄りのモデルです。
この前提の違いが、Mapper を挟む最大の理由になります。
class UserMapper
{
public function toDomain(UserEntity $entity): User
{
return new User(
new UserId($entity->id),
new UserName($entity->name),
...
);
}
public function toPersistence(User $user): UserEntity
{
return new UserEntity([
'id' => $user->id()->value(),
'name' => $user->name()->value(),
]);
}
}
これで
- ドメインの純度保持
- インフラ変更の影響を局所化
- テスト容易性 UP
が実現できます。
5. Application Service → Repository → Domain の流れ
処理の全体像はこうなります。
Controller
↓ ※Cake は Request を構築
Application Service
↓ ※ユースケース単位の処理
Repository (Interface)
↓ ※インフラ層に依存しない
Repository Implementation (CakePHP)
↓
CakePHP Table / ORM
Controller は永続化や外部接続を扱わず、主に Request/Response の整形を担うため、
DDD の分類ではインフラ層ではなく Presentation 層に位置づけられます。
フレームワーク依存であるためインフラ的な性質も持ちますが、
役割としては Request/Response を扱う I/O 層であり、
Application Service へ処理を橋渡しする“窓口”に留めます。
6. なぜここまでインフラを“隔離”するのか?
理由は3つ。
① テスト性
ドメインロジックを CakePHP なしでテストできるようになる。
② 将来の移行コスト低減
React SPA + REST API へ移行
→ CakePHP の View を捨ててもドメインは無傷。
MySQL → PostgreSQL へ移行
→ ドメインは無傷。
③ 既存の CakePHP の力技を活かしつつ、システムの寿命を延ばす
- DB は変えない
- Cake の ORM は使いたい
- でも構造は健全にしたい
既存の仕組みを活かしつつも、後から段階的な分離やリファクタリングを行えるのが大きなメリットです。
あとがき(余談)
今回は「CakePHP という既存フレームワークの制約を前提にしたまま、
どこまで DDD の思想を取り込めるか?」という、少し個人的な興味から書きはじめた内容でした。
DB スキーマを変えない・View をそのまま活かしたい・Table クラスの利点も失いたくない…。
そんな “現場ならではの制約” の中で設計をどう切り直すか・・・
実務だと、こういう場面って案外よくありますよね。
フレームワークへの依存が少しずつ剥がれてきて、
「あれ、もしかしてこっちの設計のほうがきれいかも…」と感じはじめたら、
それはきっと良い兆しなんだと思います。
読みながら、どこか一つでも “あ、ここ変えてみようかな” と感じてもらえたなら嬉しいです。
次回予告
「View をインフラ層に閉じ込めるためにはどうするか」
次回は、CakePHP を使う場合に「View と Model をインフラ層に隔離する」ための実践的な設計について扱います。
主なテーマは次の2つです。
-
なぜ View をインフラ層に閉じ込めるのか
CakePHP のフォームヘルパーや Entity を活かしつつ、ドメインへの依存を防ぐ理由と方法。 -
バリデーションをどう分担するか
CakePHP の Table バリデーションをどこまで使うか、UI / アプリケーション / ドメインの役割をどう整理するか。
これらを踏まえて、既存 CakePHP の利点を残しながら、
DDD と両立するインフラ設計を解説します。
Discussion