presseportale/app/Support/CategoryNavigation.php
Kevin Adametz 23cbd0bfe2 performance: Caching, deferred Tracking & externe Timeouts (H7, M12, M13)
- Navigations-Chrome (CategoryNavigation), Suche- und Newsroom-Seitendaten
  300s gecacht; gemeinsamer WebPageCache-Helper (eine TTL/Key-Konvention).
- Cache-Poisoning über ?page= geschlossen (Key an Paginator ausgerichtet,
  kein withQueryString im Cache); Newsrooms cachet nur den ungefilterten
  Fall und die ersten Seiten (kein unbegrenzter Key-Raum).
- Detailseiten schreiben Hits/View-Events deferred nach der Response
  (M12); OpenAI-/Markt-/USt-ID-Aufrufe mit Connect-Timeouts und
  begrenzten Retries; Admin-Memo-Caches konsistent invalidiert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:49:47 +00:00

88 lines
3.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Support;
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\Category;
use App\Scopes\PortalScope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
/**
* Liefert die Rubriken-Navigation der aktiven Ausgabe (Portal × Sprache).
*
* Es gibt bewusst keine separate Rubriken-Übersichtsseite: Die Navigation
* führt direkt auf die jeweilige Kategorieseite (`route('kategorie', …)`),
* auf der die Pressemitteilungen dieser Rubrik erscheinen.
*/
class CategoryNavigation
{
/**
* Top-Level-Rubriken der aktiven Ausgabe als Navigationsitems.
*
* Sortiert nach Anzahl veröffentlichter Pressemitteilungen; `null` als
* Limit liefert alle Rubriken (z. B. für das „Mehr“-Dropdown im Header).
*
* Die Navigation rendert auf jeder Web-Seite (Header/Footer/Branchenindex/
* Suche) und ist rein portal-/sprachabhängig → 300s gecacht (wie TickerData),
* sonst ≥2 uncachede Count-Aggregate pro anonymem Seitenaufruf.
*
* @return Collection<int, array{label:string, slug:string, href:string}>
*/
public static function items(?int $limit = 9): Collection
{
$portal = Portal::tryFrom((string) config('app.theme')) ?? Portal::Businessportal24;
$language = app()->getLocale();
$cacheKey = WebPageCache::key('category-nav', [$portal->value, $language, $limit ?? 'all']);
return WebPageCache::remember(
$cacheKey,
static fn (): Collection => self::query($portal, $language, $limit)
);
}
/**
* @return Collection<int, array{label:string, slug:string, href:string}>
*/
private static function query(Portal $portal, string $language, ?int $limit): Collection
{
$portalValues = [$portal->value, Portal::Both->value];
return Category::query()
->with(['translations' => fn ($query) => $query->where('locale', $language)])
->withCount([
'pressReleases as published_count' => function (Builder $query) use ($portalValues, $language): void {
$query
->withoutGlobalScope(PortalScope::class)
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at');
},
])
->whereIn('portal', $portalValues)
->where('is_active', true)
->whereNull('parent_id')
->orderByDesc('published_count')
->orderBy('id')
->when($limit !== null, fn (Builder $query) => $query->limit($limit))
->get()
->map(function (Category $category): ?array {
$translation = $category->translations->first();
if (! $translation || ! $translation->slug) {
return null;
}
return [
'label' => $translation->name,
'slug' => $translation->slug,
'href' => route('kategorie', ['slug' => $translation->slug]),
];
})
->filter()
->values();
}
}