|
| 1 | +## 思考 |
| 2 | +[Laravel-Event-Sourcing](https://spatie.be/docs/laravel-event-sourcing/v4/introduction) 中 StoredEvent 怎么保存 而 Projector 和 Reactor 又是怎么样监听到对应的事件的? |
| 3 | +## 案例 |
| 4 | +用户取消订单,需要触发 `BookingCancelled` 事件,我们需要在 `Projector` 监听到 `BookingCancelled` 后改变订单状态。同时需要在 `Reactor` 发送一封邮件给用户。 |
| 5 | +* 我们有一个 AggregateRoot `App\Bookings\AggregateRoot\BookingRoot`。里面有个取消订单的方法 cancle 代码如下: |
| 6 | +```php |
| 7 | +class BookingRoot extends AggregateRoot |
| 8 | + public function cancel(): self |
| 9 | + { |
| 10 | + $this->recordThat( |
| 11 | + new BookingCancelled( |
| 12 | + $this->userId, |
| 13 | + ) |
| 14 | + ); |
| 15 | + return $this; |
| 16 | + } |
| 17 | +} |
| 18 | +``` |
| 19 | + |
| 20 | +* 我们有一个 Projector `App\Bookings\Projector\BookingsProjector` 。他是同步的,代码如下: |
| 21 | +```php |
| 22 | +class BookingsProjector extends Projector |
| 23 | +{ |
| 24 | + public function onBookingCancelled( |
| 25 | + StoredEvent $storedEvent, |
| 26 | + BookingCancelled $event, |
| 27 | + string $aggregateUuid |
| 28 | + ) { |
| 29 | + $projection = ProjectionBooking::query() |
| 30 | + ->where('booking_aggregate_id', $aggregateUuid) |
| 31 | + ->first(); |
| 32 | + $projection->status = BookingStatus::STATUS_CANCELLED; |
| 33 | + $projection->cancelled = $storedEvent->created_at; |
| 34 | + $projection->save(); |
| 35 | + } |
| 36 | +} |
| 37 | +``` |
| 38 | +* 我们有一个 Reactor `App\Bookings\Reactor\NotificationReactor`。他是异步的,代码如下: |
| 39 | +```php |
| 40 | +class NotificationReactor extends Reactor implements ShouldQueue |
| 41 | +{ |
| 42 | + public function onBookingCancelled(BookingCancelled $event, StoredEvent $storedEvent) |
| 43 | + { |
| 44 | + $user = User::query()->findOrFail($event->userId); |
| 45 | + Notification::send($user, new BookingCancelledNotification('订单取消')); |
| 46 | + } |
| 47 | +``` |
| 48 | + |
| 49 | +## 源码分析 |
| 50 | +当我们执行如下代码: |
| 51 | +```php |
| 52 | +BookingRoot::retrieve($bookingAggregateId)->cancel()->persist(); |
| 53 | +``` |
| 54 | +* ##### `Spatie\EventSourcing\AggregateRoots\AggregateRoot` 的 persist 方法(将事件保存到数据库): |
| 55 | +```php |
| 56 | + public function persist() |
| 57 | + { |
| 58 | + $storedEvents = $this->persistWithoutApplyingToEventHandlers(); |
| 59 | + |
| 60 | + $storedEvents->each(fn (StoredEvent $storedEvent) => $storedEvent->handleForAggregateRoot()); |
| 61 | + |
| 62 | + $this->aggregateVersionAfterReconstitution = $this->aggregateVersion; |
| 63 | + |
| 64 | + return $this; |
| 65 | + } |
| 66 | + |
| 67 | + protected function persistWithoutApplyingToEventHandlers(): LazyCollection |
| 68 | + { |
| 69 | + $this->ensureNoOtherEventsHaveBeenPersisted(); |
| 70 | + |
| 71 | + $storedEvents = $this |
| 72 | + ->getStoredEventRepository() |
| 73 | + ->persistMany( |
| 74 | + $this->getAndClearRecordedEvents(), |
| 75 | + $this->uuid(), |
| 76 | + $this->aggregateVersion, |
| 77 | + ); |
| 78 | + |
| 79 | + return $storedEvents; |
| 80 | + } |
| 81 | +``` |
| 82 | +在代码 `$this->persistWithoutApplyingToEventHandlers` 会调用 `Spatie\EventSourcing\StoredEvents\Repositories\EloquentStoredEventRepository` 的 persistMany 再循环要保存的事件通过 persist 保存到 `Spatie\EventSourcing\StoredEvents\Models\EloquentStoredEvent` 最后存到数据库表 stored_events 中。`Spatie\EventSourcing\StoredEvents\Repositories\EloquentStoredEventRepository` 代码如下: |
| 83 | +```php |
| 84 | +class EloquentStoredEventRepository implements StoredEventRepository |
| 85 | +{ |
| 86 | + public function persistMany(array $events, string $uuid = null, int $aggregateVersion = null): LazyCollection |
| 87 | + { |
| 88 | + $storedEvents = []; |
| 89 | + foreach ($events as $event) { |
| 90 | + $storedEvents[] = $this->persist($event, $uuid, $aggregateVersion); |
| 91 | + } |
| 92 | + |
| 93 | + return new LazyCollection($storedEvents); |
| 94 | + } |
| 95 | + |
| 96 | + public function persist(ShouldBeStored $event, string $uuid = null, int $aggregateVersion = null): StoredEvent |
| 97 | + { |
| 98 | + /** @var EloquentStoredEvent $eloquentStoredEvent */ |
| 99 | + $eloquentStoredEvent = new $this->storedEventModel(); |
| 100 | + |
| 101 | + $eloquentStoredEvent->setOriginalEvent($event); |
| 102 | + |
| 103 | + $eloquentStoredEvent->setRawAttributes([ |
| 104 | + 'event_properties' => app(EventSerializer::class)->serialize(clone $event), |
| 105 | + 'aggregate_uuid' => $uuid, |
| 106 | + 'aggregate_version' => $aggregateVersion, |
| 107 | + 'event_class' => $this->getEventClass(get_class($event)), |
| 108 | + 'meta_data' => json_encode($event->metaData()), |
| 109 | + 'created_at' => Carbon::now(), |
| 110 | + ]); |
| 111 | + |
| 112 | + $eloquentStoredEvent->save(); |
| 113 | + |
| 114 | + return $eloquentStoredEvent->toStoredEvent(); |
| 115 | + } |
| 116 | +``` |
| 117 | +* ##### 事件监听代码逻辑: |
| 118 | + ```php |
| 119 | +$storedEvents = $this->persistWithoutApplyingToEventHandlers(); |
| 120 | + ``` |
| 121 | +返回的是 item 为 `Spatie\EventSourcing\StoredEvents\StoredEvent` 的 `LazyCollection` |
| 122 | +第二步: |
| 123 | +```php |
| 124 | + $storedEvents->each(fn (StoredEvent $storedEvent) => $storedEvent->handleForAggregateRoot()); |
| 125 | +``` |
| 126 | +循环调用每一个 `Spatie\EventSourcing\StoredEvents\StoredEvent` 的 `handleForAggregateRoot` 方法。 |
| 127 | +```php |
| 128 | +namespace Spatie\EventSourcing\StoredEvents; |
| 129 | +class StoredEvent implements Arrayable |
| 130 | +{ |
| 131 | + |
| 132 | + public function handleForAggregateRoot(): void |
| 133 | + { |
| 134 | + $this->handle(); |
| 135 | + |
| 136 | + if (! config('event-sourcing.dispatch_events_from_aggregate_roots', false)) { |
| 137 | + return; |
| 138 | + } |
| 139 | + |
| 140 | + $this->event->firedFromAggregateRoot = true; |
| 141 | + event($this->event); |
| 142 | + } |
| 143 | + |
| 144 | + public function handle() |
| 145 | + { |
| 146 | + // Projector 和 Reactor 如果是同步 那么在 Projectionist 的 handleWithSyncEventHandlers 方法处理 |
| 147 | + Projectionist::handleWithSyncEventHandlers($this); |
| 148 | + |
| 149 | + if (method_exists($this->event, 'tags')) { |
| 150 | + $tags = $this->event->tags(); |
| 151 | + } |
| 152 | + |
| 153 | + if (! $this->shouldDispatchJob()) { |
| 154 | + return; |
| 155 | + } |
| 156 | + $storedEventJob = call_user_func( |
| 157 | + [config('event-sourcing.stored_event_job'), 'createForEvent'], |
| 158 | + $this, |
| 159 | + $tags ?? [] |
| 160 | + ); |
| 161 | + // Projector 和 Reactor 如果是异步就把 Spatie\EventSourcing\StoredEvents\HandleStoredEventJob 推送到队列 |
| 162 | + dispatch($storedEventJob->onQueue($this->getQueueName())); |
| 163 | + } |
| 164 | +``` |
| 165 | +上面代码中的 $this->event 为 `App\Bookings\Event\BookingCancelled` |
| 166 | +$storedEventJob 为 `Spatie\EventSourcing\StoredEvents\HandleStoredEventJob` 属性 $storedEvent 为代码中的 $this。 |
| 167 | + |
| 168 | +* ##### Projector 和 Reactor 同步监听逻辑 |
| 169 | +上面代码中同步调用代码: |
| 170 | + ```php |
| 171 | + Projectionist::handleWithSyncEventHandlers($this); |
| 172 | + ``` |
| 173 | +我们找到 `Spatie\EventSourcing\Projectionist` 的 handleWithSyncEventHandlers |
| 174 | + ```php |
| 175 | + namespace Spatie\EventSourcing; |
| 176 | + use Spatie\EventSourcing\EventHandlers\EventHandlerCollection; |
| 177 | + class Projectionist |
| 178 | +{ |
| 179 | + private EventHandlerCollection $projectors; |
| 180 | + |
| 181 | + private EventHandlerCollection $reactors; |
| 182 | + |
| 183 | + private bool $catchExceptions; |
| 184 | + |
| 185 | + private bool $replayChunkSize; |
| 186 | + |
| 187 | + private bool $isProjecting = false; |
| 188 | + |
| 189 | + private bool $isReplaying = false; |
| 190 | + |
| 191 | + public function __construct(array $config) |
| 192 | + { |
| 193 | + $this->projectors = new EventHandlerCollection(); |
| 194 | + $this->reactors = new EventHandlerCollection(); |
| 195 | + |
| 196 | + $this->catchExceptions = $config['catch_exceptions']; |
| 197 | + $this->replayChunkSize = $config['replay_chunk_size']; |
| 198 | + } |
| 199 | + public function handleWithSyncEventHandlers(StoredEvent $storedEvent): void |
| 200 | + { |
| 201 | + $projectors = $this->projectors |
| 202 | + ->forEvent($storedEvent) |
| 203 | + ->syncEventHandlers(); |
| 204 | + |
| 205 | + $this->applyStoredEventToProjectors($storedEvent, $projectors); |
| 206 | + |
| 207 | + $reactors = $this->reactors |
| 208 | + ->forEvent($storedEvent) |
| 209 | + ->syncEventHandlers(); |
| 210 | + |
| 211 | + $this->applyStoredEventToReactors($storedEvent, $reactors); |
| 212 | + } |
| 213 | +} |
| 214 | + ``` |
| 215 | +上面代码中的 `handleWithSyncEventHandlers ` 方法其实是通过 `EventHandlerCollection` 筛选出同步的 Projector 和 Reactor。 |
| 216 | +如果是 Projector 执行 `$this->applyStoredEventToProjectors($storedEvent, $projectors)`, |
| 217 | +如果是 Reactor 执行 `$this->applyStoredEventToReactors(StoredEvent $storedEvent, Collection $reactors):` |
| 218 | + ```php |
| 219 | + private function applyStoredEventToProjectors(StoredEvent $storedEvent, Collection $projectors): void |
| 220 | + { |
| 221 | + $this->isProjecting = true; |
| 222 | + |
| 223 | + foreach ($projectors as $projector) { |
| 224 | + $this->callEventHandler($projector, $storedEvent); |
| 225 | + } |
| 226 | + |
| 227 | + $this->isProjecting = false; |
| 228 | + } |
| 229 | + |
| 230 | + private function applyStoredEventToReactors(StoredEvent $storedEvent, Collection $reactors): void |
| 231 | + { |
| 232 | + foreach ($reactors as $reactor) { |
| 233 | + $this->callEventHandler($reactor, $storedEvent); |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + private function callEventHandler(EventHandler $eventHandler, StoredEvent $storedEvent): bool |
| 238 | + { |
| 239 | + try { |
| 240 | + $eventHandler->handle($storedEvent); |
| 241 | + } catch (Exception $exception) { |
| 242 | + if (! $this->catchExceptions) { |
| 243 | + throw $exception; |
| 244 | + } |
| 245 | + |
| 246 | + $eventHandler->handleException($exception); |
| 247 | + |
| 248 | + event(new EventHandlerFailedHandlingEvent($eventHandler, $storedEvent, $exception)); |
| 249 | + |
| 250 | + return false; |
| 251 | + } |
| 252 | + |
| 253 | + return true; |
| 254 | + } |
| 255 | + ``` |
| 256 | +在上面的案例中到了这一步,我们触发的是 `$this->applyStoredEventToProjectors($storedEvent, $projectors)`。 |
| 257 | +当调用 `$this->callEventHandler($projector, $storedEvent)` 时候: |
| 258 | +$projector 就是 `App\Bookings\Projector\BookingsProjector` , |
| 259 | +$storedEvent 就是 `Spatie\EventSourcing\StoredEvents\StoredEvent` 它的属性 event 是 `App\Bookings\Event\BookingCancelled`。 |
| 260 | +所以 `callEventHandler` 里面的 `$eventHandler->handle($storedEvent)` 可以理解为:`$projector->handle($storedEvent)`; |
| 261 | +其实就是调用了 `App\Bookings\Projector\BookingsProjector` 的 handle 方法。然后 handle 执行了`onBookingCancelled ` 方法。 |
| 262 | + |
| 263 | +* #### Projector 和 Reactor 异步监听逻辑 |
| 264 | +上面代码中异步调用代码: |
| 265 | + ```php |
| 266 | + dispatch($storedEventJob->onQueue($this->getQueueName())); |
| 267 | + ``` |
| 268 | +Laravel 的监听最后通过 `Illuminate\Bus\Dispatcher` 执行方法 `dispatchNow($command, $handler = null)` 还是会调用 $storedEventJob 的 handler 方法。 |
| 269 | +我们找到 `Spatie\EventSourcing\StoredEvents\HandleStoredEventJob` 的相关代码: |
| 270 | + ```php |
| 271 | + namespace Spatie\EventSourcing\StoredEvents; |
| 272 | + class HandleStoredEventJob implements HandleDomainEventJob, ShouldQueue |
| 273 | +{ |
| 274 | + use InteractsWithQueue, Queueable, SerializesModels; |
| 275 | + |
| 276 | + public StoredEvent $storedEvent; |
| 277 | + |
| 278 | + public array $tags; |
| 279 | + |
| 280 | + public function __construct(StoredEvent $storedEvent, array $tags) |
| 281 | + { |
| 282 | + $this->storedEvent = $storedEvent; |
| 283 | + |
| 284 | + $this->tags = $tags; |
| 285 | + } |
| 286 | + |
| 287 | + public function handle(Projectionist $projectionist): void |
| 288 | + { |
| 289 | + $projectionist->handle($this->storedEvent); |
| 290 | + } |
| 291 | +} |
| 292 | + ``` |
| 293 | +`handle` 继续调用了 `Spatie\EventSourcing\Projectionist` 的 `handle` 方法,代码如下: |
| 294 | + ```php |
| 295 | +namespace Spatie\EventSourcing; |
| 296 | +class Projectionist |
| 297 | +{ |
| 298 | + public function handle(StoredEvent $storedEvent): void |
| 299 | + { |
| 300 | + $projectors = $this->projectors |
| 301 | + ->forEvent($storedEvent) |
| 302 | + ->asyncEventHandlers(); |
| 303 | + |
| 304 | + $this->applyStoredEventToProjectors( |
| 305 | + $storedEvent, |
| 306 | + $projectors |
| 307 | + ); |
| 308 | + |
| 309 | + $reactors = $this->reactors |
| 310 | + ->forEvent($storedEvent) |
| 311 | + ->asyncEventHandlers(); |
| 312 | + |
| 313 | + $this->applyStoredEventToReactors( |
| 314 | + $storedEvent, |
| 315 | + $reactors |
| 316 | + ); |
| 317 | + } |
| 318 | +} |
| 319 | + ``` |
| 320 | +到这里其实和同步的逻辑就一样了。 `handle` 和 `handleWithSyncEventHandlers` 的区别只是前者筛选的是异步的 Projector 和 Reactor。后者筛选的是同步的。然后走到 |
| 321 | +`$this->applyStoredEventToProjectors($storedEvent, $projectors)` 以及 |
| 322 | +`$this->applyStoredEventToReactors(StoredEvent $storedEvent, Collection $reactors)`。 |
| 323 | +案例中:触发的就是`App\Bookings\Reactor\NotificationReactor` 的 `handle` 方法。然后 `handle` 执行了 `onBookingCancelled` 方法。 |
| 324 | + |
| 325 | + |
| 326 | +## 流程图 |
| 327 | + |
| 328 | + |
| 329 | +## 注意点 |
| 330 | +[Laravel-Event-Sourcing](https://spatie.be/docs/laravel-event-sourcing/v4/introduction) 异步处理机制是判断监听当前事件的 Projector 和 Reactor 是否 `instanceof ShouldQueue`,如果比对有一个则将当前事件创建的 HandleStoredEventJob 推送到 queue。无论同步还是异步的执行。都是拿到监听这个事件的所有 Projector 和 Reactor。如案例中的 BookingCancelled,则循环执行所有 Projector 和 Reactor 的 onBookingCancelled 方法。代码如下: |
| 331 | +```php |
| 332 | + private function applyStoredEventToProjectors(StoredEvent $storedEvent, Collection $projectors): void |
| 333 | + { |
| 334 | + $this->isProjecting = true; |
| 335 | + |
| 336 | + foreach ($projectors as $projector) { |
| 337 | + $this->callEventHandler($projector, $storedEvent); |
| 338 | + } |
| 339 | + |
| 340 | + $this->isProjecting = false; |
| 341 | + } |
| 342 | + |
| 343 | + private function applyStoredEventToReactors(StoredEvent $storedEvent, Collection $reactors): void |
| 344 | + { |
| 345 | + foreach ($reactors as $reactor) { |
| 346 | + $this->callEventHandler($reactor, $storedEvent); |
| 347 | + } |
| 348 | + } |
| 349 | + |
| 350 | + private function callEventHandler(EventHandler $eventHandler, StoredEvent $storedEvent): bool |
| 351 | + { |
| 352 | + try { |
| 353 | + $eventHandler->handle($storedEvent); |
| 354 | + } catch (Exception $exception) { |
| 355 | + if (! $this->catchExceptions) { |
| 356 | + throw $exception; |
| 357 | + } |
| 358 | + |
| 359 | + $eventHandler->handleException($exception); |
| 360 | + |
| 361 | + event(new EventHandlerFailedHandlingEvent($eventHandler, $storedEvent, $exception)); |
| 362 | + |
| 363 | + return false; |
| 364 | + } |
| 365 | + |
| 366 | + return true; |
| 367 | + } |
| 368 | +``` |
| 369 | +如果我们不希望循环执行所有 Projector 和 Reactor 时候被中断,可以在 event-sourcing.php 配置文件的 catch_exceptions 设置为 true,然后在 Projector 和 Reactor 自定义 handleException 方法处理我们的异常。 |
0 commit comments