'admin', 2 => 'editor', 3 => 'api-only', 4 => 'customer', ]; private const DEFAULT_ROLE = 'customer'; private const CHUNK_SIZE = 500; /** Registration-Type-Mapping aus Legacy-Wert */ private const REG_TYPE_MAP = [ 'agency' => RegistrationType::Agency, 'company' => RegistrationType::Company, 'apiuser' => RegistrationType::ApiUser, ]; public function __construct( private readonly UserRolePermissionSyncService $roleSync, ) {} public function run(ImportContext $ctx): ImportResult { $result = new ImportResult; $conn = $ctx->connection; $portal = $ctx->portalEnum; $legacyPortal = $ctx->legacyPortalValue(); // Lade alle Gruppen-Zuordnungen in Memory (klein genug) $userGroups = DB::connection($conn) ->table('sf_guard_user_group') ->get() ->groupBy('user_id') ->map(fn ($rows) => $rows->pluck('group_id')->first()); DB::connection($conn) ->table('sf_guard_user') ->join('sf_guard_user_profile', 'sf_guard_user.id', '=', 'sf_guard_user_profile.user_id') ->select([ 'sf_guard_user.id as legacy_id', 'sf_guard_user.username', 'sf_guard_user.is_active', 'sf_guard_user.is_super_admin', 'sf_guard_user.last_login', 'sf_guard_user.ip_address', 'sf_guard_user.created_at', 'sf_guard_user.updated_at', 'sf_guard_user_profile.email', 'sf_guard_user_profile.salutation_id', 'sf_guard_user_profile.title', 'sf_guard_user_profile.first_name', 'sf_guard_user_profile.last_name', 'sf_guard_user_profile.address', 'sf_guard_user_profile.country_id', 'sf_guard_user_profile.phone', 'sf_guard_user_profile.birthdate', 'sf_guard_user_profile.language', 'sf_guard_user_profile.backlink_url', 'sf_guard_user_profile.show_stats', 'sf_guard_user_profile.validation_date', 'sf_guard_user_profile.contract_date', 'sf_guard_user_profile.registration_type', 'sf_guard_user_profile.validate', 'sf_guard_user_profile.tax_id_number', 'sf_guard_user_profile.tax_exempt', 'sf_guard_user_profile.tax_exempt_reason', 'sf_guard_user_profile.disable_footer_code', ]) ->orderBy('sf_guard_user.id') ->chunk(self::CHUNK_SIZE, function ($rows) use ($ctx, $result, $portal, $legacyPortal, $userGroups): void { foreach ($rows as $row) { try { $this->importRow($row, $ctx, $result, $portal, $legacyPortal, $userGroups); } catch (\Throwable $e) { $result->addError("User legacy_id={$row->legacy_id}: {$e->getMessage()}"); } } }); return $result; } private function importRow( object $row, ImportContext $ctx, ImportResult $result, Portal $portal, string $legacyPortal, Collection $userGroups, ): void { // E-Mail-Adresse ist Pflicht $email = trim((string) $row->email); if (blank($email) || ! filter_var($email, FILTER_VALIDATE_EMAIL)) { $result->incrementSkipped(); return; } // Idempotenz-Check via legacy_import_map $alreadyImported = LegacyImportMap::query() ->where('legacy_portal', $legacyPortal) ->where('legacy_table', 'sf_guard_user') ->where('legacy_id', $row->legacy_id) ->exists(); if ($alreadyImported && ! $ctx->force) { $result->incrementSkipped(); return; } if ($ctx->dryRun) { $result->incrementImported(); return; } $name = trim(($row->first_name ?? '').' '.($row->last_name ?? '')); if (blank($name)) { $name = $row->username; } $language = in_array($row->language, ['de', 'en']) ? $row->language : 'de'; $regType = self::REG_TYPE_MAP[$row->registration_type] ?? RegistrationType::ExistingLegacy; $user = User::withoutTimestamps(function () use ($email, $name, $portal, $regType, $language, $row, $legacyPortal): User { return User::query()->updateOrCreate( ['email' => $email], [ 'name' => $name, 'portal' => $portal->value, 'registration_type' => $regType->value, 'language' => $language, 'is_active' => (bool) $row->is_active, 'is_super_admin' => (bool) $row->is_super_admin, 'last_login_at' => $row->last_login ?: null, 'last_login_ip' => $row->ip_address ?: null, 'legacy_portal' => $legacyPortal, 'legacy_id' => $row->legacy_id, 'created_at' => $row->created_at ?? now(), 'updated_at' => $row->updated_at ?? $row->created_at ?? now(), // Kein Passwort – User erhält Go-Live-Reset-Mail (D-09) ] ); }); // Rolle zuweisen $groupId = $userGroups->get($row->legacy_id); $roleName = self::GROUP_ROLE_MAP[$groupId] ?? self::DEFAULT_ROLE; $this->roleSync->assignRoleAndSyncPermissions($user, $roleName); Profile::query()->updateOrCreate( ['user_id' => $user->id], [ 'salutation_key' => $this->mapSalutation((int) ($row->salutation_id ?? 0)), 'title' => $this->cleanText($row->title ?? null, 80), 'first_name' => $this->cleanText($row->first_name ?? null, 80), 'last_name' => $this->cleanText($row->last_name ?? null, 80), 'phone' => $this->cleanText($row->phone ?? null, 40), 'address' => $this->cleanText($row->address ?? null, 1000), 'country_code' => $this->mapCountry((int) ($row->country_id ?? 0)), 'birthdate' => $this->validDateOrNull($row->birthdate ?? null), 'backlink_url' => $this->cleanText($row->backlink_url ?? null, 255), 'show_stats' => (bool) ($row->show_stats ?? false), 'validation_date' => $this->validDateOrNull($row->validation_date ?? null), 'contract_date' => $this->validDateOrNull($row->contract_date ?? null), 'validate_token' => $this->cleanText($row->validate ?? null, 64), 'tax_id_number' => $this->cleanText($row->tax_id_number ?? null, 255), 'tax_exempt' => (bool) ($row->tax_exempt ?? false), 'tax_exempt_reason' => $this->cleanText($row->tax_exempt_reason ?? null, 1000), 'disable_footer_code' => (bool) ($row->disable_footer_code ?? false), ] ); // Import-Map eintragen LegacyImportMap::query()->updateOrCreate( [ 'legacy_portal' => $legacyPortal, 'legacy_table' => 'sf_guard_user', 'legacy_id' => $row->legacy_id, ], [ 'target_table' => 'users', 'target_id' => $user->id, 'imported_at' => now(), ] ); if ($alreadyImported) { $result->incrementUpdated(); } else { $result->incrementImported(); } } private function mapSalutation(int $salutationId): ?string { return match ($salutationId) { 1 => 'mr', 2 => 'mrs', 3 => 'none', default => null, }; } private function mapCountry(int $countryId): ?string { return match ($countryId) { 177 => 'DE', 80 => 'AT', 196 => 'CH', 115 => 'LI', 117 => 'LU', 21 => 'BE', 2 => 'NL', 165 => 'FR', 30 => 'GB', 229 => 'US', default => null, }; } private function cleanText(?string $value, int $maxLength): ?string { if (blank($value)) { return null; } $clean = html_entity_decode((string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $clean = preg_replace('/[\x00-\x1F\x7F\xC2\xA0]/u', ' ', $clean) ?? $clean; $clean = trim((string) $clean); return blank($clean) ? null : mb_substr($clean, 0, $maxLength); } private function validDateOrNull(mixed $value): ?string { if (blank($value) || str_starts_with((string) $value, '0000-00-00')) { return null; } return (string) $value; } }