0Pricing
PHP Academy · レッスン

ドメインイベントとドメインサービス

ドメインイベントとサービスでビジネス上の事実を表現します。

「ドメインイベントとドメインサービス」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。

ビジネス上の事実を記録する

すべての振る舞いが1つのエンティティに収まるとは限りません。ドメインイベントは、すでに発生した意味のあるビジネス上の事実(OrderPlaced)を記録し、システムの他の部分がそれに反応できるようにします。ドメインサービスは、1つのエンティティや値オブジェクトに自然に属さないドメインロジックを担います。このレッスンでは、集約を肥大化させずにモデルを表現豊かに保つ方法を説明します。

ドメインイベントとは

ドメインイベントとは、ドメインで起きた出来事を不変な記録として表したものです。ユビキタス言語を使い、過去形で名前を付けます。例としてOrderPlaced、PaymentReceived、CustomerRelocatedがあります。事実を説明するデータと、その出来事が起きた時刻を保持します。値オブジェクトの一種であり、一度生成したら変更しません。

<?php
final class OrderPlaced {
    public function __construct(
        public readonly string $orderId,
        public readonly int $totalCents,
        public readonly DateTimeImmutable $occurredOn = new DateTimeImmutable()
    ) {}
}
$e = new OrderPlaced('o1', 1300);
echo $e->orderId, ' @ ', $e->occurredOn->format('H:i'), PHP_EOL;

集約でイベントを記録する

集約ルートは、その振る舞いの一部としてイベントを記録し、アプリケーション層がトランザクションのコミット後にディスパッチできるよう、イベントを公開します。集約自体はディスパッチせず、記録だけを行います。これにより、ドメインをインフラストラクチャに関する関心事から切り離せます。

<?php
final class OrderPlaced {
    public function __construct(public readonly string $orderId) {}
}
abstract class AggregateRoot {
    private array $events = [];
    protected function record(object $e): void { $this->events[] = $e; }
    public function releaseEvents(): array {
        $out = $this->events; $this->events = []; return $out;
    }
}
final class Order extends AggregateRoot {
    public function __construct(public readonly string $id) {
        $this->record(new OrderPlaced($id));
    }
}
$o = new Order('o1');
foreach ($o->releaseEvents() as $e) { echo get_class($e), PHP_EOL; }

コミット後にディスパッチする

アプリケーションサービスは集約を永続化し、コミットしてから、解放されたイベントをディスパッチします。コミット後にディスパッチすることで、ハンドラーがロールバックされた事実に反応することを防げます。以下の順序は、メール送信や外部呼び出しなどの副作用を安全に処理するためのテンプレートです。

<?php
final class PlaceOrderService {
    public function __construct(
        private object $repo,
        private object $dispatcher
    ) {}
    public function handle(object $order): void {
        // 1. persist inside a transaction
        $this->repo->save($order);   // commit happens here
        // 2. only AFTER commit, release and dispatch
        foreach ($order->releaseEvents() as $event) {
            $this->dispatcher->dispatch($event);
        }
    }
}
echo "order persisted, then events dispatched", PHP_EOL;

ハンドラーが反応する

各イベントには、イベントを発生させた集約から切り離された複数のハンドラーを設定できます。PSR-14ディスパッチャーがイベントを登録済みのリスナーに振り分けます。新しい反応(分析や通知)を追加するときは、集約に手を加えず、リスナーを追加するだけです。

<?php
final class OrderPlaced { public function __construct(public readonly string $orderId) {} }

final class Dispatcher {
    private array $listeners = [];
    public function on(string $event, callable $l): void { $this->listeners[$event][] = $l; }
    public function dispatch(object $event): void {
        foreach ($this->listeners[$event::class] ?? [] as $l) { $l($event); }
    }
}
$d = new Dispatcher();
$d->on(OrderPlaced::class, fn($e) => print("email for {$e->orderId}\n"));
$d->on(OrderPlaced::class, fn($e) => print("analytics for {$e->orderId}\n"));
$d->dispatch(new OrderPlaced('o1'));

境界をまたぐイベント

ドメインイベントを使うと、トランザクションを共有せずに一貫性を保つ必要がある集約同士を疎結合にできます。SalesコンテキストのOrderPlacedによって、イベント経由で別のコンテキストの在庫引当を開始できます。内部ではオブジェクトですが、サービス間ではメッセージブローカー上の統合イベントになることがよくあります。内部のドメインイベントと外部の統合イベントは明確に区別してください。公開する契約を内部モデルにしてはいけません。

