*/ private const DISPLAY_FIELDS = [ 'id', 'name', 'email', 'role_id', 'role_name', 'is_active', 'last_login_at', 'created_at', 'updated_at', ]; private readonly PasswordPolicy $passwordPolicy; public function __construct(?PasswordPolicy $passwordPolicy = null) { $this->passwordPolicy = $passwordPolicy ?? new PasswordPolicy(); } /** @return array{name: string, email: string, role_id: int|string|null, is_active: bool|mixed} */ public function normalize(array $record): array { return [ 'name' => trim(is_scalar($record['name'] ?? null) ? (string) $record['name'] : ''), 'email' => strtolower(trim(is_scalar($record['email'] ?? null) ? (string) $record['email'] : '')), 'role_id' => $this->normalizeRoleId($record['role_id'] ?? null), 'is_active' => $this->normalizeActive($record['is_active'] ?? $record['active'] ?? true), ]; } /** @return array{name: string, email: string, role_id: int|string|null, is_active: bool|mixed, valid: bool, errors: array} */ public function validate(array $record): array { $normalized = $this->normalize($record); $errors = []; if ($normalized['name'] === '') { $errors['name'] = 'User name is required.'; } elseif (mb_strlen($normalized['name']) > 120) { $errors['name'] = 'User name must be 120 characters or fewer.'; } if ($normalized['email'] === '') { $errors['email'] = 'Email address is required.'; } elseif (mb_strlen($normalized['email']) > 190) { $errors['email'] = 'Email address must be 190 characters or fewer.'; } elseif (filter_var($normalized['email'], FILTER_VALIDATE_EMAIL) === false) { $errors['email'] = 'Email address must be valid.'; } if (!is_int($normalized['role_id']) || $normalized['role_id'] < 1) { $errors['role_id'] = 'Role must be a positive integer.'; } if (!is_bool($normalized['is_active'])) { $errors['is_active'] = 'Active flag must be boolean.'; } return [...$normalized, 'valid' => $errors === [], 'errors' => $errors]; } /** * Validates a new user and its initial password without returning the * plaintext password in the result. * * @return array{name: string, email: string, role_id: int|string|null, is_active: bool|mixed, valid: bool, errors: array} */ public function validateForCreate(array $record): array { $result = $this->validate($record); $password = $this->passwordPolicy->validateInitial($record['password'] ?? null); if (!$password['valid']) { $result['errors']['password'] = 'Password requires: ' . implode(', ', $password['errors']) . '.'; $result['valid'] = false; } return $result; } /** @return array */ public function display(array $record): array { $safe = []; foreach (self::DISPLAY_FIELDS as $field) { if (array_key_exists($field, $record)) { $safe[$field] = $record[$field]; } } return $safe; } /** @return array */ public function toDisplay(array $record): array { return $this->display($record); } private function normalizeRoleId(mixed $value): int|string|null { if (is_int($value)) { return $value; } if (is_string($value) && preg_match('/^[0-9]+$/', trim($value)) === 1) { return (int) trim($value); } return is_scalar($value) || $value === null ? $value : null; } private function normalizeActive(mixed $value): mixed { if (is_bool($value)) { return $value; } if (is_int($value) && ($value === 0 || $value === 1)) { return $value === 1; } if (is_string($value)) { return match (strtolower(trim($value))) { '1', 'true', 'yes', 'on' => true, '0', 'false', 'no', 'off' => false, default => $value, }; } return $value; } }