update 20.10.2025

This commit is contained in:
Kevin Adametz 2025-10-20 17:42:08 +02:00
parent 8c11130b5d
commit a939cd51ef
616 changed files with 84821 additions and 4121 deletions

View file

@ -0,0 +1,50 @@
# Integration & Reihenfolge
Wichtig: Bestehende Dateien werden hier NICHT verändert. Die folgenden Schritte beschreiben die geplante Integration.
## Reihenfolge der Middleware (web-Gruppe)
Empfohlene Reihenfolge in `App\Http\Kernel` (schematisch):
1. `App\Dev\SubdomainOptimizationGpt5\Http\Middleware\DomainBootstrap` (NEU, sehr früh)
2. `\App\Http\Middleware\EncryptCookies`
3. `\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse`
4. `\Illuminate\Session\Middleware\StartSession`
5. `App\Dev\SubdomainOptimizationGpt5\Http\Middleware\DomainSessionSync` (NEU, nach StartSession)
6. `\Illuminate\View\Middleware\ShareErrorsFromSession`
7. `\App\Http\Middleware\VerifyCsrfToken`
8. ... weitere App-Middleware
Damit wird garantiert:
- Vor StartSession: Kein Sessionzugriff (DomainBootstrap ist „rein“)
- Nach StartSession: Synchronisierung von `user_shop` in Session + Cookie (DomainSessionSync)
## Container-Bindings
- `DomainBootstrap` erzeugt den `DomainContext` selbst (ohne Session) und registriert ihn per `app()->instance(DomainContext::class, $ctx)` sowie zusätzlich als Request-Attribut `domain.context`.
- Alte `DomainServiceProvider`-Binding-Strategie kann damit schrittweise entfernt oder angepasst werden (muss nicht sofort).
## Sticky-Shop-Verhalten
- Cookie `mvt_shop` (konfigurierbar) trägt den `user_shop`-Slug über Subdomains
- Session hält kompakten Zustand: `ctx.user_shop` (id, slug, host)
- Optional: Legacy-Keys (`user_shop`, `user_shop_domain`) zur Abwärtskompatibilität aktivierbar
## Migrationspfad (schrittweise)
1. NEUE Middleware registrieren (Kernel), alte Logik belassen
2. DomainResolver in produktivem Code um Session-Zugriffe entlasten (später)
3. Schrittweise Nutzung von `UserShopSessionManager` in Controllern/Views (statt direkter Session-Manipulation)
4. Nach Stabilisierung: Legacy-Keys deaktivieren (Konfiguration), Altpfade entfernen
## Konfiguration
Siehe `config/example.subdomain_optimization.php`. Wichtig sind Domain-/Cookie-Parameter sowie die Option für Legacy-Keys.
## Testplan (Auszug)
- Wechsel `{slug}.mivita.care``in.mivita.care` → Zurück-zum-Shop (Slug bleibt erhalten)
- Wechsel `{slug}.mivita.care``checkout.mivita.care` (Slug bleibt erhalten)
- Zugriff `mivita.shop` (Main Shop) setzt Fallback-Shop
- Ungültige Subdomain erzeugt KEINE Session (nur Redirect/Fehler, je nach bestehender App-Logik)

View file

@ -0,0 +1,48 @@
# Subdomain & Session Handling Optimierungsvorschlag (GPT-5)
Dieses Paket enthält einen sauberen, zweistufigen Ansatz für Domain- und Session-Handling, um Probleme mit doppelt initialisierten Sessions und inkonsistenten `user_shop`-Zuständen zu vermeiden. Es ändert keine bestehenden Dateien. Integration ist in `INTEGRATION.md` beschrieben.
## Ziele
- Frühe Domain-Auflösung ohne Session-Zugriff (vermeidet doppelte Session-Erstellung)
- Späte, robuste Session-/Cookie-Synchronisierung (Sticky-UserShop über Subdomains)
- Kompakte, gut getestete Einheiten (Middleware + Service)
- Abwärtskompatible Session-Keys optional möglich (für schrittweise Migration)
## Architektur (Kurzfassung)
- DomainBootstrap (früh, vor StartSession):
- Parst Host → `DomainContext` (ohne Sessionzugriff)
- Setzt `config('session.domain')` und `config('app.url')` frühzeitig
- Hinterlegt `DomainContext` im Container/Request-Attribute
- DomainSessionSync (spät, nach StartSession):
- Synchronisiert `user_shop` in Session und Cookie (sticky über Subdomains)
- Nutzt `UserShopSessionManager` (Service)
- Hält sich strikt an die „kein Sessionzugriff vor StartSession“-Regel
- UserShopSessionManager:
- Vereinheitlicht Lesen/Schreiben von `user_shop` (Session + Cookie)
- Optional: Legacy-Keys (`session('user_shop')`) für Bestands-Views/Controller setzen
## Dateien
- `src/Http/Middleware/DomainBootstrap.php`
- `src/Http/Middleware/DomainSessionSync.php`
- `src/Services/UserShopSessionManager.php`
- `config/example.subdomain_optimization.php` (Beispielkonfiguration)
- `INTEGRATION.md` (Reihenfolge/Registrierung & Migrationspfad)
- `docs/ADR-001-domain-session-handling.md` (Entscheidungsdokumentation)
## Warum dieser Ansatz?
Die aktuelle Logik greift in Teilen zu früh auf die Session zu, bevor die Kernel-`StartSession`-Middleware aktiv ist. Das kann zu zwei Session-IDs in einem Request führen und verhindert ein konsistentes Sticky-Shop-Verhalten über `in.` und `checkout.`. Der zweistufige Ansatz trennt strikt:
- Domänenauflösung/Kontext (rein, keine Session)
- Zustandssynchronisierung (nur nach Start der Session)
Damit werden doppelte Sessions vermieden und der `user_shop` zuverlässig über Subdomains hinweg getragen.
Weitere Details siehe `INTEGRATION.md` und `docs/ADR-001-domain-session-handling.md`.

