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

281 lines
9.7 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\Services\Import;
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\LegacyImportMap;
use App\Models\PressRelease;
use App\Models\PressReleaseImage;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class PressReleaseImporter
{
private const CHUNK_SIZE = 200;
/** Legacy-Status → neuer Status */
private const STATUS_MAP = [
'new' => PressReleaseStatus::Draft,
'edited' => PressReleaseStatus::Draft,
'prepublished' => PressReleaseStatus::Review,
'published' => PressReleaseStatus::Published,
'rejected' => PressReleaseStatus::Rejected,
];
public function run(ImportContext $ctx): ImportResult
{
$result = new ImportResult;
$conn = $ctx->connection;
$legacyPortal = $ctx->legacyPortalValue();
$portal = $ctx->portalEnum;
// Bilder und Kontakt-Pivots vorladen
$images = DB::connection($conn)
->table('press_release_image')
->get()
->groupBy('press_release_id');
$prContacts = DB::connection($conn)
->table('press_release_contact')
->get()
->groupBy('press_release_id');
DB::connection($conn)
->table('press_release')
->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, $images, $prContacts): void {
foreach ($rows as $row) {
try {
$this->importRow($row, $ctx, $result, $legacyPortal, $portal, $images, $prContacts);
} catch (\Throwable $e) {
$result->addError("PR legacy_id={$row->id}: {$e->getMessage()}");
}
}
});
return $result;
}
private function importRow(
object $row,
ImportContext $ctx,
ImportResult $result,
string $legacyPortal,
Portal $portal,
Collection $images,
Collection $prContacts,
): void {
$alreadyImported = LegacyImportMap::query()
->where('legacy_portal', $legacyPortal)
->where('legacy_table', 'press_release')
->where('legacy_id', $row->id)
->exists();
if ($alreadyImported && ! $ctx->force) {
$result->incrementSkipped();
return;
}
if ($ctx->dryRun) {
$result->incrementImported();
return;
}
// User aus Import-Map
$userId = null;
if ($row->user_id) {
$userMap = LegacyImportMap::query()
->where('legacy_portal', $legacyPortal)
->where('legacy_table', 'sf_guard_user')
->where('legacy_id', $row->user_id)
->first();
$userId = $userMap?->target_id;
}
if (! $userId) {
$result->incrementSkipped();
return;
}
// Company aus Import-Map
$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;
}
// Kategorie aus Import-Map
$categoryId = null;
if ($row->category_id) {
$catMap = LegacyImportMap::query()
->where('legacy_table', 'category')
->where('legacy_id', $row->category_id)
->first();
$categoryId = $catMap?->target_id;
}
// Fallback-Kategorie (erste verfügbare)
if (! $categoryId) {
$categoryId = LegacyImportMap::query()
->where('legacy_table', 'category')
->value('target_id');
}
if (! $categoryId) {
$result->incrementSkipped();
return;
}
$status = self::STATUS_MAP[$row->status] ?? PressReleaseStatus::Draft;
$language = in_array($row->language, ['de', 'en']) ? $row->language : 'de';
// Beim Update (--force): bestehende uuid + slug behalten – öffentliche
// Referenzen (API-uuid, slug-URLs) dürfen sich beim Re-Import/Delta-Lauf
// nicht ändern.
$existingPr = $alreadyImported
? PressRelease::withoutGlobalScopes()
->where('legacy_portal', $legacyPortal)
->where('legacy_id', $row->id)
->first(['id', 'uuid', 'slug'])
: null;
$slug = $existingPr?->slug ?? $this->uniqueSlug(
$row->slug ?: Str::slug($row->title) ?: 'pressemitteilung',
$portal->value,
$language,
$existingPr?->id,
);
// Veröffentlichungsdatum = Legacy-`created_at` (Anlage-/Publikationsdatum,
// im Altsystem der indexierte Sortierschlüssel). Bewusst NICHT `updated_at`:
// dort steht der letzte Bearbeitungs-/Massen-Update-Stempel (bei vielen
// Alt-Datensätzen das Migrationsdatum), was im Frontend – das nach
// `published_at` sortiert/clustert – ein falsches, "frisches" Datum erzeugt.
$publishedAt = ($status === PressReleaseStatus::Published)
? $row->created_at
: null;
$pr = PressRelease::withoutTimestamps(function () use (
$legacyPortal, $row, $portal, $userId, $companyId, $categoryId,
$language, $slug, $status, $publishedAt, $existingPr,
): PressRelease {
$pr = PressRelease::withoutGlobalScopes()->updateOrCreate(
['legacy_portal' => $legacyPortal, 'legacy_id' => $row->id],
[
'uuid' => $existingPr?->uuid ?? (string) Str::uuid(),
'portal' => $portal->value,
'user_id' => $userId,
'company_id' => $companyId,
'category_id' => $categoryId,
'language' => $language,
'title' => $row->title ?: 'Ohne Titel',
'slug' => $slug,
'text' => $row->text ?: '',
'backlink_url' => $row->backlink_url ?: null,
'keywords' => $row->keywords ?: null,
'status' => $status->value,
'hits' => max(0, (int) $row->hits),
'teaser_begin' => max(0, (int) ($row->teaser_begin ?? 0)),
'teaser_end' => max(0, (int) ($row->teaser_end ?? 0)),
'no_export' => (bool) ($row->no_export ?? false),
'published_at' => $publishedAt,
]
);
// created_at/updated_at sind nicht fillable und werden von
// updateOrCreate verworfen → direkt setzen, damit das Anlage-/
// Bearbeitungsdatum bereits durch den Import korrekt migriert ist
// (legacy:fix-timestamps bleibt der schnelle Cross-DB-Resync).
$pr->forceFill([
'created_at' => $row->created_at ?? now(),
'updated_at' => $row->updated_at ?? $row->created_at ?? now(),
])->save();
return $pr;
});
foreach ($images->get($row->id, []) as $img) {
PressReleaseImage::withoutGlobalScopes()->updateOrCreate(
['legacy_portal' => $legacyPortal, 'legacy_id' => $img->id],
[
'press_release_id' => $pr->id,
'path' => $img->image,
'title' => $img->title ?: null,
'description' => $img->description ?: null,
'copyright' => $img->copyright ?: null,
'is_preview' => (bool) $img->is_preview_image,
'sort_order' => 0,
]
);
}
// Kontakt-Pivot importieren
$contactIds = [];
foreach ($prContacts->get($row->id, []) as $pivot) {
$contactMap = LegacyImportMap::query()
->where('legacy_portal', $legacyPortal)
->where('legacy_table', 'contact')
->where('legacy_id', $pivot->contact_id)
->first();
if ($contactMap) {
$contactIds[] = $contactMap->target_id;
}
}
if ($contactIds !== []) {
$pr->contacts()->syncWithoutDetaching($contactIds);
}
LegacyImportMap::query()->updateOrCreate(
[
'legacy_portal' => $legacyPortal,
'legacy_table' => 'press_release',
'legacy_id' => $row->id,
],
[
'target_table' => 'press_releases',
'target_id' => $pr->id,
'imported_at' => now(),
]
);
if ($alreadyImported) {
$result->incrementUpdated();
} else {
$result->incrementImported();
}
}
private function uniqueSlug(string $base, string $portal, string $language, ?int $excludeId = null): string
{
$slug = $base;
$i = 2;
while (PressRelease::withoutGlobalScopes()
->where('portal', $portal)
->where('language', $language)
->where('slug', $slug)
->when($excludeId, fn ($q) => $q->where('id', '!=', $excludeId))
->exists()
) {
$slug = $base.'-'.$i++;
}
return $slug;
}
}