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,193 @@
<?php
namespace App\Domain;
use App\Models\UserShop;
use Illuminate\Support\Arr;
/**
* DomainContext - Unveränderlicher Domain-Kontext
*
* Ersetzt das alte DomainContext mit besserer Typsicherheit
* und zusätzlichen Hilfsmethoden.
*/
final readonly class DomainContext
{
public function __construct(
public DomainType $type,
public string $host,
public ?string $subdomain,
public ?UserShop $userShop = null,
public ?string $path = null,
public array $metadata = []
) {}
/**
* Erstellt DomainContext aus Domain-Informationen
*/
public static function fromDomainInfo(array $domainInfo, ?UserShop $userShop = null): self
{
return new self(
type: DomainType::fromString(Arr::get($domainInfo, 'type', 'unknown')),
host: Arr::get($domainInfo, 'host', ''),
subdomain: Arr::get($domainInfo, 'subdomain'),
userShop: $userShop,
path: Arr::get($domainInfo, 'path'),
metadata: Arr::get($domainInfo, 'metadata', [])
);
}
/**
* Erstellt DomainContext für unbekannte Domains
*/
public static function unknown(string $host): self
{
return new self(
type: DomainType::UNKNOWN,
host: $host,
subdomain: null
);
}
/**
* Prüft, ob es sich um eine bekannte Domain handelt
*/
public function isKnown(): bool
{
return $this->type->isKnown();
}
/**
* Prüft, ob es sich um eine unbekannte Domain handelt
*/
public function isUnknown(): bool
{
return $this->type === DomainType::UNKNOWN;
}
/**
* Prüft, ob es sich um eine User-Shop-Domain handelt
*/
public function isUserShop(): bool
{
return $this->type->isUserShop();
}
/**
* Prüft, ob es sich um eine feste Subdomain handelt
*/
public function isFixedSubdomain(): bool
{
return $this->type->isFixedSubdomain();
}
/**
* Prüft, ob es sich um eine Haupt-Domain handelt
*/
public function isMainDomain(): bool
{
return $this->type->isMainDomain();
}
/**
* Prüft, ob Session-Daten beibehalten werden sollen
*/
public function shouldPreserveSession(): bool
{
return $this->type->shouldPreserveSession();
}
/**
* Gibt den UserShop-Slug zurück
*/
public function getUserShopSlug(): ?string
{
return $this->userShop?->slug;
}
/**
* Gibt die Route-Gruppe zurück
*/
public function getRouteGroup(): string
{
return $this->type->getRouteGroup();
}
/**
* Gibt die Session-Domain zurück
*/
public function getSessionDomain(string $baseDomain, string $shopTld, string $careTld): string
{
return $this->type->getSessionDomain($baseDomain, $shopTld, $careTld);
}
/**
* Prüft, ob der UserShop aktiv ist
*/
public function isUserShopActive(): bool
{
return $this->userShop && $this->userShop->active;
}
/**
* Prüft, ob der UserShop-Zahlung aktiv ist
*/
public function isUserShopPaymentActive(): bool
{
return $this->userShop &&
$this->userShop->user &&
$this->userShop->user->isActiveShop();
}
/**
* Gibt Domain-Informationen als Array zurück
*/
public function toArray(): array
{
return [
'type' => $this->type->value,
'host' => $this->host,
'subdomain' => $this->subdomain,
'user_shop_id' => $this->userShop?->id,
'user_shop_slug' => $this->getUserShopSlug(),
'path' => $this->path,
'metadata' => $this->metadata,
'is_known' => $this->isKnown(),
'is_user_shop' => $this->isUserShop(),
'should_preserve_session' => $this->shouldPreserveSession(),
'route_group' => $this->getRouteGroup()
];
}
/**
* Gibt eine lesbare String-Repräsentation zurück
*/
public function __toString(): string
{
$parts = [
"DomainType: {$this->type->value}",
"Host: {$this->host}"
];
if ($this->subdomain) {
$parts[] = "Subdomain: {$this->subdomain}";
}
if ($this->userShop) {
$parts[] = "UserShop: {$this->userShop->slug} (ID: {$this->userShop->id})";
}
return '[' . implode(', ', $parts) . ']';
}
/**
* Vergleicht zwei DomainContexts
*/
public function equals(self $other): bool
{
return $this->type === $other->type &&
$this->host === $other->host &&
$this->subdomain === $other->subdomain &&
$this->userShop?->id === $other->userShop?->id;
}
}

View file

