Upgrade to Pro — share decks privately, control downloads, hide ads and more …

イベントストーミング図からコードへの変換手順 / Procedure for Converti...

Avatar for nrs nrs
June 28, 2025

イベントストーミング図からコードへの変換手順 / Procedure for Converting Event Storming Diagrams to Code

PHP Conference 2025 Japan における発表資料です。
イベントストーミング図をコードへ落とし込むやり方をお話しています。

https://fortee.jp/phpcon-2025/proposal/7368450d-070c-4d23-a12f-37371d5c7947

# URL
YouTube: https://www.youtube.com/c/narusemi
HomePage: https://nrslib.com
Twitter: https://twitter.com/nrslib
Instagram: https://www.instagram.com/nrslib/

Avatar for nrs

nrs

June 28, 2025
Tweet

More Decks by nrs

Other Decks in Programming

Transcript

  1. 27 • 大きく2つ ◦ ワーキングセッション ▪ メンバーが全員やり方や主旨を理解した上で取り組む ▪ Pros:一気にすべてが進む ▪

    Cons:初めてやると上手く進まない、長時間かかる ◦ ヒアリング ▪ ファシリテーターがヒアリングをして進める ▪ Pros:初めてやってもまとまりやすい、短時間でやれる ▪ Cons:ファシリテーターの技量に依存する部分がある イベントストーミングのやり方
  2. class DocumentController extends Controller { public function store(Request $request): JsonResponse

    { $request->validate([ 'contents' => 'required|string', ]); $command = new CreateDocumentCommand($request->input('contents')); $document = $this->documentApplicationService->createDocument($command); return response()->json([ 'id' => $document->id, 'contents' => $document->contents, 'effective_count' => $document->effectiveCount(), 'created_at' => $document->created_at, ], 201); } }
  3. class DocumentController extends Controller { public function update(Request $request, int

    $id): JsonResponse { $request->validate([ 'contents' => 'required|string', ]); $command = new UpdateDocumentCommand($id, $request->input('contents')); $document = $this->documentApplicationService->updateDocument($command); return response()->json([ 'id' => $document->id, 'contents' => $document->contents, 'effective_count' => $document->effectiveCount(), 'updated_at' => $document->updated_at, ]); } }
  4. class DocumentApplicationService { ... public function updateDocument(UpdateDocumentCommand $command): Document {

    $document = Document::findOrFail($command->id); $document->update([ 'contents' => $command->contents, ]); return $document->refresh(); } }
  5. public function markEffective(Request $request, int $id): JsonResponse { $request->validate([ 'user_id'

    => 'required|integer|exists:users,id', ]); $command = new MarkDocumentEffectiveCommand($id, $request->input('user_id')); $document = $this->effectiveApplicationService->markDocumentEffective($command); return response()->json([ 'id' => $document->id, 'contents' => $document->contents, 'effective_count' => $document->effectiveCount(), 'updated_at' => $document->updated_at, ]); }
  6. public function markDocumentEffective(MarkDocumentEffectiveCommand $command): Document { $document = Document::findOrFail($command->documentId); $user

    = User::findOrFail($command->userId); Effective::firstOrCreate([ 'document_id' => $document->id, 'user_id' => $user->id, ]); return $document->refresh(); }
  7. public function unmarkEffective(Request $request, int $id): JsonResponse { $request->validate([ 'user_id'

    => 'required|integer|exists:users,id', ]); $command = new UnmarkDocumentEffectiveCommand($id, $request->input('user_id')); $document = $this->effectiveApplicationService->unmarkDocumentEffective($command); return response()->json([ 'id' => $document->id, 'contents' => $document->contents, 'effective_count' => $document->effectiveCount(), 'updated_at' => $document->updated_at, ]); }
  8. public function unmarkDocumentEffective(UnmarkDocumentEffectiveCommand $command): Document { $document = Document::findOrFail($command->documentId); $user

    = User::findOrFail($command->userId); Effective::where([ 'document_id' => $document->id, 'user_id' => $user->id, ])->delete(); return $document->refresh(); }
  9. class AlbumController extends Controller { public function store(Request $request): JsonResponse

    { $request->validate([...]); try { $command = new CreateAlbumCommand( name: $request->input('name'), description: $request->input('description'), userId: $request->input('user_id'), isPublic: filter_var($request->input('is_public', true), FILTER_VALIDATE_BOOL photo: $request->file('photo') ); $album = $this->albumApplicationService->createAlbum($command); // アルバムの写真数を含めてレスポンス return response()->json([ 'id' => $album->getId()?->getValue(), 'name' => $album->getName(), 'description' => $album->getDescription(), 'user_id' => $album->getUserId()->getValue(), 'is_public' => $album->isPublic(),
  10. class AlbumApplicationService { ... public function createAlbum(CreateAlbumCommand $command): Album {

    $album = Album::create( name: $command->name, description: $command->description, userId: new UserId($command->userId), isPublic: $command->isPublic ); // リポジトリを使用してアルバムを保存 $savedAlbum = $this->albumRepository->save($album); // ポリシー: 写真が同時にアップロードされた場合の条件分岐 if ($command->photo !== null) { // アルバムに写真を追加 $this->addPhotoToAlbum($savedAlbum, $command->photo); } return $savedAlbum; } }
  11. class AlbumApplicationService { ... public function addPhotoToAlbum(Album $album, UploadedFile $file):

    Photo { $filename = uniqid() . '.' . $file->getClientOriginalExtension(); $path = $file->storeAs('photos', $filename, 'public'); // ドメインエンティティとしてPhotoを作成 $photo = Photo::create( filename: $filename, originalName: $file->getClientOriginalName(), mimeType: $file->getMimeType(), fileSize: $file->getSize(), path: $path ); $savedPhoto = $this->photoRepository->save($photo); $album->addPhoto($savedPhoto); $this->albumRepository->save($album); return $savedPhoto; }
  12. interface AlbumRepositoryInterface { public function save(Album $album): Album; public function

    findById(AlbumId $id): ?Album; public function findByUserId(UserId $userId): array; public function delete(AlbumId $id): void; }
  13. class AlbumApplicationService { ... public function addPhotoToAlbum(Album $album, UploadedFile $file):

    Photo { $filename = uniqid() . '.' . $file->getClientOriginalExtension(); $path = $file->storeAs('photos', $filename, 'public'); // ドメインエンティティとしてPhotoを作成 $photo = Photo::create( filename: $filename, originalName: $file->getClientOriginalName(), mimeType: $file->getMimeType(), fileSize: $file->getSize(), path: $path ); $savedPhoto = $this->photoRepository->save($photo); $album->addPhoto($savedPhoto); $this->albumRepository->save($album); return $savedPhoto; } }
  14. class Album { public function __construct( private ?AlbumId $id, private

    string $name, private ?string $description, private UserId $userId, private bool $isPublic, private array $photoIds = [], private ?\DateTimeImmutable $createdAt = null ) { } ... public function addPhoto(Photo $photo): void { $this->photoIds[] = $photo->getId(); } }
  15. class AlbumController extends Controller { ... public function addPhoto(Request $request,

    int $albumId): JsonResponse { $request->validate([ 'photo' => 'required|image|max:2048', ]); try { $command = new AddPhotoToAlbumCommand( albumId: $albumId, photo: $request->file('photo') ); $photo = $this->albumApplicationService->addPhotoToAlbumById( $command->albumId, $command->photo ); return response()->json([...], 201); } catch (Exception $e) { return response()->json([
  16. class AlbumApplicationService { ... public function addPhotoToAlbumById(int $albumId, UploadedFile $file):

    Photo { $album = $this->albumRepository->findById(new AlbumId($albumId)); if ($album === null) { throw new \Exception('Album not found'); } return $this->addPhotoToAlbum($album, $file); } }
  17. class OrderController extends Controller { public function placeOrder(Request $request): JsonResponse

    { $validated = $request->validate([...]); $command = new CreateOrderCommand( (int) $validated['user_id'], (float) $validated['amount'], $validated['currency'] ?? 'JPY' ); $order = $this->orderApplicationService->placeOrder($command); return response()->json([ 'id' => $order->id()->value(), 'user_id' => $order->userId()->getValue(), 'amount' => $order->amount(), 'currency' => $order->currency(), 'status' => $order->status()->value, 'payment_completed' => $order->isPaymentCompleted(), 'transaction_id' => $order->paymentTransactionId(), 'created_at' => $order->createdAt()->format('c'), ], 201);
  18. class OrderApplicationService { public function placeOrder(CreateOrderCommand $command): Order { $orderId

    = new OrderId(Str::uuid()->toString()); $userId = new UserId($command->userId); $order = Order::create($orderId, $userId, $command->amount, $command->currency); $paymentResult = $this->paymentGateway->processPayment($order); if ($paymentResult->isSuccess()) { $order->completePayment($paymentResult->transactionId()); $cart = $this->cartRepository->findByUserId($userId); if ($cart) { $cart->clear(); $this->cartRepository->save($cart); } } else { $order->failPayment($paymentResult->transactionId()); } $this->orderRepository->save($order); return $order;
  19. public function processPrintOrders(): array { // 1. 購入履歴から印刷対象の注文を取得 $completedOrders =

    $this->orderRepository- >findCompletedOrdersWithoutPrintOrder(); $results = []; // 2. 100件ずつのバルク処理 $chunks = array_chunk($completedOrders, self::BATCH_SIZE); foreach ($chunks as $orderBatch) { $batchResults = $this->processBatch($orderBatch); $results = array_merge($results, $batchResults); } return array_map(fn(PrintOrderResult $result) => $result->toArray(), $results); }
  20. private function processBatch(array $orders): array { $printOrders = []; $orderMap

    = []; $results = []; // 1. 印刷依頼をまとめて作成 foreach ($orders as $order) { try { $printOrder = PrintOrder::create( $order->id(), $order->photoIds() ); $printOrders[] = $printOrder; $orderMap[$printOrder->id()->value()] = $order; } catch (\Exception $e) { $results[] = PrintOrderResult::failure( $order->id()->value(), $e->getMessage() ); } } if (empty($printOrders)) {
  21. if (empty($printOrders)) { return $results; } try { // 2.

    100件まとめて印刷会社に送信 $batchResults = $this->printingService->sendBatchToPrinter($printOrders); // 3. 成功した印刷依頼の状態を更新 $successfulPrintOrders = []; foreach ($printOrders as $printOrder) { $printOrderId = $printOrder->id()->value(); $order = $orderMap[$printOrderId]; $success = $batchResults[$printOrderId] ?? false; if ($success) { $printOrder->sendToPrinter(); $successfulPrintOrders[] = $printOrder; $results[] = PrintOrderResult::success( $order->id()->value(), $printOrder->id()->value() ); } else { $results[] = PrintOrderResult::failure(
  22. $results[] = PrintOrderResult::failure( $order->id()->value(), 'Failed to send to printer' );

    } } // 4. 成功した印刷依頼をバルクで保存 if (!empty($successfulPrintOrders)) { $this->printOrderRepository->saveBatch($successfulPrintOrders); } } catch (\Exception $e) { // バッチ処理全体が失敗した場合 foreach ($printOrders as $printOrder) { $order = $orderMap[$printOrder->id()->value()]; $results[] = PrintOrderResult::failure( $order->id()->value(), 'Batch processing failed: ' . $e->getMessage() ); } } return $results;
  23. 68 • X ◦ @nrslib • HomePage ◦ https://nrslib.com/ •

    YouTube ◦ https://www.youtube.com/c/narusemi おしまい イベントストーミングしたかったらコドモンにおいでよ↓