Files
sam-manage/app/Services/FlowTester/DependencyResolver.php
hskwon 367f2159d1 feat: API Flow Tester 핵심 서비스 클래스 구현 (Day 2)
- VariableBinder: 변수 바인딩 엔진 ({{...}} 패턴 처리)
- DependencyResolver: 의존성 정렬 (Topological Sort)
- ResponseValidator: HTTP 응답 검증 (JSONPath, 연산자)
- HttpClient: Laravel HTTP Client 래퍼
- FlowExecutor: 플로우 실행 엔진
2025-11-27 19:20:07 +09:00

164 lines
4.3 KiB
PHP

<?php
namespace App\Services\FlowTester;
use Exception;
/**
* 의존성 정렬기 (Topological Sort)
*
* 플로우 스텝의 dependsOn 속성을 분석하여
* 올바른 실행 순서를 결정합니다.
*/
class DependencyResolver
{
/**
* 의존성 기반 실행 순서 결정
*
* @param array $steps 스텝 정의 배열
* @return array 정렬된 스텝 ID 배열
*
* @throws Exception 순환 의존성 발견 시
*/
public function resolve(array $steps): array
{
$graph = [];
$inDegree = [];
$stepMap = [];
// 그래프 초기화
foreach ($steps as $step) {
$id = $step['id'];
$graph[$id] = [];
$inDegree[$id] = 0;
$stepMap[$id] = $step;
}
// 간선 추가 (의존성)
foreach ($steps as $step) {
$id = $step['id'];
$deps = $step['dependsOn'] ?? [];
foreach ($deps as $dep) {
if (! isset($graph[$dep])) {
throw new Exception("Unknown dependency: '{$dep}' in step '{$id}'");
}
$graph[$dep][] = $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;
}
/**
* 의존성 그래프 시각화 (디버깅용)
*
* @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) {
if (! in_array($dep, $stepIds)) {
$errors[] = "Step '{$id}' depends on unknown step '{$dep}'";
}
// 자기 참조 체크
if ($dep === $id) {
$errors[] = "Step '{$id}' cannot depend on itself";
}
}
}
// 순환 의존성 체크
try {
$this->resolve($steps);
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
return [
'valid' => empty($errors),
'errors' => $errors,
];
}
}