From f20a95f9a48c247f5b3c72b0ffdce6f6ca070991 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Fri, 19 Jun 2026 11:35:55 +0000 Subject: [PATCH] Optimierung 4.2: Published-Release-Query ueber PressRelease-Scopes zentralisiert Vier komponierbare Scopes auf PressRelease: - published() Status freigegeben + published_at gesetzt (ohne Zeitschranke, damit Statistik-Fenster frei einschraenken koennen) - visibleInArchive() published() + published_at <= jetzt (oeffentlich sichtbar) - forPortalValues() whereIn portal - forLanguage() where language ~19 duplizierte Query-Bloecke in routes/web.php (Home, Kategorie, Detail, Veroeffentlichen, Suche, Newsrooms, Newsroom-Detail, Legacy-Redirect) nutzen jetzt diese Scopes. Jedes withoutGlobalScope(PortalScope) bleibt an seiner Stelle erhalten; die Klausel-Reihenfolge ist fuers Ergebnis irrelevant (reine AND-Verknuepfung), daher verhaltensgleich. Test: Web/PublishedReleaseScopeTest; die 89 bestehenden Web/-Tests bleiben gruen. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Models/PressRelease.php | 35 +++++ dev/optimize/entwicklungsplan.md | 2 +- routes/web.php | 138 +++++++----------- .../Feature/Web/PublishedReleaseScopeTest.php | 45 ++++++ 4 files changed, 137 insertions(+), 83 deletions(-) create mode 100644 tests/Feature/Web/PublishedReleaseScopeTest.php diff --git a/app/Models/PressRelease.php b/app/Models/PressRelease.php index c7d832f4..07a3feb9 100644 --- a/app/Models/PressRelease.php +++ b/app/Models/PressRelease.php @@ -184,6 +184,41 @@ class PressRelease extends Model return $query->whereHas('boosts', fn (Builder $q) => $q->active()); } + /** + * Eine veröffentlichte PM: freigegebener Status mit gesetztem Datum. Bewusst + * OHNE Zeitschranke, damit Statistik-Fenster (z. B. „letzte 7 Tage") frei auf + * `published_at` einschränken können. Single Source of Truth für die + * öffentlichen Web-Abfragen. + */ + public function scopePublished(Builder $query): Builder + { + return $query->where('status', PressReleaseStatus::Published) + ->whereNotNull('published_at'); + } + + /** + * Öffentlich sichtbare PM im Archiv: veröffentlicht UND der Termin liegt nicht + * in der Zukunft (`published_at <= jetzt`). Basis für alle Listen-/Detail-/ + * Lead-Abfragen der Web-Seiten. + */ + public function scopeVisibleInArchive(Builder $query): Builder + { + return $query->published()->where('published_at', '<=', now()); + } + + /** + * @param list $portalValues + */ + public function scopeForPortalValues(Builder $query, array $portalValues): Builder + { + return $query->whereIn('portal', $portalValues); + } + + public function scopeForLanguage(Builder $query, string $language): Builder + { + return $query->where('language', $language); + } + /** * Beschränkt auf PMs, die dem User gehören (als Autor) oder zu einer ihm * zugeordneten Firma. Bewusst ohne Portal-GlobalScope: ein Kunde darf diff --git a/dev/optimize/entwicklungsplan.md b/dev/optimize/entwicklungsplan.md index 01e2bc97..25908fd2 100644 --- a/dev/optimize/entwicklungsplan.md +++ b/dev/optimize/entwicklungsplan.md @@ -73,7 +73,7 @@ Keine Fachlogik-Änderung. Reduziert erreichbare Demo-Komponenten und Altlasten. `routes/web.php` > 1100 Zeilen mit kompletter Datenaufbereitung. Schrittweise, gut absicherbar. - [ ] **4.1 Portal-/Domain-Kontext extrahieren** (baut auf 3.3 `DomainContext` auf). -- [ ] **4.2 Published-Release-Query zentralisieren** — gleiche Bedingung (Portal `both`, `status=published`, Sprache, `published_at <= now()`) mehrfach, teils mit/ohne `withoutGlobalScope(PortalScope)`. → Scopes `scopePublished`, `scopeForPortalValues`, `scopeForLanguage`, `scopeVisibleInArchive`. +- [x] **4.2 Published-Release-Query zentralisieren** — vier komponierbare Scopes auf `PressRelease`: `scopePublished()` (Status + gesetztes Datum, ohne Zeitschranke → für Statistik-Fenster), `scopeVisibleInArchive()` (`published()` + `published_at <= now()`), `scopeForPortalValues(array)`, `scopeForLanguage(string)`. ~19 dupliziierte Query-Blöcke in `routes/web.php` (Home, Kategorie, Detail, Veröffentlichen, Suche, Newsrooms, Newsroom-Detail, Legacy-Redirect) darüber zentralisiert; jedes `withoutGlobalScope(PortalScope)` an seiner Stelle erhalten (Klausel-Reihenfolge ist für das Ergebnis irrelevant, da reine AND-Verknüpfung). Test: `Web/PublishedReleaseScopeTest`; die 89 bestehenden `Web/`-Tests bleiben grün. - [ ] **4.3 Page-Data in Services** — `HomePageData`/`CategoryPageData`/`NewsroomPageData` für Home/Kategorie/Newsroom/Detail/Suche/Preise. - [ ] **4.4 Routen auf schlanke (Invokable) Controller umstellen** — schrittweise je Seite. diff --git a/routes/web.php b/routes/web.php index bd65643f..28068eb4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -56,11 +56,9 @@ $webHomeData = static function (Portal $primaryPortal, string $language = 'de'): $previousSince = $referenceNow->copy()->subDays(14); $publishedQuery = static fn (): Builder => PressRelease::query() - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') - ->where('published_at', '<=', now()); + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive(); $with = [ 'company', @@ -98,26 +96,24 @@ $webHomeData = static function (Portal $primaryPortal, string $language = 'de'): ->where('is_active', true) ->whereHas('pressReleases', function (Builder $query) use ($portalValues, $recentSince, $language): void { $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() ->where('published_at', '>=', $recentSince); }) ->withCount([ 'pressReleases as recent_releases_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void { $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() ->where('published_at', '>=', $recentSince); }, 'pressReleases as today_releases_count' => function (Builder $query) use ($portalValues, $referenceNow, $language): void { $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() ->whereDate('published_at', $referenceNow->toDateString()); }, ]) @@ -138,17 +134,16 @@ $webHomeData = static function (Portal $primaryPortal, string $language = 'de'): ->withCount([ 'pressReleases as recent_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void { $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->where('published_at', '>=', $recentSince) - ->whereNotNull('published_at'); + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() + ->where('published_at', '>=', $recentSince); }, 'pressReleases as previous_count' => function (Builder $query) use ($portalValues, $recentSince, $previousSince, $language): void { $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() ->where('published_at', '>=', $previousSince) ->where('published_at', '<', $recentSince); }, @@ -182,10 +177,9 @@ $webHomeData = static function (Portal $primaryPortal, string $language = 'de'): ->withCount([ 'pressReleases as window_count' => function (Builder $query) use ($portalValues, $recentSince, $referenceNow, $language): void { $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() ->where('published_at', '>=', $recentSince) ->where('published_at', '<=', $referenceNow); }, @@ -200,11 +194,9 @@ $webHomeData = static function (Portal $primaryPortal, string $language = 'de'): if ($spotlightCategory && $spotlightTranslation = $spotlightCategory->translations->first()) { $spotlightBase = static fn (): Builder => PressRelease::query() - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') - ->where('published_at', '<=', now()) + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive() ->where('category_id', $spotlightCategory->id); $spotlightReleases = $spotlightBase() @@ -314,11 +306,9 @@ $categoryPage = static function (string $edition, string $slug) use ($applyWebDo $base = static fn (): Builder => PressRelease::query() ->withoutGlobalScope(PortalScope::class) - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') - ->where('published_at', '<=', now()) + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive() ->whereIn('category_id', $categoryIds); // Kategoriespezifischer Stichtag: jüngste Meldung dieser Branche. @@ -373,10 +363,9 @@ $categoryPage = static function (string $edition, string $slug) use ($applyWebDo $newsroomFilter = static function (Builder $query) use ($portalValues, $categoryIds, $language): void { $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() ->whereIn('category_id', $categoryIds); }; @@ -410,10 +399,9 @@ $categoryPage = static function (string $edition, string $slug) use ($applyWebDo ]); $publishedCount = static fn (Builder $query) => $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at'); + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published(); $subCategories = $childCategories ->map(function (Category $child) use ($recentSince, $previousSince, $publishedCount): ?array { @@ -501,11 +489,9 @@ $releaseDetailPage = static function (string $edition, string $slug) use ($apply $portalValues = [$portal->value, Portal::Both->value]; $publishedScope = static fn (Builder $query): Builder => $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') - ->where('published_at', '<=', now()); + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive(); $release = PressRelease::query() ->withoutGlobalScope(PortalScope::class) @@ -584,11 +570,9 @@ $veroeffentlichenPage = static function (string $edition) use ($applyWebDomainCo $publishedBase = static fn (): Builder => PressRelease::query() ->withoutGlobalScope(PortalScope::class) - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') - ->where('published_at', '<=', now()); + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive(); $archiveTotal = $publishedBase()->count(); $publishedToday = $publishedBase()->whereDate('published_at', $referenceNow->toDateString())->count(); @@ -612,10 +596,9 @@ $veroeffentlichenPage = static function (string $edition) use ($applyWebDomainCo $newsroomFilter = static function (Builder $query) use ($portalValues, $language, $recentSince): void { $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() ->where('published_at', '>=', $recentSince); }; @@ -702,11 +685,9 @@ $suchePage = static function (string $edition) use ($applyWebDomainConfig) { $mostReadReleases = PressRelease::query() ->withoutGlobalScope(PortalScope::class) - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') - ->where('published_at', '<=', now()) + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive() ->orderByDesc('hits') ->orderByDesc('published_at') ->limit(5) @@ -739,11 +720,9 @@ $newsroomsIndex = static function (string $edition) use ($applyWebDomainConfig) $published = static function (Builder $query) use ($portalValues, $language): void { $query - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') - ->where('published_at', '<=', now()); + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive(); }; $activeCompanies = static fn (): Builder => Company::query() @@ -817,11 +796,9 @@ $newsroomDetail = static function (string $edition, string $slug) use ($applyWeb $base = static fn (): Builder => PressRelease::query() ->withoutGlobalScope(PortalScope::class) ->where('company_id', $company->id) - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at') - ->where('published_at', '<=', now()); + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive(); // Keine öffentliche Newsroom-Seite ohne veröffentlichte Meldung. if (! $base()->exists()) { @@ -861,10 +838,9 @@ $newsroomDetail = static function (string $edition, string $slug) use ($applyWeb $query ->withoutGlobalScope(PortalScope::class) ->where('company_id', $company->id) - ->whereIn('portal', $portalValues) - ->where('status', PressReleaseStatus::Published) - ->where('language', $language) - ->whereNotNull('published_at'); + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published(); }; $categories = Category::query() @@ -975,9 +951,7 @@ $htmlRedirect = static function (string $edition, string $path) { && $company->is_active && $company->pressReleases() ->withoutGlobalScope(PortalScope::class) - ->where('status', PressReleaseStatus::Published) - ->whereNotNull('published_at') - ->where('published_at', '<=', now()) + ->visibleInArchive() ->exists(); return redirect( diff --git a/tests/Feature/Web/PublishedReleaseScopeTest.php b/tests/Feature/Web/PublishedReleaseScopeTest.php new file mode 100644 index 00000000..c15223f4 --- /dev/null +++ b/tests/Feature/Web/PublishedReleaseScopeTest.php @@ -0,0 +1,45 @@ +published()->create(['portal' => Portal::Presseecho->value]); + $future = PressRelease::factory()->published()->create(['portal' => Portal::Presseecho->value, 'published_at' => now()->addDays(3)]); + $draft = PressRelease::factory()->create(['status' => PressReleaseStatus::Draft->value, 'portal' => Portal::Presseecho->value]); + $review = PressRelease::factory()->inReview()->create(['portal' => Portal::Presseecho->value]); + $dateless = PressRelease::factory()->create(['status' => PressReleaseStatus::Published->value, 'published_at' => null, 'portal' => Portal::Presseecho->value]); + + $ids = PressRelease::withoutGlobalScopes()->published()->pluck('id'); + + expect($ids)->toContain($live->id) + ->toContain($future->id) + ->not->toContain($draft->id) + ->not->toContain($review->id) + ->not->toContain($dateless->id); +}); + +test('visibleInArchive excludes future-dated releases', function () { + $live = PressRelease::factory()->published()->create(['portal' => Portal::Presseecho->value]); + $future = PressRelease::factory()->published()->create(['portal' => Portal::Presseecho->value, 'published_at' => now()->addDay()]); + + $ids = PressRelease::withoutGlobalScopes()->visibleInArchive()->pluck('id'); + + expect($ids)->toContain($live->id)->not->toContain($future->id); +}); + +test('forPortalValues and forLanguage filter as expected', function () { + $match = PressRelease::factory()->published()->create(['portal' => Portal::Presseecho->value, 'language' => 'de']); + $otherPortal = PressRelease::factory()->published()->create(['portal' => Portal::Businessportal24->value, 'language' => 'de']); + $otherLang = PressRelease::factory()->published()->create(['portal' => Portal::Presseecho->value, 'language' => 'en']); + + $ids = PressRelease::withoutGlobalScopes() + ->forPortalValues([Portal::Presseecho->value, Portal::Both->value]) + ->forLanguage('de') + ->pluck('id'); + + expect($ids)->toContain($match->id) + ->not->toContain($otherPortal->id) + ->not->toContain($otherLang->id); +});