*/ private const DISPLAY_FIELDS = ['id', 'client_id', 'name', 'email', 'phone', 'is_primary', 'notes']; /** @return array */ public function validate(array $input, array $existingContacts = [], ?int $currentId = null): array { $contact = validate_client_contact($input); $contact['name'] = $this->name($contact['name']); $clientId = $this->positiveId($input['client_id'] ?? null); $notes = $this->text($input['notes'] ?? null); $errors = $contact['errors']; if ($clientId === null) $errors['client_id'] = 'Client ID must be a positive integer.'; if ($notes !== null && mb_strlen($notes) > 10000) $errors['notes'] = 'Contact notes must be 10000 characters or fewer.'; if ($clientId !== null) { foreach (['name' => $contact['name'], 'email' => $contact['email']] as $field => $value) { if ($value === null || $value === '') continue; if ($this->hasDuplicate($field, (string) $value, $clientId, $existingContacts, $currentId)) { $errors[$field] = "Contact {$field} is already in use for this client."; } } } $replace = []; if ($clientId !== null && $contact['is_primary'] === true) { foreach ($existingContacts as $row) { if (!is_array($row) || $this->positiveId($row['client_id'] ?? null) !== $clientId) continue; if (!$this->asBool($row['is_primary'] ?? false)) continue; $id = $this->positiveId($row['id'] ?? null); if ($id !== null && $id !== $currentId) $replace[] = $id; } } return [ 'client_id' => $clientId, 'name' => $contact['name'], 'email' => $contact['email'], 'phone' => $contact['phone'], 'is_primary' => $contact['is_primary'], 'notes' => $notes, 'replace_primary_contact_ids' => $replace, 'valid' => $errors === [], 'errors' => $errors, ]; } /** @return array */ public function validateForCreate(array $input, array $existingContacts = []): array { return $this->validate($input, $existingContacts); } /** @return array */ public function validateForEdit(mixed $id, array $input, array $existingContacts = []): array { $normalizedId = $this->positiveId($id); $result = $this->validate($input, $existingContacts, $normalizedId); $result['id'] = $normalizedId; if ($normalizedId === null) $result['errors']['id'] = 'Contact ID must be a positive integer.'; $result['valid'] = $result['errors'] === []; return $result; } /** @return array */ public function display(array $contact): array { $safe = []; foreach (self::DISPLAY_FIELDS as $field) { if (array_key_exists($field, $contact)) $safe[$field] = $contact[$field]; } return $safe; } /** @return array */ public function toDisplay(array $contact): array { return $this->display($contact); } private function hasDuplicate(string $field, string $value, int $clientId, array $rows, ?int $currentId): bool { $candidate = $field === 'email' ? strtolower(trim($value)) : $this->duplicateKey($value); foreach ($rows as $row) { if (!is_array($row) || $this->positiveId($row['client_id'] ?? null) !== $clientId) continue; $rowId = $this->positiveId($row['id'] ?? null); if ($currentId !== null && $rowId === $currentId) continue; $other = $row[$field] ?? null; if ($other !== null && ($field === 'email' ? strtolower(trim((string) $other)) === $candidate : $this->duplicateKey((string) $other) === $candidate)) return true; } return false; } private function duplicateKey(string $value): string { $collapsed = preg_replace('/\s+/u', ' ', trim($value)); return strtolower($collapsed === false ? trim($value) : $collapsed); } private function name(string $value): string { $collapsed = preg_replace('/\s+/u', ' ', trim($value)); return $collapsed === false ? trim($value) : $collapsed; } private function text(mixed $value): ?string { if (!is_scalar($value)) return null; $value = trim((string) $value); return $value === '' ? null : $value; } private function positiveId(mixed $value): ?int { if (is_int($value) && $value > 0) return $value; if (is_string($value) && preg_match('/^[1-9]\d*$/', trim($value)) === 1) { $id = filter_var(trim($value), FILTER_VALIDATE_INT); return $id === false ? null : $id; } return null; } private function asBool(mixed $value): bool { return $value === true || $value === 1 || (is_string($value) && in_array(strtolower(trim($value)), ['1', 'true', 'yes', 'on'], true)); } }