413 lines
14 KiB
PHP
413 lines
14 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Services\NaksSyncService;
|
||
use Illuminate\Console\Command;
|
||
|
||
/**
|
||
* NAKS Multi-Module Synchronization Command
|
||
*
|
||
* Synchronizes NAKS data across all project sites for multiple modules:
|
||
* - technology: NAKS Technology (naks_certificates)
|
||
* - welder: NAKS Welder (naks_welders)
|
||
* - consumables: NAKS Consumables (naks_consumables)
|
||
* - expert: NAKS Expert (register_of_experts)
|
||
* - equipment: NAKS Equipment (welding_equipment)
|
||
*
|
||
* Usage:
|
||
* php artisan naks:sync # Sync all modules from all projects
|
||
* php artisan naks:sync --module=welder # Sync only welder module
|
||
* php artisan naks:sync --project=101 # Sync specific project only
|
||
* php artisan naks:sync --dry-run # Test mode (no changes)
|
||
* php artisan naks:sync --force # Ignore last sync ID, sync all
|
||
* php artisan naks:sync --status # Show sync status only
|
||
* php artisan naks:sync --reset # Reset sync state
|
||
*/
|
||
class NaksSync extends Command
|
||
{
|
||
/**
|
||
* The name and signature of the console command.
|
||
*
|
||
* @var string
|
||
*/
|
||
protected $signature = 'naks:sync
|
||
{--module=all : Module to sync (all, technology, welder, consumables, expert, equipment)}
|
||
{--project= : Filter to sync only a specific project (by name or URL fragment)}
|
||
{--dry-run : Run in test mode without making any changes}
|
||
{--force : Ignore last synced ID and sync all records}
|
||
{--status : Show sync status only, do not sync}
|
||
{--reset : Reset sync state for all or specific module/project}';
|
||
|
||
/**
|
||
* The console command description.
|
||
*
|
||
* @var string
|
||
*/
|
||
protected $description = 'Synchronize NAKS data (certificates, welders, consumables, experts, equipment) from other project sites';
|
||
|
||
/**
|
||
* The sync service instance.
|
||
*/
|
||
protected NaksSyncService $syncService;
|
||
|
||
/**
|
||
* Create a new command instance.
|
||
*/
|
||
public function __construct(NaksSyncService $syncService)
|
||
{
|
||
parent::__construct();
|
||
$this->syncService = $syncService;
|
||
}
|
||
|
||
/**
|
||
* Execute the console command.
|
||
*/
|
||
public function handle(): int
|
||
{
|
||
$module = $this->option('module') ?? 'all';
|
||
$project = $this->option('project');
|
||
$dryRun = (bool) $this->option('dry-run');
|
||
$force = (bool) $this->option('force');
|
||
$statusOnly = (bool) $this->option('status');
|
||
$reset = (bool) $this->option('reset');
|
||
|
||
$this->info('╔════════════════════════════════════════════════════════════╗');
|
||
$this->info('║ NAKS Multi-Module Cross-Site Synchronization ║');
|
||
$this->info('╚════════════════════════════════════════════════════════════╝');
|
||
$this->newLine();
|
||
|
||
// Show available modules
|
||
$this->showAvailableModules();
|
||
|
||
// Handle status display
|
||
if ($statusOnly) {
|
||
return $this->showStatus($module);
|
||
}
|
||
|
||
// Handle reset
|
||
if ($reset) {
|
||
return $this->resetSyncState($module, $project);
|
||
}
|
||
|
||
// Validate module
|
||
if ($module !== 'all' && !$this->syncService->getModuleConfig($module)) {
|
||
$this->error("❌ Invalid module: {$module}");
|
||
$this->line("Available modules: all, technology, welder, consumables, expert, equipment");
|
||
return self::FAILURE;
|
||
}
|
||
|
||
// Show mode information
|
||
if ($dryRun) {
|
||
$this->warn('⚠️ DRY RUN MODE - No changes will be made');
|
||
$this->newLine();
|
||
}
|
||
|
||
if ($force) {
|
||
$this->warn('⚠️ FORCE MODE - Ignoring last sync ID, syncing all records');
|
||
$this->newLine();
|
||
}
|
||
|
||
$this->info("📦 Module(s): " . ($module === 'all' ? 'All modules' : $module));
|
||
|
||
if ($project) {
|
||
$this->info("📌 Project filter: {$project}");
|
||
}
|
||
$this->newLine();
|
||
|
||
// Check credentials
|
||
if (!env('SYNC_ADMIN_EMAIL') || !env('SYNC_ADMIN_PASSWORD')) {
|
||
$this->error('❌ SYNC_ADMIN_EMAIL and SYNC_ADMIN_PASSWORD must be set in .env');
|
||
return self::FAILURE;
|
||
}
|
||
|
||
// Show available projects
|
||
$this->showAvailableProjects();
|
||
|
||
// Run sync
|
||
$this->info('🔄 Starting synchronization...');
|
||
$this->newLine();
|
||
|
||
$startTime = microtime(true);
|
||
|
||
try {
|
||
$stats = $this->syncService->sync($module, $project, $dryRun, $force);
|
||
} catch (\Exception $e) {
|
||
$this->error('❌ Sync failed: ' . $e->getMessage());
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$duration = round(microtime(true) - $startTime, 2);
|
||
|
||
// Display results
|
||
$this->displayResults($stats, $duration, $dryRun);
|
||
|
||
// Return appropriate exit code
|
||
if (!empty($stats['errors']) || !empty($stats['failed_projects'])) {
|
||
return self::FAILURE;
|
||
}
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
/**
|
||
* Show available modules
|
||
*/
|
||
protected function showAvailableModules(): void
|
||
{
|
||
$modules = $this->syncService->getModules();
|
||
|
||
$this->info('📦 Available Modules:');
|
||
$tableData = [];
|
||
foreach ($modules as $key => $config) {
|
||
$tableData[] = [
|
||
$key,
|
||
$config['name'],
|
||
$config['table'],
|
||
implode(' + ', $config['unique_keys']),
|
||
];
|
||
}
|
||
|
||
$this->table(
|
||
['Key', 'Name', 'Table', 'Unique Keys'],
|
||
$tableData
|
||
);
|
||
$this->newLine();
|
||
}
|
||
|
||
/**
|
||
* Show available projects
|
||
*/
|
||
protected function showAvailableProjects(): void
|
||
{
|
||
try {
|
||
$projects = $this->syncService->getProjectUrls();
|
||
|
||
$this->info('📋 Available Projects:');
|
||
$this->table(
|
||
['Project Name', 'URL'],
|
||
array_map(function ($p) {
|
||
return [$p['project_name'] ?? 'Unknown', $p['url'] ?? ''];
|
||
}, $projects)
|
||
);
|
||
$this->newLine();
|
||
} catch (\Exception $e) {
|
||
$this->warn('Could not fetch project list: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display sync results
|
||
*/
|
||
protected function displayResults(array $stats, float $duration, bool $dryRun): void
|
||
{
|
||
$this->newLine();
|
||
$this->info('════════════════════════════════════════════════════════════');
|
||
$this->info(' SYNC RESULTS');
|
||
$this->info('════════════════════════════════════════════════════════════');
|
||
$this->newLine();
|
||
|
||
// Project summary
|
||
$this->table(
|
||
['Metric', 'Value'],
|
||
[
|
||
['Total Projects', $stats['total_projects']],
|
||
['Successful Projects', $stats['successful_projects']],
|
||
['Failed Projects', count($stats['failed_projects'])],
|
||
['Duration', "{$duration}s"],
|
||
]
|
||
);
|
||
|
||
// Module-specific results
|
||
if (!empty($stats['modules'])) {
|
||
$this->newLine();
|
||
$this->info('📦 Module Results:');
|
||
|
||
$moduleData = [];
|
||
$totalSynced = 0;
|
||
$totalInserted = 0;
|
||
$totalUpdated = 0;
|
||
$totalSkipped = 0;
|
||
$totalPdfs = 0;
|
||
|
||
foreach ($stats['modules'] as $key => $moduleStats) {
|
||
$moduleData[] = [
|
||
$moduleStats['name'],
|
||
$moduleStats['total_synced'],
|
||
$moduleStats['total_inserted'],
|
||
$moduleStats['total_updated'],
|
||
$moduleStats['total_skipped'],
|
||
$moduleStats['total_pdfs_downloaded'],
|
||
];
|
||
|
||
$totalSynced += $moduleStats['total_synced'];
|
||
$totalInserted += $moduleStats['total_inserted'];
|
||
$totalUpdated += $moduleStats['total_updated'];
|
||
$totalSkipped += $moduleStats['total_skipped'];
|
||
$totalPdfs += $moduleStats['total_pdfs_downloaded'];
|
||
}
|
||
|
||
// Add totals row
|
||
$moduleData[] = [
|
||
'─────────',
|
||
'─────',
|
||
'────────',
|
||
'───────',
|
||
'───────',
|
||
'────',
|
||
];
|
||
$moduleData[] = [
|
||
'TOTAL',
|
||
$totalSynced,
|
||
$totalInserted,
|
||
$totalUpdated,
|
||
$totalSkipped,
|
||
$totalPdfs,
|
||
];
|
||
|
||
$this->table(
|
||
['Module', 'Synced', 'Inserted', 'Updated', 'Skipped', 'PDFs'],
|
||
$moduleData
|
||
);
|
||
}
|
||
|
||
// Show failed projects
|
||
if (!empty($stats['failed_projects'])) {
|
||
$this->newLine();
|
||
$this->error('❌ Failed Projects:');
|
||
foreach ($stats['failed_projects'] as $failed) {
|
||
$this->line(" • {$failed['name']}: {$failed['error']}");
|
||
}
|
||
}
|
||
|
||
// Show errors
|
||
if (!empty($stats['errors'])) {
|
||
$this->newLine();
|
||
$this->error('❌ Errors:');
|
||
foreach ($stats['errors'] as $error) {
|
||
$this->line(" • {$error}");
|
||
}
|
||
}
|
||
|
||
$this->newLine();
|
||
|
||
if ($dryRun) {
|
||
$this->warn('ℹ️ This was a DRY RUN - no actual changes were made');
|
||
} else {
|
||
$hasSyncedRecords = false;
|
||
foreach ($stats['modules'] as $moduleStats) {
|
||
if ($moduleStats['total_synced'] > 0) {
|
||
$hasSyncedRecords = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ($hasSyncedRecords) {
|
||
$this->info('✅ Synchronization completed successfully!');
|
||
} else {
|
||
$this->info('ℹ️ No new records to sync');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show sync status
|
||
*/
|
||
protected function showStatus(string $module): int
|
||
{
|
||
$this->info('📊 Sync Status:');
|
||
$this->newLine();
|
||
|
||
$tableName = null;
|
||
if ($module !== 'all') {
|
||
$config = $this->syncService->getModuleConfig($module);
|
||
if ($config) {
|
||
$tableName = $config['table'];
|
||
}
|
||
}
|
||
|
||
$states = $this->syncService->getSyncStates($tableName);
|
||
|
||
if (empty($states)) {
|
||
$this->warn('No sync history found. Run "php artisan naks:sync" to start syncing.');
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$tableData = array_map(function ($state) {
|
||
return [
|
||
$state->table_name ?? 'Unknown',
|
||
$state->source_url ?? 'Unknown',
|
||
$state->last_synced_id ?? 0,
|
||
$state->synced_count ?? 0,
|
||
$state->last_synced_at ?? 'Never',
|
||
];
|
||
}, $states);
|
||
|
||
$this->table(
|
||
['Table', 'Source URL', 'Last ID', 'Total Synced', 'Last Sync'],
|
||
$tableData
|
||
);
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
/**
|
||
* Reset sync state
|
||
*/
|
||
protected function resetSyncState(string $module, ?string $project): int
|
||
{
|
||
$tableName = null;
|
||
if ($module !== 'all') {
|
||
$config = $this->syncService->getModuleConfig($module);
|
||
if ($config) {
|
||
$tableName = $config['table'];
|
||
} else {
|
||
$this->error("Invalid module: {$module}");
|
||
return self::FAILURE;
|
||
}
|
||
}
|
||
|
||
$message = 'Reset sync state for ';
|
||
$message .= $module === 'all' ? 'ALL modules' : "module: {$module}";
|
||
if ($project) {
|
||
$message .= " and project: {$project}";
|
||
}
|
||
$message .= '?';
|
||
|
||
if (!$this->confirm($message)) {
|
||
$this->info('Operation cancelled.');
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$sourceUrl = null;
|
||
|
||
if ($project) {
|
||
// Find matching project URL
|
||
try {
|
||
$projects = $this->syncService->getProjectUrls();
|
||
foreach ($projects as $p) {
|
||
if (str_contains(strtolower($p['url']), strtolower($project)) ||
|
||
str_contains(strtolower($p['project_name'] ?? ''), strtolower($project))) {
|
||
$sourceUrl = $p['url'];
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$sourceUrl) {
|
||
$this->error("Project not found: {$project}");
|
||
return self::FAILURE;
|
||
}
|
||
} catch (\Exception $e) {
|
||
$this->error('Could not fetch project list: ' . $e->getMessage());
|
||
return self::FAILURE;
|
||
}
|
||
}
|
||
|
||
$deleted = $this->syncService->resetSyncState($tableName, $sourceUrl);
|
||
|
||
$this->info("✅ Reset {$deleted} sync state record(s)");
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
}
|