View file

@ -0,0 +1,12 @@
<?php
return [
'cookie' => [
'name' => env('MIVITA_USERSHOP_COOKIE', 'mvt_shop'),
'ttl_minutes' => env('MIVITA_USERSHOP_COOKIE_TTL', 60 * 24 * 30),
],
'legacy' => [
// Setzt zusätzlich `session('user_shop')` und `session('user_shop_domain')`
'enable_legacy_session_keys' => env('MIVITA_ENABLE_LEGACY_SHOP_KEYS', true),
],
];

View file

@ -0,0 +1,34 @@
# ADR-001: Zweistufiges Domain- & Session-Handling
## Kontext
Die bestehende Middleware/Provider-Logik greift in Teilen vor `StartSession` auf Session-Daten zu. Das führt in Randfällen zu doppelt erzeugten Sessions und inkonsistentem `user_shop`-Zustand.
## Entscheidung
Wir trennen strikt in zwei Phasen:
1. DomainBootstrap (früh, rein):
- Host parsen → `DomainContext`
- `config('session.domain')` + `config('app.url')` setzen
- KEIN Sessionzugriff
2. DomainSessionSync (spät, zustandsvoll):
- Session ist aktiv
- `UserShopSessionManager` synchronisiert `user_shop` in Session und Cookie
## Alternativen
- Alles in einer Middleware: führt zu Sessionzugriff vor `StartSession` oder verspäteter Setzung von `session.domain`
- Nur Cookies, keine Session: bricht vorhandene Annahmen in Views/Controllern
## Konsequenzen
- Keine doppelten Sessions durch frühe Zugriffe
- Sticky-Shop über Subdomains stabil
- Optionale Abwärtskompatibilität via Legacy-Session-Keys
## Status
Prototyp in `dev/subdomain-optimization-gpt-5` abgelegt. Schrittweise Integration empfohlen.

View file

@ -0,0 +1,85 @@
<?php
namespace App\Dev\SubdomainOptimizationGpt5\Http\Middleware;
use App\Domain\DomainContext;
use App\Services\DomainService;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
/**
* Früh gestartete Middleware zur DOMÄNEN-Auflösung OHNE Sessionzugriff.
* - Parst den Host und erzeugt einen DomainContext
* - Setzt dynamische Konfiguration (session.domain, app.url)
* - Registriert DomainContext im Container und als Request-Attribut
*/
class DomainBootstrap
{
public function handle(Request $request, Closure $next)
{
// Nur für relevante HTTP-Requests (keine Assets/API) minimalistischer Filter
if ($this->shouldSkip($request)) {
return $next($request);
}
/** @var DomainService $domainService */
$domainService = app(DomainService::class);
$domainInfo = $domainService->parseDomain($request->getHost());
$userShop = null;
if (($domainInfo['type'] ?? 'unknown') === 'user-shop' && !empty($domainInfo['subdomain'])) {
// KEIN Sessionzugriff lediglich Context vorbereiten
$userShop = $domainService->getUserShop($domainInfo['subdomain']);
if (!$userShop) {
// Ungültig → unknown, keine Session anlegen
$domainInfo['type'] = 'unknown';
}
} elseif (($domainInfo['type'] ?? null) === 'main-shop' && !empty($domainInfo['default_user_shop'])) {
$userShop = $domainService->getUserShop($domainInfo['default_user_shop']);
}
$context = DomainContext::fromArray($domainInfo, $userShop);
// Früh: session.domain + app.url setzen (wir greifen noch nicht auf Session zu)
$this->configureRuntime($context);
// Context bereitstellen
app()->instance(DomainContext::class, $context);
$request->attributes->set('domain.context', $context);
return $next($request);
}
private function shouldSkip(Request $request): bool
{
if ($request->is('api/*')) {
return true;
}
if ($request->isMethod('GET') && preg_match('/\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i', $request->path())) {
return true;
}
if ($request->isMethod('GET') && in_array($request->path(), ['_debugbar/*'])) {
return true;
}
if ($request->isMethod('GET') && in_array($request->path(), ['health', 'status', 'ping'])) {
return true;
}
return false;
}
private function configureRuntime(DomainContext $context): void
{
// session.domain abhängig vom Kontext setzen
if ($context->type === 'shop' || $context->type === 'user-shop' || $context->type === 'main-shop') {
Config::set('session.domain', '.' . config('app.domain') . config('app.tld_shop'));
} else {
Config::set('session.domain', '.' . config('app.domain') . config('app.tld_care'));
}
// app.url für URL-Generierung
if (!empty($context->host)) {
Config::set('app.url', $context->host);
}
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Dev\SubdomainOptimizationGpt5\Http\Middleware;
use App\Dev\SubdomainOptimizationGpt5\Services\UserShopSessionManager;
use App\Domain\DomainContext;
use Closure;
use Illuminate\Http\Request;
/**
* Spät gestartete Middleware (NACH StartSession).
* - Synchronisiert `user_shop` zwischen Cookie und Session
* - Setzt optional Legacy-Session-Keys für Abwärtskompatibilität
*/
class DomainSessionSync
{
public function __construct(private readonly UserShopSessionManager $manager) {}
public function handle(Request $request, Closure $next)
{
/** @var DomainContext|null $context */
$context = app(DomainContext::class);
// Session ist aktiv konsolidieren
$this->manager->synchronize($request, $context);
return $next($request);
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace App\Dev\SubdomainOptimizationGpt5\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\Session;
class UserShopSessionManager
{
public function __construct(
private readonly DomainService $domainService,
private readonly CookieFactory $cookies
) {}
/**
* Synchronisiert UserShop zwischen Cookie und Session. Setzt optional Legacy-Keys.
*/
public function synchronize(Request $request, ?DomainContext $context): void
{
$config = Config::get('subdomain_optimization', []);
$cookieName = $config['cookie']['name'] ?? 'mvt_shop';
$legacy = (bool)($config['legacy']['enable_legacy_session_keys'] ?? true);
$cookieTtl = (int)($config['cookie']['ttl_minutes'] ?? 60 * 24 * 30);
// 1) Quelle bestimmen: DomainContext (falls user-shop) → Cookie → Session
$slugFromContext = ($context && $context->userShop) ? $context->userShop->slug : null;
$slugFromCookie = $request->cookie($cookieName);
$slugFromSession = Session::get('ctx.user_shop.slug');
$effectiveSlug = $slugFromContext ?: ($slugFromCookie ?: $slugFromSession);
if (!$effectiveSlug) {
// Fallback: main-shop mit default_user_shop
if ($context && $context->type === 'main-shop') {
$default = $this->domainService->getDefaultUserShop();
$effectiveSlug = $default?->slug;
}
}
if (!$effectiveSlug) {
return; // Nichts zu synchronisieren
}
$shop = $this->domainService->getUserShop($effectiveSlug);
if (!$shop) {
return; // Ungültig → keine Seiteneffekte
}
// 2) Session updaten (kompakter Namespace)
Session::put('ctx.user_shop', [
'id' => $shop->id,
'slug' => $shop->slug,
'host' => $this->domainService->buildUrl('user-shop', null, $shop->slug),
]);
// 3) Legacy-Keys optional bedienen
if ($legacy) {
Session::put('user_shop', $shop);
Session::put('user_shop_domain', parse_url($this->domainService->buildUrl('user-shop', null, $shop->slug), PHP_URL_HOST));
}
Session::save();
// 4) Cookie aktualisieren (sticky über Subdomains)
$domainForCookie = config('session.domain');
cookie()->queue(
cookie(
$cookieName,
$shop->slug,
$cookieTtl,
path: '/',
domain: $domainForCookie,
secure: config('session.secure', false),
httpOnly: true,
sameSite: config('session.same_site', 'lax')
)
);
}
}