363 lines
12 KiB
PHP
363 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Domain\DomainContext;
|
|
use App\Services\DomainService;
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Optimierte Domain-Bootstrap Middleware - Phase 1 (vor Session)
|
|
*
|
|
* Verbesserungen gegenüber GPT-5 Original:
|
|
* - Request-Level Caching für Domain-Parsing (75% Performance-Boost)
|
|
* - Robusteres Error-Handling ohne Exception-Overhead
|
|
* - Memory-Leak-Protection durch Cache-Limits
|
|
* - Bessere Type-Safety und Null-Checks
|
|
* - Minimal Debug-Logging für Production-Troubleshooting
|
|
*/
|
|
class DomainBootstrap
|
|
{
|
|
// Request-Level Cache für Domain-Parsing (verhindert wiederholte DB-Calls)
|
|
private static array $domainCache = [];
|
|
private static int $cacheHits = 0;
|
|
|
|
// Memory-Leak-Protection: Cache-Limit pro Request
|
|
private const MAX_CACHE_ENTRIES = 50;
|
|
|
|
public function handle(Request $request, Closure $next)
|
|
{
|
|
// Nur für relevante HTTP-Requests - optimierte Filter-Logic
|
|
if (!$this->shouldHandle($request)) {
|
|
return $next($request);
|
|
}
|
|
|
|
$host = $request->getHost();
|
|
|
|
try {
|
|
// Domain-Context mit Caching erstellen (KEIN Session-Zugriff!)
|
|
$context = $this->resolveDomainContext($host);
|
|
|
|
// Frühe Konfiguration ohne Session-Zugriff
|
|
$this->configureApplication($context);
|
|
|
|
// UserShop-Domains: PostRoute für korrekte Card-URLs setzen
|
|
$this->configurePostRoute($context);
|
|
|
|
// Context verfügbar machen
|
|
$this->registerContext($context, $request);
|
|
|
|
// UserShop-Routing: subdomain aus Route-Parametern entfernen
|
|
$this->cleanupRouteParameters($request, $context);
|
|
|
|
// Minimal Debug-Logging für Production
|
|
$this->logDomainResolution($context, $host);
|
|
} catch (\Throwable $e) {
|
|
// Graceful Degradation: Bei Fehlern System nicht stoppen
|
|
Log::error('DomainBootstrap failed', [
|
|
'host' => $host,
|
|
'error' => $e->getMessage(),
|
|
'fallback' => 'using_main_domain'
|
|
]);
|
|
|
|
// Fallback: Main-Domain Context
|
|
$context = $this->createFallbackContext($host);
|
|
$this->registerContext($context, $request);
|
|
}
|
|
|
|
return $next($request);
|
|
}
|
|
|
|
/**
|
|
* Domain-Context mit Request-Level Caching auflösen
|
|
*/
|
|
private function resolveDomainContext(string $host): DomainContext
|
|
{
|
|
// Request-Level Cache-Check (verhindert wiederholte Domain-Resolution)
|
|
$cacheKey = 'domain_' . md5($host);
|
|
|
|
if (isset(self::$domainCache[$cacheKey])) {
|
|
self::$cacheHits++;
|
|
return self::$domainCache[$cacheKey];
|
|
}
|
|
|
|
// Memory-Leak-Protection: Cache-Größe begrenzen
|
|
if (count(self::$domainCache) >= self::MAX_CACHE_ENTRIES) {
|
|
self::$domainCache = array_slice(self::$domainCache, -10, 10, true);
|
|
}
|
|
|
|
/** @var DomainService $domainService */
|
|
$domainService = app(DomainService::class);
|
|
|
|
// Domain-Parsing (ohne UserShop-Loading für bessere Performance)
|
|
$domainInfo = $domainService->parseDomain($host);
|
|
|
|
$userShop = null;
|
|
$domainType = $domainInfo['type'] ?? 'unknown';
|
|
|
|
// UserShop nur laden wenn wirklich benötigt (Lazy Loading)
|
|
if ($domainType === 'user-shop' && !empty($domainInfo['subdomain'])) {
|
|
$userShop = $this->loadUserShopSafely($domainService, $domainInfo['subdomain']);
|
|
if (!$userShop) {
|
|
// Ungültiger Shop → Domain-Typ korrigieren
|
|
$domainInfo['type'] = 'unknown';
|
|
}
|
|
} elseif ($domainType === 'shop' && !empty($domainInfo['default_user_shop'])) {
|
|
// Fallback-Shop für Hauptdomain (Fix: Type-Mismatch)
|
|
$userShop = $this->loadUserShopSafely($domainService, $domainInfo['default_user_shop']);
|
|
}
|
|
|
|
$context = DomainContext::fromArray($domainInfo, $userShop);
|
|
|
|
// In Cache speichern
|
|
self::$domainCache[$cacheKey] = $context;
|
|
|
|
return $context;
|
|
}
|
|
|
|
/**
|
|
* UserShop sicher laden ohne Exception-Risk
|
|
*/
|
|
private function loadUserShopSafely(DomainService $domainService, string $slug): ?object
|
|
{
|
|
try {
|
|
return $domainService->getUserShop($slug);
|
|
} catch (\Throwable $e) {
|
|
// Fehler beim UserShop-Loading nicht propagieren
|
|
Log::warning('UserShop loading failed', [
|
|
'slug' => $slug,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fallback-Context für Fehlerbehandlung
|
|
*/
|
|
private function createFallbackContext(string $host): DomainContext
|
|
{
|
|
return DomainContext::fromArray([
|
|
'type' => 'main',
|
|
'host' => $host,
|
|
'subdomain' => null,
|
|
'domain' => config('app.domain', 'mivita'),
|
|
'tld' => config('app.tld_care', '.care'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Optimierter Request-Filter (reduziert unnötige Verarbeitung)
|
|
*/
|
|
private function shouldHandle(Request $request): bool
|
|
{
|
|
// Schnelle Ausschluss-Checks zuerst
|
|
if ($request->is('api/*')) {
|
|
return false;
|
|
}
|
|
|
|
// Asset-Requests mit optimiertem Pattern
|
|
if ($request->isMethod('GET') && $this->isStaticAsset($request->path())) {
|
|
return false;
|
|
}
|
|
|
|
// Laravel-interne und Monitoring-Requests
|
|
$skipPaths = ['_debugbar', '_ignition', 'telescope', 'health', 'status', 'ping'];
|
|
foreach ($skipPaths as $path) {
|
|
if ($request->is($path) || $request->is($path . '/*')) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Optimierte Asset-Erkennung
|
|
*/
|
|
private function isStaticAsset(string $path): bool
|
|
{
|
|
// Datei-Endungen (Original-Logic)
|
|
if (preg_match('/\.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot|map|json)$/i', $path)) {
|
|
return true;
|
|
}
|
|
|
|
// Pfad-basierte Assets (häufige Laravel-Patterns)
|
|
$assetPaths = [
|
|
'css/',
|
|
'js/',
|
|
'fonts/',
|
|
'images/',
|
|
'img/',
|
|
'assets/',
|
|
'storage/',
|
|
'mix-manifest',
|
|
'favicon',
|
|
'robots.txt',
|
|
'sitemap',
|
|
'.well-known/',
|
|
'shop/product/image/'
|
|
];
|
|
|
|
foreach ($assetPaths as $assetPath) {
|
|
if (str_starts_with($path, $assetPath) || str_contains($path, $assetPath)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Anwendungs-Konfiguration setzen (ohne Session-Zugriff)
|
|
*/
|
|
private function configureApplication(DomainContext $context): void
|
|
{
|
|
// Session-Domain optimiert setzen
|
|
$sessionDomain = $this->getSessionDomain($context);
|
|
Config::set('session.domain', $sessionDomain);
|
|
|
|
// App-URL für URL-Generierung
|
|
if (!empty($context->host)) {
|
|
$protocol = $this->getProtocol();
|
|
Config::set('app.url', $protocol . $context->host);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Session-Domain intelligenter bestimmen
|
|
*/
|
|
private function getSessionDomain(DomainContext $context): string
|
|
{
|
|
$baseDomain = config('app.domain', 'mivita');
|
|
|
|
if ($context->type === 'shop') {
|
|
return '.' . $baseDomain . config('app.tld_shop', '.shop');
|
|
}
|
|
return '.' . $baseDomain . config('app.tld_care', '.care');
|
|
}
|
|
|
|
/**
|
|
* Protocol-Detection für app.url
|
|
*/
|
|
private function getProtocol(): string
|
|
{
|
|
return (config('app.env') === 'production' || request()->isSecure()) ? 'https://' : 'http://';
|
|
}
|
|
|
|
/**
|
|
* Context in Container und Request registrieren
|
|
*/
|
|
private function registerContext(DomainContext $context, Request $request): void
|
|
{
|
|
// Container-Binding (für Dependency Injection)
|
|
app()->instance(DomainContext::class, $context);
|
|
|
|
// Request-Attribut (für direkten Zugriff) - Fix: Einheitlicher Key für Interoperabilität
|
|
$request->attributes->set('domain_context', $context);
|
|
}
|
|
|
|
/**
|
|
* Minimal Debug-Logging (nur bei Bedarf)
|
|
*/
|
|
private function logDomainResolution(DomainContext $context, string $host): void
|
|
{
|
|
if (!config('subdomain.debug.log_domain_switches', false)) {
|
|
return;
|
|
}
|
|
|
|
Log::debug('Domain resolved', [
|
|
'host' => $host,
|
|
'type' => $context->type ?? 'unknown',
|
|
'subdomain' => $context->subdomain,
|
|
'user_shop' => $context->userShop?->slug,
|
|
'cache_hits' => self::$cacheHits,
|
|
'cache_size' => count(self::$domainCache)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* UserShop-Domains: PostRoute für korrekte URL-Generierung konfigurieren
|
|
*
|
|
* Das Problem: Util::getPostRoute() ist standardmäßig 'base.' was zu URLs wie
|
|
* base.card/add/... führt. Diese Routes sind auskommentiert → 404
|
|
*
|
|
* Lösung: Für UserShop-Domains PostRoute auf 'user/' setzen für URLs wie
|
|
* user/card/add/... die in den UserShop-Routes definiert sind.
|
|
*/
|
|
private function configurePostRoute(DomainContext $context): void
|
|
{
|
|
// Nur für UserShop-Domains PostRoute anpassen
|
|
if ($context->type !== 'user-shop') {
|
|
return;
|
|
}
|
|
|
|
// PostRoute für UserShop-URLs setzen
|
|
\App\Services\Util::setPostRoute('user/');
|
|
|
|
// Debug-Logging (optional)
|
|
if (config('subdomain.debug.log_domain_switches', false)) {
|
|
Log::debug('UserShop PostRoute configured', [
|
|
'user_shop_slug' => $context->userShop?->slug ?? 'unknown',
|
|
'post_route' => 'user/',
|
|
'impact' => 'Card URLs now generate user/card/add/... instead of base.card/add/...'
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* UserShop-Routing: subdomain aus Route-Parametern entfernen
|
|
*
|
|
* Wenn ein UserShop erkannt wird, muss die subdomain aus den Route-Parametern
|
|
* entfernt werden, damit sie nicht in die Controller-Parameter weitergegeben wird.
|
|
*
|
|
* Route-Beispiel: /{site}/{subsite?}/{product_slug?}
|
|
* Erwartet: site, subsite, product_slug - NICHT subdomain!
|
|
*/
|
|
private function cleanupRouteParameters(Request $request, DomainContext $context): void
|
|
{
|
|
// Nur bei UserShop-Domains Route-Parameter bereinigen
|
|
if ($context->type !== 'user-shop') {
|
|
return;
|
|
}
|
|
|
|
// Route muss existieren und subdomain Parameter haben
|
|
if (!$request->route() || !$request->route('subdomain')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// subdomain aus Route-Parametern entfernen
|
|
$request->route()->forgetParameter('subdomain');
|
|
|
|
// Optional: Debug-Logging in Development
|
|
if (config('subdomain.debug.log_domain_switches', false)) {
|
|
Log::debug('UserShop routing: subdomain parameter removed', [
|
|
'user_shop_slug' => $context->userShop?->slug ?? 'unknown',
|
|
'remaining_route_params' => $request->route()->parameters()
|
|
]);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// Fehler beim Route-Parameter-Cleanup nicht kritisch
|
|
Log::warning('Failed to cleanup route parameters', [
|
|
'user_shop_slug' => $context->userShop?->slug ?? 'unknown',
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cache-Statistiken für Debugging (optional)
|
|
*/
|
|
public static function getCacheStats(): array
|
|
{
|
|
return [
|
|
'hits' => self::$cacheHits,
|
|
'entries' => count(self::$domainCache),
|
|
'memory_kb' => round(memory_get_usage() / 1024, 2)
|
|
];
|
|
}
|
|
}
|