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; } }