Files
2026-04-28 21:14:25 +03:00

413 lines
14 KiB
PHP
Raw Permalink 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\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;
}
}