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,84 @@
<?php
namespace App\Providers;
use App\Domain\DomainContext;
use App\Services\DomainService;
use Illuminate\Support\ServiceProvider;
/**
* ## Vorschlag für einen überarbeiteten DomainServiceProvider
*
* In dieser Version wird die Middleware **nicht mehr** hier registriert.
* Die Registrierung der Middleware sollte zentral in `app/Http/Kernel.php`
* in der korrekten Reihenfolge erfolgen, um Session-Konflikte zu vermeiden.
*/
class DomainServiceProvider extends ServiceProvider
{
/**
* Registriert die Domain-Dienste im Service-Container.
*
* @return void
*/
public function register()
{
// 1. DomainService als Singleton registrieren.
// Diese Logik ist gut und bleibt unverändert.
$this->app->singleton(DomainService::class, function ($app) {
$domainService = new DomainService($app['config']['domains']);
// Validiere Konfiguration in der Development-Umgebung
if (config('app.debug')) {
$configErrors = $domainService->validateConfiguration();
if (!empty($configErrors)) {
\Log::channel('domain')->warning('Domain configuration errors detected', ['errors' => $configErrors]);
}
}
return $domainService;
});
// 2. DomainContext als Singleton registrieren.
// Die Logik hier wird nur einmal pro Anfrage ausgeführt (lazy-loaded),
// wenn der Context das erste Mal benötigt wird. Das ist effizient und korrekt.
$this->app->singleton(DomainContext::class, function ($app) {
/** @var DomainService $domainService */
$domainService = $app->make(DomainService::class);
$request = $app->make('request');
// Analysiere den Host der aktuellen Anfrage
$domainInfo = $domainService->parseDomain($request->getHost());
$userShop = null;
// Wenn es sich um eine User-Shop-Domain handelt, versuche das Shop-Objekt zu finden.
if ($domainInfo['type'] === 'user-shop' && $domainInfo['subdomain']) {
$userShop = $domainService->getUserShop($domainInfo['subdomain']);
// Wenn der Shop ungültig ist, wird der Typ auf 'unknown' gesetzt.
if (!$userShop) {
$domainInfo['type'] = 'unknown';
}
}
// Wenn es sich um die Haupt-Shop-Domain handelt (z.B. mivita.shop),
// lade den konfigurierten Fallback-Shop.
if ($domainInfo['type'] === 'main-shop' && !empty($domainInfo['default_user_shop'])) {
$userShop = $domainService->getUserShop($domainInfo['default_user_shop']);
}
return DomainContext::fromArray($domainInfo, $userShop);
});
}
/**
* Führt Aktionen nach der Registrierung aller Provider aus.
*
* @return void
*/
public function boot()
{
// Die Middleware-Registrierung wird von hier entfernt.
// $kernel = $this->app->make(\Illuminate\Contracts\Http\Kernel::class);
// $kernel->prependMiddlewareToGroup('web', DomainResolver::class);
}
}

View file

