Skip to content

Commit 3a17fae

Browse files
committed
we are writing Bitcoin baby
1 parent 21103a0 commit 3a17fae

3 files changed

Lines changed: 52 additions & 93 deletions

File tree

ddd-src/Application/Process/LongProcess.php

Lines changed: 21 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace TangibleDDD\Application\Process;
44

55
use DateTimeImmutable;
6-
use TangibleDDD\Domain\Events\IIntegrationEvent;
76
use TangibleDDD\Domain\Shared\Aggregate;
87
use TangibleDDD\Domain\Shared\JsonLifecycleValue;
98

@@ -13,47 +12,54 @@
1312
* Processes are defined as a series of steps (methods) that execute in declaration order.
1413
* Each step returns a Result that tells the runner what to do next:
1514
* - payload: pass data to the next step (must be JsonLifecycleValue)
16-
* - queries: execute these queries, pass results to next step
17-
* - commands: execute these commands (they send() themselves)
15+
* - commands: execute these commands (fire-and-forget side effects)
1816
* - await: suspend until an integration event fires
1917
*
18+
* ## Step Signatures
19+
*
20+
* Steps can have different signatures depending on what they need:
21+
* - `protected function step_name(): Result` - no input needed
22+
* - `protected function step_name(SomePayload $payload): Result` - receives payload
23+
* - `protected function step_name(?SomePayload $payload, SomeEvent $event): Result` - post-await step
24+
*
2025
* ## DI Registration
2126
*
2227
* Register your process in services.yaml with the 'ddd.long_process' tag.
2328
* If your process uses AwaitEvent, declare the awaited event classes:
2429
*
2530
* ```yaml
26-
* App\Process\GenerateLearningPath:
31+
* App\Process\OrderFulfillmentProcess:
2732
* tags:
2833
* - name: 'ddd.long_process'
2934
* awaits:
30-
* - App\Events\UserApprovedPath
35+
* - App\Events\PaymentReceived
3136
* ```
3237
*
3338
* ## Example
3439
*
3540
* ```php
36-
* class GenerateLearningPath extends LongProcess {
41+
* class OrderFulfillmentProcess extends LongProcess {
3742
* public function __construct(
38-
* private readonly int $user_id,
43+
* private readonly int $order_id,
3944
* ) {
4045
* parent::__construct(null);
4146
* }
4247
*
43-
* protected function fetch_history(): Result {
44-
* return new Result(payload: new HistoryPayload($this->user_id));
48+
* protected function initialize(): Result {
49+
* return new Result(payload: new OrderPayload($this->order_id));
4550
* }
4651
*
47-
* protected function request_approval(HistoryPayload $payload): Result {
52+
* protected function request_payment(OrderPayload $payload): Result {
53+
* // Dispatch payment request...
4854
* return new Result(
49-
* await: new AwaitEvent(UserApprovedPath::class, ['user_id' => $this->user_id])
55+
* payload: $payload,
56+
* await: new AwaitEvent(PaymentReceived::class, ['order_id' => $this->order_id])
5057
* );
5158
* }
5259
*
53-
* // Next step receives the event via $this->resume_event()
54-
* protected function process_approval(): Result {
55-
* $event = $this->resume_event(UserApprovedPath::class);
56-
* // ... use event data
60+
* // Post-await step receives both payload and the event that woke it
61+
* protected function process_payment(?OrderPayload $payload, PaymentReceived $event): Result {
62+
* // Use $event->amount, $event->transaction_id, etc.
5763
* return new Result();
5864
* }
5965
* }
@@ -85,17 +91,6 @@ abstract class LongProcess extends Aggregate {
8591
*/
8692
protected ?ProcessSteps $steps = null;
8793

88-
// ─────────────────────────────────────────────────────────────────────────
89-
// Transient state (NOT persisted - only valid during current execution)
90-
// ─────────────────────────────────────────────────────────────────────────
91-
92-
/**
93-
* Event that triggered resume from suspension.
94-
* Available to the step immediately after an await.
95-
* Cleared after the step executes.
96-
*/
97-
private ?IIntegrationEvent $resume_event = null;
98-
9994
// ─────────────────────────────────────────────────────────────────────────
10095
// Accessors (framework state)
10196
// ─────────────────────────────────────────────────────────────────────────
@@ -132,50 +127,6 @@ public function updated_at(): ?DateTimeImmutable {
132127
return $this->updated_at;
133128
}
134129

135-
// ─────────────────────────────────────────────────────────────────────────
136-
// Transient state accessors
137-
// ─────────────────────────────────────────────────────────────────────────
138-
139-
/**
140-
* Get the event that triggered resume from suspension.
141-
* Only available to the step immediately after an await.
142-
*
143-
* @template T of IIntegrationEvent
144-
* @param class-string<T>|null $expected_class Optional type check
145-
* @return T|IIntegrationEvent|null
146-
*/
147-
public function resume_event(?string $expected_class = null): ?IIntegrationEvent {
148-
if ($this->resume_event === null) {
149-
return null;
150-
}
151-
152-
if ($expected_class !== null && !($this->resume_event instanceof $expected_class)) {
153-
throw new \RuntimeException(sprintf(
154-
'Expected resume event of type %s, got %s',
155-
$expected_class,
156-
get_class($this->resume_event)
157-
));
158-
}
159-
160-
return $this->resume_event;
161-
}
162-
163-
/**
164-
* Set the resume event (called by ProcessRunner).
165-
* @internal
166-
*/
167-
public function set_resume_event(IIntegrationEvent $event): void {
168-
$this->resume_event = $event;
169-
}
170-
171-
/**
172-
* Clear the resume event after step execution.
173-
* @internal
174-
*/
175-
public function clear_resume_event(): void {
176-
$this->resume_event = null;
177-
}
178-
179130
// ─────────────────────────────────────────────────────────────────────────
180131
// Step state accessors (delegates to ProcessSteps, hides internal structure)
181132
// ─────────────────────────────────────────────────────────────────────────

ddd-src/Application/Process/ProcessRunner.php

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* Responsibilities:
1717
* - Register event types for process resume
1818
* - Discover steps via reflection (methods in declaration order)
19-
* - Execute steps and dispatch returned commands/queries
19+
* - Execute steps and dispatch returned commands
2020
* - Suspend on AwaitEvent and persist state
2121
* - Resume when matching integration event fires
2222
* - Reschedule via ActionScheduler when resources exhausted or #[Async] step
@@ -30,6 +30,9 @@ final class ProcessRunner {
3030
/** @var array<string, bool> Tracks which events have action hooks registered */
3131
private array $registered_events = [];
3232

33+
/** @var IIntegrationEvent|null Transient - event that triggered current resume */
34+
private ?IIntegrationEvent $resume_event = null;
35+
3336
public function __construct(
3437
private readonly IDDDConfig $config,
3538
private readonly IProcessRepository $repository,
@@ -134,12 +137,15 @@ public function resume_on_event(IIntegrationEvent $event): void {
134137

135138
// The awaited event arrived - complete the waiting step and move forward
136139
$process->advance_step();
137-
$process->set_resume_event($event);
140+
$this->resume_event = $event; // Store for next step to receive
138141
$process->advance(status: 'running', payload: $process->payload());
139142
$this->repository->save($process);
140143

141144
$this->run($process);
142145

146+
// Clear transient state
147+
$this->resume_event = null;
148+
143149
// Only resume first matching process per event
144150
// (if you need fan-out, remove this return)
145151
return;
@@ -204,7 +210,7 @@ private function execute_forward(LongProcess $process): void {
204210
);
205211
}
206212

207-
$this->dispatch_side_effects($result);
213+
$this->dispatch_commands($result);
208214

209215
// Check for event-based suspension
210216
if ($result->should_suspend()) {
@@ -218,7 +224,7 @@ private function execute_forward(LongProcess $process): void {
218224
// Advance to next step
219225
$process->advance_step();
220226
$process->advance(status: 'running', payload: $result->payload);
221-
$process->clear_resume_event();
227+
$this->resume_event = null; // Clear after first step post-resume
222228
$this->repository->save($process);
223229

224230
// Check resources after each step
@@ -283,7 +289,7 @@ private function execute_compensation(LongProcess $process): void {
283289
$checkpoint = $process->checkpoint_for($step_name);
284290
$result = $method->invoke($process, $cause, $checkpoint);
285291

286-
$this->dispatch_side_effects($result);
292+
$this->dispatch_commands($result);
287293

288294
if ($result->should_suspend()) {
289295
$this->suspend_for_event($process, $result);
@@ -318,25 +324,32 @@ private function execute_compensation(LongProcess $process): void {
318324

319325
/**
320326
* Execute a single step method.
327+
*
328+
* Supports three signatures:
329+
* - step(): Result - no params
330+
* - step($payload): Result - receives payload
331+
* - step($payload, $event): Result - receives payload + event (post-await)
321332
*/
322333
private function execute_step(LongProcess $process, ReflectionMethod $step): mixed {
323334
$params = $step->getParameters();
335+
$param_count = count($params);
324336

325-
if (empty($params)) {
337+
if ($param_count === 0) {
326338
return $step->invoke($process);
327339
}
328340

329-
return $step->invoke($process, $process->payload());
341+
if ($param_count === 1) {
342+
return $step->invoke($process, $process->payload());
343+
}
344+
345+
// 2+ params: pass payload and event
346+
return $step->invoke($process, $process->payload(), $this->resume_event);
330347
}
331348

332349
/**
333-
* Dispatch commands and queries from a Result.
350+
* Dispatch commands from a Result (fire-and-forget side effects).
334351
*/
335-
private function dispatch_side_effects(Result $result): void {
336-
foreach ($result->queries as $query) {
337-
$query->send();
338-
}
339-
352+
private function dispatch_commands(Result $result): void {
340353
foreach ($result->commands as $command) {
341354
$command->send();
342355
}

ddd-src/Application/Process/Result.php

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
*
1010
* Each step returns a Result that tells the runner what to do:
1111
* - payload: pass data to the next step (must be JsonLifecycleValue)
12-
* - queries: execute these queries, pass results to next step
13-
* - commands: execute these commands
12+
* - commands: execute these commands (fire-and-forget side effects)
1413
* - await: suspend until this event fires
1514
* - checkpoint: data for compensation (must be JsonLifecycleValue)
15+
*
16+
* Steps that need to query for data should do so directly within the step
17+
* method, then return the relevant data in the payload.
1618
*/
1719
final class Result {
1820
public function __construct(
@@ -22,10 +24,7 @@ public function __construct(
2224
*/
2325
public readonly ?JsonLifecycleValue $payload = null,
2426

25-
/** @var array Queries to execute (results passed to next step) */
26-
public readonly array $queries = [],
27-
28-
/** @var array Commands to dispatch */
27+
/** @var array Commands to dispatch (fire-and-forget) */
2928
public readonly array $commands = [],
3029

3130
/** Suspend and wait for this event */
@@ -39,10 +38,6 @@ public function __construct(
3938
public readonly ?JsonLifecycleValue $checkpoint = null,
4039
) {}
4140

42-
public function has_queries(): bool {
43-
return !empty($this->queries);
44-
}
45-
4641
public function has_commands(): bool {
4742
return !empty($this->commands);
4843
}

0 commit comments

Comments
 (0)