array( 'label' => 'Dev préprod', 'config' => $arrConDatabasePreProd, 'is_source' => true, ), 'dev_prod' => array( 'label' => 'Dev prod', 'config' => $arrConDatabaseProd, 'is_source' => false, ), ); if (!empty($arrConDatabaseClientPreProd)) { $tabEnvs['client_preprod'] = array( 'label' => 'Client préprod', 'config' => $arrConDatabaseClientPreProd, 'is_source' => false, ); } if (!empty($arrConDatabaseClientProd)) { $tabEnvs['client_prod'] = array( 'label' => 'Client prod', 'config' => $arrConDatabaseClientProd, 'is_source' => false, ); } return $tabEnvs; } /** Connexion MySQL avec timeout court, sans die() — pour BD client additionnelles. */ class clsMysqlStaticSync extends clsMysql { function __construct($arrDatabase = array()) { $this->last_id = 0; $this->affected_rows = 0; $this->con = null; if (empty($arrDatabase['host']) || empty($arrDatabase['db'])) { return; } $mysqli = mysqli_init(); if (!$mysqli) { return; } mysqli_options($mysqli, MYSQLI_OPT_CONNECT_TIMEOUT, 3); if (!@mysqli_real_connect( $mysqli, $arrDatabase['host'], $arrDatabase['user'], $arrDatabase['pass'], $arrDatabase['db'], 3306 )) { return; } mysqli_set_charset($mysqli, 'utf8'); $this->con = $mysqli; } } function fxStaticSyncConnect($arrConfig) { if (empty($arrConfig['host']) || empty($arrConfig['db'])) { return null; } $obj = new clsMysqlStaticSync($arrConfig); return !empty($obj->con) ? $obj : null; } /** * Dev : réutilise objDatabase* de inc_fonctions.php. * Client : $arrConDatabaseClient* de inc_settings.php (si définis pour l'environnement). */ function fxStaticSyncOpenConnections() { global $objDatabasePreProd, $objDatabaseProd; global $arrConDatabaseClientPreProd, $arrConDatabaseClientProd; $tabConnections = array( 'dev_preprod' => !empty($objDatabasePreProd) ? $objDatabasePreProd : null, 'dev_prod' => !empty($objDatabaseProd) ? $objDatabaseProd : null, ); if (!empty($arrConDatabaseClientPreProd)) { $tabConnections['client_preprod'] = fxStaticSyncConnect($arrConDatabaseClientPreProd); } if (!empty($arrConDatabaseClientProd)) { $tabConnections['client_prod'] = fxStaticSyncConnect($arrConDatabaseClientProd); } return $tabConnections; } function fxStaticSyncTableExists($objDb, $strTable) { if (!$objDb) { return false; } $sql = "SHOW TABLES LIKE '" . $objDb->fxEscape($strTable) . "'"; $row = $objDb->fxGetRow($sql); return !empty($row); } function fxStaticSyncRowBusinessKey(array $row, array $tabKeyCols) { $tabParts = array(); foreach ($tabKeyCols as $strCol) { $tabParts[] = isset($row[$strCol]) ? (string)$row[$strCol] : ''; } return implode("\0", $tabParts); } /** * Colonnes communes source/cible pour compare et copy (alignés). * missing_in_target / extra_in_target : écarts de structure (hors ignore_cols). */ function fxStaticSyncResolveCompareColumns($objSource, $objTarget, $strTable, array $tabIgnoreCols) { $tabSourceCols = fxStaticSyncGetTableColumnNames($objSource, $strTable); $tabTargetCols = fxStaticSyncGetTableColumnNames($objTarget, $strTable); $tabCommon = array(); foreach ($tabSourceCols as $strCol) { if (in_array($strCol, $tabTargetCols, true)) { $tabCommon[] = $strCol; } } $tabCompareCols = array(); foreach ($tabCommon as $strCol) { if (!in_array($strCol, $tabIgnoreCols, true)) { $tabCompareCols[] = $strCol; } } $tabMissingInTarget = array(); foreach ($tabSourceCols as $strCol) { if (!in_array($strCol, $tabTargetCols, true) && !in_array($strCol, $tabIgnoreCols, true)) { $tabMissingInTarget[] = $strCol; } } $tabExtraInTarget = array(); foreach ($tabTargetCols as $strCol) { if (!in_array($strCol, $tabSourceCols, true) && !in_array($strCol, $tabIgnoreCols, true)) { $tabExtraInTarget[] = $strCol; } } return array( 'copy_cols' => $tabCommon, 'compare_cols' => $tabCompareCols, 'missing_in_target' => $tabMissingInTarget, 'extra_in_target' => $tabExtraInTarget, ); } function fxStaticSyncSchemasAligned(array $tabSchema) { return count($tabSchema['missing_in_target']) === 0 && count($tabSchema['extra_in_target']) === 0; } function fxStaticSyncFormatSchemaDiff(array $tabSchema) { $tabParts = array(); if (!empty($tabSchema['missing_in_target'])) { $tabParts[] = 'absentes en cible : ' . implode(', ', $tabSchema['missing_in_target']); } if (!empty($tabSchema['extra_in_target'])) { $tabParts[] = 'en trop en cible : ' . implode(', ', $tabSchema['extra_in_target']); } return implode(' · ', $tabParts); } function fxStaticSyncRowContentHash(array $row, array $tabCompareCols) { $tabData = array(); foreach ($tabCompareCols as $strCol) { $tabData[$strCol] = array_key_exists($strCol, $row) ? $row[$strCol] : null; } ksort($tabData); return md5(json_encode($tabData, JSON_UNESCAPED_UNICODE)); } function fxStaticSyncLoadKeyedRows($objDb, array $tabDef, array $tabCompareCols = null) { $strTable = $tabDef['table']; $tabKeys = $tabDef['keys']; $tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array(); if (!fxStaticSyncTableExists($objDb, $strTable)) { return array('error' => 'missing_table', 'rows' => array(), 'count' => 0); } $sql = 'SELECT * FROM `' . $strTable . '`'; $tabResults = $objDb->fxGetResults($sql); $tabKeyed = array(); if (!is_array($tabResults)) { return array('error' => null, 'rows' => array(), 'count' => 0); } if ($tabCompareCols === null) { $tabCompareCols = array(); if (count($tabResults) > 0) { foreach (array_keys($tabResults[1]) as $strCol) { if (!in_array($strCol, $tabIgnore, true)) { $tabCompareCols[] = $strCol; } } } } foreach ($tabResults as $row) { if (!is_array($row)) { continue; } $strKey = fxStaticSyncRowBusinessKey($row, $tabKeys); $tabKeyed[$strKey] = array( 'hash' => fxStaticSyncRowContentHash($row, $tabCompareCols), 'row' => $row, ); } return array('error' => null, 'rows' => $tabKeyed, 'count' => count($tabKeyed)); } /** Sync d'écriture interdite vers client prod (comparaison seule). */ function fxStaticSyncWriteAllowed($strEnvId) { return $strEnvId !== 'client_prod'; } /** Environnements client : confirmation renforcée (nom de BD à retaper). */ function fxStaticSyncRequiresDbConfirm($strEnvId) { return ($strEnvId === 'client_preprod' || $strEnvId === 'client_prod'); } function fxStaticSyncFinalizeCompareResult(array $tabCmp) { if (in_array($tabCmp['status'], array('missing_table', 'schema_mismatch', 'no_connection'), true)) { return $tabCmp; } $intMissingKeys = count($tabCmp['missing_keys']); $intExtraKeys = count($tabCmp['extra_keys']); $intDiffKeys = count($tabCmp['diff_keys']); if ($intMissingKeys === 0 && $intExtraKeys === 0 && $intDiffKeys === 0) { $tabCmp['status'] = 'ok'; return $tabCmp; } $intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count']; if ($intDelta < 0) { $tabCmp['status'] = 'missing_rows'; } elseif ($intDelta > 0) { $tabCmp['status'] = 'extra_rows'; } else { $tabCmp['status'] = 'content_diff'; } return $tabCmp; } /** * @return array status: ok|content_diff|missing_rows|extra_rows|missing_table|schema_mismatch|no_connection|error */ function fxStaticSyncCompareTable(array $tabSourceData, array $tabTargetData, array $tabSchemaInfo = array()) { if (!empty($tabTargetData['error']) && $tabTargetData['error'] === 'missing_table') { return array( 'status' => 'missing_table', 'source_count' => $tabSourceData['count'], 'target_count' => 0, 'missing_keys' => array(), 'extra_keys' => array(), 'diff_keys' => array(), ); } $tabSourceRows = $tabSourceData['rows']; $tabTargetRows = $tabTargetData['rows']; $tabMissing = array(); $tabDiff = array(); foreach ($tabSourceRows as $strKey => $tabRow) { if (!isset($tabTargetRows[$strKey])) { $tabMissing[] = $strKey; continue; } if ($tabTargetRows[$strKey]['hash'] !== $tabRow['hash']) { $tabDiff[] = $strKey; } } $tabExtra = array(); foreach ($tabTargetRows as $strKey => $tabRow) { if (!isset($tabSourceRows[$strKey])) { $tabExtra[] = $strKey; } } return fxStaticSyncFinalizeCompareResult(array( 'status' => 'pending', 'source_count' => $tabSourceData['count'], 'target_count' => $tabTargetData['count'], 'missing_keys' => $tabMissing, 'extra_keys' => $tabExtra, 'diff_keys' => $tabDiff, 'schema_missing_cols' => isset($tabSchemaInfo['missing_in_target']) ? $tabSchemaInfo['missing_in_target'] : array(), 'schema_extra_cols' => isset($tabSchemaInfo['extra_in_target']) ? $tabSchemaInfo['extra_in_target'] : array(), )); } function fxStaticSyncDbLastError($objDb) { if (!$objDb || empty($objDb->con)) { return ''; } return mysqli_error($objDb->con); } /** * Exécute une requête et remonte l'erreur MySQL (sans die ni echo legacy). */ function fxStaticSyncExecQuery($objDb, $strSql) { if (!$objDb) { return array('ok' => false, 'error' => 'Connexion BD invalide.'); } $blnOk = $objDb->fxQuery($strSql); if ($blnOk === false) { return array( 'ok' => false, 'error' => fxStaticSyncDbLastError($objDb) ?: 'Erreur SQL inconnue.', ); } return array('ok' => true, 'error' => null); } function fxStaticSyncGetTableColumnNames($objDb, $strTable) { if (!$objDb) { return array(); } $tabCols = $objDb->fxGetResults('SHOW COLUMNS FROM `' . $strTable . '`'); $tabNames = array(); if (!is_array($tabCols)) { return array(); } foreach ($tabCols as $row) { if (is_array($row) && !empty($row['Field'])) { $tabNames[] = $row['Field']; } } return $tabNames; } function fxStaticSyncGetTableEngine($objDb, $strTable) { if (!$objDb) { return ''; } $row = $objDb->fxGetRow( "SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" . $objDb->fxEscape($strTable) . "'" ); return !empty($row['ENGINE']) ? strtoupper($row['ENGINE']) : ''; } function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef = null) { if (!$objSource || !$objTarget) { return array('ok' => false, 'message' => 'Connexion BD invalide.'); } if (!fxStaticSyncTableExists($objSource, $strTable)) { return array('ok' => false, 'message' => 'Table absente dans la source.'); } if (!fxStaticSyncTableExists($objTarget, $strTable)) { return array('ok' => false, 'message' => 'Table absente dans la cible — exécuter le DDL d\'abord.'); } $tabIgnore = ($tabDef && isset($tabDef['ignore_cols'])) ? $tabDef['ignore_cols'] : array(); $tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $strTable, $tabIgnore); if (!fxStaticSyncSchemasAligned($tabSchema)) { return array( 'ok' => false, 'message' => 'Structures non alignées — ' . fxStaticSyncFormatSchemaDiff($tabSchema) . '. Aligner les structures (Navicat) avant toute sync. Aucune donnée modifiée.', ); } $tabRows = $objSource->fxGetResults('SELECT * FROM `' . $strTable . '`'); $tabCopyCols = $tabSchema['copy_cols']; $tabSkippedCols = array(); if (count($tabCopyCols) === 0 && is_array($tabRows) && count($tabRows) > 0) { return array( 'ok' => false, 'message' => 'Aucune colonne commune entre source et cible — exécuter les scripts DDL manquants.', ); } // Préparer les INSERT avant toute écriture destructive (évite une table vide si échec). $tabInsertBatches = array(); $intCopied = 0; if (count($tabCopyCols) > 0 && is_array($tabRows) && count($tabRows) > 0) { $strColList = '`' . implode('`,`', $tabCopyCols) . '`'; $intBatchSize = 50; $tabBatch = array(); foreach ($tabRows as $row) { if (!is_array($row)) { continue; } $tabVals = array(); foreach ($tabCopyCols as $strCol) { if (!array_key_exists($strCol, $row) || $row[$strCol] === null) { $tabVals[] = 'NULL'; } else { $tabVals[] = "'" . $objTarget->fxEscape($row[$strCol]) . "'"; } } $tabBatch[] = '(' . implode(',', $tabVals) . ')'; if (count($tabBatch) >= $intBatchSize) { $tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch); $intCopied += count($tabBatch); $tabBatch = array(); } } if (count($tabBatch) > 0) { $tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch); $intCopied += count($tabBatch); } } $blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB'); $blnFkDisabled = false; $tabFkOff = fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=0'); if (!$tabFkOff['ok']) { return array('ok' => false, 'message' => 'FK checks : ' . $tabFkOff['error']); } $blnFkDisabled = true; if ($blnTransactional) { $tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION'); if (!$tabTx['ok']) { fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1'); return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']); } } // DELETE plutôt que TRUNCATE : compatible FK (InnoDB) avec checks désactivés. $tabDelete = fxStaticSyncExecQuery($objTarget, 'DELETE FROM `' . $strTable . '`'); if (!$tabDelete['ok']) { if ($blnTransactional) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); } if ($blnFkDisabled) { fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1'); } return array('ok' => false, 'message' => 'Vidage table : ' . $tabDelete['error']); } foreach ($tabInsertBatches as $strSql) { $tabInsert = fxStaticSyncExecQuery($objTarget, $strSql); if (!$tabInsert['ok']) { if ($blnTransactional) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); } else { return array( 'ok' => false, 'message' => 'Insertion : ' . $tabInsert['error'] . ' — la table cible peut être vide (MyISAM, pas de rollback).', ); } if ($blnFkDisabled) { fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1'); } return array('ok' => false, 'message' => 'Insertion : ' . $tabInsert['error'] . ' (rollback effectué).'); } } if ($blnTransactional) { $tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT'); if (!$tabCommit['ok']) { fxStaticSyncExecQuery($objTarget, 'ROLLBACK'); if ($blnFkDisabled) { fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1'); } return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']); } } if ($blnFkDisabled) { fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1'); } $strMessage = $intCopied . ' ligne(s) copiée(s).'; return array( 'ok' => true, 'message' => $strMessage, 'rows' => $intCopied, ); } function fxStaticSyncStatusLabel($strStatus, array $tabCmp = null) { switch ($strStatus) { case 'ok': return 'Données alignées'; case 'missing_table': return 'Table absente'; case 'schema_mismatch': return 'Structures non alignées'; case 'no_connection': return 'Connexion impossible'; } if (is_array($tabCmp)) { $intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count']; if ($intDelta < 0) { return 'Il en manque ' . abs($intDelta); } if ($intDelta > 0) { return 'Il y en a ' . $intDelta . ' en trop'; } return 'Contenu différent'; } switch ($strStatus) { case 'content_diff': return 'Contenu différent'; case 'missing_rows': return 'Il en manque'; case 'extra_rows': return 'Il y en a en trop'; default: return 'Erreur'; } } function fxStaticSyncStatusClass($strStatus) { switch ($strStatus) { case 'ok': return 'ss-ok'; case 'content_diff': case 'missing_rows': return 'ss-warn'; case 'extra_rows': return 'ss-bad'; case 'missing_table': case 'no_connection': return 'ss-muted'; case 'schema_mismatch': return 'ss-schema'; default: return 'ss-bad'; } } function fxStaticSyncFormatKeyPreview($strKey) { $strKey = str_replace("\0", ' · ', $strKey); if (strlen($strKey) > 120) { return substr($strKey, 0, 117) . '…'; } return $strKey; } function fxStaticSyncEnsureCsrfToken() { if (empty($_SESSION['ms1_static_sync_csrf'])) { $_SESSION['ms1_static_sync_csrf'] = bin2hex(random_bytes(16)); } return $_SESSION['ms1_static_sync_csrf']; } function fxStaticSyncValidateCsrf($strToken) { return !empty($_SESSION['ms1_static_sync_csrf']) && hash_equals($_SESSION['ms1_static_sync_csrf'], (string)$strToken); } function fxStaticSyncFindTableDef($strTable) { foreach (fxStaticSyncTableDefinitions() as $tabDef) { if ($tabDef['table'] === $strTable) { return $tabDef; } } return null; } function fxStaticSyncFindEnvDef($strEnvId) { $tabEnvs = fxStaticSyncEnvironmentDefinitions(); return isset($tabEnvs[$strEnvId]) ? $tabEnvs[$strEnvId] : null; } function fxStaticSyncCompareEnvPair($objSource, $objTarget, array $tabDef) { $tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array(); if (!fxStaticSyncTableExists($objTarget, $tabDef['table'])) { $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef); return array( 'status' => 'missing_table', 'source_count' => $tabSourceData['count'], 'target_count' => 0, 'missing_keys' => array(), 'extra_keys' => array(), 'diff_keys' => array(), 'schema_missing_cols' => array(), 'schema_extra_cols' => array(), ); } $tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $tabDef['table'], $tabIgnore); if (!fxStaticSyncSchemasAligned($tabSchema)) { $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef); $tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef); return array( 'status' => 'schema_mismatch', 'source_count' => $tabSourceData['count'], 'target_count' => $tabTargetData['count'], 'missing_keys' => array(), 'extra_keys' => array(), 'diff_keys' => array(), 'schema_missing_cols' => $tabSchema['missing_in_target'], 'schema_extra_cols' => $tabSchema['extra_in_target'], ); } $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']); $tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']); return fxStaticSyncCompareTable($tabSourceData, $tabTargetData, $tabSchema); } function fxStaticSyncBuildReport($tabConnections) { $tabDefs = fxStaticSyncTableDefinitions(); $tabEnvs = fxStaticSyncEnvironmentDefinitions(); $objSource = $tabConnections['dev_preprod']; $tabReport = array(); foreach ($tabDefs as $tabDef) { $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef); $tabRow = array( 'def' => $tabDef, 'source_count' => $tabSourceData['count'], 'envs' => array(), ); foreach ($tabEnvs as $strEnvId => $tabEnv) { if (!empty($tabEnv['is_source'])) { continue; } $objTarget = isset($tabConnections[$strEnvId]) ? $tabConnections[$strEnvId] : null; if (!$objTarget) { $tabRow['envs'][$strEnvId] = array( 'status' => 'no_connection', 'source_count' => $tabSourceData['count'], 'target_count' => 0, 'missing_keys' => array(), 'extra_keys' => array(), 'diff_keys' => array(), 'schema_missing_cols' => array(), 'schema_extra_cols' => array(), ); continue; } $tabRow['envs'][$strEnvId] = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef); } $tabReport[] = $tabRow; } return $tabReport; } function fxStaticSyncGetDiffDetail($strTable, $strEnvId, $tabConnections) { $tabDef = fxStaticSyncFindTableDef($strTable); $tabEnv = fxStaticSyncFindEnvDef($strEnvId); if (!$tabDef || !$tabEnv || !empty($tabEnv['is_source'])) { return null; } $objSource = $tabConnections['dev_preprod']; $objTarget = $tabConnections[$strEnvId]; if (!$objSource || !$objTarget) { return null; } $tabCompare = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef); $tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array(); $tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $tabDef['table'], $tabIgnore); $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']); $tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']); $tabDetail = array( 'compare' => $tabCompare, 'samples' => array(), ); $tabKeysToShow = array_slice( array_merge($tabCompare['missing_keys'], $tabCompare['diff_keys'], $tabCompare['extra_keys']), 0, 30 ); foreach ($tabKeysToShow as $strKey) { $tabSample = array( 'key' => $strKey, 'type' => in_array($strKey, $tabCompare['extra_keys'], true) ? 'extra' : (in_array($strKey, $tabCompare['missing_keys'], true) ? 'missing' : 'diff'), 'source' => isset($tabSourceData['rows'][$strKey]['row']) ? $tabSourceData['rows'][$strKey]['row'] : null, 'target' => isset($tabTargetData['rows'][$strKey]['row']) ? $tabTargetData['rows'][$strKey]['row'] : null, ); if ($tabDef['table'] === 'info' && $tabSample['type'] === 'diff') { $tabSample['source_texte'] = isset($tabSample['source']['info_texte']) ? $tabSample['source']['info_texte'] : ''; $tabSample['target_texte'] = isset($tabSample['target']['info_texte']) ? $tabSample['target']['info_texte'] : ''; } $tabDetail['samples'][] = $tabSample; } return $tabDetail; }