extractDependencyId($dep); if (! isset($graph[$depId])) { throw new Exception("Unknown dependency: '{$depId}' in step '{$id}'"); } $graph[$depId][] = $id; $inDegree[$id]++; } } // Kahn's Algorithm $queue = []; foreach ($inDegree as $id => $degree) { if ($degree === 0) { $queue[] = $id; } } $sorted = []; while (! empty($queue)) { $current = array_shift($queue); $sorted[] = $current; foreach ($graph[$current] as $neighbor) { $inDegree[$neighbor]--; if ($inDegree[$neighbor] === 0) { $queue[] = $neighbor; } } } if (count($sorted) !== count($steps)) { // 순환 의존성 발견 - 어떤 스텝들이 문제인지 파악 $remaining = array_diff(array_keys($graph), $sorted); throw new Exception('Circular dependency detected in steps: '.implode(', ', $remaining)); } return $sorted; } /** * 의존성에서 스텝 ID 추출 * * @param mixed $dependency 의존성 (문자열 또는 조건부 객체) * @return string 스텝 ID */ private function extractDependencyId(mixed $dependency): string { if (is_string($dependency)) { return $dependency; } if (is_array($dependency) && isset($dependency['step'])) { return $dependency['step']; } throw new Exception('Invalid dependency format: '.json_encode($dependency)); } /** * 의존성 조건 파싱 * * @param mixed $dependency 의존성 (문자열 또는 조건부 객체) * @return array ['stepId' => string, 'condition' => string] */ public function parseDependency(mixed $dependency): array { if (is_string($dependency)) { return [ 'stepId' => $dependency, 'condition' => 'success', // 기본값: 성공 시에만 ]; } if (is_array($dependency)) { return [ 'stepId' => $dependency['step'] ?? '', 'condition' => $dependency['onlyIf'] ?? 'success', ]; } return [ 'stepId' => '', 'condition' => 'success', ]; } /** * 의존성 그래프 시각화 (디버깅용) * * @param array $steps 스텝 정의 배열 * @return array 의존성 정보 */ public function visualize(array $steps): array { $result = []; foreach ($steps as $step) { $id = $step['id']; $deps = $step['dependsOn'] ?? []; $result[$id] = [ 'name' => $step['name'] ?? $id, 'depends_on' => $deps, 'depended_by' => [], ]; } // 역방향 의존성 추가 foreach ($steps as $step) { $id = $step['id']; $deps = $step['dependsOn'] ?? []; foreach ($deps as $dep) { if (isset($result[$dep])) { $result[$dep]['depended_by'][] = $id; } } } return $result; } /** * 의존성 유효성 검사 * * @param array $steps 스텝 정의 배열 * @return array ['valid' => bool, 'errors' => array] */ public function validate(array $steps): array { $errors = []; $stepIds = array_column($steps, 'id'); // 중복 ID 체크 $duplicates = array_diff_assoc($stepIds, array_unique($stepIds)); if (! empty($duplicates)) { $errors[] = 'Duplicate step IDs found: '.implode(', ', array_unique($duplicates)); } // 존재하지 않는 의존성 체크 foreach ($steps as $step) { $id = $step['id']; $deps = $step['dependsOn'] ?? []; foreach ($deps as $dep) { // 조건부 의존성 지원 $parsed = $this->parseDependency($dep); $depId = $parsed['stepId']; if (! in_array($depId, $stepIds)) { $errors[] = "Step '{$id}' depends on unknown step '{$depId}'"; } // 자기 참조 체크 if ($depId === $id) { $errors[] = "Step '{$id}' cannot depend on itself"; } // 조건부 의존성 조건값 검증 $validConditions = ['success', 'failure', 'executed', 'skipped', 'any']; if (! in_array($parsed['condition'], $validConditions)) { $errors[] = "Step '{$id}': Invalid dependency condition '{$parsed['condition']}'. Valid values: ".implode(', ', $validConditions); } } } // 순환 의존성 체크 try { $this->resolve($steps); } catch (Exception $e) { $errors[] = $e->getMessage(); } return [ 'valid' => empty($errors), 'errors' => $errors, ]; } }