集約、リポジトリ、ファクトリ
集約で不変条件を守り、データを適切に永続化します。
「集約、リポジトリ、ファクトリ」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
不変条件の保護
エンティティと値オブジェクトを用意したら、それらのまとまりを一貫した状態に保ち、適切に永続化するためのパターンが必要になります。DDDでは、3つの戦術的パターンでこれに応えます。すなわち、集約(一貫性の境界)、リポジトリ(コレクションのように扱える永続化の抽象化)、ファクトリ(複雑な構築)です。このレッスンでは、PHPでこれらがどのように連携するかを説明します。
集約とは
集約とは、データを変更する際に単一の単位として扱う、エンティティと値オブジェクトのまとまりです。そのうち1つのエンティティが集約ルートになります。集約ルートは、外部のコードが参照を保持できる唯一のメンバーです。すべての変更はルートを通じて行い、ルートが集約の不変条件を守ります。集約はトランザクション境界でもあり、アトミックに読み込みと保存を行います。
ルートが全体を守る
外部の呼び出し側が内部のメンバーに直接触れることはありません。明細を追加するときはルートのメソッドを呼び出し、そのメソッドが検証と一貫性(合計や上限)の維持を行います。これにより、不変条件を1か所に集約できます。
<?php
final class LineItem {
public function __construct(
public readonly string $sku,
public readonly int $qty,
public readonly int $unitCents
) {}
public function subtotal(): int { return $this->qty * $this->unitCents; }
}
final class Order { // Aggregate Root
/** @var LineItem[] */
private array $items = [];
public function __construct(public readonly string $id) {}
public function addItem(string $sku, int $qty, int $unitCents): void {
if ($qty < 1) { throw new DomainException('qty must be >= 1'); }
$this->items[] = new LineItem($sku, $qty, $unitCents);
}
public function total(): int {
return array_sum(array_map(fn(LineItem $i) => $i->subtotal(), $this->items));
}
}
$o = new Order('o1');
$o->addItem('A', 2, 500);
$o->addItem('B', 1, 300);
echo $o->total(), PHP_EOL; // 1300
小さな集約を設計する
よくある間違いは、集約を大きくしすぎることです(完全なCustomerグラフも所有するOrderなど)。基本的な指針は次のとおりです。
- 集約は小さく保ちます。他の集約はオブジェクトを保持せず、IDで参照します。
- 1つのトランザクションでは1つの集約を変更します。集約をまたぐ処理はドメインイベントで連携します。
- 常に満たされなければならない不変条件が、境界を定義します。
IDで参照する
注文にはCustomerエンティティではなく、customerId値オブジェクトを保持させます。これにより一貫性の境界を狭く保ち、大規模なオブジェクトグラフの読み込みを避けられます。集約をまたぐ一貫性は結果整合性となり、1つの巨大なトランザクションではなく、イベントで処理します。
<?php
final class CustomerId {
public function __construct(public readonly string $value) {}
}
final class Order {
public function __construct(
public readonly string $id,
public readonly CustomerId $customerId // reference, not object
) {}
}
$order = new Order('o1', new CustomerId('cus_99'));
echo $order->customerId->value, PHP_EOL;
リポジトリの契約
リポジトリは、集約ルートのインメモリコレクションを扱っているような抽象化を提供します。ドメインが依存するのはインターフェースだけであり、実装(Doctrine、PDO、インメモリ)はインフラストラクチャ層に置きます。リポジトリが扱うのは集約全体であり、部分的な行を扱うことはありません。
<?php
interface OrderRepository {
public function ofId(string $id): ?Order;
public function save(Order $order): void;
public function nextIdentity(): string;
}
インメモリ実装
インメモリリポジトリは、データベースを使わない高速なユニットテストに非常に役立ちます。ドメインが依存するのはインターフェースなので、実装を自由に入れ替えられます(依存性逆転を実践できます)。
<?php
interface OrderRepository {
public function ofId(string $id): ?object;
public function save(object $order): void;
public function nextIdentity(): string;
}
final class Order { public function __construct(public readonly string $id) {} }
final class InMemoryOrderRepository implements OrderRepository {
private array $store = [];
public function ofId(string $id): ?object { return $this->store[$id] ?? null; }
public function save(object $order): void { $this->store[$order->id] = $order; }
public function nextIdentity(): string { return 'o_' . bin2hex(random_bytes(4)); }
}
$repo = new InMemoryOrderRepository();
$repo->save(new Order('o1'));
var_dump($repo->ofId('o1') !== null);
リポジトリはDAOではない
リポジトリは汎用的なCRUD DAOではありません。ドメイン上の意味を持つクエリ(findOverdueOrders())を公開し、不変条件を保ったまま完全な集約を再構成します。SQLやORMの詳細を意図的に隠すことで、ドメインを永続化から独立させます。クエリビルダーや、汎用的なsave($anyEntity)メソッドをドメインに漏出させないでください。
複雑な生成のためのファクトリ
集約の構築に、同一性の生成、値オブジェクトの組み立て、生成時の不変条件の適用など、実際のロジックが伴う場合は、それをファクトリ(専用クラスまたは名前付き静的コンストラクター)に移します。これにより、エンティティのコンストラクターを単純に保ち、有効な生成に関するルールを一元化できます。
<?php
final class Order {
private function __construct(
public readonly string $id,
public readonly string $customerId
) {}
public static function place(string $customerId): self {
if ($customerId === '') { throw new DomainException('customer required'); }
return new self('o_' . bin2hex(random_bytes(4)), $customerId);
}
}
$order = Order::place('cus_1');
echo $order->id, PHP_EOL;
連携の仕組み
アプリケーションサービスでの典型的な流れは次のとおりです。
- ファクトリ(または名前付きコンストラクター)が有効な集約を生成します。
- 集約ルートのメソッドが、利用中の不変条件を適用します。
- リポジトリが集約全体を永続化し、後で再構成します。
アプリケーションサービスは、インターフェースだけに依存しながら、集約ごとに1つのトランザクション内でこれらを調整します。
集約全体にまたがる不変条件の適用
ルートの真の価値は、メンバーにまたがる不変条件を適用できることです。ここでは、合計が与信限度額を超える場合に注文が明細を拒否します。これは単一のLineItemだけでは適用できないルールです。すべての変更がルートを通じて行われるため、このルールを決して回避できません。
<?php
final class Order {
private array $items = [];
public function __construct(
public readonly string $id,
private int $creditLimitCents
) {}
public function addItem(int $cents): void {
if ($this->total() + $cents > $this->creditLimitCents) {
throw new DomainException('Exceeds credit limit');
}
$this->items[] = $cents;
}
public function total(): int { return array_sum($this->items); }
}
$o = new Order('o1', 1000);
$o->addItem(600);
try { $o->addItem(600); } catch (DomainException $e) { echo $e->getMessage(), PHP_EOL; }
echo $o->total(), PHP_EOL; // 600
理解度チェック
集約の設計
まとめ
不変条件を保護し、適切に永続化する方法を学びました。集約は一貫性とトランザクションの境界を形成し、ルートを通じてのみ変更されます。また、他の集約をIDで参照することで小さく保ちます。リポジトリは、ドメインインターフェースの背後で集約ルートをコレクションとして扱えるようにし、ORMやSQLの詳細を隠し、インメモリのテスト用実装を可能にします。ファクトリは、複雑で不変条件を適用する生成処理を一元化します。これらを組み合わせることで、ドメインモデルの一貫性を保ち、永続化から独立させられます。
よくある質問
「集約、リポジトリ、ファクトリ」レッスンは無料ですか?
はい。「集約、リポジトリ、ファクトリ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「集約、リポジトリ、ファクトリ」で何を学びますか?
集約で不変条件を守り、データを適切に永続化します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「集約、リポジトリ、ファクトリ」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。