update 20.10.2025
This commit is contained in:
parent
8c11130b5d
commit
a939cd51ef
616 changed files with 84821 additions and 4121 deletions
50
dev/subdomain-optimization-gpt-5-v2/INTEGRATION.md
Normal file
50
dev/subdomain-optimization-gpt-5-v2/INTEGRATION.md
Normal 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)
|
||||
48
dev/subdomain-optimization-gpt-5-v2/README.md
Normal file
48
dev/subdomain-optimization-gpt-5-v2/README.md
Normal 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`.
|
||||
|
|
@ -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),
|
||||
],
|
||||
];
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dev\SubdomainOptimizationGpt5V2\Http\Middleware;
|
||||
|
||||
use App\Domain\DomainContext;
|
||||
use App\Services\DomainService; // Assuming this is the existing central domain service
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Early-stage middleware for domain resolution WITHOUT session access.
|
||||
* This version is optimized for clarity, type safety, and better dependency injection.
|
||||
*/
|
||||
class DomainBootstrap
|
||||
{
|
||||
public function __construct(private readonly DomainService $domainService) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
if ($this->shouldSkip($request)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
try {
|
||||
$host = $request->getHost();
|
||||
$domainInfo = $this->domainService->parseDomain($host);
|
||||
|
||||
$userShop = null;
|
||||
if (($domainInfo['type'] ?? 'unknown') === 'user-shop' && !empty($domainInfo['subdomain'])) {
|
||||
$userShop = $this->domainService->getUserShop($domainInfo['subdomain']);
|
||||
if (!$userShop) {
|
||||
$domainInfo['type'] = 'unknown'; // Invalidate type if shop is not found
|
||||
}
|
||||
} elseif (($domainInfo['type'] ?? null) === 'main-shop' && !empty($domainInfo['default_user_shop'])) {
|
||||
$userShop = $this->domainService->getUserShop($domainInfo['default_user_shop']);
|
||||
}
|
||||
|
||||
$context = DomainContext::fromArray($domainInfo, $userShop);
|
||||
|
||||
$this->configureRuntime($context);
|
||||
|
||||
// Provide context for the rest of the application
|
||||
app()->instance(DomainContext::class, $context);
|
||||
$request->attributes->set('domain.context', $context);
|
||||
} catch (\Throwable $e) {
|
||||
// Log any error during bootstrap but don't break the request flow.
|
||||
// A fallback DomainContext could be created here if needed.
|
||||
Log::channel('domain')->error('Domain bootstrapping failed.', [
|
||||
'host' => $request->getHost(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function shouldSkip(Request $request): bool
|
||||
{
|
||||
// A more streamlined way to check for asset-like requests.
|
||||
if ($request->isMethod('GET') && str_contains($request->path(), '.')) {
|
||||
$extension = pathinfo($request->path(), PATHINFO_EXTENSION);
|
||||
if (in_array($extension, ['css', 'js', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'svg', 'woff', 'woff2', 'ttf', 'eot'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return $request->is(['api/*', '_debugbar/*', 'health', 'status', 'ping']);
|
||||
}
|
||||
|
||||
private function configureRuntime(DomainContext $context): void
|
||||
{
|
||||
// Centralized logic to determine the correct session domain.
|
||||
// This is more maintainable than multiple checks.
|
||||
$isShopDomain = in_array($context->type, ['shop', 'user-shop', 'main-shop'], true);
|
||||
$tld = $isShopDomain ? config('app.tld_shop') : config('app.tld_care');
|
||||
Config::set('session.domain', '.' . config('app.domain') . $tld);
|
||||
|
||||
if (!empty($context->host)) {
|
||||
Config::set('app.url', 'https://' . $context->host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dev\SubdomainOptimizationGpt5V2\Http\Middleware;
|
||||
|
||||
use App\Dev\SubdomainOptimizationGpt5V2\Services\UserShopSessionManager;
|
||||
use App\Domain\DomainContext;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Late-stage middleware (AFTER StartSession).
|
||||
* Synchronizes `user_shop` between cookie and session.
|
||||
*/
|
||||
class DomainSessionSync
|
||||
{
|
||||
public function __construct(private readonly UserShopSessionManager $manager) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
/** @var DomainContext|null $context */
|
||||
$context = $request->attributes->get('domain.context');
|
||||
|
||||
// Session is active – consolidate state
|
||||
if ($context) {
|
||||
$this->manager->synchronize($request, $context);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dev\SubdomainOptimizationGpt5V2\Services;
|
||||
|
||||
use App\Domain\DomainContext;
|
||||
use App\Models\UserShop;
|
||||
use App\Services\DomainService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
/**
|
||||
* Manages the synchronization of UserShop context between session and cookies.
|
||||
* This version is optimized for clarity, maintainability, and modern PHP standards.
|
||||
*/
|
||||
class UserShopSessionManager
|
||||
{
|
||||
private const CONFIG_KEY = 'subdomain_optimization';
|
||||
private const COOKIE_NAME_KEY = 'cookie.name';
|
||||
private const COOKIE_TTL_KEY = 'cookie.ttl_minutes';
|
||||
private const LEGACY_KEYS_ENABLED_KEY = 'legacy.enable_legacy_session_keys';
|
||||
|
||||
public function __construct(private readonly DomainService $domainService) {}
|
||||
|
||||
/**
|
||||
* Synchronizes the UserShop between the current domain context, session, and cookies.
|
||||
*/
|
||||
public function synchronize(Request $request, ?DomainContext $context): void
|
||||
{
|
||||
$effectiveSlug = $this->determineEffectiveSlug($request, $context);
|
||||
|
||||
if (!$effectiveSlug) {
|
||||
// Nothing to synchronize if no slug can be determined.
|
||||
return;
|
||||
}
|
||||
|
||||
$shop = $this->domainService->getUserShop($effectiveSlug);
|
||||
if (!$shop) {
|
||||
// If the slug resolves to an invalid shop, do nothing to prevent side effects.
|
||||
// Optionally, we could clear the session/cookie here.
|
||||
return;
|
||||
}
|
||||
|
||||
$this->updateSession($shop);
|
||||
$this->updateCookie($shop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the authoritative UserShop slug from various sources.
|
||||
* Precedence: Domain Context > Cookie > Session.
|
||||
*/
|
||||
private function determineEffectiveSlug(Request $request, ?DomainContext $context): ?string
|
||||
{
|
||||
$cookieName = Config::get(self::CONFIG_KEY . '.' . self::COOKIE_NAME_KEY, 'mvt_shop');
|
||||
|
||||
$slugFromContext = $context?->userShop?->slug;
|
||||
$slugFromCookie = $request->cookie($cookieName);
|
||||
$slugFromSession = Session::get('ctx.user_shop.slug');
|
||||
|
||||
$effectiveSlug = $slugFromContext ?? $slugFromCookie ?? $slugFromSession;
|
||||
|
||||
// If no slug is found, check for a default shop on the main-shop domain.
|
||||
if (!$effectiveSlug && $context?->type === 'main-shop') {
|
||||
return $this->domainService->getDefaultUserShop()?->slug;
|
||||
}
|
||||
|
||||
return $effectiveSlug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the session with the provided UserShop data.
|
||||
*/
|
||||
private function updateSession(UserShop $shop): void
|
||||
{
|
||||
Session::put('ctx.user_shop', [
|
||||
'id' => $shop->id,
|
||||
'slug' => $shop->slug,
|
||||
'host' => $this->domainService->buildUrl('user-shop', null, $shop->slug),
|
||||
]);
|
||||
|
||||
if (Config::get(self::CONFIG_KEY . '.' . self::LEGACY_KEYS_ENABLED_KEY, true)) {
|
||||
Session::put('user_shop', $shop);
|
||||
$shopDomain = parse_url($this->domainService->buildUrl('user-shop', null, $shop->slug), PHP_URL_HOST);
|
||||
Session::put('user_shop_domain', $shopDomain);
|
||||
}
|
||||
|
||||
Session::save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the 'sticky' UserShop cookie.
|
||||
*/
|
||||
private function updateCookie(UserShop $shop): void
|
||||
{
|
||||
$cookieName = Config::get(self::CONFIG_KEY . '.' . self::COOKIE_NAME_KEY, 'mvt_shop');
|
||||
$cookieTtl = (int) Config::get(self::CONFIG_KEY . '.' . self::COOKIE_TTL_KEY, 60 * 24 * 30);
|
||||
|
||||
cookie()->queue(
|
||||
cookie(
|
||||
name: $cookieName,
|
||||
value: $shop->slug,
|
||||
minutes: $cookieTtl,
|
||||
path: '/',
|
||||
domain: config('session.domain'),
|
||||
secure: config('session.secure', false),
|
||||
httpOnly: true,
|
||||
sameSite: config('session.same_site', 'lax')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue