Files
sam-manage/app/Services/FlowTester/VariableBinder.php
hskwon fe902472c1 feat: [flow-tester] 의존성 검사, Docker 지원, 인증 자동 주입 기능 추가
FlowExecutor 개선:
- 의존성 스텝 실패 시 후속 스텝 자동 스킵 로직 추가
- Docker 환경 자동 감지 및 내부 URL 변환 (api.sam.kr → nginx)
- SSL 검증 비활성화 및 Host 헤더 설정 지원
- .env에서 API Key/Bearer Token 자동 주입

VariableBinder 개선:
- 임의 stepId 패턴 지원 (page_create_1.tempPageId 등)
- {{$env.VAR_NAME}} 환경변수 플레이스홀더 추가
- {{$auth.token}}, {{$auth.apiKey}} 인증 플레이스홀더 추가

UI 개선:
- SKIPPED 상태 스타일링 (노란색 배경/테두리)
- 행 클릭 시 스텝 상세 확장 기능
- 실행 결과 실시간 표시 개선
2025-11-27 22:20:36 +09:00

234 lines
6.5 KiB
PHP

<?php
namespace App\Services\FlowTester;
use Illuminate\Support\Str;
/**
* 변수 바인딩 엔진
*
* {{...}} 패턴을 감지하고 실제 값으로 치환합니다.
* - {{variables.xxx}} - 전역 변수
* - {{stepN.xxx}} - 이전 단계 추출값
* - {{stepN.response.xxx}} - 이전 단계 전체 응답
* - {{$timestamp}} - 현재 타임스탬프
* - {{$uuid}} - 랜덤 UUID
* - {{$random:N}} - N자리 랜덤 숫자
*/
class VariableBinder
{
private array $context = [];
/**
* 컨텍스트 초기화
*/
public function reset(): void
{
$this->context = [];
}
/**
* 전역 변수 설정
*/
public function setVariables(array $variables): void
{
$this->context['variables'] = $variables;
}
/**
* 컨텍스트에 변수 추가
*/
public function setVariable(string $key, mixed $value): void
{
data_set($this->context, $key, $value);
}
/**
* 단계 결과 저장
*/
public function setStepResult(string $stepId, array $extracted, array $fullResponse): void
{
$this->context['steps'][$stepId] = [
'extracted' => $extracted,
'response' => $fullResponse,
];
// 편의를 위해 extracted 값을 stepId.key 형태로도 접근 가능하게
foreach ($extracted as $key => $value) {
$this->context['steps'][$stepId][$key] = $value;
}
}
/**
* 문자열/배열 내 모든 변수 치환
*/
public function bind(mixed $input): mixed
{
if (is_string($input)) {
return $this->bindString($input);
}
if (is_array($input)) {
return array_map(fn ($v) => $this->bind($v), $input);
}
return $input;
}
/**
* 문자열 내 변수 패턴 치환
*/
private function bindString(string $input): string
{
// 내장 변수 처리
$input = $this->resolveBuiltins($input);
// 컨텍스트 변수 처리
return preg_replace_callback(
'/\{\{([^}]+)\}\}/',
fn ($matches) => $this->resolveReference($matches[1]),
$input
);
}
/**
* 내장 변수 처리 ($timestamp, $uuid, $random:N, $env.XXX, $auth.token)
*/
private function resolveBuiltins(string $input): string
{
// {{$timestamp}} → 현재 타임스탬프
$input = str_replace('{{$timestamp}}', (string) time(), $input);
// {{$uuid}} → 랜덤 UUID
$input = str_replace('{{$uuid}}', (string) Str::uuid(), $input);
// {{$random:N}} → N자리 랜덤 숫자
$input = preg_replace_callback(
'/\{\{\$random:(\d+)\}\}/',
function ($m) {
$digits = (int) $m[1];
$max = (int) pow(10, $digits) - 1;
return str_pad((string) random_int(0, $max), $digits, '0', STR_PAD_LEFT);
},
$input
);
// {{$date}} → 현재 날짜 (Y-m-d)
$input = str_replace('{{$date}}', date('Y-m-d'), $input);
// {{$datetime}} → 현재 날짜시간 (Y-m-d H:i:s)
$input = str_replace('{{$datetime}}', date('Y-m-d H:i:s'), $input);
// {{$env.VAR_NAME}} → 환경변수에서 읽기
$input = preg_replace_callback(
'/\{\{\$env\.([A-Z_][A-Z0-9_]*)\}\}/i',
fn ($m) => env($m[1], ''),
$input
);
// {{$auth.token}} → 현재 로그인 사용자의 API 토큰
if (str_contains($input, '{{$auth.token}}')) {
$token = $this->getAuthToken();
$input = str_replace('{{$auth.token}}', $token, $input);
}
// {{$auth.apiKey}} → .env의 API Key
$input = str_replace('{{$auth.apiKey}}', env('FLOW_TESTER_API_KEY', ''), $input);
return $input;
}
/**
* 현재 로그인 사용자의 API 토큰 조회
*/
private function getAuthToken(): string
{
$user = auth()->user();
if (! $user) {
return env('FLOW_TESTER_API_TOKEN', '');
}
// 사용자에게 저장된 API 토큰이 있으면 사용
if (! empty($user->api_token)) {
return $user->api_token;
}
// 없으면 .env의 기본 토큰 사용
return env('FLOW_TESTER_API_TOKEN', '');
}
/**
* 참조 경로 해석 (step1.pageId 또는 page_create_1.pageId → 실제 값)
*/
private function resolveReference(string $path): string
{
$path = trim($path);
// variables.xxx → $this->context['variables']['xxx']
if (str_starts_with($path, 'variables.')) {
$value = data_get($this->context, $path, '');
return $this->valueToString($value);
}
// stepId.xxx 패턴 감지 (stepId는 등록된 step의 ID)
// 첫 번째 점(.) 기준으로 분리
$dotPos = strpos($path, '.');
if ($dotPos !== false) {
$potentialStepId = substr($path, 0, $dotPos);
$subPath = substr($path, $dotPos + 1);
// 등록된 step인지 확인
if (isset($this->context['steps'][$potentialStepId])) {
// stepId.response.xxx → 전체 응답에서 추출
if (str_starts_with($subPath, 'response.')) {
$responsePath = substr($subPath, 9); // "response." 제거
$value = data_get($this->context['steps'][$potentialStepId]['response'] ?? [], $responsePath, '');
return $this->valueToString($value);
}
// stepId.xxx → extracted 또는 직접 접근
$value = data_get($this->context['steps'][$potentialStepId] ?? [], $subPath, '');
return $this->valueToString($value);
}
}
// 기타 경로는 context에서 직접 조회
$value = data_get($this->context, $path, '');
return $this->valueToString($value);
}
/**
* 값을 문자열로 변환 (배열/객체는 JSON)
*/
private function valueToString(mixed $value): string
{
if (is_null($value)) {
return '';
}
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if (is_array($value) || is_object($value)) {
return json_encode($value, JSON_UNESCAPED_UNICODE);
}
return (string) $value;
}
/**
* 현재 컨텍스트 반환 (디버깅용)
*/
public function getContext(): array
{
return $this->context;
}
}