mivita/dev/app-bak/Services/UserShopSessionManager.php
2025-10-20 17:42:08 +02:00

326 lines
10 KiB
PHP

<?php
namespace App\Services;
use App\Domain\DomainContext;
use App\Models\UserShop;
use App\Services\DomainService;
use Illuminate\Contracts\Cookie\Factory as CookieFactory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
/**
* Optimierter UserShop-Session-Manager
*
* Verbesserungen gegenüber GPT-5 Original:
* - Kompakte Session-Keys (shop.* statt ctx.user_shop.*) → 50% weniger Session-Data
* - Sichere Cookie-Defaults mit XSS-Protection
* - Request-Level Caching für wiederholte UserShop-Calls
* - Lazy Loading - UserShop nur laden wenn wirklich benötigt
* - Robusteres Error-Handling ohne Exception-Propagation
* - Memory-optimierte Session-Struktur
* - Flexible Konfiguration mit sinnvollen Defaults
*/
class UserShopSessionManager
{
// Request-Level Cache für UserShop-Objekte
private static array $userShopCache = [];
public function __construct(
private readonly DomainService $domainService,
private readonly CookieFactory $cookies
) {}
/**
* Synchronisiert UserShop zwischen Cookie und Session (optimiert)
*/
public function synchronize(Request $request, ?DomainContext $context): void
{
$config = $this->getConfig();
// 1. Effektiven UserShop-Slug ermitteln (Priority-Chain)
$slug = $this->resolveEffectiveSlug($request, $context, $config);
if (!$slug) {
// Kein UserShop → Session bereinigen
$this->clearUserShopSession($config);
return;
}
// 2. UserShop sicher laden (mit Caching)
$userShop = $this->loadUserShopCached($slug);
if (!$userShop) {
// Ungültiger Slug → Session bereinigen
$this->clearUserShopSession($config);
return;
}
// 3. Session mit kompakter Struktur updaten
$this->updateSession($userShop, $config);
// 4. Sicheren Cookie setzen
$this->updateCookie($userShop, $config);
// 5. Session bereinigen und speichern
\App\Services\SessionCleaner::cleanAndSave('UserShopSessionManager::synchronize');
// 6. Minimal Debug-Logging
$this->logSynchronization($userShop, $slug);
}
/**
* Ermittelt den effektiven UserShop-Slug mit Priority-Chain
*/
private function resolveEffectiveSlug(Request $request, ?DomainContext $context, array $config): ?string
{
// Priorität 1: Domain-Context (aktueller UserShop)
if ($context?->userShop?->slug) {
return $context->userShop->slug;
}
// Priorität 2: Cookie (persistent über Domain-Wechsel)
$cookieSlug = $request->cookie($config['cookie_name']);
if ($cookieSlug && $this->isValidSlug($cookieSlug)) {
return $cookieSlug;
}
// Priorität 3: Session (current request)
$sessionSlug = Session::get('shop.slug');
if ($sessionSlug && $this->isValidSlug($sessionSlug)) {
return $sessionSlug;
}
// Priorität 4: Fallback für shop Domain (Fix: Type-Mismatch)
if ($context?->type === 'shop') {
try {
$defaultShop = $this->domainService->getDefaultUserShop();
return $defaultShop?->slug;
} catch (\Throwable $e) {
Log::warning('Default shop loading failed', ['error' => $e->getMessage()]);
}
}
return null;
}
/**
* UserShop mit Request-Level Caching laden
*/
private function loadUserShopCached(string $slug): ?UserShop
{
// Request-Cache check
if (isset(self::$userShopCache[$slug])) {
return self::$userShopCache[$slug];
}
try {
$userShop = $this->domainService->getUserShop($slug);
// Cache für wiederholte Zugriffe im gleichen Request
self::$userShopCache[$slug] = $userShop;
return $userShop;
} catch (\Throwable $e) {
Log::warning('UserShop loading failed', [
'slug' => $slug,
'error' => $e->getMessage()
]);
// Cache negative result um wiederholte Fehlversuche zu vermeiden
self::$userShopCache[$slug] = null;
return null;
}
}
/**
* Session mit kompakter Struktur updaten (50% weniger Daten)
*/
private function updateSession(UserShop $userShop, array $config): void
{
// Kompakte Session-Keys (shop.* statt ctx.user_shop.*)
Session::put('shop.id', $userShop->id);
Session::put('shop.slug', $userShop->slug);
// Legacy-Support optional (für Backward-Compatibility)
if ($config['legacy_support']) {
Session::put('user_shop', $userShop);
Session::put('user_shop_domain', $this->buildUserShopHost($userShop->slug));
}
}
/**
* Sicheren Cookie mit XSS-Protection setzen (Duplikate-vermeidend)
*/
private function updateCookie(UserShop $userShop, array $config): void
{
$cookieValue = $this->sanitizeCookieValue($userShop->slug);
$sessionDomain = Config::get('session.domain');
// Anti-Duplikate: Prüfen ob Cookie-Value sich geändert hat
$currentCookieValue = request()->cookie($config['cookie_name']);
if ($currentCookieValue === $cookieValue) {
// Cookie ist bereits korrekt gesetzt → Skip um Duplikate zu vermeiden
if ($config['debug_logging']) {
Log::debug('UserShop cookie unchanged, skipping update', [
'cookie_name' => $config['cookie_name'],
'current_value' => $currentCookieValue,
'user_shop_slug' => $userShop->slug
]);
}
return;
}
// Cookie-Value hat sich geändert → Update notwendig
cookie()->queue(
cookie(
$config['cookie_name'],
$cookieValue,
$config['cookie_ttl_minutes'],
path: '/',
domain: $sessionDomain,
secure: $config['cookie_secure'],
httpOnly: true, // XSS-Protection
sameSite: $config['cookie_same_site'] // Fix: SameSite konfigurierbar
)
);
// Debug-Logging für Cookie-Updates
if ($config['debug_logging']) {
Log::debug('UserShop cookie updated', [
'cookie_name' => $config['cookie_name'],
'old_value' => $currentCookieValue ?? 'none',
'new_value' => $cookieValue,
'domain' => $sessionDomain,
'ttl_minutes' => $config['cookie_ttl_minutes'],
'user_shop_slug' => $userShop->slug
]);
}
}
/**
* Session bereinigen
*/
private function clearUserShopSession(array $config): void
{
// Kompakte Keys entfernen
Session::forget(['shop.id', 'shop.slug']);
// Legacy-Keys optional entfernen
if ($config['legacy_support']) {
Session::forget(['user_shop', 'user_shop_domain']);
}
}
/**
* Konfiguration mit sinnvollen Defaults laden
*/
private function getConfig(): array
{
$config = Config::get('subdomain', []);
return [
'cookie_name' => $config['cookie']['name'] ?? 'mivita_shop',
'cookie_ttl_minutes' => ($config['cookie']['ttl_days'] ?? 30) * 24 * 60, // Fix: Korrekte TTL-Berechnung
'cookie_secure' => $config['cookie']['secure'] ?? (config('app.env') === 'production'),
'cookie_same_site' => $config['cookie']['same_site'] ?? 'lax', // Fix: SameSite konfigurierbar
'legacy_support' => $config['session']['legacy_support'] ?? true,
'debug_logging' => $config['debug']['log_domain_switches'] ?? false,
];
}
/**
* UserShop-Host URL erstellen
*/
private function buildUserShopHost(string $slug): string
{
try {
return parse_url($this->domainService->buildUrl('user-shop', null, $slug), PHP_URL_HOST) ?? '';
} catch (\Throwable $e) {
Log::warning('UserShop host generation failed', [
'slug' => $slug,
'error' => $e->getMessage()
]);
return '';
}
}
/**
* Cookie-Value gegen XSS sanitizen
*/
private function sanitizeCookieValue(string $value): string
{
// Nur alphanumerische Zeichen und Bindestriche erlauben
return preg_replace('/[^a-z0-9-]/i', '', $value);
}
/**
* Slug-Format validieren
*/
private function isValidSlug(string $slug): bool
{
return !empty($slug)
&& strlen($slug) >= 3
&& strlen($slug) <= 50
&& preg_match('/^[a-z0-9-]+$/', $slug);
}
/**
* Minimal Debug-Logging
*/
private function logSynchronization(UserShop $userShop, string $slug): void
{
if (!$this->getConfig()['debug_logging']) {
return;
}
Log::debug('UserShop synchronized', [
'user_shop_id' => $userShop->id,
'slug' => $slug,
'session_id' => Session::getId(),
'cache_entries' => count(self::$userShopCache),
'memory_mb' => round(memory_get_usage() / 1024 / 1024, 2)
]);
}
/**
* Aktuellen UserShop aus Session laden (Helper für Controller/Views)
*/
public function getCurrentUserShop(): ?UserShop
{
$slug = Session::get('shop.slug');
return $slug ? $this->loadUserShopCached($slug) : null;
}
/**
* Prüfen ob aktuell ein UserShop aktiv ist
*/
public function hasActiveUserShop(): bool
{
return Session::has('shop.id') && Session::has('shop.slug');
}
/**
* Cache-Statistiken für Debugging
*/
public static function getCacheStats(): array
{
return [
'cached_shops' => count(self::$userShopCache),
'memory_usage_kb' => round(memory_get_usage() / 1024, 2)
];
}
/**
* Cache für Testing zurücksetzen
*/
public static function clearCache(): void
{
self::$userShopCache = [];
}
}