このプロジェクトは、PHPでCQRS(Command Query Responsibility Segregation)とEvent Sourcingの実装例を示すプロジェクトです。
- 参照実装: https://github.com/j5ik2o/cqrs-es-example-go のPHP版
- 他言語実装例: https://github.com/j5ik2o/cqrs-es-example
- アーキテクチャ: CQRS + Event Sourcing
- 主要技術: PHP 8.2+, GraphQL, DynamoDB, Docker
すべてのコマンドはDockerコンテナ内で実行してください。
# コンテナ起動
make docker-compose-build
make docker-compose-up
# コンテナ停止
make docker-compose-down
# アプリケーションコンテナ内でのコマンド実行
docker compose exec app [コマンド]# コードフォーマット
make fmt
# 静的解析
make phpstan
# リント(フォーマット + 静的解析)
make lint# 全テスト実行
docker compose exec app composer test
# カバレッジ付きテスト
docker compose exec app composer test:coverage
# 特定のテストファイル実行
docker compose exec app vendor/bin/phpunit tests/Path/To/TestFile.php --testdox- 変数名: スネークケース(snake_case)を使用
- クラス名: パスカルケース(PascalCase)
- メソッド名: キャメルケース(camelCase)
// Good
$group_chat_id = new GroupChatId();
$user_account = $this->findUserAccount($user_id);
// Bad
$groupChatId = new GroupChatId();
$userAccount = $this->findUserAccount($userId);新機能実装時は必ずRed-Green-Refactorサイクルに従ってください。
- Red: テストを先に書いてfailすることを確認
- Green: テストが通る最小限のコードを実装
- Refactor: 必要に応じてコードを改善
/**
* @dataProvider testMethodNameProvider
*/
public function testMethodName(...): void {
// テスト実装
}
// データプロバイダーメソッドはテストメソッドの直下に配置
public function testMethodNameProvider(): array {
return [
'case name' => [
// テストデータ
],
];
}- Command側: データの変更(Mutation)のみ
- Query側: データの読み取り(Query)のみ
.
├── src/
│ ├── Command/ # Command側(書き込み)
│ │ ├── Domain/ # ドメインロジック
│ │ ├── InterfaceAdaptor/ # GraphQL Mutation、Repository実装
│ │ └── Processor/ # コマンド処理
│ ├── Query/ # Query側(読み取り)
│ │ ├── Domain/ # ReadModel定義
│ │ └── InterfaceAdaptor/ # GraphQL Query、Repository実装
│ └── Rmu/ # Read Model Updater(イベントハンドラー、DAO)
├── public/ # APIエンドポイント
│ ├── read-api-server.php # Query側GraphQL APIサーバー
│ └── write-api-server.php # Command側GraphQL APIサーバー
└── bin/ # CLIツール
└── read-model-updater-streams.php # RMUプロセス
プロジェクトには2つのGraphQL APIエンドポイントがあります:
-
Write API (
http://localhost:18080): Command側(Mutation)- エンドポイント:
/query - GraphQL Playground:
/ - 実装:
public/write-api-server.php
- エンドポイント:
-
Read API (
http://localhost:18000): Query側(Query)- エンドポイント:
/query - GraphQL Playground:
/ - 実装:
public/read-api-server.php
- エンドポイント:
- Command側: MutationResolverを実装
- Query側: QueryResolverを実装済み
// Command側のMutationResolver例
public function createGroupChat(mixed $root_value, array $args): array {
// バリデーション
if (!is_string($args['name'])) {
throw new \InvalidArgumentException('Name must be a string');
}
// ドメインロジック実行
$event = $this->command_processor->createGroupChat($name, $executor_id);
return $result;
}
// Query側のQueryResolver例
public function getGroupChats(mixed $root_value, array $args): array {
// リポジトリから読み取り専用データを取得
return $this->group_chat_query_repository->findAll();
}RMUはDynamoDB Streamsからイベントを読み取り、Query側のReadModelを更新するプロセスです。
- 実装:
bin/read-model-updater-streams.php - 動作: DynamoDB StreamsからイベントをポーリングしてMySQLのReadModelを更新
- イベントハンドラー:
- GroupChatCreatedEventHandler
- GroupChatRenamedEventHandler
- GroupChatDeletedEventHandler
- GroupChatMemberAddedEventHandler
- GroupChatMemberRemovedEventHandler
- GroupChatMessagePostedEventHandler
- GroupChatMessageEditedEventHandler
- GroupChatMessageDeletedEventHandler
- 言語: タイトルと本文は日本語で記述する
- コミットメッセージ: "by Claude Code" を含める
- テンプレート:
.github/PULL_REQUEST_TEMPLATE.mdがあれば使用する - チェック項目:
- 全テストが通ることを確認
- PHPStanエラーがないことを確認
- コードフォーマットを適用済み
- 関連するテストを追加・修正した場合のみチェック
composer test # テスト実行
composer test:coverage # カバレッジ付きテスト
composer cs # コードスタイルチェック(dry-run)
composer cs:fix # コードフォーマット実行
composer fmt # cs:fixのエイリアス
composer lint # cs + phpstan
composer phpstan # 静的解析- CQRS/ES Example プロジェクト - 他言語での実装例
- Go版リファレンス実装 - 本プロジェクトの参照実装
- Event Store Adapter PHP - PHPイベントストアアダプター