From cef247336eac322ee17c9560d98c3d93c1cb5274 Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Sun, 3 May 2026 23:08:22 +0200 Subject: [PATCH] feat(export): use MacroResolutionService in ProExportService for flexible macro injection --- app/Http/Controllers/ProFileController.php | 4 +- app/Services/PlaylistExportService.php | 8 +- app/Services/ProBundleExportService.php | 3 +- app/Services/ProExportService.php | 53 ++++++------ tests/Feature/ProFileExportTest.php | 95 ++++++++++++++++++++++ 5 files changed, 128 insertions(+), 35 deletions(-) diff --git a/app/Http/Controllers/ProFileController.php b/app/Http/Controllers/ProFileController.php index 919820d..4ff1599 100644 --- a/app/Http/Controllers/ProFileController.php +++ b/app/Http/Controllers/ProFileController.php @@ -27,7 +27,7 @@ public function importPro(Request $request): JsonResponse } try { - $service = new ProImportService; + $service = new ProImportService(); $songs = $service->import($file); return response()->json([ @@ -53,7 +53,7 @@ public function downloadPro(Song $song): BinaryFileResponse abort(422, 'Song hat keine Gruppen oder Slides zum Exportieren.'); } - $exportService = new ProExportService; + $exportService = app(ProExportService::class); $tempPath = $exportService->generateProFile($song); $filename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro'; diff --git a/app/Services/PlaylistExportService.php b/app/Services/PlaylistExportService.php index 30cbab9..5c89a4b 100644 --- a/app/Services/PlaylistExportService.php +++ b/app/Services/PlaylistExportService.php @@ -53,7 +53,7 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda $announcementPatterns = Setting::get('agenda_announcement_position'); $announcementInserted = false; - $exportService = new ProExportService; + $exportService = app(ProExportService::class); $tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid(); mkdir($tempDir, 0755, true); @@ -90,7 +90,7 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda continue; } - $proPath = $exportService->generateProFile($song); + $proPath = $exportService->generateProFile($song, $service); $proFilename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro'; $destPath = $tempDir.'/'.$proFilename; rename($proPath, $destPath); @@ -176,7 +176,7 @@ private function generatePlaylistLegacy(Service $service): array $skippedUnmatched = $service->serviceSongs()->whereNull('song_id')->count(); $skippedEmpty = 0; - $exportService = new ProExportService; + $exportService = app(ProExportService::class); $tempDir = sys_get_temp_dir().'/playlist-export-'.uniqid(); mkdir($tempDir, 0755, true); @@ -201,7 +201,7 @@ private function generatePlaylistLegacy(Service $service): array continue; } - $proPath = $exportService->generateProFile($song); + $proPath = $exportService->generateProFile($song, $service); $proFilename = preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $song->title).'.pro'; $destPath = $tempDir.'/'.$proFilename; rename($proPath, $destPath); diff --git a/app/Services/ProBundleExportService.php b/app/Services/ProBundleExportService.php index a4ea129..41a4442 100644 --- a/app/Services/ProBundleExportService.php +++ b/app/Services/ProBundleExportService.php @@ -34,6 +34,7 @@ public function generateBundle(Service $service, string $blockType): string public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string { $agendaItem->loadMissing([ + 'service', 'slides', 'serviceSong.song.arrangements.arrangementLabels.label.songSlides', ]); @@ -52,7 +53,7 @@ public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string throw new RuntimeException('Song "'.$song->title.'" hat keine Gruppen.'); } - $parserSong = (new ProExportService)->generateParserSong($song); + $parserSong = app(ProExportService::class)->generateParserSong($song, $agendaItem->service); $proFilename = self::safeFilename($song->title).'.pro'; $bundle = new PresentationBundle($parserSong, $proFilename); diff --git a/app/Services/ProExportService.php b/app/Services/ProExportService.php index 0820451..315a82d 100644 --- a/app/Services/ProExportService.php +++ b/app/Services/ProExportService.php @@ -2,20 +2,25 @@ namespace App\Services; -use App\Models\Setting; +use App\Models\Service; use App\Models\Song; use ProPresenter\Parser\ProFileGenerator; class ProExportService { - public function generateProFile(Song $song): string + public function __construct( + private readonly MacroResolutionService $macroResolutionService, + ) { + } + + public function generateProFile(Song $song, ?Service $service = null): string { $tempPath = sys_get_temp_dir().'/'.uniqid('pro-export-').'.pro'; ProFileGenerator::generateAndWrite( $tempPath, $song->title, - $this->buildGroups($song), + $this->buildGroups($song, $service), $this->buildArrangements($song), $this->buildCcliMetadata($song), ); @@ -23,19 +28,19 @@ public function generateProFile(Song $song): string return $tempPath; } - public function generateParserSong(Song $song): \ProPresenter\Parser\Song + public function generateParserSong(Song $song, ?Service $service = null): \ProPresenter\Parser\Song { $song->loadMissing(['arrangements.arrangementLabels.label.songSlides']); return ProFileGenerator::generate( $song->title, - $this->buildGroups($song), + $this->buildGroups($song, $service), $this->buildArrangements($song), $this->buildCcliMetadata($song), ); } - private function buildGroups(Song $song): array + private function buildGroups(Song $song, ?Service $service = null): array { $defaultArr = $song->arrangements->firstWhere('is_default', true) ?? $song->arrangements->first(); @@ -45,7 +50,6 @@ private function buildGroups(Song $song): array $defaultArr->loadMissing('arrangementLabels.label.songSlides'); - $macroData = $this->buildMacroData(); $groups = []; $seenLabelIds = []; @@ -61,18 +65,28 @@ private function buildGroups(Song $song): array } $seenLabelIds[] = $label->id; - $isCopyrightGroup = strcasecmp($label->name, 'COPYRIGHT') === 0; $slides = []; + $labelSlides = $label->songSlides->sortBy('order')->values(); + $totalSlides = $labelSlides->count(); - foreach ($label->songSlides->sortBy('order') as $slide) { + foreach ($labelSlides as $slideIndex => $slide) { $slideData = ['text' => $slide->text_content ?? '']; if ($slide->text_content_translated) { $slideData['translation'] = $slide->text_content_translated; } - if ($isCopyrightGroup && $macroData) { - $slideData['macro'] = $macroData; + if ($service !== null) { + $macros = $this->macroResolutionService->macrosForSlide( + $service, + 'song', + ['index' => $slideIndex, 'total' => $totalSlides, 'label_id' => $label->id], + ); + + if (! empty($macros)) { + // ProPresenter parser currently supports one `macro` entry per slide; keep the first resolved macro until stacked macros are supported. + $slideData['macro'] = $macros[0]; + } } $slides[] = $slideData; @@ -88,23 +102,6 @@ private function buildGroups(Song $song): array return $groups; } - private function buildMacroData(): ?array - { - $name = Setting::get('macro_name'); - $uuid = Setting::get('macro_uuid'); - - if (! $name || ! $uuid) { - return null; - } - - return [ - 'name' => $name, - 'uuid' => $uuid, - 'collectionName' => Setting::get('macro_collection_name', '--MAIN--'), - 'collectionUuid' => Setting::get('macro_collection_uuid', '8D02FC57-83F8-4042-9B90-81C229728426'), - ]; - } - private function buildArrangements(Song $song): array { $arrangements = []; diff --git a/tests/Feature/ProFileExportTest.php b/tests/Feature/ProFileExportTest.php index f613f32..33b230a 100644 --- a/tests/Feature/ProFileExportTest.php +++ b/tests/Feature/ProFileExportTest.php @@ -3,8 +3,13 @@ namespace Tests\Feature; use App\Models\Label; +use App\Models\Macro; +use App\Models\MacroAssignment; +use App\Models\MacroCollection; +use App\Models\Service; use App\Models\Song; use App\Models\User; +use App\Services\ProExportService; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -202,6 +207,96 @@ public function test_download_pro_roundtrip_preserves_content(): void } } + public function test_export_ohne_service_context_enthaelt_keine_macros(): void + { + $song = $this->createSongWithContent(); + $macro = $this->createMacroForExport('Service Macro'); + MacroAssignment::create([ + 'part_type' => 'song', + 'macro_id' => $macro->id, + 'position' => 'all_slides', + 'order' => 0, + ]); + + $parserSong = app(ProExportService::class)->generateParserSong($song); + + foreach ($this->allParserSlides($parserSong) as $slide) { + $this->assertFalse($slide->hasMacro()); + } + } + + public function test_export_mit_globaler_song_zuweisung_enthaelt_macro_auf_allen_slides(): void + { + $service = Service::factory()->create(); + $song = $this->createSongWithContent(); + $macro = $this->createMacroForExport('Alle Folien Macro'); + MacroAssignment::create([ + 'part_type' => 'song', + 'macro_id' => $macro->id, + 'position' => 'all_slides', + 'order' => 0, + ]); + + $parserSong = app(ProExportService::class)->generateParserSong($song, $service); + $slides = $this->allParserSlides($parserSong); + + $this->assertNotEmpty($slides); + foreach ($slides as $slide) { + $this->assertTrue($slide->hasMacro()); + $this->assertSame('Alle Folien Macro', $slide->getMacroName()); + $this->assertSame($macro->uuid, $slide->getMacroUuid()); + $this->assertSame('Export Collection', $slide->getMacroCollectionName()); + } + } + + public function test_export_mit_ausgeblendeter_macro_enthaelt_keine_macro(): void + { + $service = Service::factory()->create(); + $song = $this->createSongWithContent(); + $macro = $this->createMacroForExport('Ausgeblendete Macro', ['hidden_at' => now()]); + MacroAssignment::create([ + 'part_type' => 'song', + 'macro_id' => $macro->id, + 'position' => 'all_slides', + 'order' => 0, + ]); + + $parserSong = app(ProExportService::class)->generateParserSong($song, $service); + + foreach ($this->allParserSlides($parserSong) as $slide) { + $this->assertFalse($slide->hasMacro()); + } + } + + private function createMacroForExport(string $name, array $attributes = []): Macro + { + $macro = Macro::factory()->create(array_merge([ + 'uuid' => '11111111-2222-4333-8444-555555555555', + 'name' => $name, + ], $attributes)); + + $collection = MacroCollection::create([ + 'uuid' => '99999999-8888-4777-8666-555555555555', + 'name' => 'Export Collection', + ]); + $collection->macros()->attach($macro->id, ['order' => 0]); + + return $macro; + } + + private function allParserSlides(\ProPresenter\Parser\Song $parserSong): array + { + $slides = []; + + foreach ($parserSong->getGroups() as $group) { + foreach ($parserSong->getSlidesForGroup($group) as $slide) { + $slides[] = $slide; + } + } + + return $slides; + } + private function assertStringContains(string $needle, ?string $haystack): void { $this->assertNotNull($haystack);