diff --git a/README.ja.md b/README.ja.md index 3d7c937..f28bfd0 100644 --- a/README.ja.md +++ b/README.ja.md @@ -158,6 +158,20 @@ flowchart LR --- +## Been — 存在証明 + +一部のデモでは `Been` を実装しています。`Been`はFinalオブジェクトが「なぜ今の状態にあるか」を証明する不変キャリアです。Finalは`#[Inject]`で`Been`を受け取り、`with()`でドメインイベントを記録し、`assert`で自らの因果を検証できます — 証明をテストコードからプロダクションコードへ移動させます。 + +| デモ | 証明する内容 | +|---|---| +| [contact-form](./demos/contact-form/) | レシートが正規化されたメールに対して生成された | +| [user-registration](./demos/user-registration/) | ユーザーが入力メールで作成された(assert) | +| [order-processing](./demos/order-processing/) | 全Momentがconfirmedステータスで完了した(assert) | + +概念の詳細は Be Framework ドキュメントの[意味的ログ](https://be-framework.github.io/manuals/1.0/ja/10-semantic-logging.html)を参照してください。 + +--- + ## テスト実行 ```bash diff --git a/README.md b/README.md index 2695c6f..f15eaa9 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,20 @@ Insurance claim processing. `ClaimInput` and `PolicyInput` both declare `#[Be([C --- +## Been — Proof of Existence + +Some demos demonstrate `Been`, an immutable carrier that a Final object uses to prove why it is what it is. The Final injects `Been` via `#[Inject]`, records domain events with `with()`, and can assert its own causal chain — moving proof from test code into production code. + +| Demo | What is proved | +|---|---| +| [contact-form](./demos/contact-form/) | Receipt was generated for the normalized email | +| [user-registration](./demos/user-registration/) | User was created with the input email (assert) | +| [order-processing](./demos/order-processing/) | All moments completed with confirmed status (assert) | + +See [Semantic Logging](https://be-framework.github.io/manuals/1.0/en/10-semantic-logging.html) in the Be Framework documentation for the full concept. + +--- + ## Running tests ```bash diff --git a/demos/contact-form/src/Context/ReceiptGeneratedContext.php b/demos/contact-form/src/Context/ReceiptGeneratedContext.php new file mode 100644 index 0000000..8c7f545 --- /dev/null +++ b/demos/contact-form/src/Context/ReceiptGeneratedContext.php @@ -0,0 +1,20 @@ +receiptId = $receipt->generate(); $this->receivedAt = date('Y-m-d\TH:i:sP'); + $this->been = $been->with(new ReceiptGeneratedContext( + receiptId: $this->receiptId, + email: $normalizedEmail, + receivedAt: $this->receivedAt, + )); + + // Proof: the receipt was generated for this email + $event = $this->been->events[0]; + assert($event instanceof ReceiptGeneratedContext); + assert($event->email === $this->normalizedEmail, 'Receipt must be for the normalized email'); } } diff --git a/demos/medical-triage/src/Module/AppModule.php b/demos/medical-triage/src/Module/AppModule.php index 2f425a7..59e6af2 100644 --- a/demos/medical-triage/src/Module/AppModule.php +++ b/demos/medical-triage/src/Module/AppModule.php @@ -4,6 +4,7 @@ namespace Be\Pattern\MedicalTriage\Module; +use Be\Framework\Module\BeModule; use Be\Pattern\MedicalTriage\Reason\JTASProtocol; use Ray\Di\AbstractModule; @@ -11,6 +12,8 @@ final class AppModule extends AbstractModule { protected function configure(): void { + $this->install(new BeModule('Be\Pattern\MedicalTriage\Semantic')); + $this->bind(JTASProtocol::class); } } diff --git a/demos/order-processing/src/Context/OrderFinalizedContext.php b/demos/order-processing/src/Context/OrderFinalizedContext.php new file mode 100644 index 0000000..e49953a --- /dev/null +++ b/demos/order-processing/src/Context/OrderFinalizedContext.php @@ -0,0 +1,19 @@ +inventory->be(); @@ -34,6 +38,14 @@ public function __construct( $this->orderId = $this->generateOrderId(); $this->status = 'confirmed'; + $this->been = $been->with(new OrderFinalizedContext( + orderId: $this->orderId, + status: $this->status, + )); + + $event = $this->been->events[0]; + assert($event instanceof OrderFinalizedContext); + assert($event->status === 'confirmed'); } private function generateOrderId(): string diff --git a/demos/order-processing/src/Module/AppModule.php b/demos/order-processing/src/Module/AppModule.php index 790815b..f7e9e16 100644 --- a/demos/order-processing/src/Module/AppModule.php +++ b/demos/order-processing/src/Module/AppModule.php @@ -4,6 +4,7 @@ namespace Be\Pattern\OrderProcessing\Module; +use Be\Framework\Module\BeModule; use Be\Pattern\OrderProcessing\Attribute\Address; use Be\Pattern\OrderProcessing\Attribute\Amount; use Be\Pattern\OrderProcessing\Attribute\CardNumber; @@ -28,6 +29,8 @@ final class AppModule extends AbstractModule { protected function configure(): void { + $this->install(new BeModule('Be\Pattern\OrderProcessing\Semantic')); + // Order processing - Reason bindings (via interfaces for testability) $this->bind(WarehouseLocator::class); $this->bind(InventoryChecker::class); diff --git a/demos/order-processing/tests/Final/OrderConfirmedTest.php b/demos/order-processing/tests/Final/OrderConfirmedTest.php index c48b04c..533ee50 100644 --- a/demos/order-processing/tests/Final/OrderConfirmedTest.php +++ b/demos/order-processing/tests/Final/OrderConfirmedTest.php @@ -4,6 +4,8 @@ namespace Be\Pattern\OrderProcessing\Tests\Final; +use Be\Framework\SemanticLog\Been; +use Be\Pattern\OrderProcessing\Context\OrderFinalizedContext; use Be\Pattern\OrderProcessing\Final\OrderConfirmed; use Be\Pattern\OrderProcessing\Moment\InventoryReserved; use Be\Pattern\OrderProcessing\Moment\PaymentCompleted; @@ -11,6 +13,7 @@ use Be\Pattern\OrderProcessing\Reason\InventoryReserver; use Be\Pattern\OrderProcessing\Reason\PaymentGateway; use Be\Pattern\OrderProcessing\Reason\ShippingArranger; +use Koriym\SemanticLogger\SemanticLogger; use PHPUnit\Framework\TestCase; class OrderConfirmedTest extends TestCase @@ -28,13 +31,16 @@ public function testOrderConfirmed(): void $shipping = new ShippingArranged('YAMATO', '〒150-0001 渋谷区神宮前1-1-1', $shippingArranger); // Converge into Final (realizes all potentials) - $final = new OrderConfirmed($inventory, $payment, $shipping); + $been = new Been(new SemanticLogger()); + $final = new OrderConfirmed($inventory, $payment, $shipping, $been); $this->assertStringStartsWith('ORD-', $final->orderId); $this->assertSame('confirmed', $final->status); $this->assertSame($inventory, $final->inventory); $this->assertSame($payment, $final->payment); $this->assertSame($shipping, $final->shipping); + $this->assertCount(1, $final->been->events); + $this->assertInstanceOf(OrderFinalizedContext::class, $final->been->events[0]); } public function testOrderIdFormat(): void @@ -48,7 +54,8 @@ public function testOrderIdFormat(): void $shippingArranger = new ShippingArranger(); $shipping = new ShippingArranged('YAMATO', '〒150-0001 渋谷区神宮前1-1-1', $shippingArranger); - $final = new OrderConfirmed($inventory, $payment, $shipping); + $been = new Been(new SemanticLogger()); + $final = new OrderConfirmed($inventory, $payment, $shipping, $been); // Order ID format: ORD-YYYYMMDD-xxxxxxxx $this->assertMatchesRegularExpression('/^ORD-\d{8}-[a-f0-9]{8}$/', $final->orderId); diff --git a/demos/user-registration/src/Context/UserCreatedContext.php b/demos/user-registration/src/Context/UserCreatedContext.php new file mode 100644 index 0000000..f066596 --- /dev/null +++ b/demos/user-registration/src/Context/UserCreatedContext.php @@ -0,0 +1,19 @@ +userId = $idGen->generate(); $this->welcomeToken = $tokenGen->generate($email); + $this->been = $been->with(new UserCreatedContext( + userId: $this->userId, + email: $email, + )); + + // Proof: the user was created with the input email + $event = $this->been->events[0]; + assert($event instanceof UserCreatedContext); + assert($event->email === $this->email, 'User must be created with the input email'); } }