@ -0,0 +1,157 @@
<?php
namespace App\Http\Middleware;
use Closure;
use App\Services\Util;
use App\Models\UserShop;
use Illuminate\Http\Request;
use App\Domain\DomainContext;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Session;
/**
* ## Vorschlag für eine neue Middleware: HandleDomainLogic
*
* Diese Middleware ersetzt den alten `DomainResolver`.
* Sie läuft NACH `StartSession` und kann daher sicher auf die Session zugreifen.
* Ihre Hauptaufgaben sind die Validierung des Domain-Kontexts und die
* intelligente Persistierung des User-Shops in der Session.
*/
class HandleDomainLogic
{
/**
* Behandelt eine eingehende Anfrage.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// Die Prüfung, ob die Middleware ausgeführt werden soll, bleibt gleich.
if (!$this->shouldHandleRequest($request)) {
return $next($request);
}
/** @var DomainContext $context */
$context = app(DomainContext::class);
// Die Konfiguration der Session-Domain ist hier weiterhin sinnvoll.
$this->configureSessionDomain($context);
// 1. Umgang mit unbekannten Domains
// Wenn der DomainContext nicht aufgelöst werden konnte, sicher umleiten.
if ($context->isUnknown()) {
\Log::channel('domain')->warning('Unknown domain accessed', [
'host' => $request->getHost(),
'user_agent' => $request->userAgent(),
'ip' => $request->ip(),
'path' => $request->getPathInfo()
]);
$mainUrl = app(\App\Services\DomainService::class)->buildUrl('main');
return redirect()->away($mainUrl, 301);
}
// 2. Validierung für User-Shop-Domains
if ($context->isUserShop()) {
$this->validateUserShop($context);
// Der `RouteCleanup` sollte als eigene Middleware nach dem Routing laufen
// und nicht mehr hier angedeutet werden.
if ($request->route('subdomain')) {
$request->route()->forgetParameter('subdomain');
}
}
// 3. Persistierung des Kontexts in der Session (vereinfachte Logik)
$this->persistUserShopContext($context);
return $next($request);
}
/**
* Konfiguriert die `session.domain` basierend auf dem Domain-Typ.
*/
private function configureSessionDomain(DomainContext $context): void
{
if ($context->type === 'shop' || $context->type === 'main-shop' || $context->type === 'user-shop') {
Config::set('session.domain', '.' . config('app.domain') . config('app.tld_shop'));
} else {
Config::set('session.domain', '.' . config('app.domain') . config('app.tld_care'));
}
}
/**
* Führt die notwendigen Validierungen für eine User-Shop-Domain durch.
*/
private function validateUserShop(DomainContext $context): void
{
if (!$context->userShop) {
\Log::channel('domain')->warning('UserShop not found for subdomain', ['subdomain' => $context->subdomain]);
abort(503, 'Shop not available');
}
if (!$context->userShop->active) {
\Log::channel('domain')->info('Inactive UserShop accessed', ['shop_id' => $context->userShop->id]);
abort(503, 'Shop temporarily unavailable');
}
if (!$context->userShop->user || !$context->userShop->user->isActiveShop()) {
\Log::channel('domain')->info('UserShop with expired payment accessed', ['shop_id' => $context->userShop->id]);
abort(503, 'Shop access denied');
}
}
/**
* Speichert den User-Shop-Kontext in der Session.
*
* Diese Methode verfolgt eine einfachere, robustere Strategie:
* - Wenn ein User-Shop-Kontext vorhanden ist (`user-shop` oder `main-shop`),
* wird er in die Session geschrieben.
* - Auf allen anderen Domains (`main`, `crm`, `portal` etc.) wird die
* Session NICHT mehr angefasst. Ein einmal gesetzter `user_shop` bleibt
* also erhalten, bis der Benutzer einen anderen Shop besucht.
* Das `Session::forget('user_shop')` wird bewusst entfernt.
*/
private function persistUserShopContext(DomainContext $context): void
{
// Setze die `app.url` zur Laufzeit für korrekte URL-Generierung.
Config::set('app.url', 'https://' . $context->host);
if ($context->userShop) {
// Ein UserShop ist im aktuellen Kontext aktiv (entweder via Subdomain oder als Fallback).
// Wir aktualisieren die Session mit diesem Shop.
Session::put('user_shop', $context->userShop);
Session::put('user_shop_domain', $context->host);
// Kompatibilität mit der alten Util-Klasse.
Util::setPostRoute('user/');
}
// WICHTIG: Es gibt keinen `else`-Block mehr.
// Wenn kein `userShop` im Kontext ist (z.B. auf mivita.care, in.mivita.care),
// wird ein eventuell vorhandener `user_shop` aus einem früheren Besuch
// in der Session belassen. Dies löst das Problem des Kontextverlusts.
}
/**
* Prüft, ob die Middleware für den Request relevant ist.
* (Logik aus dem alten DomainResolver übernommen)
*/
private function shouldHandleRequest(Request $request): bool
{
if ($request->is('api/*')) {
return false;
}
if ($request->isMethod('GET') && preg_match('/\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i', $request->path())) {
return false;
}
if (in_array($request->path(), ['_debugbar/!*', 'health', 'status', 'ping'])) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
/**
* ## Vorschlag für `app/Http/Kernel.php`
*
* Diese Datei zeigt die empfohlene Anordnung der Middleware, um das Session-Problem zu lösen.
* Die `DomainResolver`-Middleware wird durch `HandleDomainLogic` ersetzt und an die richtige Stelle verschoben.
*/
class ProposedKernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
// ... andere globale Middleware
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
// ...
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class, // Die Session wird hier gestartet.
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// ===================================================================
// NEUE ANORDNUNG:
// Die Domain-Logik wird NACH `StartSession` ausgeführt.
// Der alte `DomainResolver` wird durch `HandleDomainLogic` ersetzt.
// `InitializeDomainContext` könnte hier oder sogar global laufen,
// wenn der Kontext noch früher gebraucht wird.
// ===================================================================
// \App\Http\Middleware\InitializeDomainContext::class, // Optional, falls benötigt
\App\Http\Middleware\HandleDomainLogic::class, // <-- HIER
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
// ... andere Route-Middleware
];
}

