has('value')) { return; } $default = $this->declaredDefault(); $value = $this->input('value'); // 문자열 설정을 비우면 빈 문자열로 남긴다. `ConvertEmptyStringsToNull` 미들웨어가 // 이미 `''` 를 null 로 바꿔 두므로, 여기서 되돌리지 않으면 선언 타입이 문자열인 // 설정에 null 이 저장되어 그 값을 읽는 쪽이 기본값(`''`)과 다른 형태를 받는다. if (is_string($default) && $value === null) { $this->merge(['value' => '']); return; } // 선언 타입이 문자열/배열이거나 기본값이 없으면 원본 유지 if (! is_bool($default) && ! is_int($default) && ! is_float($default)) { return; } // 비-문자열 설정에서 빈 문자열은 "값을 지운다" 는 뜻이다 if ($value === '') { $this->merge(['value' => null]); return; } if (! is_string($value)) { return; } if (is_bool($default)) { $casted = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($casted !== null) { $this->merge(['value' => $casted]); } return; } if (is_int($default) && preg_match('/^-?\d+$/', $value) === 1) { $this->merge(['value' => (int) $value]); return; } if (is_float($default) && is_numeric($value)) { $this->merge(['value' => (float) $value]); } } /** * Get the validation rules that apply to the request. * * @return array|string> */ public function rules(): array { $rules = [ // `required` 가 아니라 `present` 다 — 빈 문자열/null 은 "이 설정을 비운다" 는 정상 // 입력이고, 이를 거부하면 운영자가 한 번 넣은 값을 화면에서 지울 수 없다. // 대신 `value` 키 자체가 빠진 요청은 그대로 거부한다. 'value' => ['present', 'nullable', $this->valueShapeRule()], ]; // 모듈/플러그인이 validation rules를 동적으로 추가할 수 있도록 훅 제공 return HookManager::applyFilters('core.settings.update_validation_rules', $rules, $this); } /** * 값의 형태(타입·길이)를 검증하는 규칙을 반환합니다. * * @return \Closure 검증 클로저 */ private function valueShapeRule(): \Closure { return function (string $attribute, mixed $value, callable $fail): void { if ($value === null) { return; } $default = $this->declaredDefault(); // 선언 타입과 다른 값은 거부한다 — 정규화가 해석하지 못한 입력이 여기로 온다. if (is_bool($default) && ! is_bool($value)) { $fail(__('validation.setting.value.boolean')); return; } if (is_int($default) && ! is_int($value)) { $fail(__('validation.setting.value.integer')); return; } if (is_float($default) && ! is_int($value) && ! is_float($value)) { $fail(__('validation.setting.value.numeric')); return; } if (is_string($value)) { if (mb_strlen($value) > self::MAX_STRING_LENGTH) { $fail(__('validation.setting.value.max', ['max' => self::MAX_STRING_LENGTH])); } return; } if (is_array($value)) { if (mb_strlen((string) json_encode($value)) > self::MAX_ARRAY_JSON_LENGTH) { $fail(__('validation.setting.value.array_max', ['max' => self::MAX_ARRAY_JSON_LENGTH])); } return; } if (! is_bool($value) && ! is_int($value) && ! is_float($value)) { $fail(__('validation.setting.value.type')); } }; } /** * 저장 대상 키의 선언 기본값을 반환합니다. * * 반환값의 타입이 곧 그 설정의 선언 타입입니다. 선언이 없으면 null 을 반환하며, * 이 경우 타입 강제 없이 형태 검증만 수행합니다 (확장이 추가한 키 등). * * @return mixed 선언 기본값 또는 null */ private function declaredDefault(): mixed { $key = $this->route('key'); if (! is_string($key) || $key === '') { return null; } return Arr::get(app(ConfigRepositoryInterface::class)->getDefaults(), $key); } /** * Get custom messages for validator errors. * * @return array */ public function messages(): array { return [ 'value.present' => __('validation.setting.value.present'), ]; } }