@ -0,0 +1,144 @@
<?php
namespace App\Domain;
/**
* DomainType Enum - Definiert alle verfügbaren Domain-Typen
*
* Diese Enum ersetzt die String-basierten Domain-Typen und bietet
* typsichere Methoden für Domain-spezifische Logik.
*/
enum DomainType: string
{
case MAIN = 'main'; // mivita.care - Hauptdomain
case SHOP = 'shop'; // mivita.shop - Shop-Domain
case CRM = 'crm'; // my.mivita.care - CRM/Backend
case PORTAL = 'portal'; // in.mivita.care - Partner-Portal
case CHECKOUT = 'checkout'; // checkout.mivita.care - Checkout
case USER_SHOP = 'user-shop'; // {slug}.mivita.care - Berater-Shops
case UNKNOWN = 'unknown'; // Unbekannte Domain
/**
* Prüft, ob es sich um eine bekannte Domain handelt
*/
public function isKnown(): bool
{
return $this !== self::UNKNOWN;
}
/**
* Prüft, ob es sich um eine User-Shop-Domain handelt
*/
public function isUserShop(): bool
{
return $this === self::USER_SHOP;
}
/**
* Prüft, ob es sich um eine feste Subdomain handelt
*/
public function isFixedSubdomain(): bool
{
return in_array($this, [
self::CRM,
self::PORTAL,
self::CHECKOUT
]);
}
/**
* Prüft, ob es sich um eine Haupt-Domain handelt
*/
public function isMainDomain(): bool
{
return in_array($this, [
self::MAIN,
self::SHOP
]);
}
/**
* Prüft, ob Session-Daten beibehalten werden sollen
*/
public function shouldPreserveSession(): bool
{
return in_array($this, [
self::PORTAL, // in.* - Partner-Bereich
self::CHECKOUT, // checkout.* - Zahlung
self::USER_SHOP // Berater-Shops
]);
}
/**
* Gibt den erwarteten Subdomain-Prefix zurück
*/
public function getSubdomainPrefix(): ?string
{
return match ($this) {
self::CRM => 'my',
self::PORTAL => 'in',
self::CHECKOUT => 'checkout',
self::USER_SHOP => null, // Dynamisch
default => null
};
}
/**
* Gibt die Route-Gruppe für diesen Domain-Typ zurück
*/
public function getRouteGroup(): string
{
return match ($this) {
self::MAIN => 'main',
self::SHOP => 'shop',
self::CRM => 'crm',
self::PORTAL => 'portal',
self::CHECKOUT => 'checkout',
self::USER_SHOP => 'user-shop',
self::UNKNOWN => 'unknown'
};
}
/**
* Gibt die Session-Domain-Konfiguration zurück
*/
public function getSessionDomain(string $baseDomain, string $shopTld, string $careTld): string
{
return match ($this) {
self::SHOP => '.' . $baseDomain . $shopTld,
default => '.' . $baseDomain . $careTld
};
}
/**
* Erstellt DomainType aus String
*/
public static function fromString(string $type): self
{
return match ($type) {
'main' => self::MAIN,
'shop' => self::SHOP,
'crm' => self::CRM,
'portal' => self::PORTAL,
'checkout' => self::CHECKOUT,
'user-shop' => self::USER_SHOP,
'unknown' => self::UNKNOWN,
default => self::UNKNOWN
};
}
/**
* Gibt alle verfügbaren Domain-Typen zurück
*/
public static function all(): array
{
return [
self::MAIN,
self::SHOP,
self::CRM,
self::PORTAL,
self::CHECKOUT,
self::USER_SHOP
];
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace App\Events;
use App\Domain\DomainContext;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
/**
* DomainChangedEvent - Wird ausgelöst, wenn sich die Domain ändert
*
* Ermöglicht es anderen Teilen der Anwendung, auf Domain-Änderungen zu reagieren,
* z.B. für Analytics, Logging oder Session-Bereinigung.
*/
class DomainChangedEvent
{
use Dispatchable, SerializesModels;
public function __construct(
public DomainContext $currentContext,
public ?DomainContext $previousContext = null,
public ?string $sessionId = null,
public array $metadata = []
) {}
/**
* Prüft, ob es sich um einen UserShop-Wechsel handelt
*/
public function isUserShopChange(): bool
{
if (!$this->previousContext) {
return false;
}
return $this->currentContext->isUserShop() || $this->previousContext->isUserShop();
}
/**
* Prüft, ob es sich um einen Wechsel von Shop zu fester Domain handelt
*/
public function isShopToFixedDomainChange(): bool
{
if (!$this->previousContext) {
return false;
}
return $this->previousContext->isUserShop() &&
($this->currentContext->isFixedSubdomain() || $this->currentContext->isMainDomain());
}
/**
* Prüft, ob es sich um einen Wechsel von fester Domain zu Shop handelt
*/
public function isFixedDomainToShopChange(): bool
{
if (!$this->previousContext) {
return false;
}
return $this->currentContext->isUserShop() &&
($this->previousContext->isFixedSubdomain() || $this->previousContext->isMainDomain());
}
/**
* Gibt die Domain-Änderung als lesbare Beschreibung zurück
*/
public function getChangeDescription(): string
{
if (!$this->previousContext) {
return "Initial domain: {$this->currentContext->host}";
}
return "Domain changed: {$this->previousContext->host}{$this->currentContext->host}";
}
/**
* Gibt Event-Daten als Array zurück
*/
public function toArray(): array
{
return [
'current_context' => $this->currentContext->toArray(),
'previous_context' => $this->previousContext?->toArray(),
'session_id' => $this->sessionId,
'metadata' => $this->metadata,
'change_description' => $this->getChangeDescription(),
'is_user_shop_change' => $this->isUserShopChange(),
'is_shop_to_fixed_change' => $this->isShopToFixedDomainChange(),
'is_fixed_to_shop_change' => $this->isFixedDomainToShopChange(),
'timestamp' => now()->toISOString()
];
}
}

View file

@ -0,0 +1,192 @@
<?php
namespace App\Http\Middleware;
use App\Domain\DomainContext;
use App\Domain\DomainType;
use App\Events\DomainChangedEvent;
use App\Services\DomainService;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
/**
* DomainResolver Middleware - Optimierte Version
*
* Diese Middleware läuft NACH der Cookie-Verschlüsselung aber VOR der Session.
* Sie löst nur die Domain auf und setzt den Kontext, ohne Session-Daten zu manipulieren.
*
* Session-Management erfolgt in DomainSessionHandler (nach Session-Start).
*/
class DomainResolver
{
public function __construct(
private DomainService $domainService
) {}
/**
* Behandelt die Domain-Auflösung für jede Anfrage
*/
public function handle(Request $request, Closure $next)
{
// Überspringe für API und Asset-Requests
if ($this->shouldSkipRequest($request)) {
return $next($request);
}
$startTime = microtime(true);
$host = $request->getHost();
try {
// Domain auflösen
$context = $this->domainService->resolveDomain($host);
// Domain-Context in Request speichern (NICHT in Session!)
$request->merge(['domain_context' => $context]);
// Session-Domain für Cookies setzen
$this->configureSessionDomain($context);
// Domain-Changed Event auslösen (falls nötig)
$this->handleDomainChange($request, $context);
// Logging für bekannte Domains
if ($context->isKnown()) {
$this->logDomainResolution($context, $host, microtime(true) - $startTime);
}
// Unbekannte Domains umleiten
if ($context->isUnknown()) {
return $this->handleUnknownDomain($request, $context);
}
return $next($request);
} catch (\Throwable $e) {
Log::channel('domain')->error('Domain resolution failed', [
'host' => $host,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// Fallback: Unbekannte Domain
$context = DomainContext::unknown($host);
$request->merge(['domain_context' => $context]);
return $this->handleUnknownDomain($request, $context);
}
}
/**
* Prüft, ob diese Middleware für den Request ausgeführt werden soll
*/
private function shouldSkipRequest(Request $request): bool
{
// API-Requests überspringen
if ($request->is('api/*')) {
return true;
}
// Asset-Requests überspringen
if ($request->isMethod('GET') && $this->isAssetRequest($request)) {
return true;
}
// System-Requests überspringen
if ($this->isSystemRequest($request)) {
return true;
}
return false;
}
/**
* Prüft, ob es sich um einen Asset-Request handelt
*/
private function isAssetRequest(Request $request): bool
{
return preg_match('/\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf)$/i', $request->path());
}
/**
* Prüft, ob es sich um einen System-Request handelt
*/
private function isSystemRequest(Request $request): bool
{
$systemPaths = ['health', 'status', 'ping', '_debugbar/*'];
foreach ($systemPaths as $path) {
if ($request->is($path)) {
return true;
}
}
return false;
}
/**
* Konfiguriert die Session-Domain für Cookies
*/
private function configureSessionDomain(DomainContext $context): void
{
$sessionDomain = $context->getSessionDomain(
config('app.domain'),
config('app.tld_shop'),
config('app.tld_care')
);
Config::set('session.domain', $sessionDomain);
}
/**
* Behandelt Domain-Änderungen
*/
private function handleDomainChange(Request $request, DomainContext $context): void
{
$previousContext = session('domain_context');
if ($previousContext && !$context->equals($previousContext)) {
// Domain hat sich geändert
event(new DomainChangedEvent($context, $previousContext));
}
}
/**
* Behandelt unbekannte Domains
*/
private function handleUnknownDomain(Request $request, DomainContext $context)
{
if (config('app.debug')) {
Log::channel('domain')->warning('Unknown domain accessed', [
'host' => $request->getHost(),
'subdomain' => $context->subdomain,
'user_agent' => $request->userAgent(),
'ip' => $request->ip(),
'referer' => $request->header('referer'),
'path' => $request->getPathInfo()
]);
}
// Umleitung zur Haupt-Domain
$mainUrl = $this->domainService->buildUrl(DomainType::MAIN->value);
return redirect()->away($mainUrl, 301);
}
/**
* Logging für erfolgreiche Domain-Auflösung
*/
private function logDomainResolution(DomainContext $context, string $host, float $resolutionTime): void
{
if (!config('app.debug')) {
return;
}
Log::channel('domain')->debug('Domain resolved', [
'context' => $context->toArray(),
'host' => $host,
'resolution_time_ms' => round($resolutionTime * 1000, 2),
'user_shop_loaded' => $context->userShop !== null,
'cache_used' => true // Wird vom Service bestimmt
]);
}
}

View file

@ -0,0 +1,245 @@
<?php
namespace App\Http\Middleware;
use App\Domain\DomainContext;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
/**
* DomainSessionHandler Middleware - Session-Management für Domains
*
* Diese Middleware läuft NACH der Session-Initialisierung und übernimmt
* die Verwaltung von Session-Daten basierend auf dem Domain-Kontext.
*
* Löst das Problem der doppelten Session-Erstellung.
*/
class DomainSessionHandler
{
/**
* Behandelt Session-Management für Domain-Anfragen
*/
public function handle(Request $request, Closure $next)
{
$response = $next($request);
// Domain-Context aus Request holen
$context = $request->get('domain_context');
if (!$context instanceof DomainContext) {
return $response;
}
try {
// Session für Domain aktualisieren
$this->updateSessionForDomain($context, $request);
// Domain-Context in Session speichern für nächste Anfrage
$this->storeDomainContextInSession($context);
} catch (\Throwable $e) {
Log::channel('domain')->error('Session update failed', [
'domain_context' => $context->toArray(),
'error' => $e->getMessage(),
'session_id' => Session::getId()
]);
}
return $response;
}
/**
* Aktualisiert Session-Daten basierend auf dem Domain-Kontext
*/
private function updateSessionForDomain(DomainContext $context, Request $request): void
{
// UserShop-Verwaltung
$this->handleUserShopSession($context);
// Domain-spezifische Session-Daten
$this->handleDomainSpecificSession($context);
// Session-Domain sicherstellen
$this->ensureSessionDomain($context);
// Session speichern
Session::save();
// Debug-Logging
$this->logSessionUpdate($context);
}
/**
* Behandelt UserShop-Session-Daten
*/
private function handleUserShopSession(DomainContext $context): void
{
if ($context->isUserShop() && $context->userShop) {
// Validierung für UserShop
if (!$this->isValidUserShopContext($context)) {
$this->handleInvalidUserShop($context);
return;
}
// UserShop in Session setzen
Session::put('user_shop', $context->userShop);
Session::put('user_shop_domain', $context->host);
} elseif ($context->shouldPreserveSession()) {
// Für Domains die Session erhalten sollen:
// Nichts ändern, bestehende UserShop-Daten beibehalten
} else {
// Für andere Domains: UserShop-Daten entfernen
$this->clearUserShopSession();
}
}
/**
* Behandelt domain-spezifische Session-Daten
*/
private function handleDomainSpecificSession(DomainContext $context): void
{
match ($context->type) {
\App\Domain\DomainType::MAIN => $this->handleMainDomainSession(),
\App\Domain\DomainType::SHOP => $this->handleShopDomainSession(),
\App\Domain\DomainType::CRM => $this->handleCrmDomainSession(),
\App\Domain\DomainType::PORTAL => $this->handlePortalDomainSession(),
\App\Domain\DomainType::CHECKOUT => $this->handleCheckoutDomainSession(),
default => null
};
}
/**
* Stellt sicher, dass die Session-Domain korrekt gesetzt ist
*/
private function ensureSessionDomain(DomainContext $context): void
{
$expectedDomain = $context->getSessionDomain(
config('app.domain'),
config('app.tld_shop'),
config('app.tld_care')
);
$currentDomain = Config::get('session.domain');
if ($currentDomain !== $expectedDomain) {
Config::set('session.domain', $expectedDomain);
Log::channel('domain')->debug('Session domain updated', [
'from' => $currentDomain,
'to' => $expectedDomain,
'domain_type' => $context->type->value
]);
}
}
/**
* Speichert Domain-Context in Session für nächste Anfrage
*/
private function storeDomainContextInSession(DomainContext $context): void
{
Session::put('domain_context', $context);
Session::put('domain_context_updated', now()->toISOString());
}
/**
* Validiert UserShop-Kontext
*/
private function isValidUserShopContext(DomainContext $context): bool
{
return $context->isUserShopActive() && $context->isUserShopPaymentActive();
}
/**
* Behandelt ungültige UserShop-Kontexte
*/
private function handleInvalidUserShop(DomainContext $context): void
{
Log::channel('domain')->warning('Invalid UserShop context', [
'shop_id' => $context->userShop?->id,
'shop_active' => $context->isUserShopActive(),
'payment_active' => $context->isUserShopPaymentActive(),
'subdomain' => $context->subdomain
]);
// UserShop aus Session entfernen
$this->clearUserShopSession();
// Hier könnte eine Umleitung erfolgen, aber das übernimmt der Controller
}
/**
* Entfernt UserShop-Daten aus Session
*/
private function clearUserShopSession(): void
{
Session::forget('user_shop');
Session::forget('user_shop_domain');
}
/**
* Handler für Haupt-Domain
*/
private function handleMainDomainSession(): void
{
// Haupt-Domain: Session-Daten beibehalten aber UserShop entfernen
$this->clearUserShopSession();
}
/**
* Handler für Shop-Domain
*/
private function handleShopDomainSession(): void
{
// Shop-Domain verwendet Fallback-UserShop
// Session-Daten werden vom DomainService gesetzt
}
/**
* Handler für CRM-Domain
*/
private function handleCrmDomainSession(): void
{
// CRM: Keine speziellen Session-Änderungen
// UserShop-Daten werden beibehalten für "Zurück zum Shop" Funktionalität
}
/**
* Handler für Portal-Domain
*/
private function handlePortalDomainSession(): void
{
// Portal: UserShop-Daten beibehalten für Shop-Wechsel
// Zusätzliche Portal-spezifische Daten können hier gesetzt werden
}
/**
* Handler für Checkout-Domain
*/
private function handleCheckoutDomainSession(): void
{
// Checkout: Alle Session-Daten beibehalten
// Warenkorb und UserShop müssen verfügbar bleiben
}
/**
* Debug-Logging für Session-Updates
*/
private function logSessionUpdate(DomainContext $context): void
{
if (!config('app.debug')) {
return;
}
Log::channel('domain')->debug('DomainSessionHandler: Session updated', [
'domain_type' => $context->type->value,
'host' => $context->host,
'user_shop_id' => $context->userShop?->id,
'session_id' => Session::getId(),
'session_user_shop_id' => session('user_shop')?->id,
'session_domain' => Config::get('session.domain'),
'has_user_shop' => Session::has('user_shop')
]);
}
}

View file

@ -0,0 +1,270 @@
<?php
namespace App\Providers;
use App\Domain\DomainContext;
use App\Domain\DomainType;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
/**
* RouteServiceProvider - Optimierte Version
*
* Lädt Routen basierend auf dem Domain-Kontext mit besserer Performance
* und saubererer Logik.
*/
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to the "home" route for your application.
*/
public const HOME = '/';
/**
* The controller namespace for the application.
*/
protected $namespace = 'App\\Http\\Controllers';
/**
* Define your route model bindings, pattern filters, etc.
*/
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
// API-Routen werden immer global geladen
$this->loadApiRoutes();
// Web-Routen werden domain-bewusst geladen
Route::middleware('web')
->namespace($this->namespace)
->group(function () {
$this->loadDomainAwareRoutes();
});
});
}
/**
* Lädt API-Routen
*/
protected function loadApiRoutes(): void
{
Route::domain('api.' . config('app.domain') . config('app.tld_care'))
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
/**
* Lädt Routen basierend auf dem Domain-Kontext
*/
protected function loadDomainAwareRoutes(): void
{
/** @var DomainContext $context */
$context = app(DomainContext::class);
// Gemeinsame Routen laden (auf allen Domains verfügbar)
$this->loadSharedRoutes();
// Domain-spezifische Routen laden
$this->loadDomainSpecificRoutes($context);
// Debug-Logging
$this->logRouteLoading($context);
}
/**
* Lädt domain-spezifische Routen
*/
protected function loadDomainSpecificRoutes(DomainContext $context): void
{
match ($context->type) {
DomainType::MAIN => $this->loadDomainRoutes('main', 'main.php'),
DomainType::SHOP => $this->loadDomainRoutes('shop', 'shop.php'),
DomainType::CRM => $this->loadDomainRoutes('crm', 'crm.php'),
DomainType::PORTAL => $this->loadDomainRoutes('portal', 'portal.php'),
DomainType::CHECKOUT => $this->loadDomainRoutes('checkout', 'checkout.php'),
DomainType::USER_SHOP => $this->loadUserShopRoutes($context),
DomainType::UNKNOWN => $this->loadUnknownDomainRoutes(),
default => $this->loadAllDomainRoutesForCaching(),
};
}
/**
* Lädt Routen für User-Shops
*/
protected function loadUserShopRoutes(DomainContext $context): void
{
// Basis-Shop-Routen
$this->loadDomainRoutes('user-shop', 'user-shop.php');
// Portal-Routen für "Zurück zum Shop" Funktionalität
$this->loadDomainRoutes('portal', 'portal.php');
}
/**
* Lädt Routen für unbekannte Domains
*/
protected function loadUnknownDomainRoutes(): void
{
// Bei unbekannten Domains: Nur grundlegende Routen laden
// Die DomainResolver-Middleware wird die Umleitung übernehmen
$this->loadSharedRoutes();
}
/**
* Lädt eine spezifische Routendatei für eine Domain
*/
protected function loadDomainRoutes(string $domainType, string $fileName): void
{
$domain = config("domains.domains.{$domainType}.host");
if (!$domain) {
\Log::channel('domain')->warning("Domain configuration missing for type: {$domainType}");
return;
}
Route::domain($domain)
->group(base_path('routes/domains/' . $fileName));
}
/**
* Lädt alle domainspezifischen Routen für Route-Caching
*/
protected function loadAllDomainRoutesForCaching(): void
{
if (app()->routesAreCached()) {
return;
}
$domainTypes = ['main', 'shop', 'crm', 'portal', 'checkout', 'user-shop'];
$routeFiles = [
'main' => 'main.php',
'shop' => 'shop.php',
'crm' => 'crm.php',
'portal' => 'portal.php',
'checkout' => 'checkout.php',
'user-shop' => 'user-shop.php'
];
foreach ($domainTypes as $type) {
$file = $routeFiles[$type] ?? null;
if ($file) {
$this->loadDomainRoutes($type, $file);
}
}
}
/**
* Lädt Routen, die auf allen Domains verfügbar sein sollen
*/
protected function loadSharedRoutes(): void
{
Route::group([], base_path('routes/shared/common.php'));
}
/**
* Logging für Route-Loading
*/
protected function logRouteLoading(DomainContext $context): void
{
if (!config('app.debug')) {
return;
}
\Log::channel('domain')->info('RouteServiceProvider: Routes loaded', [
'domain_type' => $context->type->value,
'host' => $context->host,
'subdomain' => $context->subdomain,
'user_shop_id' => $context->userShop?->id,
'route_group' => $context->getRouteGroup(),
'routes_cached' => app()->routesAreCached(),
'loaded_routes_count' => count(app('router')->getRoutes())
]);
}
/**
* Konfiguriert die Rate-Limiter für die Anwendung
*/
protected function configureRateLimiting(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Domain-spezifische Rate-Limiting
$this->configureDomainRateLimiting();
}
/**
* Konfiguriert domain-spezifisches Rate-Limiting
*/
protected function configureDomainRateLimiting(): void
{
// User-Shop Rate-Limiting (höher für Shops)
RateLimiter::for('user-shop', function (Request $request) {
return Limit::perMinute(120)->by($request->user()?->id ?: $request->ip());
});
// Checkout Rate-Limiting (strenger für Zahlungen)
RateLimiter::for('checkout', function (Request $request) {
return [
Limit::perMinute(30)->by($request->user()?->id ?: $request->ip()),
Limit::perMinute(10)->by($request->ip())
];
});
// Portal Rate-Limiting (moderater)
RateLimiter::for('portal', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
/**
* Gibt verfügbare Domain-Typen für Debugging zurück
*/
public function getAvailableDomainTypes(): array
{
return array_map(
fn(DomainType $type) => [
'type' => $type->value,
'name' => $type->name,
'is_known' => $type->isKnown(),
'route_group' => $type->getRouteGroup()
],
DomainType::all()
);
}
/**
* Validiert Route-Konfiguration
*/
public function validateRouteConfiguration(): array
{
$errors = [];
$domainTypes = ['main', 'shop', 'crm', 'portal', 'checkout', 'user-shop'];
foreach ($domainTypes as $type) {
$routeFile = base_path('routes/domains/' . $type . '.php');
if (!file_exists($routeFile)) {
$errors[] = "Route file missing: {$routeFile}";
}
$domain = config("domains.domains.{$type}.host");
if (!$domain) {
$errors[] = "Domain configuration missing for type: {$type}";
}
}
$sharedRoutes = base_path('routes/shared/common.php');
if (!file_exists($sharedRoutes)) {
$errors[] = "Shared routes file missing: {$sharedRoutes}";
}
return $errors;
}
}

View file

@ -0,0 +1,317 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
/**
* DomainCacheManager - Optimierte Cache-Verwaltung für Domains
*
* Zentralisiert die Cache-Logik und bietet intelligente Cache-Invalidierung
* basierend auf Domain-Änderungen und Time-to-Live Strategien.
*/
class DomainCacheManager
{
private const CACHE_TAG_DOMAIN = 'domain_resolution';
private const CACHE_TAG_USER_SHOP = 'user_shops';
private const CACHE_TAG_SESSION = 'domain_sessions';
// Cache-TTL für verschiedene Cache-Typen
private const TTL_DOMAIN_RESOLUTION = 3600; // 1 Stunde
private const TTL_USER_SHOP = 1800; // 30 Minuten
private const TTL_SESSION_DATA = 7200; // 2 Stunden
/**
* Cache für Domain-Resolution setzen
*/
public function setDomainResolution(string $host, mixed $data, ?int $ttl = null): void
{
$key = $this->getDomainResolutionKey($host);
$ttl = $ttl ?? self::TTL_DOMAIN_RESOLUTION;
Cache::tags([self::CACHE_TAG_DOMAIN])
->put($key, $data, $ttl);
$this->logCacheOperation('set', $key, $ttl);
}
/**
* Cache für Domain-Resolution abrufen
*/
public function getDomainResolution(string $host): mixed
{
$key = $this->getDomainResolutionKey($host);
$data = Cache::tags([self::CACHE_TAG_DOMAIN])
->get($key);
$this->logCacheOperation('get', $key, hit: $data !== null);
return $data;
}
/**
* Cache für UserShop setzen
*/
public function setUserShop(string $slug, mixed $data, ?int $ttl = null): void
{
$key = $this->getUserShopKey($slug);
$ttl = $ttl ?? self::TTL_USER_SHOP;
Cache::tags([self::CACHE_TAG_USER_SHOP])
->put($key, $data, $ttl);
$this->logCacheOperation('set', $key, $ttl);
}
/**
* Cache für UserShop abrufen
*/
public function getUserShop(string $slug): mixed
{
$key = $this->getUserShopKey($slug);
$data = Cache::tags([self::CACHE_TAG_USER_SHOP])
->get($key);
$this->logCacheOperation('get', $key, hit: $data !== null);
return $data;
}
/**
* Session-Daten cachen
*/
public function setSessionData(string $sessionId, string $domainType, mixed $data): void
{
$key = $this->getSessionDataKey($sessionId, $domainType);
Cache::tags([self::CACHE_TAG_SESSION])
->put($key, $data, self::TTL_SESSION_DATA);
$this->logCacheOperation('set', $key, self::TTL_SESSION_DATA);
}
/**
* Session-Daten abrufen
*/
public function getSessionData(string $sessionId, string $domainType): mixed
{
$key = $this->getSessionDataKey($sessionId, $domainType);
$data = Cache::tags([self::CACHE_TAG_SESSION])
->get($key);
$this->logCacheOperation('get', $key, hit: $data !== null);
return $data;
}
/**
* Einzelne Domain-Resolution Cache invalidieren
*/
public function invalidateDomainResolution(string $host): void
{
$key = $this->getDomainResolutionKey($host);
Cache::tags([self::CACHE_TAG_DOMAIN])->forget($key);
$this->logCacheOperation('invalidate', $key);
}
/**
* UserShop Cache invalidieren
*/
public function invalidateUserShop(string $slug): void
{
$validityKey = $this->getUserShopValidityKey($slug);
$dataKey = $this->getUserShopKey($slug);
Cache::tags([self::CACHE_TAG_USER_SHOP])->forget($validityKey);
Cache::tags([self::CACHE_TAG_USER_SHOP])->forget($dataKey);
$this->logCacheOperation('invalidate', [$validityKey, $dataKey]);
}
/**
* Alle Domain-Resolution Caches invalidieren
*/
public function invalidateAllDomainResolutions(): void
{
Cache::tags([self::CACHE_TAG_DOMAIN])->flush();
Log::channel('domain')->info('DomainCacheManager: All domain resolution caches invalidated');
}
/**
* Alle UserShop Caches invalidieren
*/
public function invalidateAllUserShops(): void
{
Cache::tags([self::CACHE_TAG_USER_SHOP])->flush();
Log::channel('domain')->info('DomainCacheManager: All user shop caches invalidated');
}
/**
* Alle Session Caches invalidieren
*/
public function invalidateAllSessions(): void
{
Cache::tags([self::CACHE_TAG_SESSION])->flush();
Log::channel('domain')->info('DomainCacheManager: All session caches invalidated');
}
/**
* Cache-Statistiken abrufen
*/
public function getCacheStatistics(): array
{
return [
'domain_resolution' => [
'tag' => self::CACHE_TAG_DOMAIN,
'ttl' => self::TTL_DOMAIN_RESOLUTION,
'estimated_count' => $this->getEstimatedCacheCount(self::CACHE_TAG_DOMAIN)
],
'user_shops' => [
'tag' => self::CACHE_TAG_USER_SHOP,
'ttl' => self::TTL_USER_SHOP,
'estimated_count' => $this->getEstimatedCacheCount(self::CACHE_TAG_USER_SHOP)
],
'sessions' => [
'tag' => self::CACHE_TAG_SESSION,
'ttl' => self::TTL_SESSION_DATA,
'estimated_count' => $this->getEstimatedCacheCount(self::CACHE_TAG_SESSION)
]
];
}
/**
* Cache-Keys generieren
*/
private function getDomainResolutionKey(string $host): string
{
return 'domain_resolution_' . md5(strtolower(trim($host)));
}
private function getUserShopKey(string $slug): string
{
return 'user_shop_' . md5(strtolower(trim($slug)));
}
private function getUserShopValidityKey(string $slug): string
{
return 'user_shop_valid_' . md5(strtolower(trim($slug)));
}
private function getSessionDataKey(string $sessionId, string $domainType): string
{
return 'session_' . md5($sessionId . '_' . $domainType);
}
/**
* Geschätzte Anzahl Cache-Einträge (Laravel bietet keine direkte API dafür)
*/
private function getEstimatedCacheCount(string $tag): int
{
// In Laravel ist es schwierig, die genaue Anzahl zu ermitteln
// Hier könnte eine benutzerdefinierte Cache-Store Implementierung helfen
return 0; // Placeholder
}
/**
* Cache-Operation loggen
*/
private function logCacheOperation(string $operation, string|array $key, ?int $ttl = null, ?bool $hit = null): void
{
if (!config('app.debug')) {
return;
}
$context = [
'operation' => $operation,
'key' => is_array($key) ? implode(', ', $key) : $key,
'timestamp' => now()->toISOString()
];
if ($ttl !== null) {
$context['ttl_seconds'] = $ttl;
}
if ($hit !== null) {
$context['cache_hit'] = $hit;
}
Log::channel('domain')->debug('DomainCacheManager: Cache operation', $context);
}
/**
* Cache vorwärmen (für häufig verwendete Domains)
*/
public function warmupCache(array $commonHosts = [], array $commonSlugs = []): void
{
$domainService = app(DomainService::class);
// Häufige Hosts cachen
foreach ($commonHosts as $host) {
try {
$context = $domainService->resolveDomain($host);
$this->setDomainResolution($host, $context);
} catch (\Throwable $e) {
Log::channel('domain')->warning("Failed to warmup cache for host: {$host}", [
'error' => $e->getMessage()
]);
}
}
// Häufige UserShops cachen
foreach ($commonSlugs as $slug) {
try {
$userShop = $domainService->getUserShop($slug);
if ($userShop) {
$this->setUserShop($slug, $userShop);
}
} catch (\Throwable $e) {
Log::channel('domain')->warning("Failed to warmup cache for slug: {$slug}", [
'error' => $e->getMessage()
]);
}
}
Log::channel('domain')->info('DomainCacheManager: Cache warmup completed', [
'hosts_count' => count($commonHosts),
'slugs_count' => count($commonSlugs)
]);
}
/**
* Cache-Health-Check
*/
public function performHealthCheck(): array
{
$results = [];
try {
// Test Domain-Resolution Cache
$testHost = 'test.' . config('app.domain') . config('app.tld_care');
$this->setDomainResolution($testHost, 'test_data', 60);
$retrieved = $this->getDomainResolution($testHost);
$results['domain_resolution'] = $retrieved === 'test_data';
// Test UserShop Cache
$testSlug = 'test-shop';
$this->setUserShop($testSlug, ['id' => 1, 'name' => 'Test Shop'], 60);
$retrieved = $this->getUserShop($testSlug);
$results['user_shop'] = is_array($retrieved) && $retrieved['id'] === 1;
$results['overall_health'] = $results['domain_resolution'] && $results['user_shop'];
} catch (\Throwable $e) {
$results['overall_health'] = false;
$results['error'] = $e->getMessage();
}
return $results;
}
}

View file

@ -0,0 +1,404 @@
<?php
namespace App\Services;
use App\Domain\DomainContext;
use App\Domain\DomainType;
use App\Models\UserShop;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
/**
* DomainService - Optimierte Version
*
* Zentraler Service für Domain-Management mit verbesserter Performance
* und besserer Cache-Strategie.
*/
class DomainService
{
private const CACHE_TTL = 3600; // 1 Stunde
private const CACHE_TAG_DOMAIN = 'domain_resolution';
private const CACHE_TAG_USER_SHOP = 'user_shops';
private array $domainConfig;
private array $reservedSubdomains;
public function __construct(?array $domainConfig = null)
{
$this->domainConfig = $domainConfig ?? config('domains');
$this->reservedSubdomains = $this->domainConfig['reserved_subdomains'] ?? [
'my',
'in',
'checkout',
'www',
'api',
'mail'
];
}
/**
* Hauptmethode: Domain auflösen
*/
public function resolveDomain(string $host): DomainContext
{
// Host normalisieren
$normalizedHost = $this->normalizeHost($host);
// Cache-Key für Domain-Resolution
$cacheKey = "domain_resolution_{$normalizedHost}";
return Cache::tags([self::CACHE_TAG_DOMAIN])
->remember($cacheKey, self::CACHE_TTL, function () use ($normalizedHost) {
return $this->resolveDomainUncached($normalizedHost);
});
}
/**
* Ungecachte Domain-Auflösung
*/
private function resolveDomainUncached(string $host): DomainContext
{
// Domain-Info parsen
$domainInfo = $this->parseDomain($host);
// Domain-Type bestimmen
$domainType = $this->determineDomainType($domainInfo);
// UserShop laden (nur bei Bedarf)
$userShop = $this->loadUserShopIfNeeded($domainType, $domainInfo);
// DomainContext erstellen
return new DomainContext(
type: $domainType,
host: $host,
subdomain: $domainInfo['subdomain'],
userShop: $userShop,
metadata: $domainInfo
);
}
/**
* Host normalisieren (lowercase, trim)
*/
private function normalizeHost(string $host): string
{
return strtolower(trim($host));
}
/**
* Domain-Informationen parsen
*/
public function parseDomain(string $host): array
{
$parts = explode('.', $host);
if (count($parts) < 2) {
return $this->createInvalidDomainInfo($host);
}
// TLD und Domain extrahieren
$tld = '.' . end($parts);
$domain = $parts[count($parts) - 2];
// Subdomain extrahieren
$subdomain = null;
if (count($parts) > 2) {
$subdomain = $parts[0];
}
return [
'type' => 'unknown', // Wird später bestimmt
'domain' => $domain,
'subdomain' => $subdomain,
'tld' => $tld,
'host' => $host,
'parts' => $parts,
'default_user_shop' => $this->domainConfig['domains']['shop']['default_user_shop'] ?? null
];
}
/**
* Domain-Type bestimmen
*/
private function determineDomainType(array $domainInfo): DomainType
{
$host = $domainInfo['host'];
$subdomain = $domainInfo['subdomain'];
// Gegen konfigurierte Domains prüfen
foreach ($this->domainConfig['domains'] as $type => $config) {
if (isset($config['host'])) {
// User-Shop Pattern prüfen
if ($type === 'user-shop' && $this->matchesUserShopPattern($host, $config['host'])) {
return DomainType::USER_SHOP;
}
// Exakte Übereinstimmung prüfen
if ($host === $config['host']) {
return DomainType::fromString($type);
}
}
}
// Subdomain-basierte Erkennung
if ($subdomain) {
$subdomainType = $this->getSubdomainType($subdomain);
if ($subdomainType !== DomainType::UNKNOWN) {
return $subdomainType;
}
}
return DomainType::UNKNOWN;
}
/**
* Prüft, ob Host dem User-Shop-Pattern entspricht
*/
private function matchesUserShopPattern(string $host, string $pattern): bool
{
$regex = str_replace('{subdomain}', '([a-z0-9-]+)', $pattern);
return preg_match("/^{$regex}$/", $host) === 1;
}
/**
* Subdomain-Type bestimmen
*/
public function getSubdomainType(string $subdomain): DomainType
{
// Reservierte Subdomains prüfen
if (in_array($subdomain, $this->reservedSubdomains)) {
return match ($subdomain) {
'my' => DomainType::CRM,
'in' => DomainType::PORTAL,
'checkout' => DomainType::CHECKOUT,
default => DomainType::UNKNOWN
};
}
// UserShop-Validierung
if ($this->isValidUserShopSlug($subdomain)) {
return DomainType::USER_SHOP;
}
return DomainType::UNKNOWN;
}
/**
* Validiert UserShop-Slug
*/
public function isValidUserShopSlug(string $slug): bool
{
// Grundlegende Validierung
if (!preg_match('/^[a-z0-9-]+$/', $slug) || strlen($slug) < 3) {
return false;
}
// DB-Validierung mit Cache
return $this->isValidUserShop($slug);
}
/**
* Prüft, ob Slug einem gültigen UserShop entspricht
*/
public function isValidUserShop(string $slug): bool
{
$cacheKey = "user_shop_valid_{$slug}";
return Cache::tags([self::CACHE_TAG_USER_SHOP])
->remember($cacheKey, self::CACHE_TTL, function () use ($slug) {
return UserShop::where('slug', $slug)
->where('active', true)
->whereHas('user', function ($query) {
$query->whereNotNull('payment_shop')
->where('payment_shop', '>', now());
})
->exists();
});
}
/**
* UserShop laden (nur bei Bedarf)
*/
private function loadUserShopIfNeeded(DomainType $type, array $domainInfo): ?UserShop
{
if (!$type->isUserShop()) {
return null;
}
$subdomain = $domainInfo['subdomain'];
if (!$subdomain) {
return null;
}
return $this->getUserShop($subdomain);
}
/**
* UserShop mit optimiertem Caching laden
*/
public function getUserShop(string $slug): ?UserShop
{
$cacheKey = "user_shop_{$slug}";
return Cache::tags([self::CACHE_TAG_USER_SHOP])
->remember($cacheKey, self::CACHE_TTL, function () use ($slug) {
return UserShop::where('slug', $slug)
->where('active', true)
->whereHas('user', function ($query) {
$query->whereNotNull('payment_shop')
->where('payment_shop', '>', now());
})
->with('user')
->first();
});
}
/**
* URL für Domain-Typ bauen
*/
public function buildUrl(string $type, ?string $path = null, ?string $slug = null): string
{
$protocol = $this->domainConfig['protocol'] ?? 'https://';
$domainConfig = $this->domainConfig['domains'][$type] ?? null;
if (!$domainConfig) {
throw new \InvalidArgumentException("Unknown domain type: {$type}");
}
$host = $domainConfig['host'];
// User-Shop Platzhalter ersetzen
if ($type === 'user-shop') {
if (!$slug) {
throw new \InvalidArgumentException('Slug required for user-shop URLs');
}
$host = str_replace('{subdomain}', $slug, $host);
}
$url = $protocol . $host;
if ($path) {
$url .= '/' . ltrim($path, '/');
}
return $url;
}
/**
* Cache für UserShop invalidieren
*/
public function clearUserShopCache(string $slug): void
{
Cache::tags([self::CACHE_TAG_USER_SHOP])->forget("user_shop_valid_{$slug}");
Cache::tags([self::CACHE_TAG_USER_SHOP])->forget("user_shop_{$slug}");
}
/**
* Domain-Resolution Cache invalidieren
*/
public function clearDomainCache(string $host): void
{
$normalizedHost = $this->normalizeHost($host);
Cache::tags([self::CACHE_TAG_DOMAIN])->forget("domain_resolution_{$normalizedHost}");
}
/**
* Alle UserShop-Caches invalidieren
*/
public function clearAllUserShopCaches(): void
{
Cache::tags([self::CACHE_TAG_USER_SHOP])->flush();
}
/**
* Alle Domain-Caches invalidieren
*/
public function clearAllDomainCaches(): void
{
Cache::tags([self::CACHE_TAG_DOMAIN])->flush();
}
/**
* Standard-UserShop für Fallback laden
*/
public function getDefaultUserShop(): ?UserShop
{
$defaultSlug = $this->domainConfig['domains']['shop']['default_user_shop'] ?? 'aloevera';
return $this->getUserShop($defaultSlug);
}
/**
* Domain-Konfiguration validieren
*/
public function validateConfiguration(): array
{
$errors = [];
$requiredDomains = ['main', 'shop', 'crm', 'portal', 'checkout', 'user-shop'];
foreach ($requiredDomains as $domain) {
if (empty($this->domainConfig['domains'][$domain]['host'])) {
$errors[] = "Domain '{$domain}' not configured";
}
}
if (empty($this->domainConfig['protocol'])) {
$errors[] = 'Protocol not configured';
}
if (empty($this->domainConfig['reserved_subdomains'])) {
$errors[] = 'Reserved subdomains not configured';
}
$defaultShop = $this->domainConfig['domains']['shop']['default_user_shop'] ?? null;
if (!$defaultShop) {
$errors[] = 'Default user shop not configured for shop domain';
}
return $errors;
}
/**
* Prüft, ob Konfiguration gültig ist
*/
public function isConfigurationValid(): bool
{
return empty($this->validateConfiguration());
}
/**
* Erstellt Domain-Info für ungültige Hosts
*/
private function createInvalidDomainInfo(string $host): array
{
return [
'type' => 'invalid',
'domain' => $host,
'subdomain' => null,
'tld' => null,
'host' => $host,
'parts' => explode('.', $host)
];
}
/**
* Debug-Informationen für Domain-Resolution
*/
public function getDebugInfo(string $host): array
{
$domainInfo = $this->parseDomain($host);
$domainType = $this->determineDomainType($domainInfo);
return [
'host' => $host,
'normalized_host' => $this->normalizeHost($host),
'domain_info' => $domainInfo,
'determined_type' => $domainType->value,
'is_known' => $domainType->isKnown(),
'cache_key' => "domain_resolution_{$this->normalizeHost($host)}",
'configuration_valid' => $this->isConfigurationValid(),
'reserved_subdomains' => $this->reservedSubdomains
];
}
}