refactor: optimize legacy data migration commands with chunking, progress tracking, and S3 media handling

This commit is contained in:
Yoga Pangestu 2026-04-14 14:51:31 +07:00
parent 28e01c6148
commit 83caa33768
12 changed files with 1062 additions and 336 deletions

View File

@ -5,6 +5,7 @@
use App\Enums\AnnouncementType;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class MigrateAnnouncementCommand extends Command
{
@ -20,29 +21,48 @@ class MigrateAnnouncementCommand extends Command
*
* @var string
*/
protected $description = 'Migrate announcement data from legacy news table where category = 1';
protected $description = 'Migrate announcement data from legacy news table where category = 1 with optimized processing';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting announcement migration...');
$this->info('Starting optimized announcement migration...');
$legacyConn = DB::connection('mysql_second');
$query = $legacyConn->table('news')->where('category', '1');
// Get legacy news with category 1 (Announcements)
$legacyAnnouncements = $legacyConn->table('news')
->where('category', '1')
->get();
if ($legacyAnnouncements->isEmpty()) {
$this->warn('No announcements found with category 1.');
$totalCount = $query->count();
if ($totalCount === 0) {
$this->warn('No announcements found in legacy database.');
return;
}
$this->withProgressBar($legacyAnnouncements, function ($legacy) {
$this->info("Found {$totalCount} legacy announcement records.");
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processAnnouncement($legacy);
}
});
$this->newLine();
$this->info('Announcement migration completed successfully!');
}
/**
* Process a single legacy announcement record.
*/
private function processAnnouncement($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->output->write("{$idOutput} Processing: <comment>".Str::limit($legacy->title, 40).'</comment>... ');
try {
DB::table('announcements')->updateOrInsert(
['id' => $legacy->id],
[
@ -54,9 +74,10 @@ public function handle()
'deleted_at' => $legacy->deleted_at,
]
);
});
$this->newLine();
$this->info('Announcement migration completed successfully!');
$this->output->writeln('<info>DONE</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
}

View File

