262 lines
9.3 KiB
PHP
262 lines
9.3 KiB
PHP
<?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;
|
||
}
|
||
}
|