route('modelId'); $formulaId = $this->route('formulaId'); $rules = [ 'formula_name' => [ 'required', 'string', 'max:100', 'regex:/^[a-zA-Z][a-zA-Z0-9_\s]*$/', Rule::unique('model_formulas') ->where('model_id', $modelId) ->where('tenant_id', auth()->user()?->currentTenant?->id) ->whereNull('deleted_at') ], 'formula_expression' => ['required', 'string', 'max:1000'], 'unit' => ['nullable', 'string', 'max:20'], 'description' => ['nullable', 'string', 'max:500'], 'calculation_order' => ['integer', 'min:0'], 'dependencies' => ['nullable', 'array'], 'dependencies.*' => ['string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'], ]; // For update requests, ignore current record in unique validation if ($formulaId) { $rules['formula_name'][4] = $rules['formula_name'][4]->ignore($formulaId); } return $rules; } /** * Get custom messages for validator errors. */ public function messages(): array { return [ 'formula_name.required' => '공식 이름은 필수입니다.', 'formula_name.regex' => '공식 이름은 영문자로 시작하고 영문자, 숫자, 언더스코어, 공백만 사용할 수 있습니다.', 'formula_name.unique' => '해당 모델에 이미 동일한 공식 이름이 존재합니다.', 'formula_expression.required' => '공식 표현식은 필수입니다.', 'formula_expression.max' => '공식 표현식은 1000자를 초과할 수 없습니다.', 'unit.max' => '단위는 20자를 초과할 수 없습니다.', 'description.max' => '설명은 500자를 초과할 수 없습니다.', 'calculation_order.min' => '계산 순서는 0 이상이어야 합니다.', 'dependencies.array' => '의존성은 배열 형태여야 합니다.', 'dependencies.*.regex' => '의존성 변수명은 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용할 수 있습니다.', ]; } /** * Get custom attribute names for validator errors. */ public function attributes(): array { return [ 'formula_name' => '공식 이름', 'formula_expression' => '공식 표현식', 'unit' => '단위', 'description' => '설명', 'calculation_order' => '계산 순서', 'dependencies' => '의존성', ]; } /** * Prepare the data for validation. */ protected function prepareForValidation(): void { $this->merge([ 'calculation_order' => $this->integer('calculation_order', 0), ]); // Convert dependencies to array if it's a string if ($this->has('dependencies') && is_string($this->input('dependencies'))) { $dependencies = json_decode($this->input('dependencies'), true); if (json_last_error() === JSON_ERROR_NONE) { $this->merge(['dependencies' => $dependencies]); } } // Clean up formula expression - remove extra whitespace if ($this->has('formula_expression')) { $expression = preg_replace('/\s+/', ' ', trim($this->input('formula_expression'))); $this->merge(['formula_expression' => $expression]); } } /** * Configure the validator instance. */ public function withValidator($validator) { $validator->after(function ($validator) { $this->validateFormulaExpression($validator); $this->validateDependencies($validator); $this->validateNoCircularDependency($validator); }); } /** * Validate formula expression syntax. */ private function validateFormulaExpression($validator): void { $expression = $this->input('formula_expression'); if (!$expression) { return; } // Check for potentially dangerous characters or functions $dangerousPatterns = [ '/\b(eval|exec|system|shell_exec|passthru|file_get_contents|file_put_contents|fopen|fwrite)\b/i', '/[;{}]/', // Semicolons and braces '/\$[a-zA-Z_]/', // PHP variables '/\bfunction\s*\(/i', // Function definitions ]; foreach ($dangerousPatterns as $pattern) { if (preg_match($pattern, $expression)) { $validator->errors()->add('formula_expression', '공식 표현식에 허용되지 않는 문자나 함수가 포함되어 있습니다.'); return; } } // Validate mathematical expression format if (!$this->isValidMathExpression($expression)) { $validator->errors()->add('formula_expression', '공식 표현식의 형식이 올바르지 않습니다. 수학 연산자와 변수명만 사용할 수 있습니다.'); } // Extract variables from expression and validate they exist as parameters $this->validateExpressionVariables($validator, $expression); } /** * Check if expression contains valid mathematical operations. */ private function isValidMathExpression(string $expression): bool { // Allow: numbers, variables, basic math operators, parentheses, math functions $allowedPattern = '/^[a-zA-Z0-9_\s\+\-\*\/\(\)\.\,]+$/'; // Allow common math functions $mathFunctions = ['sin', 'cos', 'tan', 'log', 'exp', 'sqrt', 'pow', 'abs', 'ceil', 'floor', 'round', 'max', 'min']; $functionPattern = '/\b(' . implode('|', $mathFunctions) . ')\s*\(/i'; // Remove math functions for basic pattern check $cleanExpression = preg_replace($functionPattern, '', $expression); return preg_match($allowedPattern, $cleanExpression); } /** * Validate that variables in expression exist as model parameters. */ private function validateExpressionVariables($validator, string $expression): void { $modelId = $this->route('modelId'); // Extract variable names from expression preg_match_all('/\b[a-zA-Z][a-zA-Z0-9_]*\b/', $expression, $matches); $variables = $matches[0]; // Remove math functions from variables $mathFunctions = ['sin', 'cos', 'tan', 'log', 'exp', 'sqrt', 'pow', 'abs', 'ceil', 'floor', 'round', 'max', 'min']; $variables = array_diff($variables, $mathFunctions); if (empty($variables)) { return; } // Get existing parameters for this model $existingParameters = ModelParameter::where('model_id', $modelId) ->where('tenant_id', auth()->user()?->currentTenant?->id) ->whereNull('deleted_at') ->pluck('parameter_name') ->toArray(); // Check for undefined variables $undefinedVariables = array_diff($variables, $existingParameters); if (!empty($undefinedVariables)) { $validator->errors()->add('formula_expression', '공식에 정의되지 않은 변수가 사용되었습니다: ' . implode(', ', $undefinedVariables) ); } } /** * Validate dependencies array. */ private function validateDependencies($validator): void { $dependencies = $this->input('dependencies', []); $modelId = $this->route('modelId'); if (empty($dependencies)) { return; } // Get existing parameters for this model $existingParameters = ModelParameter::where('model_id', $modelId) ->where('tenant_id', auth()->user()?->currentTenant?->id) ->whereNull('deleted_at') ->pluck('parameter_name') ->toArray(); // Check that all dependencies exist as parameters $invalidDependencies = array_diff($dependencies, $existingParameters); if (!empty($invalidDependencies)) { $validator->errors()->add('dependencies', '존재하지 않는 매개변수가 의존성에 포함되어 있습니다: ' . implode(', ', $invalidDependencies) ); } } /** * Validate there's no circular dependency. */ private function validateNoCircularDependency($validator): void { $dependencies = $this->input('dependencies', []); $formulaName = $this->input('formula_name'); if (empty($dependencies) || !$formulaName) { return; } // Check for direct self-reference if (in_array($formulaName, $dependencies)) { $validator->errors()->add('dependencies', '공식이 자기 자신을 참조할 수 없습니다.'); return; } // For more complex circular dependency check, this would require // analyzing all formulas in the model - simplified version here // In production, implement full dependency graph analysis } }