update 20.10.2025
This commit is contained in:
parent
8c11130b5d
commit
a939cd51ef
616 changed files with 84821 additions and 4121 deletions
193
dev/subdomain-optimization-grok/src/Domain/DomainContext.php
Normal file
193
dev/subdomain-optimization-grok/src/Domain/DomainContext.php
Normal 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;
|
||||
}
|
||||
}
|
||||
144
dev/subdomain-optimization-grok/src/Domain/DomainType.php
Normal file
144
dev/subdomain-optimization-grok/src/Domain/DomainType.php
Normal 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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
404
dev/subdomain-optimization-grok/src/Services/DomainService.php
Normal file
404
dev/subdomain-optimization-grok/src/Services/DomainService.php
Normal 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
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue