presseportale/app/Services/Admin/AdminPerformanceCache.php
Kevin Adametz 22975e762e 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

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(),
];
}
}