*/ private array $protectedPrefixes = ['www.', 'api.', 'checkout.', 'preview.']; public function __construct(private KasController $kas) { parent::__construct(); } /** * Execute the console command. */ public function handle(): int { $domain = trim((string) $this->option('domain'), '.'); $startId = max(1, (int) $this->option('start')); $limit = $this->option('limit') !== null ? (int) $this->option('limit') : null; $isForce = (bool) $this->option('force'); if ($domain === '') { $this->error('Die Option --domain darf nicht leer sein.'); return self::FAILURE; } if ($limit !== null && $limit < 1) { $this->error('Die Option --limit muss größer als 0 sein.'); return self::FAILURE; } try { $subdomains = $this->getKasSubdomains(); $shopHosts = $this->getPartnerShopHosts($domain, $startId); $candidates = $this->getDeletionCandidates($subdomains, $shopHosts, $domain); if ($limit !== null) { $candidates = $candidates->take($limit)->values(); } $this->printPreview($candidates, $isForce); if ($candidates->isEmpty()) { $this->info('Keine löschbaren Partnershop-Subdomains gefunden.'); return self::SUCCESS; } if (! $isForce) { $this->warn('Dry-Run: Es wurde nichts gelöscht. Mit --force werden die gefundenen Subdomains gelöscht.'); return self::SUCCESS; } return $this->deleteSubdomains($candidates); } catch (Exception $e) { $this->error('Fehler beim Löschen der Partnershop-Subdomains: '.$e->getMessage()); Log::error('KAS Partnershop-Subdomain-Löschung fehlgeschlagen', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); return self::FAILURE; } } private function getKasSubdomains(): Collection { $this->info('Rufe Subdomains von KAS ab...'); $subdomains = $this->kas->action('get_subdomains'); if (! is_iterable($subdomains)) { throw new Exception('KAS hat keine auswertbare Subdomain-Liste zurückgegeben: '.json_encode($subdomains)); } return collect($subdomains); } private function getPartnerShopHosts(string $domain, int $startId): Collection { $query = UserShop::query() ->where('id', '>=', $startId) ->orderBy('id'); if ($this->option('active-only')) { $query->where('active', true); } else { $query->withTrashed(); } return $query ->pluck('slug') ->filter() ->map(fn (string $slug): string => strtolower($slug.'.'.$domain)) ->unique() ->values(); } private function getDeletionCandidates(Collection $subdomains, Collection $shopHosts, string $domain): Collection { $shopHostLookup = array_flip($shopHosts->all()); $includePathMatches = (bool) $this->option('include-path-matches'); $partnerShopPath = (string) $this->option('path'); return $subdomains ->filter(function (array $subdomain) use ($domain, $includePathMatches, $partnerShopPath, $shopHostLookup): bool { $name = strtolower((string) ($subdomain['subdomain_name'] ?? '')); if ($name === '' || ! $this->belongsToDomain($name, $domain) || $this->isProtectedSubdomain($name)) { return false; } if (isset($shopHostLookup[$name])) { return true; } return $includePathMatches && trim((string) ($subdomain['subdomain_path'] ?? '')) === $partnerShopPath; }) ->map(fn (array $subdomain): array => [ 'subdomain_name' => (string) $subdomain['subdomain_name'], 'subdomain_path' => (string) ($subdomain['subdomain_path'] ?? ''), 'php_version' => (string) ($subdomain['php_version'] ?? ''), ]) ->unique('subdomain_name') ->sortBy('subdomain_name') ->values(); } private function belongsToDomain(string $subdomainName, string $domain): bool { return str_ends_with($subdomainName, '.'.strtolower($domain)); } private function isProtectedSubdomain(string $subdomainName): bool { foreach ($this->protectedPrefixes as $prefix) { if (str_starts_with($subdomainName, $prefix)) { return true; } } return false; } private function printPreview(Collection $candidates, bool $isForce): void { $this->newLine(); $this->info('Modus: '.($isForce ? 'Löschen' : 'Dry-Run')); $this->info('Gefundene Lösch-Kandidaten: '.$candidates->count()); if ($candidates->isEmpty()) { return; } $this->table( ['Subdomain', 'Pfad', 'PHP'], $candidates->take(25)->map(fn (array $candidate): array => [ $candidate['subdomain_name'], $candidate['subdomain_path'] ?: '-', $candidate['php_version'] ?: '-', ])->all() ); if ($candidates->count() > 25) { $this->line('... weitere '.($candidates->count() - 25).' Subdomains ausgeblendet.'); } } private function deleteSubdomains(Collection $candidates): int { $deleted = 0; $failed = 0; foreach ($candidates as $candidate) { $subdomainName = $candidate['subdomain_name']; $result = $this->kas->action('delete_subdomain', [ 'subdomain_name' => $subdomainName, ]); if ($this->isSuccessfulDeleteResult($result, $subdomainName)) { $deleted++; $this->info("Gelöscht: {$subdomainName}"); continue; } $failed++; $this->error("Fehler beim Löschen von {$subdomainName}: ".json_encode($result)); } $this->newLine(); $this->info("Gelöscht: {$deleted}"); $this->info("Fehler: {$failed}"); return $failed === 0 ? self::SUCCESS : self::FAILURE; } private function isSuccessfulDeleteResult(mixed $result, string $subdomainName): bool { return $result === true || $result === 'TRUE' || $result === $subdomainName; } }