filledString($service->moderator_name); if ($override !== null) { return $override; } // Prefer the person whose responsible role is "Moderation". $byRole = $this->nameForRole($service, 'moderation'); if ($byRole !== null) { return $byRole; } $firstAgendaItem = $service->agendaItems() ->where('is_before_event', false) ->orderBy('sort_order') ->orderBy('id') ->first(); return $firstAgendaItem ? $this->namesFromResponsible($firstAgendaItem->responsible) : null; } public function worshipLeaderFor(Service $service): ?string { return $this->nameForRole($service, 'lobpreis'); } public function preacherFor(Service $service): ?string { $override = $this->filledString($service->preacher_name_override); if ($override !== null) { return $override; } $preacherName = $this->filledString($service->preacher_name); if ($preacherName !== null) { return $preacherName; } // Prefer the person whose responsible role is "Predigt". $byRole = $this->nameForRole($service, 'predigt'); if ($byRole !== null) { return $byRole; } $sermonItem = $service->agendaItems() ->where('is_before_event', false) ->whereNull('service_song_id') ->orderBy('sort_order') ->orderBy('id') ->get() ->first(fn (ServiceAgendaItem $item) => $this->isSermonItem($item)); return $sermonItem ? $this->namesFromResponsible($sermonItem->responsible) : null; } /** * Find the first visible agenda item carrying a responsible person whose * role (persons[].service, e.g. "[Lobpreis]") matches $roleNeedle, and * return that person's display name. */ private function nameForRole(Service $service, string $roleNeedle): ?string { $items = $service->agendaItems() ->where('is_before_event', false) ->orderBy('sort_order') ->orderBy('id') ->get(); foreach ($items as $item) { $name = $this->nameFromResponsibleByRole($item->responsible, $roleNeedle); if ($name !== null) { return $name; } } return null; } private function filledString(?string $value): ?string { $trimmed = trim((string) $value); return $trimmed === '' ? null : $trimmed; } private function namesFromResponsible(mixed $responsible): ?string { if (! is_array($responsible) || $responsible === []) { return null; } // Real CTS shape: { "text": "...", "persons": [ { "service": "[Lobpreis]", "person": { "title": "...", ... } } ] } if (array_key_exists('persons', $responsible)) { $entries = $responsible['persons']; } elseif (Arr::isAssoc($responsible)) { $entries = [$responsible]; } else { $entries = $responsible; } $names = collect($entries) ->map(fn (mixed $entry) => $this->nameFromResponsibleEntry($entry)) ->filter() ->values() ->all(); return $names === [] ? null : implode(', ', $names); } /** * Extract a person name from a responsible entry. * Handles both the real CTS shape (entry has a 'person' sub-key) and the * legacy/test shape (entry is a flat array with 'name'/'firstName'/'lastName' or a string). */ private function nameFromResponsibleEntry(mixed $entry): ?string { if (is_string($entry)) { return $this->filledString($entry); } if (! is_array($entry)) { return null; } // Real CTS shape: { "service": "[Lobpreis]", "person": { "title": "Kornelius Weiß", ... } } if (isset($entry['person']) && is_array($entry['person'])) { return $this->nameFromPersonNode($entry['person']); } // Legacy / test shape: { "name": "Anna Müller" } or { "firstName": "...", "lastName": "..." } return $this->nameFromPersonNode($entry); } /** Extract display name from a person node (title preferred, then firstName+lastName). */ private function nameFromPersonNode(mixed $node): ?string { if (! is_array($node)) { return null; } $title = $this->filledString($node['title'] ?? null); if ($title !== null) { return $title; } $name = $this->filledString($node['name'] ?? null); if ($name !== null) { return $name; } $domainAttrs = $node['domainAttributes'] ?? []; $firstName = $this->filledString( $node['firstName'] ?? $node['first_name'] ?? ($domainAttrs['firstName'] ?? null) ) ?? ''; $lastName = $this->filledString( $node['lastName'] ?? $node['last_name'] ?? ($domainAttrs['lastName'] ?? null) ) ?? ''; $fullName = trim($firstName.' '.$lastName); return $fullName === '' ? null : $fullName; } /** * Scan a responsible array for a person whose role (persons[].service) * contains $roleNeedle (e.g. 'lobpreis', 'predigt', 'moderation'). * Returns the person's display name, or null if none found. */ private function nameFromResponsibleByRole(mixed $responsible, string $roleNeedle): ?string { if (! is_array($responsible) || $responsible === []) { return null; } // Real CTS shape: { "persons": [ { "service": "[Lobpreis]", "person": { ... } } ] } $entries = array_key_exists('persons', $responsible) ? $responsible['persons'] : []; foreach ($entries as $entry) { if (! is_array($entry)) { continue; } $role = $this->normalizeRole($entry['service'] ?? ''); if ($role !== '' && str_contains($role, $roleNeedle)) { $name = $this->nameFromResponsibleEntry($entry); if ($name !== null) { return $name; } } } return null; } /** Strip brackets, whitespace, and lowercase a role string for comparison. */ private function normalizeRole(string $role): string { return Str::lower(preg_replace('/[^a-zA-ZäöüÄÖÜß]/', '', $role) ?? ''); } private function isSermonItem(ServiceAgendaItem $item): bool { $configuredPatterns = $this->patternsFromSetting(Setting::get('agenda_sermon_matching')); if ($configuredPatterns !== []) { return $this->agendaMatcherService->matchesAny($item->title, $configuredPatterns); } $title = Str::lower($item->title); $type = Str::lower($item->type ?? ''); return str_contains($title, 'predigt') || str_contains($title, 'sermon') || str_contains($type, 'predigt') || str_contains($type, 'sermon'); } /** @return array */ private function patternsFromSetting(?string $patterns): array { if ($patterns === null || trim($patterns) === '') { return []; } return array_values(array_filter( array_map(fn (string $pattern) => trim($pattern), explode(',', $patterns)), fn (string $pattern) => $pattern !== '', )); } }