login update
This commit is contained in:
197
app/Console/Commands/MigrateStoriesCommand.php
Normal file
197
app/Console/Commands/MigrateStoriesCommand.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Story;
|
||||
use App\Models\StoryAuthor;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Migrate legacy interview records into the new Stories system.
|
||||
*
|
||||
* Usage:
|
||||
* php artisan stories:migrate-legacy
|
||||
* php artisan stories:migrate-legacy --dry-run
|
||||
* php artisan stories:migrate-legacy --legacy-connection=legacy --chunk=100
|
||||
*
|
||||
* Idempotent: running multiple times will not duplicate records.
|
||||
* Legacy records are identified via `legacy_interview_id` column on stories table.
|
||||
*/
|
||||
final class MigrateStoriesCommand extends Command
|
||||
{
|
||||
protected $signature = 'stories:migrate-legacy
|
||||
{--chunk=50 : number of records to process per batch}
|
||||
{--dry-run : preview migration without persisting changes}
|
||||
{--legacy-connection= : DB connection name for legacy database (default: uses default connection)}
|
||||
{--legacy-table=interviews : legacy interviews table name}
|
||||
';
|
||||
|
||||
protected $description = 'Migrate legacy interview records into the new nova Stories system (idempotent)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$chunk = max(1, (int) $this->option('chunk'));
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$legacyConn = $this->option('legacy-connection') ?: null;
|
||||
$table = (string) $this->option('legacy-table');
|
||||
|
||||
$this->info('Nova Stories — legacy interview migration');
|
||||
$this->info("Table: {$table} | Chunk: {$chunk} | Dry-run: " . ($dryRun ? 'YES' : 'NO'));
|
||||
$this->newLine();
|
||||
|
||||
try {
|
||||
$db = $legacyConn ? DB::connection($legacyConn) : DB::connection();
|
||||
// Quick existence check
|
||||
$db->table($table)->limit(1)->get();
|
||||
} catch (Throwable $e) {
|
||||
$this->error("Cannot access table `{$table}`: " . $e->getMessage());
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
|
||||
$db->table($table)->orderBy('id')->chunkById($chunk, function ($rows) use (
|
||||
$dryRun, &$inserted, &$skipped, &$failed
|
||||
) {
|
||||
foreach ($rows as $row) {
|
||||
$legacyId = (int) ($row->id ?? 0);
|
||||
|
||||
if (! $legacyId) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Idempotency: skip if already migrated
|
||||
if (Story::where('legacy_interview_id', $legacyId)->exists()) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// ── Resolve / create author ──────────────────────────────
|
||||
$authorName = $this->coerceString($row->username ?? $row->author ?? $row->uname ?? '');
|
||||
$authorAvatar = $this->coerceString($row->icon ?? $row->avatar ?? '');
|
||||
|
||||
$author = null;
|
||||
if ($authorName) {
|
||||
$author = StoryAuthor::firstOrCreate(
|
||||
['name' => $authorName],
|
||||
['avatar' => $authorAvatar ?: null]
|
||||
);
|
||||
}
|
||||
|
||||
// ── Build slug ───────────────────────────────────────────
|
||||
$rawTitle = $this->coerceString(
|
||||
$row->headline ?? $row->title ?? $row->subject ?? ''
|
||||
) ?: 'interview-' . $legacyId;
|
||||
|
||||
$slugBase = Str::slug(Str::limit($rawTitle, 180));
|
||||
$slug = $slugBase ?: 'interview-' . $legacyId;
|
||||
|
||||
// Ensure uniqueness
|
||||
$slug = $this->uniqueSlug($slug);
|
||||
|
||||
// ── Excerpt ──────────────────────────────────────────────
|
||||
$fullContent = $this->coerceString(
|
||||
$row->content ?? $row->tekst ?? $row->body ?? $row->text ?? ''
|
||||
);
|
||||
|
||||
$excerpt = $this->coerceString($row->excerpt ?? $row->intro ?? $row->lead ?? '');
|
||||
if (! $excerpt && $fullContent) {
|
||||
$excerpt = Str::limit(strip_tags($fullContent), 200);
|
||||
}
|
||||
|
||||
// ── Cover image ──────────────────────────────────────────
|
||||
$coverRaw = $this->coerceString($row->pic ?? $row->image ?? $row->cover ?? $row->photo ?? '');
|
||||
$coverImage = $coverRaw ? 'legacy/interviews/' . ltrim($coverRaw, '/') : null;
|
||||
|
||||
// ── Published date ───────────────────────────────────────
|
||||
$publishedAt = null;
|
||||
foreach (['datum', 'published_at', 'date', 'created_at'] as $field) {
|
||||
$val = $row->{$field} ?? null;
|
||||
if ($val) {
|
||||
$ts = strtotime((string) $val);
|
||||
if ($ts) {
|
||||
$publishedAt = date('Y-m-d H:i:s', $ts);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line(" [DRY-RUN] Would import: #{$legacyId} → {$slug}");
|
||||
$inserted++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Story::create([
|
||||
'slug' => $slug,
|
||||
'title' => Str::limit($rawTitle, 255),
|
||||
'excerpt' => $excerpt ?: null,
|
||||
'content' => $fullContent ?: null,
|
||||
'cover_image' => $coverImage,
|
||||
'author_id' => $author?->id,
|
||||
'views' => max(0, (int) ($row->views ?? $row->hits ?? 0)),
|
||||
'featured' => false,
|
||||
'status' => 'published',
|
||||
'published_at' => $publishedAt,
|
||||
'legacy_interview_id' => $legacyId,
|
||||
]);
|
||||
|
||||
$this->line(" Imported: #{$legacyId} → {$slug}");
|
||||
$inserted++;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$failed++;
|
||||
$this->warn(" FAILED #{$legacyId}: " . $e->getMessage());
|
||||
Log::warning("stories:migrate-legacy failed for id={$legacyId}", ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Migration complete.");
|
||||
$this->table(
|
||||
['Inserted', 'Skipped (existing)', 'Failed'],
|
||||
[[$inserted, $skipped, $failed]]
|
||||
);
|
||||
|
||||
return $failed > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private function coerceString(mixed $value, string $default = ''): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return $default;
|
||||
}
|
||||
$str = trim((string) $value);
|
||||
return $str !== '' ? $str : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the slug is unique, appending a numeric suffix if needed.
|
||||
*/
|
||||
private function uniqueSlug(string $slug): string
|
||||
{
|
||||
if (! Story::where('slug', $slug)->exists()) {
|
||||
return $slug;
|
||||
}
|
||||
|
||||
$i = 2;
|
||||
do {
|
||||
$candidate = $slug . '-' . $i++;
|
||||
} while (Story::where('slug', $candidate)->exists());
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user