presseportale/app/Services/Import/ContactImporter.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

152 lines
4.9 KiB
PHP

<?php
namespace App\Services\Import;
use App\Enums\Portal;
use App\Models\Contact;
use App\Models\LegacyImportMap;
use Illuminate\Support\Facades\DB;
class ContactImporter
{
private const CHUNK_SIZE = 500;
public function run(ImportContext $ctx): ImportResult
{
$result = new ImportResult;
$conn = $ctx->connection;
$legacyPortal = $ctx->legacyPortalValue();
$portal = $ctx->portalEnum;
DB::connection($conn)
->table('contact')
->when($ctx->since, fn ($query) => $query->where(function ($window) use ($ctx): void {
$window->where('created_at', '>=', $ctx->since)
->orWhere('updated_at', '>=', $ctx->since);
}))
->orderBy('id')
->chunk(self::CHUNK_SIZE, function ($rows) use ($ctx, $result, $legacyPortal, $portal): void {
foreach ($rows as $row) {
try {
$this->importRow($row, $ctx, $result, $legacyPortal, $portal);
} catch (\Throwable $e) {
$result->addError("Contact legacy_id={$row->id}: {$e->getMessage()}");
}
}
});
return $result;
}
private function importRow(
object $row,
ImportContext $ctx,
ImportResult $result,
string $legacyPortal,
Portal $portal,
): void {
$alreadyImported = LegacyImportMap::query()
->where('legacy_portal', $legacyPortal)
->where('legacy_table', 'contact')
->where('legacy_id', $row->id)
->exists();
if ($alreadyImported && ! $ctx->force) {
$result->incrementSkipped();
return;
}
if ($ctx->dryRun) {
$result->incrementImported();
return;
}
// Firma aus Import-Map auflösen
$companyId = null;
if ($row->company_id) {
$companyMap = LegacyImportMap::query()
->where('legacy_portal', $legacyPortal)
->where('legacy_table', 'company')
->where('legacy_id', $row->company_id)
->first();
$companyId = $companyMap?->target_id;
}
if (! $companyId) {
// Kontakt ohne zugeordnete Firma überspringen (FK-Constraint)
$result->incrementSkipped();
return;
}
$salutationKey = $this->mapSalutation($row->salutation_id ?? 0);
$contact = Contact::withoutTimestamps(function () use ($legacyPortal, $row, $companyId, $portal, $salutationKey): Contact {
return Contact::withoutGlobalScopes()->updateOrCreate(
['legacy_portal' => $legacyPortal, 'legacy_id' => $row->id],
[
'company_id' => $companyId,
'portal' => $portal->value,
'salutation_key' => $salutationKey,
'title' => $this->cleanText($row->title, 80),
'first_name' => $this->cleanText($row->first_name, 80),
'last_name' => $this->cleanText($row->last_name, 80),
'responsibility' => $this->cleanText($row->responsibility, 255),
'phone' => $this->cleanText($row->phone, 255),
'fax' => $this->cleanText($row->fax, 255),
'email' => $this->cleanText($row->email, 190),
]
);
});
// created_at/updated_at sind nicht fillable und würden von updateOrCreate
// verworfen → explizit per forceFill setzen (Muster PressReleaseImporter).
$contact->forceFill([
'created_at' => $row->created_at ?? now(),
'updated_at' => $row->updated_at ?? $row->created_at ?? now(),
])->save();
LegacyImportMap::query()->updateOrCreate(
[
'legacy_portal' => $legacyPortal,
'legacy_table' => 'contact',
'legacy_id' => $row->id,
],
[
'target_table' => 'contacts',
'target_id' => $contact->id,
'imported_at' => now(),
]
);
if ($alreadyImported) {
$result->incrementUpdated();
} else {
$result->incrementImported();
}
}
private function mapSalutation(int $salutationId): ?string
{
return match ($salutationId) {
1 => 'mr',
2 => 'mrs',
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);
}
}