*/ private const TYPES = [ 'jobcard_created', 'jobcard_status_changed', 'assignment_created', 'time_entry_created', 'sla_threshold', 'attachment_uploaded', ]; /** @return array */ public function normalize(array $record): array { $type = is_scalar($record['type'] ?? null) ? strtolower(trim((string) $record['type'])) : ''; $rawRecipients = $record['recipients'] ?? ($record['recipient'] ?? []); if (!is_array($rawRecipients)) $rawRecipients = [$rawRecipients]; $recipients = []; foreach ($rawRecipients as $recipient) { if (is_scalar($recipient)) { $value = strtolower(trim((string) $recipient)); if ($value !== '' && !in_array($value, $recipients, true)) $recipients[] = $value; } } $key = $record['deduplication_key'] ?? $record['dedup_key'] ?? null; $key = is_scalar($key) ? strtolower(trim((string) $key)) : ''; return [ 'type' => $type, 'recipients' => $recipients, 'is_read' => $this->normalizeBoolean($record['is_read'] ?? $record['read'] ?? false), 'deduplication_key' => $key, ]; } /** @return array */ public function validate(array $record): array { $normalized = $this->normalize($record); $errors = []; if (!in_array($normalized['type'], self::TYPES, true)) { $errors['type'] = 'Notification type is not supported.'; } if ($normalized['recipients'] === []) { $errors['recipients'] = 'At least one notification recipient is required.'; } else { foreach ($normalized['recipients'] as $recipient) { if (filter_var($recipient, FILTER_VALIDATE_EMAIL) === false) { $errors['recipients'] = 'Notification recipients must be valid email addresses.'; break; } } } if (!is_bool($normalized['is_read'])) $errors['is_read'] = 'Read state must be boolean.'; if ($normalized['deduplication_key'] === '' || mb_strlen($normalized['deduplication_key']) > 190 || preg_match('/[\x00-\x1F\x7F]/', $normalized['deduplication_key']) === 1) { $errors['deduplication_key'] = 'A safe deduplication key is required and must be 190 characters or fewer.'; } return [...$normalized, 'valid' => $errors === [], 'errors' => $errors]; } public function deduplicationKey(array $record): string { return $this->normalize($record)['deduplication_key']; } /** @return array */ public function display(array $record): array { $normalized = $this->normalize($record); return [ 'type' => $normalized['type'], 'recipients' => $normalized['recipients'], 'is_read' => $normalized['is_read'], 'deduplication_key' => $normalized['deduplication_key'], ]; } /** @return array */ public function toDisplay(array $record): array { return $this->display($record); } private function normalizeBoolean(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; } }