- VariableBinder: 변수 바인딩 엔진 ({{...}} 패턴 처리)
- DependencyResolver: 의존성 정렬 (Topological Sort)
- ResponseValidator: HTTP 응답 검증 (JSONPath, 연산자)
- HttpClient: Laravel HTTP Client 래퍼
- FlowExecutor: 플로우 실행 엔진
375 lines
11 KiB
PHP
375 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services\FlowTester;
|
|
|
|
use Exception;
|
|
|
|
/**
|
|
* 플로우 실행 엔진
|
|
*
|
|
* 플로우 정의를 받아 단계별로 실행하고 결과를 반환합니다.
|
|
*
|
|
* 실행 흐름:
|
|
* 1. 플로우 정의 로드 및 검증
|
|
* 2. 의존성 기반 실행 순서 결정 (TopSort)
|
|
* 3. 변수 바인딩 초기화
|
|
* 4. 각 단계 순차 실행
|
|
* 5. 결과 수집 및 반환
|
|
*/
|
|
class FlowExecutor
|
|
{
|
|
private VariableBinder $binder;
|
|
|
|
private DependencyResolver $resolver;
|
|
|
|
private ResponseValidator $validator;
|
|
|
|
private HttpClient $httpClient;
|
|
|
|
/**
|
|
* 실행 로그
|
|
*/
|
|
private array $executionLog = [];
|
|
|
|
/**
|
|
* 실행 상태
|
|
*/
|
|
private string $status = 'PENDING';
|
|
|
|
/**
|
|
* 현재 진행 상황
|
|
*/
|
|
private int $completedSteps = 0;
|
|
|
|
private int $totalSteps = 0;
|
|
|
|
public function __construct(
|
|
?VariableBinder $binder = null,
|
|
?DependencyResolver $resolver = null,
|
|
?ResponseValidator $validator = null,
|
|
?HttpClient $httpClient = null
|
|
) {
|
|
$this->binder = $binder ?? new VariableBinder;
|
|
$this->resolver = $resolver ?? new DependencyResolver;
|
|
$this->validator = $validator ?? new ResponseValidator;
|
|
$this->httpClient = $httpClient ?? new HttpClient;
|
|
}
|
|
|
|
/**
|
|
* 플로우 실행
|
|
*
|
|
* @param array $flowDefinition 플로우 정의 JSON
|
|
* @param array $inputVariables 입력 변수 (실행 시 주입)
|
|
* @return array 실행 결과
|
|
*/
|
|
public function execute(array $flowDefinition, array $inputVariables = []): array
|
|
{
|
|
$startTime = microtime(true);
|
|
$this->reset();
|
|
|
|
try {
|
|
// 1. 플로우 정의 검증
|
|
$this->validateFlowDefinition($flowDefinition);
|
|
|
|
// 2. 설정 적용
|
|
$this->applyConfig($flowDefinition['config'] ?? []);
|
|
|
|
// 3. 전역 변수 초기화
|
|
$variables = array_merge(
|
|
$flowDefinition['variables'] ?? [],
|
|
$inputVariables
|
|
);
|
|
$this->binder->setVariables($variables);
|
|
|
|
// 4. 스텝 정보 추출
|
|
$steps = $flowDefinition['steps'] ?? [];
|
|
$this->totalSteps = count($steps);
|
|
|
|
if ($this->totalSteps === 0) {
|
|
return $this->buildResult($startTime, 'SUCCESS', 'No steps to execute');
|
|
}
|
|
|
|
// 5. 의존성 정렬
|
|
$orderedStepIds = $this->resolver->resolve($steps);
|
|
$stepMap = $this->buildStepMap($steps);
|
|
|
|
// 6. 실행 상태 변경
|
|
$this->status = 'RUNNING';
|
|
|
|
// 7. 각 단계 실행
|
|
$stopOnFailure = $flowDefinition['config']['stopOnFailure'] ?? true;
|
|
|
|
foreach ($orderedStepIds as $stepId) {
|
|
$step = $stepMap[$stepId];
|
|
$stepResult = $this->executeStep($step);
|
|
|
|
$this->executionLog[] = $stepResult;
|
|
|
|
if ($stepResult['success']) {
|
|
$this->completedSteps++;
|
|
} else {
|
|
if (! ($step['continueOnFailure'] ?? false) && $stopOnFailure) {
|
|
$this->status = 'FAILED';
|
|
|
|
return $this->buildResult(
|
|
$startTime,
|
|
'FAILED',
|
|
"Step '{$stepId}' failed: ".($stepResult['error'] ?? 'Unknown error'),
|
|
$stepId
|
|
);
|
|
}
|
|
// continueOnFailure가 true면 계속 진행
|
|
}
|
|
}
|
|
|
|
// 8. 최종 상태 결정
|
|
$this->status = $this->completedSteps === $this->totalSteps ? 'SUCCESS' : 'PARTIAL';
|
|
|
|
return $this->buildResult($startTime, $this->status);
|
|
} catch (Exception $e) {
|
|
$this->status = 'FAILED';
|
|
|
|
return $this->buildResult($startTime, 'FAILED', $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 단일 스텝 실행
|
|
*/
|
|
private function executeStep(array $step): array
|
|
{
|
|
$stepId = $step['id'];
|
|
$stepName = $step['name'] ?? $stepId;
|
|
$startTime = microtime(true);
|
|
|
|
try {
|
|
// 1. 딜레이 적용
|
|
$delay = $step['delay'] ?? 0;
|
|
if ($delay > 0) {
|
|
usleep($delay * 1000); // ms → μs
|
|
}
|
|
|
|
// 2. 변수 바인딩
|
|
$endpoint = $this->binder->bind($step['endpoint']);
|
|
$headers = $this->binder->bind($step['headers'] ?? []);
|
|
$body = $this->binder->bind($step['body'] ?? []);
|
|
|
|
// 3. HTTP 요청 실행
|
|
$method = strtoupper($step['method']);
|
|
$response = $this->httpClient->request($method, $endpoint, [
|
|
'headers' => $headers,
|
|
'body' => $body,
|
|
]);
|
|
|
|
// 4. HTTP 에러 체크
|
|
if ($response['error']) {
|
|
return $this->buildStepResult($stepId, $stepName, $startTime, false, [
|
|
'error' => $response['error'],
|
|
'request' => [
|
|
'method' => $method,
|
|
'endpoint' => $endpoint,
|
|
'headers' => $headers,
|
|
'body' => $body,
|
|
],
|
|
]);
|
|
}
|
|
|
|
// 5. 응답 검증
|
|
$expect = $step['expect'] ?? [];
|
|
$validation = $this->validator->validate($response, $expect);
|
|
|
|
// 6. 변수 추출
|
|
$extracted = [];
|
|
if (isset($step['extract'])) {
|
|
$extracted = $this->validator->extractValues($response['body'], $step['extract']);
|
|
$this->binder->setStepResult($stepId, $extracted, $response['body']);
|
|
}
|
|
|
|
// 7. 결과 구성
|
|
$success = $validation['success'];
|
|
|
|
return $this->buildStepResult($stepId, $stepName, $startTime, $success, [
|
|
'request' => [
|
|
'method' => $method,
|
|
'endpoint' => $endpoint,
|
|
'headers' => $headers,
|
|
'body' => $body,
|
|
],
|
|
'response' => [
|
|
'status' => $response['status'],
|
|
'body' => $response['body'],
|
|
'duration' => $response['duration'],
|
|
],
|
|
'extracted' => $extracted,
|
|
'validation' => $validation,
|
|
'error' => $success ? null : implode('; ', $validation['errors']),
|
|
]);
|
|
} catch (Exception $e) {
|
|
return $this->buildStepResult($stepId, $stepName, $startTime, false, [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 플로우 정의 검증
|
|
*/
|
|
private function validateFlowDefinition(array $definition): void
|
|
{
|
|
// steps 필드 필수
|
|
if (! isset($definition['steps']) || ! is_array($definition['steps'])) {
|
|
throw new Exception('Flow definition must have a "steps" array');
|
|
}
|
|
|
|
// 각 스텝 검증
|
|
foreach ($definition['steps'] as $index => $step) {
|
|
if (! isset($step['id'])) {
|
|
throw new Exception("Step at index {$index} must have an 'id' field");
|
|
}
|
|
if (! isset($step['method'])) {
|
|
throw new Exception("Step '{$step['id']}' must have a 'method' field");
|
|
}
|
|
if (! isset($step['endpoint'])) {
|
|
throw new Exception("Step '{$step['id']}' must have an 'endpoint' field");
|
|
}
|
|
}
|
|
|
|
// 의존성 검증
|
|
$validation = $this->resolver->validate($definition['steps']);
|
|
if (! $validation['valid']) {
|
|
throw new Exception('Dependency validation failed: '.implode('; ', $validation['errors']));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 설정 적용
|
|
*/
|
|
private function applyConfig(array $config): void
|
|
{
|
|
// Base URL
|
|
if (isset($config['baseUrl'])) {
|
|
$this->httpClient->setBaseUrl($config['baseUrl']);
|
|
}
|
|
|
|
// Timeout
|
|
if (isset($config['timeout'])) {
|
|
$timeout = (int) ($config['timeout'] / 1000); // ms → s
|
|
$this->httpClient->setTimeout(max(1, $timeout));
|
|
}
|
|
|
|
// 기본 헤더
|
|
if (isset($config['headers'])) {
|
|
$this->httpClient->setDefaultHeaders($config['headers']);
|
|
}
|
|
|
|
// 인증
|
|
if (isset($config['apiKey'])) {
|
|
$this->httpClient->setApiKey($config['apiKey']);
|
|
}
|
|
if (isset($config['bearerToken'])) {
|
|
$this->httpClient->setBearerToken($config['bearerToken']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 스텝 맵 구성 (id → step)
|
|
*/
|
|
private function buildStepMap(array $steps): array
|
|
{
|
|
$map = [];
|
|
foreach ($steps as $step) {
|
|
$map[$step['id']] = $step;
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
/**
|
|
* 스텝 결과 구성
|
|
*/
|
|
private function buildStepResult(
|
|
string $stepId,
|
|
string $stepName,
|
|
float $startTime,
|
|
bool $success,
|
|
array $details = []
|
|
): array {
|
|
$duration = (int) ((microtime(true) - $startTime) * 1000);
|
|
|
|
return array_merge([
|
|
'stepId' => $stepId,
|
|
'stepName' => $stepName,
|
|
'success' => $success,
|
|
'duration' => $duration,
|
|
'timestamp' => date('Y-m-d H:i:s'),
|
|
], $details);
|
|
}
|
|
|
|
/**
|
|
* 최종 결과 구성
|
|
*/
|
|
private function buildResult(
|
|
float $startTime,
|
|
string $status,
|
|
?string $errorMessage = null,
|
|
?string $failedStep = null
|
|
): array {
|
|
$duration = (int) ((microtime(true) - $startTime) * 1000);
|
|
|
|
return [
|
|
'status' => $status,
|
|
'duration' => $duration,
|
|
'totalSteps' => $this->totalSteps,
|
|
'completedSteps' => $this->completedSteps,
|
|
'failedStep' => $failedStep,
|
|
'errorMessage' => $errorMessage,
|
|
'executionLog' => $this->executionLog,
|
|
'startedAt' => date('Y-m-d H:i:s', (int) $startTime),
|
|
'completedAt' => date('Y-m-d H:i:s'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 상태 초기화
|
|
*/
|
|
private function reset(): void
|
|
{
|
|
$this->executionLog = [];
|
|
$this->status = 'PENDING';
|
|
$this->completedSteps = 0;
|
|
$this->totalSteps = 0;
|
|
$this->binder->reset();
|
|
}
|
|
|
|
/**
|
|
* 현재 진행 상황 조회
|
|
*/
|
|
public function getProgress(): array
|
|
{
|
|
return [
|
|
'status' => $this->status,
|
|
'completedSteps' => $this->completedSteps,
|
|
'totalSteps' => $this->totalSteps,
|
|
'percent' => $this->totalSteps > 0
|
|
? round(($this->completedSteps / $this->totalSteps) * 100, 1)
|
|
: 0,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 실행 로그 조회
|
|
*/
|
|
public function getExecutionLog(): array
|
|
{
|
|
return $this->executionLog;
|
|
}
|
|
|
|
/**
|
|
* 현재 상태 조회
|
|
*/
|
|
public function getStatus(): string
|
|
{
|
|
return $this->status;
|
|
}
|
|
}
|