12-05-2026 Frontend dev
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

This commit is contained in:
Kevin Adametz 2026-05-12 18:32:33 +02:00
parent 405df0a122
commit 5b8bdf4182
779 changed files with 480564 additions and 6241 deletions

View file

@ -0,0 +1,262 @@
<?php
namespace App\Services\Import;
use App\Enums\Portal;
use App\Enums\RegistrationType;
use App\Models\LegacyImportMap;
use App\Models\Profile;
use App\Models\User;
use App\Services\Auth\UserRolePermissionSyncService;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class UserImporter
{
/** Legacy-Gruppen → Spatie-Rollen */
private const GROUP_ROLE_MAP = [
1 => '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;
}
}