called = true; $this->capturedEvent = $event; $this->capturedPayload = $payload; } }; $container = new AppContainer(); $container->set($listener::class, static fn () => $listener); $dispatcher = new ModuleEventDispatcher( ['user.deleted' => [['class' => $listener::class, 'method' => 'handle']]], $container ); $dispatcher->dispatch('user.deleted', ['user_id' => 42]); self::assertTrue($called); self::assertSame('user.deleted', $capturedEvent); self::assertSame(['user_id' => 42], $capturedPayload); } public function testDispatchIsNoOpForUnknownEvent(): void { $container = new AppContainer(); $dispatcher = new ModuleEventDispatcher([], $container); // Should not throw $dispatcher->dispatch('unknown.event', ['data' => 'test']); self::addToAssertionCount(1); } public function testDispatchContinuesAfterListenerException(): void { $secondCalled = false; $failingListener = new class () implements EventListener { public function handle(string $event, array $payload): void { throw new \RuntimeException('Listener failure'); } }; $successListener = new class ($secondCalled) implements EventListener { public function __construct(private bool &$called) { } public function handle(string $event, array $payload): void { $this->called = true; } }; $container = new AppContainer(); $container->set($failingListener::class, static fn () => $failingListener); $container->set($successListener::class, static fn () => $successListener); $dispatcher = new ModuleEventDispatcher( [ 'user.created' => [ ['class' => $failingListener::class, 'method' => 'handle'], ['class' => $successListener::class, 'method' => 'handle'], ], ], $container ); $dispatcher->dispatch('user.created', ['user_id' => 1]); self::assertTrue($secondCalled, 'Second listener should be called even after the first one throws'); } }