ドメインサービスとは

ドメインサービスは、次のようなドメインロジックを担います。

  • 複数の集約またはエンティティに関係するため、どの集約にも適切に収まらないこと
  • ユビキタス言語で表現される、実際のドメイン概念であること

ドメインサービスはステートレスで、ドメインオブジェクトを操作します。1つの口座から引き落とし、別の口座に入金するFundsTransferサービスが典型例です。

コードで見るドメインサービス

このサービスは2つの集約を調整しますが、独自の状態は保持しません。ロジックはどちらの口座にも属さないため、ドメイン上の操作を表す名前のサービスに置きます。

<?php
final class Account {
    public function __construct(public readonly string $id, private int $cents) {}
    public function withdraw(int $c): void {
        if ($c > $this->cents) { throw new DomainException('insufficient funds'); }
        $this->cents -= $c;
    }
    public function deposit(int $c): void { $this->cents += $c; }
    public function balance(): int { return $this->cents; }
}
final class MoneyTransferService {
    public function transfer(Account $from, Account $to, int $cents): void {
        $from->withdraw($cents);
        $to->deposit($cents);
    }
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new MoneyTransferService())->transfer($a, $b, 400);
echo $a->balance(), '/', $b->balance(), PHP_EOL; // 600/400

ドメインサービスとアプリケーションサービス

この2つを混同しないでください。

  • ドメインサービスはビジネスロジックを含み、ドメイン層に置かれます。トランザクションやHTTPについては何も知りません。
  • アプリケーションサービスはユースケースを調整します。リポジトリから集約を読み込み、ドメインロジックを呼び出し、トランザクションをコミットして、イベントをディスパッチします。独自のビジネスルールを持たない、薄い調整役です。

サービスの乱用を避ける

ドメインサービスは簡単に使いすぎてしまいます。ロジックをエンティティや値オブジェクトに置ける場合は、そこに置くべきです。それが貧血モデルを避ける方法です。操作が本当に複数の集約にまたがる場合や、自然に置ける場所がない場合にだけドメインサービスを使ってください。そうしないと、エンティティから振る舞いを奪い出し、手続き型コードを再び作ることになります。

アプリケーションサービスによる調整

ここまでをまとめると、薄いアプリケーションサービスがリポジトリから集約を読み込み、集約をまたぐロジックをドメインサービスに呼び出させ、結果を永続化し、イベントをディスパッチします。含まれるのはオーケストレーションだけで、独自のビジネスルールは持ちません。

<?php
final class Account {
    public function __construct(public readonly string $id, public int $cents) {}
}
final class MoneyTransferService { // domain service: the rule
    public function transfer(Account $from, Account $to, int $c): void {
        if ($c > $from->cents) { throw new DomainException('insufficient'); }
        $from->cents -= $c; $to->cents += $c;
    }
}
final class TransferApp { // application service: orchestration only
    public function __construct(private MoneyTransferService $domain) {}
    public function run(Account $a, Account $b, int $c): void {
        $this->domain->transfer($a, $b, $c); // then repo->save + dispatch
    }
}
$a = new Account('a', 1000); $b = new Account('b', 0);
(new TransferApp(new MoneyTransferService()))->run($a, $b, 250);
echo $a->cents, '/', $b->cents, PHP_EOL; // 750/250

クイックチェック

イベントとサービスです。

まとめ

ビジネス上の事実と、置き場所のなかったロジックを捉えました。ドメインイベントは不変の過去形の記録であり、集約が記録し、アプリケーション層がコミット後にディスパッチします。これにより反応を疎結合にし、集約間の境界を越えられます。ドメインサービスは複数の集約にまたがるステートレスなドメインロジックを保持します。ユースケースを単にオーケストレーションする薄いアプリケーションサービスとは異なります。どちらも必要最小限に使用し、振る舞いは本来置くべきエンティティや値オブジェクトにできるだけ保持させます。

よくある質問

「ドメインイベントとドメインサービス」レッスンは無料ですか?

はい。「ドメインイベントとドメインサービス」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。

「ドメインイベントとドメインサービス」で何を学びますか?

ドメインイベントとサービスでビジネス上の事実を表現します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

PHP Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「ドメインイベントとドメインサービス」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPHP Academyレッスンでコードを書いて実行できますか?

はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. DDDの構成要素:エンティティと値オブジェクト
  2. 集約、リポジトリ、ファクトリ
  3. ドメインイベントとドメインサービス
  4. 境界づけられたコンテキストとコンテキストマッピング
← PHP Academyに戻る