componentIndex = $componentIndex; } /** * 권한 구조를 검증합니다. * * @param string $attribute 검증 대상 속성명 * @param mixed $value 검증 대상 값 * @param Closure $fail 실패 콜백 */ public function validate(string $attribute, mixed $value, Closure $fail): void { // null은 nullable 규칙에서 처리 if ($value === null) { return; } // 배열이 아닌 경우 if (! is_array($value)) { $fail($this->formatMessage(__('validation.layout.permissions_must_be_array_or_object'))); return; } // 빈 배열은 유효 (권한 불필요) if (empty($value)) { return; } $this->validateStructure($value, $fail, 1); } /** * 재귀적으로 권한 구조를 검증합니다. * * @param array $permissions 권한 구조 * @param Closure $fail 실패 콜백 * @param int $depth 현재 깊이 * @return bool 검증 성공 여부 */ private function validateStructure(array $permissions, Closure $fail, int $depth): bool { // 깊이 초과 검증 if ($depth > self::MAX_DEPTH) { $fail($this->formatMessage(__('validation.layout.permissions_max_depth_exceeded', [ 'max' => self::MAX_DEPTH, ]))); return false; } // flat array (리스트형 배열) if (array_is_list($permissions)) { return $this->validateFlatArray($permissions, $fail, $depth); } // 구조화 객체 (or/and) return $this->validateOperator($permissions, $fail, $depth); } /** * flat array를 검증합니다. * * @param array $items 권한 항목 배열 * @param Closure $fail 실패 콜백 * @param int $depth 현재 깊이 * @return bool 검증 성공 여부 */ private function validateFlatArray(array $items, Closure $fail, int $depth): bool { foreach ($items as $index => $item) { if (! $this->validateItem($item, $fail, $depth, $index)) { return false; } } return true; } /** * or/and 연산자 구조를 검증합니다. * * @param array $structure 구조화 객체 * @param Closure $fail 실패 콜백 * @param int $depth 현재 깊이 * @return bool 검증 성공 여부 */ private function validateOperator(array $structure, Closure $fail, int $depth): bool { $keys = array_keys($structure); // 키가 정확히 1개이고, or 또는 and여야 함 if (count($keys) !== 1 || ! in_array($keys[0], ['or', 'and'])) { $fail($this->formatMessage(__('validation.layout.permissions_invalid_operator'))); return false; } $operator = $keys[0]; $items = $structure[$operator]; // 값은 리스트형 배열이어야 함 if (! is_array($items) || ! array_is_list($items)) { $fail($this->formatMessage(__('validation.layout.permissions_operator_must_be_array', [ 'operator' => $operator, ]))); return false; } // 최소 항목 수 검증 if (count($items) < self::MIN_OPERATOR_ITEMS) { $fail($this->formatMessage(__('validation.layout.permissions_operator_min_items', [ 'operator' => $operator, 'min' => self::MIN_OPERATOR_ITEMS, ]))); return false; } // 각 항목 검증 foreach ($items as $index => $item) { if (! $this->validateItem($item, $fail, $depth, $index)) { return false; } } return true; } /** * 단일 항목을 검증합니다. * * @param mixed $item 항목 (문자열 또는 중첩 구조) * @param Closure $fail 실패 콜백 * @param int $depth 현재 깊이 * @param int|string $index 항목 인덱스 * @return bool 검증 성공 여부 */ private function validateItem(mixed $item, Closure $fail, int $depth, int|string $index): bool { if (is_string($item)) { return $this->validatePermissionIdentifier($item, $fail, $index); } if (is_array($item)) { return $this->validateStructure($item, $fail, $depth + 1); } $fail($this->formatMessage(__('validation.layout.permission_must_be_string_or_group', [ 'index' => $index, ]))); return false; } /** * 권한 식별자 형식을 검증합니다. * * @param string $identifier 권한 식별자 * @param Closure $fail 실패 콜백 * @param int|string $index 항목 인덱스 * @return bool 검증 성공 여부 */ private function validatePermissionIdentifier(string $identifier, Closure $fail, int|string $index): bool { if ($identifier === '') { $fail($this->formatMessage(__('validation.layout.permission_must_be_string', [ 'index' => $this->componentIndex ?? 'root', 'perm_index' => $index, ]))); return false; } if (! preg_match(self::PERMISSION_REGEX, $identifier)) { $fail($this->formatMessage(__('validation.layout.permission_invalid_format', [ 'index' => $this->componentIndex ?? 'root', 'permission' => $identifier, ]))); return false; } return true; } /** * 컴포넌트 인덱스를 포함한 에러 메시지를 생성합니다. * * @param string $message 원본 메시지 * @return string 포맷된 메시지 */ private function formatMessage(string $message): string { return $message; } }