option('source'); $step = $this->option('step'); $isDryRun = (bool) $this->option('dry-run'); $isForce = (bool) $this->option('force'); $since = null; if (filled($this->option('since'))) { try { $since = Carbon::parse((string) $this->option('since'), config('app.timezone')); } catch (\Throwable) { $this->error('Ungültiger --since-Wert: '.$this->option('since')); return self::FAILURE; } } $portals = $source === 'all' ? self::PORTALS : [$source]; $steps = $step === 'all' ? self::STEPS : [$step]; if ($since) { $this->info('Delta-Modus: nur Legacy-Zeilen mit created_at/updated_at ab '.$since->format('d.m.Y H:i:s').'.'); if (! $isForce) { $this->warn('Hinweis: ohne --force werden im Zeitfenster GEÄNDERTE, bereits importierte Datensätze übersprungen — nur Neue kommen dazu.'); } } if (! $isDryRun && ! $this->option('force')) { $this->warn('WICHTIG: Dieser Command schreibt Daten in die Produktions-DB.'); $this->line(' Portale: '.implode(', ', $portals)); $this->line(' Schritte: '.implode(', ', $steps)); $this->newLine(); if (! $this->confirm('Import starten?')) { $this->info('Abgebrochen.'); return self::SUCCESS; } } if ($isDryRun) { $this->warn('[DRY-RUN] Kein tatsächlicher Schreibvorgang.'); } $totalStart = microtime(true); foreach ($steps as $currentStep) { // link-associations arbeitet auf der neuen DB → einmalig, portal-unabhängig if ($currentStep === 'link-associations') { $ctx = new ImportContext('all', $isDryRun, $isForce, $since); $this->info('▶ Schritt [link-associations] (beide Portale, neue DB)'); $start = microtime(true); $result = app(UserAssociationLinker::class)->run($ctx); $elapsed = round(microtime(true) - $start, 1); $this->line(" ✓ {$result->summary()} ({$elapsed}s)"); continue; } // Kategorien sind portal-übergreifend → nur einmal $stepPortals = ($currentStep === 'categories') ? [$portals[0]] : $portals; foreach ($stepPortals as $portal) { $ctx = new ImportContext($portal, $isDryRun, $isForce, $since); $this->info("▶ Schritt [{$currentStep}] Portal [{$portal}]"); $start = microtime(true); $result = $this->runStep($currentStep, $ctx, $roleSync); $elapsed = round(microtime(true) - $start, 1); $this->line(" ✓ {$result->summary()} ({$elapsed}s)"); foreach (array_slice($result->errors(), 0, 10) as $err) { $this->warn(" ! {$err}"); } if (count($result->errors()) > 10) { $this->warn(' ! ... und '.(count($result->errors()) - 10).' weitere Fehler.'); } } } // Selbstheilung: Ein User-Insert (updateOrCreate) und die anschließende // Rollen-Zuweisung sind nicht atomar – schlägt der Rollen-Sync nach dem // Insert fehl, bliebe ein aktives, rollenloses Konto zurück (hängt beim // Login auf der Startseite). Nach einem User-Import daher absichern. if (! $isDryRun && in_array('users', $steps, true)) { $this->newLine(); $this->info('▶ Nachlauf: aktive rollenlose Nutzer absichern'); $this->call('users:backfill-portal-role'); } $total = round(microtime(true) - $totalStart, 1); $this->newLine(); $this->info("Import abgeschlossen in {$total}s."); return self::SUCCESS; } private function runStep( string $step, ImportContext $ctx, UserRolePermissionSyncService $roleSync, ): ImportResult { return match ($step) { 'categories' => app(CategoryImporter::class)->run($ctx), 'users' => app(UserImporter::class, ['roleSync' => $roleSync])->run($ctx), 'companies' => app(CompanyImporter::class)->run($ctx), 'contacts' => app(ContactImporter::class)->run($ctx), 'press-releases' => app(PressReleaseImporter::class)->run($ctx), default => throw new \InvalidArgumentException("Unbekannter Schritt: {$step}"), }; } }