From 714a5cd5b135cff57a1e2774e96c69c0771c539a Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Fri, 19 Jun 2026 11:42:00 +0000 Subject: [PATCH] Optimierung 4.1: Web-Portal-/Domain-Kontext extrahiert - WebPortalContext::fromRequest() buendelt die zuvor ~6x je Route-Closure wiederholte Host->Portal-Ableitung (portal, language, portalValues, domainConfig). - DomainAssetContext::applyWebDomainConfig() ist die Single Source of Truth fuer die Domain-/Theme-Anwendung (config + View::share + Vite); die fruehere $applyWebDomainConfig-Closure in routes/web.php ist nur noch ein duenner Delegations-Wrapper, damit Bare-Root- und Edition-Home-Route unveraendert per Closure aufrufen koennen. - Die 6 Portal-Seiten-Closures (Kategorie, Detail, Veroeffentlichen, Suche, Newsrooms, Newsroom-Detail) sowie Preise nutzen jetzt den zentralen Kontext. Abgesichert durch die 92 Web/-Tests (jede Seite laeuft ueber den neuen Kontext). Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Support/DomainAssetContext.php | 32 ++++++++ app/Support/WebPortalContext.php | 42 ++++++++++ dev/optimize/entwicklungsplan.md | 2 +- routes/web.php | 128 +++++++---------------------- 4 files changed, 106 insertions(+), 98 deletions(-) create mode 100644 app/Support/WebPortalContext.php diff --git a/app/Support/DomainAssetContext.php b/app/Support/DomainAssetContext.php index 415b47bb..5cf7248c 100644 --- a/app/Support/DomainAssetContext.php +++ b/app/Support/DomainAssetContext.php @@ -3,10 +3,42 @@ namespace App\Support; use Illuminate\Http\Request; +use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\Vite; class DomainAssetContext { + /** + * Wendet die Domain-/Theme-Konfiguration einer öffentlichen Web-Ausgabe an + * (config + View::share + Vite) und liefert die aufgelöste Domain-Config. + * Single Source of Truth für die zuvor je Route-Closure wiederholte Logik. + * + * @return array + */ + public static function applyWebDomainConfig(string $domainKey): array + { + /** @var array $domainConfig */ + $domainConfig = config("domains.domains.{$domainKey}", []); + + config([ + 'app.theme' => $domainConfig['theme'] ?? $domainKey, + 'app.view_prefix' => $domainConfig['view_prefix'] ?? 'web', + 'app.domain_name' => $domainConfig['domain_name'] ?? request()->getHost(), + 'app.url' => $domainConfig['url'] ?? config('app.url'), + ]); + + View::share('theme', $domainConfig['theme'] ?? $domainKey); + View::share('viewPrefix', $domainConfig['view_prefix'] ?? 'web'); + View::share('domainName', $domainConfig['domain_name'] ?? request()->getHost()); + View::share('domainConfig', $domainConfig); + View::share('domainUrl', $domainConfig['url'] ?? config('app.url')); + View::share('assetUrl', self::staticAssetOrigin($domainConfig)); + + self::configureVite($domainConfig); + + return $domainConfig; + } + /** * @param array $defaults * @param array> $configuredDomains diff --git a/app/Support/WebPortalContext.php b/app/Support/WebPortalContext.php new file mode 100644 index 00000000..2cba8dc1 --- /dev/null +++ b/app/Support/WebPortalContext.php @@ -0,0 +1,42 @@ + $portalValues Eigenes Portal plus `both`. + * @param array $domainConfig + */ + private function __construct( + public readonly string $domainKey, + public readonly Portal $portal, + public readonly string $language, + public readonly array $portalValues, + public readonly array $domainConfig, + ) {} + + public static function fromRequest(): self + { + $host = request()->getHost(); + + [$domainKey, $portal] = str_contains($host, 'presseecho') + ? ['presseecho', Portal::Presseecho] + : ['businessportal24', Portal::Businessportal24]; + + return new self( + domainKey: $domainKey, + portal: $portal, + language: app()->getLocale(), + portalValues: [$portal->value, Portal::Both->value], + domainConfig: DomainAssetContext::applyWebDomainConfig($domainKey), + ); + } +} diff --git a/dev/optimize/entwicklungsplan.md b/dev/optimize/entwicklungsplan.md index 25908fd2..05eebb57 100644 --- a/dev/optimize/entwicklungsplan.md +++ b/dev/optimize/entwicklungsplan.md @@ -72,7 +72,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). +- [x] **4.1 Portal-/Domain-Kontext extrahieren** — Die je Route-Closure ~6× wiederholte Host→Portal-Ableitung liegt jetzt in `WebPortalContext::fromRequest()` (liefert `portal`, `language`, `portalValues`, `domainConfig`). Die Domain-/Theme-Anwendung (config + `View::share` + Vite) ist als `DomainAssetContext::applyWebDomainConfig()` zentralisiert (vorher die `$applyWebDomainConfig`-Closure in `routes/web.php`, jetzt ein dünner Delegations-Wrapper, damit Bare-Root- und Edition-Home-Route unverändert bleiben). Abgesichert durch die 92 `Web/`-Tests, die jede Seite über den neuen Kontext laufen lassen. - [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 28068eb4..2b7f1170 100644 --- a/routes/web.php +++ b/routes/web.php @@ -12,10 +12,10 @@ use App\Models\PressRelease; use App\Scopes\PortalScope; use App\Support\DomainAssetContext; use App\Support\EditorialClock; +use App\Support\WebPortalContext; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Route; -use Illuminate\Support\Facades\View; use Illuminate\Support\Str; Route::get('/pm-vorschau/{token}', PressReleasePreviewController::class) @@ -25,27 +25,9 @@ Route::get('/pm-vorschau/{token}', PressReleasePreviewController::class) // Gemeinsame Web-Routes für alle Landingpages // Jede Landing-Page hat das gleiche Gerüst, aber unterschiedliches Styling -$applyWebDomainConfig = static function (string $domainKey): array { - $domainConfig = config("domains.domains.{$domainKey}", []); - - config([ - 'app.theme' => $domainConfig['theme'] ?? $domainKey, - 'app.view_prefix' => $domainConfig['view_prefix'] ?? 'web', - 'app.domain_name' => $domainConfig['domain_name'] ?? request()->getHost(), - 'app.url' => $domainConfig['url'] ?? config('app.url'), - ]); - - View::share('theme', $domainConfig['theme'] ?? $domainKey); - View::share('viewPrefix', $domainConfig['view_prefix'] ?? 'web'); - View::share('domainName', $domainConfig['domain_name'] ?? request()->getHost()); - View::share('domainConfig', $domainConfig); - View::share('domainUrl', $domainConfig['url'] ?? config('app.url')); - View::share('assetUrl', DomainAssetContext::staticAssetOrigin($domainConfig)); - - DomainAssetContext::configureVite($domainConfig); - - return $domainConfig; -}; +// Dünner Delegations-Wrapper auf die Single Source of Truth (DomainAssetContext), +// damit Bare-Root- und Edition-Home-Route unverändert per Closure aufrufen können. +$applyWebDomainConfig = static fn (string $domainKey): array => DomainAssetContext::applyWebDomainConfig($domainKey); $webHomeData = static function (Portal $primaryPortal, string $language = 'de'): array { $portalValues = [$primaryPortal->value, Portal::Both->value]; @@ -269,18 +251,10 @@ $webHomeData = static function (Portal $primaryPortal, string $language = 'de'): }; // Kategorie-/Branchenseite je Ausgabe. -$categoryPage = static function (string $edition, string $slug) use ($applyWebDomainConfig) { - $domain = request()->getHost(); - $language = app()->getLocale(); - - [$domainKey, $portal] = match (true) { - str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho], - default => ['businessportal24', Portal::Businessportal24], - }; - - $applyWebDomainConfig($domainKey); - - $portalValues = [$portal->value, Portal::Both->value]; +$categoryPage = static function (string $edition, string $slug) { + $context = WebPortalContext::fromRequest(); + $language = $context->language; + $portalValues = $context->portalValues; $category = Category::query() ->whereIn('portal', $portalValues) @@ -475,18 +449,10 @@ $categoryPage = static function (string $edition, string $slug) use ($applyWebDo }; // Detailseite einer Pressemitteilung je Ausgabe. -$releaseDetailPage = static function (string $edition, string $slug) use ($applyWebDomainConfig) { - $domain = request()->getHost(); - $language = app()->getLocale(); - - [$domainKey, $portal] = match (true) { - str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho], - default => ['businessportal24', Portal::Businessportal24], - }; - - $applyWebDomainConfig($domainKey); - - $portalValues = [$portal->value, Portal::Both->value]; +$releaseDetailPage = static function (string $edition, string $slug) { + $context = WebPortalContext::fromRequest(); + $language = $context->language; + $portalValues = $context->portalValues; $publishedScope = static fn (Builder $query): Builder => $query ->forPortalValues($portalValues) @@ -553,18 +519,10 @@ $releaseDetailPage = static function (string $edition, string $slug) use ($apply }; // Veröffentlichen-Marketingseite je Ausgabe (Einreichung läuft im Publisher-Hub). -$veroeffentlichenPage = static function (string $edition) use ($applyWebDomainConfig) { - $domain = request()->getHost(); - $language = app()->getLocale(); - - [$domainKey, $portal] = match (true) { - str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho], - default => ['businessportal24', Portal::Businessportal24], - }; - - $applyWebDomainConfig($domainKey); - - $portalValues = [$portal->value, Portal::Both->value]; +$veroeffentlichenPage = static function (string $edition) { + $context = WebPortalContext::fromRequest(); + $language = $context->language; + $portalValues = $context->portalValues; $referenceNow = EditorialClock::reference($portalValues, $language); $recentSince = $referenceNow->copy()->subDays(30); @@ -630,11 +588,11 @@ $veroeffentlichenPage = static function (string $edition) use ($applyWebDomainCo // Preise-/Tarifseite je Ausgabe: Abos (Plan-Modell), Einzel-PM (config/billing) // und Add-on-Einzelkosten (config/credits). Inhalte liegen in lang/*/pricing.php. -$preisePage = static function (string $edition) use ($applyWebDomainConfig) { +$preisePage = static function (string $edition) { $domain = request()->getHost(); $domainKey = str_contains($domain, 'presseecho') ? 'presseecho' : 'businessportal24'; - $applyWebDomainConfig($domainKey); + DomainAssetContext::applyWebDomainConfig($domainKey); // Add-on-Einzelkosten aus der Credit-Ökonomie (1 Credit = 1 €). $extraPm = collect(config('credits.extra_pm', [])) @@ -670,18 +628,10 @@ $preisePage = static function (string $edition) use ($applyWebDomainConfig) { }; // Archiv-Suchseite je Ausgabe (interaktive Suche läuft in der Volt-Komponente). -$suchePage = static function (string $edition) use ($applyWebDomainConfig) { - $domain = request()->getHost(); - $language = app()->getLocale(); - - [$domainKey, $portal] = match (true) { - str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho], - default => ['businessportal24', Portal::Businessportal24], - }; - - $applyWebDomainConfig($domainKey); - - $portalValues = [$portal->value, Portal::Both->value]; +$suchePage = static function (string $edition) { + $context = WebPortalContext::fromRequest(); + $language = $context->language; + $portalValues = $context->portalValues; $mostReadReleases = PressRelease::query() ->withoutGlobalScope(PortalScope::class) @@ -694,25 +644,17 @@ $suchePage = static function (string $edition) use ($applyWebDomainConfig) { ->get(['id', 'slug', 'title', 'hits', 'portal', 'language']); return view('web.suche', [ - 'portal' => $portal->value, + 'portal' => $context->portal->value, 'language' => $language, 'mostReadReleases' => $mostReadReleases, ]); }; // Newsrooms-Verzeichnis je Ausgabe: alle aktiven Unternehmen mit Meldungen. -$newsroomsIndex = static function (string $edition) use ($applyWebDomainConfig) { - $domain = request()->getHost(); - $language = app()->getLocale(); - - [$domainKey, $portal] = match (true) { - str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho], - default => ['businessportal24', Portal::Businessportal24], - }; - - $applyWebDomainConfig($domainKey); - - $portalValues = [$portal->value, Portal::Both->value]; +$newsroomsIndex = static function (string $edition) { + $context = WebPortalContext::fromRequest(); + $language = $context->language; + $portalValues = $context->portalValues; $referenceNow = EditorialClock::reference($portalValues, $language); $recentSince = $referenceNow->copy()->subDays(30); @@ -768,18 +710,10 @@ $newsroomsIndex = static function (string $edition) use ($applyWebDomainConfig) }; // Newsroom-Markenseite eines Unternehmens je Ausgabe: alle Meldungen der Firma. -$newsroomDetail = static function (string $edition, string $slug) use ($applyWebDomainConfig) { - $domain = request()->getHost(); - $language = app()->getLocale(); - - [$domainKey, $portal] = match (true) { - str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho], - default => ['businessportal24', Portal::Businessportal24], - }; - - $applyWebDomainConfig($domainKey); - - $portalValues = [$portal->value, Portal::Both->value]; +$newsroomDetail = static function (string $edition, string $slug) { + $context = WebPortalContext::fromRequest(); + $language = $context->language; + $portalValues = $context->portalValues; $company = Company::query() ->withoutGlobalScope(PortalScope::class)