- 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>
88 lines
3.2 KiB
PHP
88 lines
3.2 KiB
PHP
<?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();
|
||
}
|
||
}
|