- 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>
139 lines
4.2 KiB
PHP
139 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin;
|
|
|
|
use App\Enums\Portal;
|
|
use App\Enums\PressReleaseStatus;
|
|
use App\Models\PressRelease;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
class AdminPerformanceCache
|
|
{
|
|
public const DashboardStats = 'admin.dashboard.stats';
|
|
|
|
public const PressReleaseStats = 'admin.press-releases.stats';
|
|
|
|
public const PressReleaseReviewCount = 'admin.press-releases.review-count';
|
|
|
|
public const PressReleaseCategoryOptions = 'admin.press-releases.category-options';
|
|
|
|
public const ActiveCategoryOptions = 'admin.categories.active-options';
|
|
|
|
public const RoleOptions = 'admin.roles.options';
|
|
|
|
public const UserStats = 'admin.users.stats';
|
|
|
|
public const NewsletterStats = 'admin.newsletter.stats';
|
|
|
|
public const PresetAreas = 'admin.presets.areas';
|
|
|
|
public const PresetTypes = 'admin.presets.types';
|
|
|
|
public const StatsTtl = 60;
|
|
|
|
public const OptionsTtl = 300;
|
|
|
|
public function remember(string $key, int $seconds, callable $callback): mixed
|
|
{
|
|
if (app()->runningUnitTests()) {
|
|
return $callback();
|
|
}
|
|
|
|
return Cache::memo()->remember($key, $seconds, $callback);
|
|
}
|
|
|
|
public function flush(): void
|
|
{
|
|
foreach ($this->keys() as $key) {
|
|
Cache::memo()->forget($key);
|
|
}
|
|
}
|
|
|
|
public function forgetPressReleaseStats(): void
|
|
{
|
|
Cache::memo()->forget(self::PressReleaseStats);
|
|
Cache::memo()->forget(self::PressReleaseReviewCount);
|
|
}
|
|
|
|
/**
|
|
* Aktiv-/Inaktiv-Kennzahlen einer `is_active`-Tabelle in einer einzigen
|
|
* Aggregat-Abfrage. Single Source of Truth für die identischen Stat-Blöcke
|
|
* der Benutzer- und Firmen-Liste.
|
|
*
|
|
* @param Builder<covariant \Illuminate\Database\Eloquent\Model> $query
|
|
* @return array{total: int, active: int, inactive: int}
|
|
*/
|
|
public function activeInactiveStats(Builder $query): array
|
|
{
|
|
$stats = $query->toBase()
|
|
->selectRaw('COUNT(*) as total')
|
|
->selectRaw('SUM(CASE WHEN is_active = ? THEN 1 ELSE 0 END) as active', [true])
|
|
->selectRaw('SUM(CASE WHEN is_active = ? THEN 1 ELSE 0 END) as inactive', [false])
|
|
->first();
|
|
|
|
return [
|
|
'total' => (int) ($stats->total ?? 0),
|
|
'active' => (int) ($stats->active ?? 0),
|
|
'inactive' => (int) ($stats->inactive ?? 0),
|
|
];
|
|
}
|
|
|
|
public function pressReleaseReviewCount(): int
|
|
{
|
|
return (int) $this->remember(self::PressReleaseReviewCount, self::StatsTtl, function (): int {
|
|
return PressRelease::query()
|
|
->withoutGlobalScopes()
|
|
->where('status', PressReleaseStatus::Review->value)
|
|
->count();
|
|
});
|
|
}
|
|
|
|
public function companiesStatsKey(?string $portal): string
|
|
{
|
|
return 'admin.companies.stats.'.($portal ?? 'all');
|
|
}
|
|
|
|
public function contactsStatsKey(?string $portal): string
|
|
{
|
|
return 'admin.contacts.stats.'.($portal ?? 'all');
|
|
}
|
|
|
|
public function categoriesStatsKey(?string $portal): string
|
|
{
|
|
return 'admin.categories.stats.'.($portal ?? 'all');
|
|
}
|
|
|
|
public function permissionGroupsKey(string $guard): string
|
|
{
|
|
return "admin.permissions.groups.{$guard}";
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function keys(): array
|
|
{
|
|
$portalKeys = collect([null, ...array_map(
|
|
static fn (Portal $portal): string => $portal->value,
|
|
Portal::cases(),
|
|
)]);
|
|
|
|
return [
|
|
self::DashboardStats,
|
|
self::PressReleaseStats,
|
|
self::PressReleaseReviewCount,
|
|
self::PressReleaseCategoryOptions,
|
|
self::ActiveCategoryOptions,
|
|
self::RoleOptions,
|
|
self::UserStats,
|
|
self::NewsletterStats,
|
|
self::PresetAreas,
|
|
self::PresetTypes,
|
|
$this->permissionGroupsKey('web'),
|
|
...$portalKeys->map(fn (?string $portal): string => $this->companiesStatsKey($portal))->all(),
|
|
...$portalKeys->map(fn (?string $portal): string => $this->contactsStatsKey($portal))->all(),
|
|
...$portalKeys->map(fn (?string $portal): string => $this->categoriesStatsKey($portal))->all(),
|
|
];
|
|
}
|
|
}
|