@ -5,6 +5,7 @@
use App\Enums\IsActive;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class MigrateClassificationCommand extends Command
{
@ -20,55 +21,113 @@ class MigrateClassificationCommand extends Command
*
* @var string
*/
protected $description = 'Migrate classification data from legacy database';
protected $description = 'Migrate classification and sub-classification data from legacy database with optimized processing';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting classification migration...');
$this->info('Starting optimized classification migration...');
$this->migrateClassifications();
$this->newLine();
$this->migrateSubClassifications();
$this->newLine();
$this->info('Classification migration completed successfully!');
}
/**
* Migrate main classifications.
*/
private function migrateClassifications(): void
{
$legacyConn = DB::connection('mysql_second');
$newConn = DB::connection('mysql');
$query = $legacyConn->table('classifications');
// Migrate Classifications
$this->info('Migrating classifications...');
$legacyClassifications = $legacyConn->table('classifications')->get();
$totalCount = $query->count();
$this->info("Found {$totalCount} legacy classification records.");
$this->withProgressBar($legacyClassifications, function ($legacy) {
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processClassification($legacy);
}
});
}
/**
* Process a single legacy classification record.
*/
private function processClassification($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->output->write("{$idOutput} (CLA) Processing: <comment>".Str::limit($legacy->name, 40).'</comment>... ');
try {
DB::table('classifications')->updateOrInsert(
['id' => $legacy->id],
[
'name' => $legacy->name,
'is_active' => $legacy->status == '1' ? IsActive::ACTIVE->value : IsActive::INACTIVE->value,
'created_at' => $legacy->created_at,
'updated_at' => $legacy->updated_at,
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
'deleted_at' => $legacy->deleted_at,
]
);
$this->output->writeln('<info>DONE</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
/**
* Migrate sub-classifications.
*/
private function migrateSubClassifications(): void
{
$legacyConn = DB::connection('mysql_second');
$query = $legacyConn->table('sub_classifications');
$totalCount = $query->count();
$this->info("Found {$totalCount} legacy sub-classification records.");
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processSubClassification($legacy);
}
});
$this->newLine();
}
// Migrate Sub Classifications
$this->info('Migrating sub classifications...');
$legacySubClassifications = $legacyConn->table('sub_classifications')->get();
/**
* Process a single legacy sub-classification record.
*/
private function processSubClassification($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->withProgressBar($legacySubClassifications, function ($legacy) {
$this->output->write("{$idOutput} (SUB) Processing: <comment>".Str::limit($legacy->name, 40).'</comment>... ');
try {
DB::table('sub_classifications')->updateOrInsert(
['id' => $legacy->id],
[
'classification_id' => $legacy->classification,
'classification_id' => $legacy->classification, // Foreign key to classifications
'name' => $legacy->name,
'is_active' => $legacy->status == '1' ? IsActive::ACTIVE->value : IsActive::INACTIVE->value,
'created_at' => $legacy->created_at,
'updated_at' => $legacy->updated_at,
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
'deleted_at' => $legacy->deleted_at,
]
);
});
$this->newLine();
$this->info('Migration completed successfully!');
$this->output->writeln('<info>DONE</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
}

View File

@ -7,10 +7,12 @@
use App\Enums\MediaType;
use App\Enums\RoleEnum;
use App\Enums\VerificationStatus;
use App\Models\Company;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class MigrateCompanyCommand extends Command
@ -27,39 +29,78 @@ class MigrateCompanyCommand extends Command
*
* @var string
*/
protected $description = 'Migrate companies, partner media, and verification requests from legacy database';
protected $description = 'Migrate companies, partner media, and all verification documents from legacy database';
/**
* Cache for migrated IDs with media to avoid redundant processing.
*/
private array $migratedIds = [];
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting company migration...');
$this->info('Starting optimized company migration with document support...');
$legacyConn = DB::connection('mysql_second');
$legacyCompanies = $legacyConn->table('companies')
->whereNull('deleted_at')
->get();
$query = $legacyConn->table('companies');
if ($legacyCompanies->isEmpty()) {
$totalCount = $query->count();
if ($totalCount === 0) {
$this->warn('No companies found in legacy database.');
return;
}
$this->withProgressBar($legacyCompanies, function ($legacy) use ($legacyConn) {
// Find user in NEW database. Legcy enhancer is the user_id.
$newUser = DB::table('users')->where('id', $legacy->enhancer)->first();
$legacyUser = $legacyConn->table('users')->where('id', $legacy->enhancer)->first();
$this->info("Found {$totalCount} legacy company records.");
// If not found by ID, try by email (sometimes IDs change during user migration if not careful)
// Pre-cache migrated IDs using their associated media records
$this->migratedIds = DB::table('media')
->where('model_type', Company::class)
->where('collection_name', 'companies')
->pluck('model_id')
->flip()
->all();
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processCompany($legacy);
}
});
$this->newLine();
$this->info('Company migration completed successfully!');
}
/**
* Process a single legacy company record.
*/
private function processCompany($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->output->write("{$idOutput} Processing: <comment>".Str::limit($legacy->name, 40).'</comment>... ');
try {
$legacyConn = DB::connection('mysql_second');
// Find user
$newUser = DB::table('users')->where('id', $legacy->enhancer)->first();
if (! $newUser) {
$newUser = DB::table('users')->where('email', $legacy->email)->first();
}
if (! $newUser) {
$this->output->writeln('<error>USER NOT FOUND</error>');
return;
}
// 1. Migrate Company
$legacyUser = $legacyConn->table('users')->where('id', $legacy->enhancer)->first();
// 1. Update/Insert Company
DB::table('companies')->updateOrInsert(
['id' => $legacy->id],
[
@ -70,73 +111,176 @@ public function handle()
'address' => $legacy->address,
'director_name' => $legacy->director,
'director_nik' => $legacy->nick_director,
'deed_incorporation' => str()->limit($legacy->deed_of_establishment, 150, ''),
'trade_license' => str()->limit($legacy->trade_license, 150, ''),
'tax_id_number' => str()->limit($legacy->tax_id_number, 150, ''),
'taxable_enterprise' => str()->limit($legacy->taxable_enterprise, 150, ''),
'annual_tax_return' => str()->limit($legacy->annual_tax_statement, 150, ''),
'domicile_certificate' => str()->limit($legacy->domicile, 150, ''),
'profile' => str()->limit($legacy->profile ?? '-', 150, ''),
'deed_incorporation' => Str::limit($legacy->deed_of_establishment, 150, ''),
'trade_license' => Str::limit($legacy->trade_license, 150, ''),
'tax_id_number' => Str::limit($legacy->tax_id_number, 150, ''),
'taxable_enterprise' => Str::limit($legacy->taxable_enterprise, 150, ''),
'annual_tax_return' => Str::limit($legacy->annual_tax_statement, 150, ''),
'domicile_certificate' => Str::limit($legacy->domicile, 150, ''),
'profile' => Str::limit($legacy->profile ?? '-', 150, ''),
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
'deleted_at' => $legacy->deleted_at,
]
);
// 2. Migrate Verification Request
$status = $this->mapVerificationStatus($legacyUser->status);
DB::table('verification_requests')->updateOrInsert(
['company_id' => $legacy->id],
[
'submitted_by' => $newUser->id,
'status' => $status,
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
]
);
// 2. Verification Request & Review
$this->handleVerification($legacy, $legacyUser, $newUser);
// 3. Migrate Verification Review
$decision = $this->mapDecision($legacyUser->status);
// 3. Partner Media
$this->handlePartnerMedia($legacy);
// 4. Media Documents Migration (Skip if already has media in 'companies' collection)
if (isset($this->migratedIds[$legacy->id])) {
$this->output->writeln('<info>DONE (SKIP DOCS)</info>');
return;
}
$company = Company::withTrashed()->find($legacy->id);
if ($company) {
$this->migrateAllDocuments($company, $legacy);
}
$this->output->writeln('<info>DONE + DOCS</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
/**
* Handle verification logic.
*/
private function handleVerification($legacy, $legacyUser, $newUser): void
{
$status = $this->mapVerificationStatus($legacyUser->status);
DB::table('verification_requests')->updateOrInsert(
['company_id' => $legacy->id],
[
'submitted_by' => $newUser->id,
'status' => $status,
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
]
);
$decision = $this->mapDecision($legacyUser->status);
if ($decision !== DecisionAdmin::NEED_REVISION->value) {
$verificationRequest = DB::table('verification_requests')->where('company_id', $legacy->id)->first();
if ($decision !== DecisionAdmin::NEED_REVISION->value) {
$adminUser = User::role(RoleEnum::ADMINISTRATOR)->first();
if ($verificationRequest && $adminUser) {
DB::table('verification_reviews')->updateOrInsert(
['verification_request_id' => $verificationRequest->id],
[
'reviewer_id' => User::role(RoleEnum::ADMINISTRATOR)->first()->id,
'reviewer_id' => $adminUser->id,
'decision' => $decision,
'note' => $legacy->reject_message,
'created_at' => Carbon::parse($legacy->validation_date)->setTime(23, 0, 0),
'created_at' => $legacy->validation_date ? Carbon::parse($legacy->validation_date)->setTime(23, 0, 0) : now(),
'updated_at' => $legacy->updated_at ?? now(),
]
);
}
// 4. Migrate Partner Media (from legacy 'media' table matching company ID)
$legacyMediaList = $legacyConn->table('media')->where('company', $legacy->id)->get();
foreach ($legacyMediaList as $legacyMedia) {
DB::table('partner_media')->updateOrInsert(
['id' => $legacyMedia->id],
[
'company_id' => $legacy->id,
'name' => $legacyMedia->name,
'address' => $legacyMedia->address,
'link' => $this->extractMediaLink($legacyMedia->link),
'type' => $this->mapMediaType($legacyMedia->type),
'classification' => $this->mapMediaClassification($legacyMedia->classification),
'journalism_organization' => $legacyMedia->organization,
'press_council_certificate' => $legacyMedia->certificate,
'created_at' => $legacyMedia->created_at ?? now(),
'updated_at' => $legacyMedia->updated_at ?? now(),
'deleted_at' => $legacyMedia->deleted_at,
]
);
}
});
$this->newLine();
$this->info('Company migration completed successfully!');
}
}
/**
* Handle partner media.
*/
private function handlePartnerMedia($legacy): void
{
$legacyConn = DB::connection('mysql_second');
$legacyMediaList = $legacyConn->table('media')->where('company', $legacy->id)->get();
foreach ($legacyMediaList as $legacyMedia) {
DB::table('partner_media')->updateOrInsert(
['id' => $legacyMedia->id],
[
'company_id' => $legacy->id,
'name' => $legacyMedia->name,
'address' => $legacyMedia->address,
'link' => $this->extractMediaLink($legacyMedia->link),
'type' => $this->mapMediaType($legacyMedia->type),
'classification' => $this->mapMediaClassification($legacyMedia->classification),
'journalism_organization' => $legacyMedia->organization,
'press_council_certificate' => $legacyMedia->certificate,
'created_at' => $legacyMedia->created_at ?? now(),
'updated_at' => $legacyMedia->updated_at ?? now(),
'deleted_at' => $legacyMedia->deleted_at,
]
);
}
}
/**
* Migrate all company documents from S3.
*/
private function migrateAllDocuments(Company $company, $legacy): void
{
$docMapping = [
'doc_of_nick_director' => 'director_nik',
'doc_of_deed_of_establishment' => 'deed_incorporation',
'doc_of_trade_license' => 'trade_license',
'doc_of_tax_id_number' => 'tax_id_number',
'doc_of_taxable_enterprise' => 'taxable_enterprise',
'doc_of_annual_tax_statement' => 'annual_tax_return',
'doc_of_domicile' => 'domicile_certificate',
'doc_of_profile' => 'profile',
];
foreach ($docMapping as $legacyField => $docTypeSuffix) {
$legacyPath = trim($legacy->{$legacyField} ?? '');
if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') {
continue;
}
// Path mapping: old data/companies/ + path from db
$sourcePath = 'old data/companies/'.$legacyPath;
$this->importOneDocument($company, $sourcePath, $docTypeSuffix, $legacy);
}
}
/**
* Import a single document to Media Library.
*/
private function importOneDocument(Company $company, string $sourcePath, string $docType, $legacy): void
{
$attempts = 0;
$maxAttempts = 3;
$exists = false;
while ($attempts < $maxAttempts) {
try {
$exists = Storage::disk('s3')->exists($sourcePath);
break;
} catch (\Exception $e) {
$attempts++;
usleep(200000);
}
}
if (! $exists) {
return;
}
try {
$company->addMediaFromDisk($sourcePath, 's3')
->withCustomProperties([
'feature' => 'companies',
'date' => Carbon::parse($legacy->created_at ?? now())->toDateString(),
'doc_type' => $docType, // Match CompanySaveService slug pattern
])
->preservingOriginal()
->toMediaCollection('companies', 's3');
} catch (\Exception $e) {
// Log quietly or handle error
}
}
// ... mapping methods (mapVerificationStatus, mapDecision, mapMediaType, mapMediaClassification, extractMediaLink) remain as previously defined
private function mapVerificationStatus($legacyStatus): int
{
return match ((string) $legacyStatus) {
@ -182,33 +326,19 @@ private function extractMediaLink(?string $link): ?string
if (empty($link)) {
return null;
}
// Find all URLs (starts with http or https)
preg_match_all('/(https?:\/\/[^\s]+)/i', $link, $matches);
// If no URL found, return the original string truncated
if (empty($matches[0])) {
return Str::limit($link, 50, '');
return Str::limit($link, 255, '');
}
// Try to find the main media website URL
foreach ($matches[0] as $url) {
$url = rtrim($url, ','); // clean trailing comma if any
// Skip common non-media-website URLs if there's an alternative
if (count($matches[0]) > 1 && (
str_contains($url, 'google.') ||
str_contains($url, 'drive.') ||
str_contains($url, 'share.') ||
str_contains($url, 'bit.ly')
)) {
$url = rtrim($url, ',');
if (count($matches[0]) > 1 && (str_contains($url, 'google.') || str_contains($url, 'drive.') || str_contains($url, 'share.') || str_contains($url, 'bit.ly'))) {
continue;
}
return Str::limit($url, 50, '');
return Str::limit($url, 255, '');
}
// Fallback to the first URL found
return Str::limit($matches[0][0], 50, '');
return Str::limit($matches[0][0], 255, '');
}
}

View File

@ -6,8 +6,11 @@
use App\Enums\ContentType;
use App\Enums\IsActive;
use App\Enums\SocialMedia;
use App\Models\ContentRecap;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class MigrateContentRecapCommand extends Command
@ -24,72 +27,107 @@ class MigrateContentRecapCommand extends Command
*
* @var string
*/
protected $description = 'Migrate content recap data from legacy database';
protected $description = 'Migrate content recap data and classifications with S3 images and optimized processing';
/**
* Cache for migrated IDs with media to avoid redundant processing.
*/
private array $migratedIds = [];
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting content recap migration...');
$this->info('Starting optimized content recap migration...');
$legacyConn = DB::connection('mysql_second');
// 1. Migrate Classifications first
$this->info('Migrating classifications for content recap...');
$legacyRecapClasses = $legacyConn->table('content_recap_classifications')->get();
$classMapping = [];
// 1. Migrate Classifications first (Small table, can handle simply)
$this->migrateClassifications();
foreach ($legacyRecapClasses as $legacyClass) {
// Check by name to avoid duplicates in classifications table
$existing = DB::table('classifications')->where('name', $legacyClass->name)->first();
// 2. Pre-cache migrated IDs using their associated media records
// Using 'content-recaps' collection to match the (typo) name in ContentRecapForm
$this->migratedIds = DB::table('media')
->where('model_type', ContentRecap::class)
->where('collection_name', 'content-recaps')
->pluck('model_id')
->flip()
->all();
if ($existing) {
$classMapping[$legacyClass->id] = $existing->id;
} else {
$newId = DB::table('classifications')->insertGetId([
'name' => $legacyClass->name,
'is_active' => IsActive::ACTIVE->value,
'created_at' => $legacyClass->created_at ?? now(),
'updated_at' => $legacyClass->updated_at ?? now(),
'deleted_at' => $legacyClass->deleted_at,
]);
$classMapping[$legacyClass->id] = $newId;
}
}
// 3. Migrate Content Recaps
$query = $legacyConn->table('content_recap');
$totalCount = $query->count();
// 2. Migrate Content Recaps
$this->info('Migrating content recaps...');
$legacyRecaps = $legacyConn->table('content_recap')->get();
if ($legacyRecaps->isEmpty()) {
if ($totalCount === 0) {
$this->warn('No content recaps found in legacy database.');
return;
}
$this->withProgressBar($legacyRecaps, function ($legacy) use ($classMapping) {
$classificationId = $classMapping[$legacy->classification_id] ?? null;
$this->info("Found {$totalCount} legacy content recap records.");
// Fallback for classification if not found in master table mapping
if (! $classificationId && $legacy->classification) {
$classificationId = DB::table('classifications')
->where('name', $legacy->classification)
->value('id');
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processRecap($legacy);
}
});
$this->newLine();
$this->info('Content recap migration completed successfully!');
}
/**
* Migrate classifications related to content recaps.
*/
private function migrateClassifications(): void
{
$this->info('Migrating classifications...');
$legacyConn = DB::connection('mysql_second');
$legacyRecapClasses = $legacyConn->table('content_recap_classifications')->get();
foreach ($legacyRecapClasses as $legacyClass) {
DB::table('classifications')->updateOrInsert(
['name' => $legacyClass->name],
[
'is_active' => IsActive::ACTIVE->value,
'created_at' => $legacyClass->created_at ?? now(),
'updated_at' => $legacyClass->updated_at ?? now(),
'deleted_at' => $legacyClass->deleted_at,
]
);
}
}
/**
* Process a single legacy content recap record.
*/
private function processRecap($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->output->write("{$idOutput} Processing: <comment>".Str::limit($legacy->title, 40).'</comment>... ');
try {
// Find classification in master table
$classificationId = DB::table('classifications')
->where('name', $legacy->classification)
->value('id');
// If still no classification, skip or use a default one?
// The table requires classification_id so we must have one.
if (! $classificationId) {
return; // Skip if no classification
$this->output->writeln('<error>CLASS NOT FOUND</error>');
return;
}
// Step 1: Update/Insert record
DB::table('content_recaps')->updateOrInsert(
['id' => $legacy->id],
[
'classification_id' => $classificationId,
'title' => $legacy->title,
'link' => Str::limit($legacy->link, 50, ''),
'link' => Str::limit($legacy->link, 255, ''),
'posting_date' => $legacy->posting_date === '0000-00-00' ? now()->toDateString() : $legacy->posting_date,
'type' => $this->mapContentType($legacy->content_type),
'channel' => $this->mapChannel($legacy->channel),
@ -99,10 +137,68 @@ public function handle()
'deleted_at' => $legacy->deleted_at,
]
);
});
$this->newLine();
$this->info('Content recap migration completed successfully!');
// Step 2: Media Migration
if (isset($this->migratedIds[$legacy->id])) {
$this->output->writeln('<info>SKIP (Media Exists)</info>');
return;
}
if (! empty($legacy->image) && $legacy->image !== 'no-image.png') {
$recap = ContentRecap::withTrashed()->find($legacy->id);
if ($recap) {
$this->migrateMedia($recap, $legacy);
}
} else {
$this->output->writeln('<info>DONE</info>');
}
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
/**
* Handle S3 media migration.
*/
private function migrateMedia(ContentRecap $recap, $legacy): void
{
$filename = trim(basename($legacy->image));
$sourcePath = 'old data/content-recaps/'.$filename;
$attempts = 0;
$maxAttempts = 3;
$exists = false;
while ($attempts < $maxAttempts) {
try {
$exists = Storage::disk('s3')->exists($sourcePath);
break;
} catch (\Exception $e) {
$attempts++;
usleep(200000);
}
}
if (! $exists) {
$this->output->writeln('<error>IMAGE MISSING</error>');
return;
}
try {
$recap->addMediaFromDisk($sourcePath, 's3')
->withCustomProperties([
'feature' => 'content-recaps',
'date' => Carbon::parse($legacy->created_at ?? now())->toDateString(),
])
->preservingOriginal()
->toMediaCollection('content-recaps', 's3'); // Match typo in ContentRecapForm
$this->output->writeln('<info>DONE + IMG</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>S3 ERROR: '.Str::limit($e->getMessage(), 50).'</error>');
}
}
private function mapContentType(?string $legacyType): ?int

View File

@ -21,22 +21,56 @@ class MigrateDepartmentCommand extends Command
*
* @var string
*/
protected $description = 'Migrate department data from legacy agencies table';
protected $description = 'Migrate department data from legacy agencies table with optimized processing';
/**
* Store migrated record IDs to optimize processing.
*/
private array $migratedIds = [];
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting department migration...');
$this->info('Starting optimized department migration...');
$legacyConn = DB::connection('mysql_second');
$query = $legacyConn->table('agencies');
// Migrate Departments (Agencies)
$this->info('Migrating departments...');
$legacyAgencies = $legacyConn->table('agencies')->get();
$totalCount = $query->count();
if ($totalCount === 0) {
$this->warn('No departments found in legacy database.');
$this->withProgressBar($legacyAgencies, function ($legacy) {
return;
}
$this->info("Found {$totalCount} legacy department records.");
// Cache existing IDs for faster processing
$this->migratedIds = DB::table('departments')->pluck('id')->flip()->all();
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processDepartment($legacy);
}
});
$this->newLine();
$this->info('Department migration completed successfully!');
}
/**
* Process a single legacy department record.
*/
private function processDepartment($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->output->write("{$idOutput} Processing: <comment>".Str::limit($legacy->name, 40).'</comment>... ');
try {
DB::table('departments')->updateOrInsert(
['id' => $legacy->id],
[
@ -48,9 +82,10 @@ public function handle()
'deleted_at' => $legacy->deleted_at,
]
);
});
$this->newLine();
$this->info('Migration completed successfully!');
$this->output->writeln('<info>DONE</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
}

View File

@ -23,27 +23,49 @@ class MigrateIssueManagementCommand extends Command
*
* @var string
*/
protected $description = 'Migrate issue management data from legacy database';
protected $description = 'Migrate issue management data from legacy database with optimized processing';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting issue management migration...');
$this->info('Starting optimized issue management migration...');
$legacyConn = DB::connection('mysql_second');
$query = $legacyConn->table('issue_management');
// Get legacy issue management
$legacyIssues = $legacyConn->table('issue_management')->get();
if ($legacyIssues->isEmpty()) {
$totalCount = $query->count();
if ($totalCount === 0) {
$this->warn('No issue management records found in legacy database.');
return;
}
$this->withProgressBar($legacyIssues, function ($legacy) {
$this->info("Found {$totalCount} legacy issue management records.");
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processIssue($legacy);
}
});
$this->newLine();
$this->info('Issue management migration completed successfully!');
}
/**
* Process a single legacy issue management record.
*/
private function processIssue($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->output->write("{$idOutput} Processing: <comment>".Str::limit($legacy->description, 40).'</comment>... ');
try {
// Step 1: Insert/Update main record
DB::table('issue_management')->updateOrInsert(
['id' => $legacy->id],
[
@ -61,46 +83,57 @@ public function handle()
]
);
// Handle Departments (Legacy 'agency' field is string, can be multiple separated by comma)
$agencyString = trim($legacy->agency);
if (! empty($agencyString)) {
$agencyNames = explode(',', $agencyString);
// Step 2: Handle Departments pivot mapping
$this->handleDepartments($legacy);
foreach ($agencyNames as $agencyName) {
$agencyName = trim($agencyName);
if (empty($agencyName)) {
continue;
}
$department = Department::firstOrCreate(
['name' => $agencyName],
[
'alias' => Str::limit($agencyName, 20, ''),
'is_active' => IsActive::ACTIVE,
]
);
DB::table('department_issue_management')->updateOrInsert(
[
'department_id' => $department->id,
'issue_management_id' => $legacy->id,
],
[
'created_at' => now(),
'updated_at' => now(),
]
);
}
}
});
$this->newLine();
$this->info('Issue management migration completed successfully!');
$this->output->writeln('<info>DONE</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
private function mapSentiment(string $legacyValue): int
/**
* Handle department pivot table mapping.
*/
private function handleDepartments($legacy): void
{
return match (strtolower($legacyValue)) {
$agencyString = trim($legacy->agency);
if (empty($agencyString)) {
return;
}
$agencyNames = array_filter(array_map('trim', explode(',', $agencyString)));
foreach ($agencyNames as $agencyName) {
// Ensure department exists in master table
$department = Department::firstOrCreate(
['name' => $agencyName],
[
'alias' => Str::limit($agencyName, 20, ''),
'is_active' => IsActive::ACTIVE,
]
);
// Link to pivot table
DB::table('department_issue_management')->updateOrInsert(
[
'department_id' => $department->id,
'issue_management_id' => $legacy->id,
],
[
'created_at' => now(),
'updated_at' => now(),
]
);
}
}
/**
* Map sentiment strings to IssueSentiment Enum values.
*/
private function mapSentiment(?string $legacyValue): int
{
return match (strtolower($legacyValue ?? '')) {
'positif' => IssueSentiment::POSITIVE->value,
'negatif' => IssueSentiment::NEGATIVE->value,
'netral' => IssueSentiment::NEUTRAL->value,

View File

@ -2,8 +2,11 @@
namespace App\Console\Commands;
use App\Models\Journalist;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class MigrateJournalistCommand extends Command
@ -20,34 +23,70 @@ class MigrateJournalistCommand extends Command
*
* @var string
*/
protected $description = 'Migrate journalists from legacy database';
protected $description = 'Migrate journalists and their documents from legacy database';
/**
* Cache for migrated IDs with media to avoid redundant processing.
*/
private array $migratedIds = [];
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting journalist migration...');
$this->info('Starting optimized journalist migration with document support...');
$legacyConn = DB::connection('mysql_second');
$legacyJournalists = $legacyConn->table('journalists')->get();
$query = $legacyConn->table('journalists');
if ($legacyJournalists->isEmpty()) {
$totalCount = $query->count();
if ($totalCount === 0) {
$this->warn('No journalists found in legacy database.');
return;
}
$this->withProgressBar($legacyJournalists, function ($legacy) {
// Find corresponding partner media (outlet) in the NEW database
// legacy 'media' column is the partner_media_id
$partnerMedia = DB::table('partner_media')->where('id', $legacy->media)->first();
$this->info("Found {$totalCount} legacy journalist records.");
// Pre-cache migrated IDs
$this->migratedIds = DB::table('media')
->where('model_type', Journalist::class)
->where('collection_name', 'journalists')
->pluck('model_id')
->flip()
->all();
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processJournalist($legacy);
}
});
$this->newLine();
$this->info('Journalist migration completed successfully!');
}
/**
* Process a single legacy journalist record.
*/
private function processJournalist($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->output->write("{$idOutput} Processing: <comment>".Str::limit($legacy->name, 40).'</comment>... ');
try {
// Find corresponding partner media
$partnerMedia = DB::table('partner_media')->where('id', $legacy->media)->first();
if (! $partnerMedia) {
// If partner media doesn't exist, we skip as it's required
$this->output->writeln('<error>OUTLET NOT FOUND</error>');
return;
}
// 1. Update/Insert Journalist
DB::table('journalists')->updateOrInsert(
['id' => $legacy->id],
[
@ -62,9 +101,82 @@ public function handle()
'deleted_at' => $legacy->deleted_at,
]
);
});
$this->newLine();
$this->info('Journalist migration completed successfully!');
// 2. Media Documents Migration (Skip if already has media)
if (isset($this->migratedIds[$legacy->id])) {
$this->output->writeln('<info>DONE (SKIP DOCS)</info>');
return;
}
$journalist = Journalist::withTrashed()->find($legacy->id);
if ($journalist) {
$this->migrateJournalistDocuments($journalist, $legacy);
}
$this->output->writeln('<info>DONE + DOCS</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
/**
* Migrate journalist documents from S3.
*/
private function migrateJournalistDocuments(Journalist $journalist, $legacy): void
{
$docMapping = [
'doc_of_press_card' => 'press-card',
'doc_of_certificate' => 'ukw-certificate',
];
foreach ($docMapping as $legacyField => $docType) {
$legacyPath = trim($legacy->{$legacyField} ?? '');
if (empty($legacyPath) || $legacyPath === '-' || $legacyPath === 'no-image.png') {
continue;
}
// Path mapping: old data/companies/ + path from db (based on legacy structure)
$sourcePath = 'old data/companies/'.$legacyPath;
$this->importOneDocument($journalist, $sourcePath, $docType, $legacy);
}
}
/**
* Import a single document to Media Library.
*/
private function importOneDocument(Journalist $journalist, string $sourcePath, string $docType, $legacy): void
{
$attempts = 0;
$maxAttempts = 3;
$exists = false;
while ($attempts < $maxAttempts) {
try {
$exists = Storage::disk('s3')->exists($sourcePath);
break;
} catch (\Exception $e) {
$attempts++;
usleep(200000);
}
}
if (! $exists) {
return;
}
try {
$journalist->addMediaFromDisk($sourcePath, 's3')
->withCustomProperties([
'feature' => 'journalists',
'date' => Carbon::parse($legacy->created_at ?? now())->toDateString(),
'doc_type' => $docType,
])
->preservingOriginal()
->toMediaCollection('journalists', 's3');
} catch (\Exception $e) {
// Log quietly
}
}
}

View File

@ -5,6 +5,7 @@
use App\Enums\IsActive;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class MigrateLocationCommand extends Command
{
@ -20,54 +21,113 @@ class MigrateLocationCommand extends Command
*
* @var string
*/
protected $description = 'Migrate location data from legacy database';
protected $description = 'Migrate location and sub-location data from legacy database with optimized processing';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting location migration...');
$this->info('Starting optimized location migration...');
$this->migrateLocations();
$this->newLine();
$this->migrateSubLocations();
$this->newLine();
$this->info('Location migration completed successfully!');
}
/**
* Migrate main locations.
*/
private function migrateLocations(): void
{
$legacyConn = DB::connection('mysql_second');
$query = $legacyConn->table('locuses');
// Migrate Locations (Locuses)
$this->info('Migrating locations...');
$legacyLocations = $legacyConn->table('locuses')->get();
$totalCount = $query->count();
$this->info("Found {$totalCount} legacy location records.");
$this->withProgressBar($legacyLocations, function ($legacy) {
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processLocation($legacy);
}
});
}
/**
* Process a single legacy location record.
*/
private function processLocation($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->output->write("{$idOutput} (LOC) Processing: <comment>".Str::limit($legacy->name, 40).'</comment>... ');
try {
DB::table('locations')->updateOrInsert(
['id' => $legacy->id],
[
'name' => $legacy->name,
'is_active' => $legacy->status == '1' ? IsActive::ACTIVE->value : IsActive::INACTIVE->value,
'created_at' => $legacy->created_at,
'updated_at' => $legacy->updated_at,
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
'deleted_at' => $legacy->deleted_at,
]
);
$this->output->writeln('<info>DONE</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
/**
* Migrate sub-locations.
*/
private function migrateSubLocations(): void
{
$legacyConn = DB::connection('mysql_second');
$query = $legacyConn->table('sub_locuses');
$totalCount = $query->count();
$this->info("Found {$totalCount} legacy sub-location records.");
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processSubLocation($legacy);
}
});
$this->newLine();
}
// Migrate Sub Locations (Sub Locuses)
$this->info('Migrating sub locations...');
$legacySubLocations = $legacyConn->table('sub_locuses')->get();
/**
* Process a single legacy sub-location record.
*/
private function processSubLocation($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->withProgressBar($legacySubLocations, function ($legacy) {
$this->output->write("{$idOutput} (SUB) Processing: <comment>".Str::limit($legacy->name, 40).'</comment>... ');
try {
DB::table('sub_locations')->updateOrInsert(
['id' => $legacy->id],
[
'location_id' => $legacy->locus,
'location_id' => $legacy->locus, // Foreign key to locations
'name' => $legacy->name,
'is_active' => $legacy->status == '1' ? IsActive::ACTIVE->value : IsActive::INACTIVE->value,
'created_at' => $legacy->created_at,
'updated_at' => $legacy->updated_at,
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
'deleted_at' => $legacy->deleted_at,
]
);
});
$this->newLine();
$this->info('Migration completed successfully!');
$this->output->writeln('<info>DONE</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
}

View File

@ -4,8 +4,11 @@
use App\Enums\Channel;
use App\Enums\IsActive;
use App\Models\MediaMonitoring;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class MigrateMediaMonitoringCommand extends Command
@ -22,81 +25,194 @@ class MigrateMediaMonitoringCommand extends Command
*
* @var string
*/
protected $description = 'Migrate media monitoring data from legacy database';
protected $description = 'Migrate media monitoring data with S3 images and optimized processing';
/**
* Cache for migrated IDs to prevent redundant processing.
*/
private array $migratedIds = [];
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting media monitoring migration...');
$this->info('Starting optimized media monitoring migration...');
$legacyConn = DB::connection('mysql_second');
$query = $legacyConn->table('media_monitorings');
// Get legacy monitorings
$legacyMonitorings = $legacyConn->table('media_monitorings')->get();
if ($legacyMonitorings->isEmpty()) {
$this->warn('No media monitorings found in legacy database.');
$totalCount = $query->count();
if ($totalCount === 0) {
$this->warn('No media monitoring found in legacy database.');
return;
}
$this->withProgressBar($legacyMonitorings, function ($legacy) {
// Main record
DB::table('media_monitorings')->updateOrInsert(
['id' => $legacy->id],
[
'code' => Str::limit($legacy->code, 10, ''),
'media_name' => Str::limit($legacy->media_name, 50, ''),
'title' => $legacy->title,
'channel' => $this->mapChannel($legacy->channel),
'writter' => Str::limit($legacy->writter, 50, ''),
'link' => $legacy->link,
'quote' => $legacy->quote,
'content' => $legacy->content,
'influencer' => Str::limit($legacy->influencer, 50, ''),
'keyword' => Str::limit($legacy->keyword, 100, ''),
'release_date' => $legacy->release_date,
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
'deleted_at' => $legacy->deleted_at,
]
);
$this->info("Found {$totalCount} legacy media monitoring records.");
// Handle Theme (Insert into themes table if not exists)
$themeName = trim($legacy->theme);
if (! empty($themeName)) {
$themeName = Str::limit($themeName, 50, '');
// Pre-cache migrated IDs using their associated media records
$this->migratedIds = DB::table('media')
->where('model_type', MediaMonitoring::class)
->where('collection_name', 'media-monitorings')
->pluck('model_id')
->flip()
->all();
// Ensure theme exists in master table
DB::table('themes')->updateOrInsert(
['name' => $themeName],
[
'is_active' => IsActive::ACTIVE->value,
'created_at' => now(),
'updated_at' => now(),
]
);
$themeId = DB::table('themes')->where('name', $themeName)->value('id');
// Link to pivot table
if ($themeId) {
DB::table('media_monitoring_theme')->updateOrInsert(
[
'media_monitoring_id' => $legacy->id,
'theme_id' => $themeId,
]
);
}
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processMonitoring($legacy);
}
});
$this->newLine();
$this->newLine();
$this->info('Media monitoring migration completed successfully!');
}
/**
* Process a single legacy media monitoring record.
*/
private function processMonitoring($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->output->write("{$idOutput} Processing: <comment>".Str::limit($legacy->title, 40).'</comment>... ');
// Step 1: Basic data update
DB::table('media_monitorings')->updateOrInsert(
['id' => $legacy->id],
[
'code' => Str::limit($legacy->code, 10, ''),
'media_name' => Str::limit($legacy->media_name, 50, ''),
'title' => $legacy->title,
'channel' => $this->mapChannel($legacy->channel),
'writter' => Str::limit($legacy->writter, 50, ''),
'link' => $legacy->link,
'quote' => $legacy->quote,
'content' => $legacy->content,
'influencer' => Str::limit($legacy->influencer, 50, ''),
'keyword' => Str::limit($legacy->keyword, 100, ''),
'release_date' => $legacy->release_date,
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
'deleted_at' => $legacy->deleted_at,
]
);
// Handle Theme
$this->handleTheme($legacy);
// Step 2: Skip heavy operations (media) if already has media
if (isset($this->migratedIds[$legacy->id])) {
$this->output->writeln('<info>SKIP (Media Exists)</info>');
return;
}
$monitoring = MediaMonitoring::withTrashed()->find($legacy->id);
if (! $monitoring) {
$this->output->writeln('<error>NOT FOUND</error>');
return;
}
// Step 3: Handle Media Migration
if (! empty($legacy->image) && $legacy->image !== 'no-image.png') {
$this->migrateMedia($monitoring, $legacy);
} else {
$this->output->writeln('<info>DONE</info>');
}
}
/**
* Handle theme pivot and master data.
*/
private function handleTheme($legacy): void
{
$themeName = trim($legacy->theme);
if (empty($themeName)) {
return;
}
$themeName = Str::limit($themeName, 50, '');
// Ensure theme exists
DB::table('themes')->updateOrInsert(
['name' => $themeName],
[
'is_active' => IsActive::ACTIVE->value,
'created_at' => now(),
'updated_at' => now(),
]
);
$themeId = DB::table('themes')->where('name', $themeName)->value('id');
if ($themeId) {
DB::table('media_monitoring_theme')->updateOrInsert(
[
'media_monitoring_id' => $legacy->id,
'theme_id' => $themeId,
]
);
}
}
/**
* Migrate media from S3 to Spatie Media Library.
*/
private function migrateMedia(MediaMonitoring $monitoring, $legacy): void
{
$filename = trim(basename($legacy->image));
$sourcePath = 'old data/mediamonitorings/'.$filename;
$exists = false;
$attempts = 0;
$maxAttempts = 3;
while ($attempts < $maxAttempts) {
try {
$exists = Storage::disk('s3')->exists($sourcePath);
break;
} catch (\Exception $e) {
$attempts++;
if ($attempts >= $maxAttempts) {
$actualError = $e->getPrevious() ? $e->getPrevious()->getMessage() : $e->getMessage();
$this->output->writeln("<error>S3 ERROR ({$attempts}x): ".Str::limit($actualError, 50).'</error>');
return;
}
usleep(200000);
}
}
if (! $exists) {
$this->output->writeln('<error>IMAGE MISSING</error>');
return;
}
try {
$date = Carbon::parse($legacy->created_at ?? now())->toDateString();
$monitoring->addMediaFromDisk($sourcePath, 's3')
->withCustomProperties([
'feature' => 'media-monitorings',
'date' => $date,
])
->preservingOriginal()
->toMediaCollection('media-monitorings', 's3');
$this->output->writeln('<info>DONE + IMG</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
/**
* Map channel string to Enum value.
*/
private function mapChannel(?string $legacyChannel): int
{
return match (strtolower($legacyChannel ?? '')) {

View File

@ -5,6 +5,7 @@
use App\Enums\IsActive;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class MigrateThemeCommand extends Command
{
@ -20,27 +21,48 @@ class MigrateThemeCommand extends Command
*
* @var string
*/
protected $description = 'Migrate theme data from legacy proof_airing_themes table';
protected $description = 'Migrate theme data from legacy proof_airing_themes table with optimized processing';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting theme migration...');
$this->info('Starting optimized theme migration...');
$legacyConn = DB::connection('mysql_second');
$query = $legacyConn->table('proof_airing_themes');
// Get legacy themes
$legacyThemes = $legacyConn->table('proof_airing_themes')->get();
if ($legacyThemes->isEmpty()) {
$totalCount = $query->count();
if ($totalCount === 0) {
$this->warn('No themes found in legacy database.');
return;
}
$this->withProgressBar($legacyThemes, function ($legacy) {
$this->info("Found {$totalCount} legacy theme records.");
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processTheme($legacy);
}
});
$this->newLine();
$this->info('Theme migration completed successfully!');
}
/**
* Process a single legacy theme record.
*/
private function processTheme($legacy): void
{
$idText = "[#{$legacy->id}]";
$idOutput = $legacy->deleted_at ? "<fg=red>{$idText}</>" : "<info>{$idText}</info>";
$this->output->write("{$idOutput} Processing: <comment>".Str::limit($legacy->name, 40).'</comment>... ');
try {
DB::table('themes')->updateOrInsert(
['id' => $legacy->id],
[
@ -51,9 +73,10 @@ public function handle()
'deleted_at' => $legacy->deleted_at,
]
);
});
$this->newLine();
$this->info('Theme migration completed successfully!');
$this->output->writeln('<info>DONE</info>');
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
}

View File

@ -24,53 +24,89 @@ class MigrateUserCommand extends Command
*
* @var string
*/
protected $description = 'Migrate users from legacy database';
protected $description = 'Migrate users from legacy database with optimized processing and detailed output';
/**
* Cache for existing user IDs and emails to prevent redundant queries.
*/
private array $existingUserIds = [];
private array $existingUserEmails = [];
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Starting user migration...');
$this->info('Starting optimized user migration...');
$legacyUsers = DB::connection('mysql_second')->table('users')->where('deleted_at', null)->get();
$legacyConn = DB::connection('mysql_second');
$query = $legacyConn->table('users')->whereNull('deleted_at');
if ($legacyUsers->isEmpty()) {
$totalCount = $query->count();
if ($totalCount === 0) {
$this->warn('No users found in legacy database.');
return;
}
foreach ($legacyUsers as $legacy) {
$existingById = User::where('id', $legacy->id)->first();
$existingByEmail = User::where('email', $legacy->email)->first();
$this->info("Found {$totalCount} legacy user records.");
$roleName = $this->mapRole($legacy->role);
// Pre-cache existing data to speed up individual checks
$this->existingUserIds = User::pluck('id')->flip()->all();
$this->existingUserEmails = User::pluck('email')->flip()->all();
if (! $existingById && ! $existingByEmail) {
$query->orderBy('id')->chunk(50, function ($chunk) {
foreach ($chunk as $legacy) {
$this->processUser($legacy);
}
});
$username = $this->generateUniqueUsername($legacy->name, $legacy->email);
$this->newLine();
$this->info('User migration completed successfully!');
}
$userId = DB::table('users')->insertGetId([
'id' => $legacy->id,
'name' => Str::limit($legacy->name, 100),
'email' => $legacy->email,
'username' => $username,
'password' => $legacy->password,
'is_active' => $legacy->status == '2'
? IsActive::INACTIVE->value
: IsActive::ACTIVE->value,
'email_verified_at' => now(),
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
]);
/**
* Process a single legacy user record.
*/
private function processUser($legacy): void
{
$this->output->write("<info>[#{$legacy->id}]</info> Processing: <comment>".Str::limit($legacy->name, 40).'</comment>... ');
$user = User::find($userId);
// Check for duplicates by ID or Email
if (isset($this->existingUserIds[$legacy->id]) || isset($this->existingUserEmails[$legacy->email])) {
$this->output->writeln('<info>SKIP (Already Exists)</info>');
if ($user && $roleName) {
return;
}
try {
$username = $this->generateUniqueUsername($legacy->name, $legacy->email);
$id = DB::table('users')->insertGetId([
'id' => $legacy->id,
'name' => Str::limit($legacy->name, 100),
'email' => $legacy->email,
'username' => $username,
'password' => $legacy->password,
'is_active' => $legacy->status == '2'
? IsActive::INACTIVE->value
: IsActive::ACTIVE->value,
'email_verified_at' => now(),
'created_at' => $legacy->created_at ?? now(),
'updated_at' => $legacy->updated_at ?? now(),
]);
$user = User::find($id);
if ($user) {
// Assign Role
$roleName = $this->mapRole($legacy->role);
if ($roleName) {
$user->syncRoles([$roleName]);
}
// Initialize User Settings if missing
if (! $user->settings()->exists()) {
$user->settings()->create([
'notif_style' => NotifStyle::FORMAL,
@ -81,18 +117,21 @@ public function handle()
'top_navigation' => false,
]);
}
} else {
$this->info('User already exists: '.$legacy->email);
}
}
$this->newLine();
$this->info('User migration completed successfully!');
$this->output->writeln('<info>DONE</info>');
} else {
$this->output->writeln('<error>FAILED TO RETRIEVE</error>');
}
} catch (\Exception $e) {
$this->output->writeln('<error>FAILED: '.$e->getMessage().'</error>');
}
}
private function generateUniqueUsername($name, $email)
/**
* Generate a unique username based on name or email.
*/
private function generateUniqueUsername($name, $email): string
{
// Try name first
$base = Str::limit(Str::slug($name, ''), 10, '');
if (empty($base)) {
$base = Str::limit(explode('@', $email)[0], 10, '');
@ -101,7 +140,6 @@ private function generateUniqueUsername($name, $email)
$username = $base;
$counter = 1;
// Ensure unique within 10 chars
while (User::where('username', $username)->exists()) {
$suffix = (string) $counter;
$maxBaseLength = 10 - strlen($suffix);
@ -110,12 +148,15 @@ private function generateUniqueUsername($name, $email)
if ($counter > 999) {
break;
} // Safety
}
}
return $username;
}
/**
* Map legacy role IDs to RoleEnum.
*/
private function mapRole($legacyRole): ?string
{
return match ((int) $legacyRole) {

View File

@ -92,7 +92,7 @@ public static function configure(Schema $schema): Schema
->disk(config('filesystems.default'))
->acceptedFileTypes(['image/*'])
->maxSize(1024 * 3)
->collection('content-recpaps')
->collection('content-recaps')
->customProperties(fn (): array => [
'feature' => 'content-recaps',
'date' => now()->toDateString(),