- VariableBinder: 변수 바인딩 엔진 ({{...}} 패턴 처리)
- DependencyResolver: 의존성 정렬 (Topological Sort)
- ResponseValidator: HTTP 응답 검증 (JSONPath, 연산자)
- HttpClient: Laravel HTTP Client 래퍼
- FlowExecutor: 플로우 실행 엔진
164 lines
4.3 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|