$projections */ public function __construct( private EventStore $eventStore, private ProjectionRepository $repository, private Queue $queue, private array $projections = [] ) { } /** * Run single projection synchronously */ public function runProjection(string $projectionName): ProjectionState { $projection = $this->findProjection($projectionName); $state = $this->repository->getState($projectionName); try { // Get events since last processed version $events = $this->getEventsSince($state->lastEventVersion); foreach ($events as $envelope) { // Check if projection subscribes to this event type if ($this->shouldProject($projection, $envelope)) { $projection->project($envelope); $state = $state->withEventProcessed($envelope->version); $this->repository->saveState($state); } } $state = $state->withCompleted(); $this->repository->saveState($state); return $state; } catch (\Throwable $e) { $state = $state->withError($e->getMessage()); $this->repository->saveState($state); throw $e; } } /** * Run projection asynchronously via queue */ public function runProjectionAsync(string $projectionName): void { $job = new RunProjectionJob($projectionName); $payload = JobPayload::immediate($job); $this->queue->push($payload); } /** * Run all projections */ public function runAllProjections(): array { $results = []; foreach ($this->projections as $projection) { $results[$projection->getName()] = $this->runProjection( $projection->getName() ); } return $results; } /** * Rebuild projection from scratch */ public function rebuildProjection(string $projectionName): ProjectionState { $projection = $this->findProjection($projectionName); // Reset projection and read model $projection->reset(); // Reset state to initial $state = ProjectionState::initial($projectionName); $this->repository->saveState($state); // Run from beginning return $this->runProjection($projectionName); } /** * Get projection status */ public function getProjectionStatus(string $projectionName): ProjectionState { return $this->repository->getState($projectionName); } /** * Get all projection statuses */ public function getAllProjectionStatuses(): array { $statuses = []; foreach ($this->projections as $projection) { $statuses[$projection->getName()] = $this->repository->getState( $projection->getName() ); } return $statuses; } private function findProjection(string $name): ProjectionInterface { foreach ($this->projections as $projection) { if ($projection->getName() === $name) { return $projection; } } throw new \RuntimeException("Projection '{$name}' not found"); } private function getEventsSince(int $version): iterable { // In production: implement efficient event fetching since version // For now: return all events (will be optimized with event store query) // This would typically be: $this->eventStore->loadStreamSince($version) // Placeholder - needs EventStore enhancement return []; } private function shouldProject( ProjectionInterface $projection, AggregateEnvelope $envelope ): bool { $subscribedEvents = $projection->subscribedTo(); $eventClass = $envelope->eventName(); return in_array($eventClass, $subscribedEvents, true); } }