presseportale/app/Console/Commands/ImportLegacyData.php
Kevin Adametz a0c2e663dc legacy: --since-Delta für legacy:import + Importer setzen Timestamps wieder korrekt
--since=<Zeitpunkt> schränkt den Quell-Scan auf Legacy-Zeilen ein, deren
created_at ODER updated_at im Fenster liegt (users/companies/contacts/
press-releases; Kategorien und link-associations bleiben ungefiltert).
Kombiniert mit --force werden geänderte Bestandszeilen aktualisiert —
damit dauert ein Tages-Delta Sekunden statt eines Volldurchlaufs über
150k+ Zeilen.

Dazu Bugfix, der den Delta erst rund macht: User-/Company-/Contact-
Importer übergaben created_at/updated_at innerhalb von updateOrCreate —
die Felder sind nicht fillable und wurden stillschweigend verworfen
(Live-Befund: NULL-Timestamps, bisher nur durch legacy:fix-timestamps
geheilt). Jetzt explizit per forceFill wie im PressReleaseImporter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 13:03:07 +00:00

177 lines
7.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Console\Commands;
use App\Services\Auth\UserRolePermissionSyncService;
use App\Services\Import\CategoryImporter;
use App\Services\Import\CompanyImporter;
use App\Services\Import\ContactImporter;
use App\Services\Import\ImportContext;
use App\Services\Import\ImportResult;
use App\Services\Import\PressReleaseImporter;
use App\Services\Import\UserAssociationLinker;
use App\Services\Import\UserImporter;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
/**
* Legacy-Import-Master-Command (Phase 6 – Datenmigration).
*
* Verwendung:
* php artisan legacy:import --source=presseecho --dry-run
* php artisan legacy:import --source=businessportal24 --step=users
* php artisan legacy:import --source=all --step=categories
* php artisan legacy:import --source=all --step=link-associations ← NEU
* php artisan legacy:import --source=presseecho --force
* php artisan legacy:import --source=presseecho --since=today --force ← Delta: nur heute Angelegtes/Geändertes
*
* Schritte (--step):
* categories – Kategorien + Translations (einmalig, beide Portale identisch)
* users – sf_guard_user + sf_guard_user_profile + Rollen
* companies – company + company_user + responsible_company_user
* contacts – contact
* press-releases – press_release + press_release_image + press_release_contact
* link-associations – User↔Kontakt direkt verknüpfen (via Firma-Zugehörigkeit)
* all – alle Schritte in korrekter Reihenfolge (Standard)
*/
class ImportLegacyData extends Command
{
protected $signature = 'legacy:import
{--source=all : Portal (presseecho|businessportal24|all)}
{--step=all : Schritt (categories|users|companies|contacts|press-releases|link-associations|all)}
{--dry-run : Nichts schreiben, nur zählen}
{--force : Bereits importierte Datensätze erneut verarbeiten}
{--since= : Nur Legacy-Zeilen mit created_at/updated_at ab diesem Zeitpunkt (z. B. "2026-07-20", "today"); für Updates mit --force kombinieren}
{--chunk-size=500 : Batch-Größe (Debugging)}';
protected $description = 'Legacy-Daten aus Presseecho / Businessportal24 in die neue DB importieren.';
private const STEPS = [
'categories',
'users',
'companies',
'contacts',
'press-releases',
'link-associations',
];
private const PORTALS = ['presseecho', 'businessportal24'];
public function handle(UserRolePermissionSyncService $roleSync): int
{
$source = $this->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}"),
};
}
}