264 lines
9.5 KiB
PHP
264 lines
9.5 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Http\Requests\Api\V1\Design;
|
||
|
|
|
||
|
|
use Illuminate\Foundation\Http\FormRequest;
|
||
|
|
use Illuminate\Validation\Rule;
|
||
|
|
use App\Models\Design\ModelParameter;
|
||
|
|
|
||
|
|
class ModelFormulaFormRequest extends FormRequest
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* Determine if the user is authorized to make this request.
|
||
|
|
*/
|
||
|
|
public function authorize(): bool
|
||
|
|
{
|
||
|
|
return true; // Authorization handled by middleware
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the validation rules that apply to the request.
|
||
|
|
*/
|
||
|
|
public function rules(): array
|
||
|
|
{
|
||
|
|
$modelId = $this->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
|
||
|
|
}
|
||
|
|
}
|