feat: implement old data migration command and related migration classes

This commit is contained in:
Yoga Pangestu 2026-08-04 10:41:01 +07:00
parent 72848d51d8
commit c063cde967
14 changed files with 558 additions and 0 deletions

View File

@ -0,0 +1,115 @@
<?php
namespace App\Console\Commands;
use App\OldDataMigrations\BaseOldDataMigration;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\DB;
use Spatie\Permission\PermissionRegistrar;
class MigrateOldDataCommand extends Command
{
protected $signature = 'app:migrate-old-data {--only=} {--skip=} {--force}';
protected $description = 'Migrate data from old database to new database';
public function handle(): int
{
$filesystem = new Filesystem;
$migrationPath = app_path('OldDataMigrations');
require_once $migrationPath.'/BaseOldDataMigration.php';
$files = collect($filesystem->glob($migrationPath.'/*.php'))
->map(fn ($file) => basename($file))
->filter(fn ($file) => $file !== 'BaseOldDataMigration.php')
->toArray();
$migrations = [];
foreach ($files as $file) {
require_once $migrationPath.'/'.$file;
$className = 'App\\OldDataMigrations\\'.str_replace('.php', '', $file);
if (! class_exists($className)) {
continue;
}
$instance = new $className;
if ($instance instanceof BaseOldDataMigration) {
$migrations[] = $instance;
}
}
usort($migrations, fn ($a, $b) => $a->priority() <=> $b->priority());
$only = $this->option('only') ? explode(',', $this->option('only')) : null;
$skip = $this->option('skip') ? explode(',', $this->option('skip')) : [];
BaseOldDataMigration::$truncateBeforeInsert = $this->option('force');
$this->newLine();
$this->info('=== Migrate Old Data ===');
$this->newLine();
DB::connection('mysql')->getPdo()->exec('SET FOREIGN_KEY_CHECKS=0');
$this->warn('Foreign key checks disabled.');
app(PermissionRegistrar::class)->forgetCachedPermissions();
$totalRows = 0;
$tableSummary = [];
foreach ($migrations as $migration) {
if ($only !== null) {
$results = $migration->migrate();
$groupTableNames = array_keys($results);
$hasOverlap = false;
foreach ($only as $t) {
if (in_array($t, $groupTableNames)) {
$hasOverlap = true;
break;
}
}
if (! $hasOverlap) {
continue;
}
}
$className = (new \ReflectionClass($migration))->getShortName();
$this->info("Running: {$className}");
$results = $migration->migrate();
foreach ($results as $table => $count) {
if (in_array($table, $skip)) {
$this->line(" SKIP {$table}");
continue;
}
$tableSummary[$table] = $count;
$totalRows += $count;
$this->line(" OK {$table}: {$count} rows");
}
$this->newLine();
}
DB::connection('mysql')->getPdo()->exec('SET FOREIGN_KEY_CHECKS=1');
app(PermissionRegistrar::class)->forgetCachedPermissions();
$this->warn('Foreign key checks re-enabled.');
$this->newLine();
$this->info('=== Summary ===');
$this->info('Total tables: '.count($tableSummary));
$this->info("Total rows: {$totalRows}");
$this->newLine();
$this->table(['Table', 'Rows'], collect($tableSummary)->map(fn ($count, $table) => [$table, $count])->values()->toArray());
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace App\OldDataMigrations;
use Illuminate\Support\Facades\DB;
abstract class BaseOldDataMigration
{
protected $old;
protected $new;
public static bool $truncateBeforeInsert = false;
public function __construct()
{
$this->old = DB::connection('mysql_old');
$this->new = DB::connection('mysql');
}
abstract public function migrate(): array;
abstract public function priority(): int;
protected function oldQuery(string $table)
{
return $this->old->table($table);
}
protected function truncateIfNeeded(string $table): void
{
if (static::$truncateBeforeInsert) {
$this->new->table($table)->truncate();
}
}
protected function insertBatch(string $table, array $rows, int $chunkSize = 500): int
{
if (empty($rows)) {
return 0;
}
$total = 0;
foreach (array_chunk($rows, $chunkSize) as $chunk) {
$this->new->table($table)->insert($chunk);
$total += count($chunk);
}
return $total;
}
protected function migrateTable(string $table, ?callable $transform = null, ?array $columns = null): int
{
$this->truncateIfNeeded($table);
$rows = $this->oldQuery($table)->get();
if ($rows->isEmpty()) {
return 0;
}
$rows = $rows->map(function ($row) use ($columns) {
$data = (array) $row;
if ($columns !== null) {
return array_intersect_key($data, array_flip($columns));
}
return $data;
})->toArray();
if ($transform !== null) {
$rows = array_map($transform, $rows);
}
return $this->insertBatch($table, $rows);
}
protected function migrateTableWithoutColumns(string $table, array $skipColumns, ?callable $transform = null): int
{
$this->truncateIfNeeded($table);
$rows = $this->oldQuery($table)->get();
if ($rows->isEmpty()) {
return 0;
}
$rows = $rows->map(function ($row) use ($skipColumns) {
$data = (array) $row;
return array_diff_key($data, array_flip($skipColumns));
})->toArray();
if ($transform !== null) {
$rows = array_map($transform, $rows);
}
return $this->insertBatch($table, $rows);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\OldDataMigrations;
class CashFinanceMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 5;
}
public function migrate(): array
{
$results = [];
$results['cash_accounts'] = $this->migrateTable('cash_accounts');
$results['cash_transactions'] = $this->migrateTable('cash_transactions');
$results['expenses'] = $this->migrateTable('expenses');
return $results;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\OldDataMigrations;
class CoreAuthMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 1;
}
public function migrate(): array
{
$results = [];
$results['users'] = $this->migrateTable('users');
$results['user_profiles'] = $this->migrateTable('user_profiles');
$results['employees'] = $this->migrateTable('employees');
$results['permissions'] = $this->migrateTable('permissions');
$results['roles'] = $this->migrateTable('roles');
$results['model_has_permissions'] = $this->migrateTable('model_has_permissions');
$results['model_has_roles'] = $this->migrateTable('model_has_roles');
$results['role_has_permissions'] = $this->migrateTable('role_has_permissions');
return $results;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\OldDataMigrations;
class CuttingMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 10;
}
public function migrate(): array
{
$results = [];
$results['cuttings'] = $this->migrateTable('cuttings');
$results['cutting_material_combinations'] = $this->migrateTable('cutting_material_combinations');
$results['cutting_materials'] = $this->migrateTable('cutting_materials', function ($row) {
if (isset($row['material_usage']) && is_string($row['material_usage'])) {
$row['material_usage'] = (int) round($row['material_usage']);
}
return $row;
});
$results['cutting_results'] = $this->migrateTable('cutting_results');
return $results;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\OldDataMigrations;
class HrMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 6;
}
public function migrate(): array
{
$results = [];
$results['employee_advances'] = $this->migrateTable('employee_advances');
$results['employee_advance_payments'] = $this->migrateTable('employee_advance_payments');
$results['attendances'] = $this->migrateTable('attendances');
$results['leave_requests'] = $this->migrateTable('leave_requests');
$results['payroll_periods'] = $this->migrateTable('payroll_periods');
$results['payrolls'] = $this->migrateTable('payrolls');
$results['payroll_adjustments'] = $this->migrateTable('payroll_adjustments');
return $results;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\OldDataMigrations;
class InventoryMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 12;
}
public function migrate(): array
{
$results = [];
$results['retail_stock_histories'] = $this->migrateTable('retail_stock_histories');
$results['stok_opnames'] = $this->migrateTable('stok_opnames');
$results['stok_opname_items'] = $this->migrateTable('stok_opname_items');
$results['restocks'] = $this->migrateTable('restocks');
$results['restock_items'] = $this->migrateTable('restock_items');
$results['stock_mutations'] = $this->migrateTable('stock_mutations', function ($row) {
$fields = ['quantity', 'stock_before', 'stock_after'];
foreach ($fields as $field) {
if (isset($row[$field]) && is_string($row[$field])) {
$row[$field] = (int) round($row[$field]);
}
}
return $row;
});
return $results;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\OldDataMigrations;
class MasterDataMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 2;
}
public function migrate(): array
{
$results = [];
$results['categories'] = $this->migrateTable('categories');
$results['suppliers'] = $this->migrateTable('suppliers');
$results['customers'] = $this->migrateTable('customers');
return $results;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\OldDataMigrations;
class MediaMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 8;
}
public function migrate(): array
{
$results = [];
$results['media'] = $this->migrateTable('media', function ($row) {
$row['disk'] = 's3';
$row['conversions_disk'] = 's3';
return $row;
});
return $results;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\OldDataMigrations;
class OrdersMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 9;
}
public function migrate(): array
{
$results = [];
$results['orders'] = $this->migrateTable('orders', function ($row) {
$row['cogs'] = $row['cogs'] ?? 0;
$row['price_type'] = match ($row['price_type']) {
'grosir' => 'wholesale',
'harga_modal' => 'capital',
default => $row['price_type'],
};
return $row;
});
$results['order_items'] = $this->migrateTable('order_items', function ($row) {
$row['stock_quality'] = match ($row['stock_quality']) {
'retail' => 'good',
default => $row['stock_quality'],
};
return $row;
});
$results['activity_log'] = $this->migrateTable('activity_log');
return $results;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\OldDataMigrations;
class ProductsMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 3;
}
public function migrate(): array
{
$results = [];
$results['products'] = $this->migrateTable('products');
$results['product_categories'] = $this->migrateTable('product_categories');
$results['product_variants'] = $this->migrateTable('product_variants');
$results['product_prices'] = $this->migrateTableWithoutColumns(
'product_prices',
['deleted_at'],
function ($row) {
$row['type'] = match ($row['type']) {
'grosir' => 'wholesale',
'harga_modal' => 'capital',
default => $row['type'],
};
return $row;
},
);
return $results;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\OldDataMigrations;
class PurchasesMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 7;
}
public function migrate(): array
{
$results = [];
$results['purchases'] = $this->migrateTable('purchases');
$results['purchase_items'] = $this->migrateTable('purchase_items');
return $results;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\OldDataMigrations;
class RawMaterialsMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 4;
}
public function migrate(): array
{
$results = [];
$results['raw_materials'] = $this->migrateTable('raw_materials', function ($row) {
$row['unit'] = match ($row['unit']) {
'kilogram' => 'kg',
default => $row['unit'],
};
return $row;
});
$results['raw_material_prices'] = $this->migrateTable('raw_material_prices');
return $results;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\OldDataMigrations;
class SystemConfigMigration extends BaseOldDataMigration
{
public function priority(): int
{
return 11;
}
public function migrate(): array
{
$results = [];
$results['system_configurations'] = $this->migrateTable('system_configurations');
$results['push_subscriptions'] = $this->migrateTable('push_subscriptions', function ($row) {
if (isset($row['user_id']) && $row['user_id'] !== null) {
$row['subscribable_type'] = 'App\\Models\\User';
$row['subscribable_id'] = $row['user_id'];
}
unset($row['user_id']);
return $row;
});
$results['homepage_configurations'] = $this->migrateTable('homepage_configurations');
$results['owner_verification_requests'] = $this->migrateTable('owner_verification_requests');
$results['notifications'] = $this->migrateTable('notifications');
return $results;
}
}