View file

@ -0,0 +1,75 @@
# Optimierung des Domain-Routings und Session-Handlings
Dieses Dokument beschreibt eine vorgeschlagene Optimierung für die bestehende Domain-Routing- und Session-Verwaltungslogik.
## 1. Analyse der aktuellen Architektur
Die Anwendung nutzt eine komplexe, aber leistungsstarke Multi-Domain-Architektur, um verschiedene Anwendungsbereiche (Hauptseite, CRM, Partner-Portal, Checkout, User-Shops) voneinander zu trennen.
- **Konfiguration**: Die Domain-Struktur wird zentral in `config/domains.php` definiert.
- **Service Provider (`DomainServiceProvider`)**:
- Registriert den `DomainService` (zuständig für Domain-Analyse und UserShop-Logik).
- Registriert den `DomainContext` als Singleton, der pro Request Informationen über die aktuelle Domain enthält (Typ, Host, Subdomain, UserShop-Objekt).
- **Wichtig**: Er registriert die `DomainResolver`-Middleware und fügt sie an den _Anfang_ der `web`-Middleware-Gruppe hinzu.
- **Middleware (`DomainResolver`)**:
- Nutzt den `DomainContext`, um die Anfrage zu validieren.
- Leitet ungültige Domains auf die Hauptdomain um.
- Prüft den Status von User-Shops (aktiv, Zahlung etc.).
- **Problembereich**: Schreibt `UserShop`-Daten aktiv in die Session (`Session::put`, `Session::save`).
- **Routing (`RouteServiceProvider`)**: Lädt die passenden Routen-Dateien basierend auf dem `type` im `DomainContext`.
## 2. Identifiziertes Problem: Konflikte bei der Session-Erstellung
Das Kernproblem liegt in der Reihenfolge, in der die Middleware ausgeführt wird.
1. Der `DomainServiceProvider` registriert die `DomainResolver`-Middleware mit `prependMiddlewareToGroup('web', ...)`.
2. Dadurch wird `DomainResolver` **vor** der Standard-Laravel-Middleware `StartSession` ausgeführt, die ebenfalls Teil der `web`-Gruppe ist.
3. Die `DomainResolver`-Middleware versucht, auf die Session zuzugreifen und sie zu modifizieren (`Session::put`, `Session::save`). Da die `StartSession`-Middleware noch nicht gelaufen ist, wird hier möglicherweise eine "provisorische" Session gestartet.
4. Anschließend läuft die `StartSession`-Middleware, die die "offizielle" Session aus dem Cookie lädt.
5. Dies kann zu einem Konflikt führen, bei dem zwei verschiedene Session-Instanzen pro Anfrage existieren oder die in `DomainResolver` gespeicherten Daten nicht in der finalen Session-Response an den Client gesendet werden. Das Resultat ist, dass der `UserShop`-Kontext beim Wechsel zwischen Domains (z.B. vom Shop zum `in.`-Portal) verloren geht.
## 3. Vorgeschlagene Lösung: Saubere Trennung und korrekte Reihenfolge
Die vorgeschlagene Lösung zielt darauf ab, die Logik zu entzerren und die Middleware in der korrekten, von Laravel vorgesehenen Reihenfolge auszuführen.
### Schritt 1: Korrektur der Middleware-Reihenfolge
Die `DomainResolver`-Middleware darf nicht mehr per `prependMiddlewareToGroup` im Service Provider registriert werden. Stattdessen sollte sie im `app/Http/Kernel.php` innerhalb der `web`-Gruppe **nach** der `\Illuminate\Session\Middleware\StartSession`-Middleware platziert werden.
Dadurch wird sichergestellt, dass die Session immer korrekt initialisiert ist, bevor unsere anwendungsspezifische Logik darauf zugreift.
### Schritt 2: Aufteilung der Middleware-Verantwortlichkeiten
Die `DomainResolver`-Middleware hat aktuell zu viele Aufgaben. Eine Aufteilung verbessert die Lesbarkeit, Wartbarkeit und Einhaltung des Single-Responsibility-Prinzips.
**Vorschlag für neue Middleware-Struktur:**
1. **`InitializeDomainContext` (Neue Middleware)**:
- Diese Middleware läuft sehr früh (kann global oder am Anfang der `web`-Gruppe stehen).
- Ihre **einzige** Aufgabe ist es, den `DomainContext` zu initialisieren, indem sie `app(DomainContext::class)` aufruft. Sie führt **keine** Session-Operationen oder Umleitungen durch. Dies stellt sicher, dass der Kontext für alle nachfolgenden Teile der Anwendung (auch andere Middleware) verfügbar ist.
2. **`HandleDomainLogic` (Umbenannter `DomainResolver`)**:
- Diese Middleware läuft **nach** `StartSession`.
- Sie nutzt den bereits initialisierten `DomainContext`.
- **Aufgaben**:
- Umleitung bei `isUnknown()`.
- Validierung des `UserShop` (aktiv, bezahlt etc.).
- Persistierung des `UserShop`-Kontexts in der Session. Die Logik hierfür wird verfeinert, um den Shop-Kontext über Domain-Grenzen hinweg intelligent zu erhalten. Zum Beispiel wird der `user_shop` nicht mehr gelöscht, nur weil man die `main`-Domain besucht. Er wird nur überschrieben, wenn ein neuer Shop-Kontext explizit gesetzt wird.
### Schritt 3: Vereinfachung des Session-Handlings
Die `setupLegacyContext`-Methode wird überarbeitet. Das Ziel ist ein klares und vorhersehbares Verhalten:
- **Auf einer User-Shop-Domain**: Der `UserShop` wird in die Session geschrieben.
- **Auf einer Nicht-Shop-Domain (`crm`, `portal`, `checkout`, `main`)**: Die Session wird **nicht angerührt**. Der bestehende `UserShop` aus der Session bleibt erhalten, sodass eine Rückkehr zum Shop jederzeit möglich ist.
- **Wechsel zu einem anderen User-Shop**: Der neue `UserShop` überschreibt den alten Wert in der Session.
## 4. Vorteile der neuen Architektur
- **Stabilität**: Das Session-Problem wird an der Wurzel behoben, indem die Middleware-Reihenfolge korrigiert wird.
- **Klarheit**: Die Verantwortlichkeiten sind sauber getrennt. Eine Middleware für die Kontext-Initialisierung, eine für die Domain-Logik.
- **Wartbarkeit**: Einfacher zu verstehen und zu erweitern. Das `TECH-DEBT` in `setupLegacyContext` wird reduziert.
- **Performance**: Die Logik zur Domain-Analyse im `DomainServiceProvider` wird nur einmal ausgeführt, wenn der Kontext das erste Mal benötigt wird (lazy-loading), was beibehalten wird.
Die folgenden Dateien in diesem Ordner enthalten eine beispielhafte Implementierung dieses Vorschlags.