jobIds = array_values(array_unique($jobIds)); $this->validate(); } public static function sequential(string $chainId, string $name, array $jobIds): self { return new self($chainId, $name, $jobIds, ChainExecutionMode::SEQUENTIAL); } public static function parallel(string $chainId, string $name, array $jobIds): self { return new self($chainId, $name, $jobIds, ChainExecutionMode::PARALLEL); } public static function conditional(string $chainId, string $name, array $jobIds): self { return new self($chainId, $name, $jobIds, ChainExecutionMode::CONDITIONAL); } public function isSequential(): bool { return $this->executionMode === ChainExecutionMode::SEQUENTIAL; } public function isParallel(): bool { return $this->executionMode === ChainExecutionMode::PARALLEL; } public function isConditional(): bool { return $this->executionMode === ChainExecutionMode::CONDITIONAL; } public function shouldStopOnFailure(): bool { return $this->stopOnFailure; } public function getJobCount(): int { return count($this->jobIds); } public function getFirstJob(): ?string { return $this->jobIds[0] ?? null; } public function getLastJob(): ?string { return end($this->jobIds) ?: null; } public function containsJob(string $jobId): bool { return in_array($jobId, $this->jobIds, true); } public function getJobPosition(string $jobId): ?int { $position = array_search($jobId, $this->jobIds, true); return $position !== false ? $position : null; } public function getNextJob(string $currentJobId): ?string { $position = $this->getJobPosition($currentJobId); if ($position === null) { return null; } return $this->jobIds[$position + 1] ?? null; } public function getPreviousJob(string $currentJobId): ?string { $position = $this->getJobPosition($currentJobId); if ($position === null || $position === 0) { return null; } return $this->jobIds[$position - 1]; } public function withMetadata(array $metadata): self { return new self( chainId: $this->chainId, name: $this->name, jobIds: $this->jobIds, executionMode: $this->executionMode, stopOnFailure: $this->stopOnFailure, metadata: array_merge($this->metadata ?? [], $metadata) ); } public function withStopOnFailure(bool $stopOnFailure): self { return new self( chainId: $this->chainId, name: $this->name, jobIds: $this->jobIds, executionMode: $this->executionMode, stopOnFailure: $stopOnFailure, metadata: $this->metadata ); } public function toArray(): array { return [ 'chain_id' => $this->chainId, 'name' => $this->name, 'job_ids' => $this->jobIds, 'execution_mode' => $this->executionMode->value, 'stop_on_failure' => $this->stopOnFailure, 'job_count' => $this->getJobCount(), 'metadata' => $this->metadata, ]; } private function validate(): void { if (empty(trim($this->chainId))) { throw new \InvalidArgumentException('Chain ID cannot be empty'); } if (empty(trim($this->name))) { throw new \InvalidArgumentException('Chain name cannot be empty'); } if (empty($this->jobIds)) { throw new \InvalidArgumentException('Job chain must contain at least one job'); } foreach ($this->jobIds as $jobId) { if (empty(trim($jobId))) { throw new \InvalidArgumentException('Job ID cannot be empty'); } } } }