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); } function fxStaticSyncRowContentHash(array $row, array $tabIgnoreCols) { $tabData = $row; foreach ($tabIgnoreCols as $strCol) { unset($tabData[$strCol]); } ksort($tabData); return md5(json_encode($tabData, JSON_UNESCAPED_UNICODE)); } function fxStaticSyncLoadKeyedRows($objDb, array $tabDef) { $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); } foreach ($tabResults as $row) { if (!is_array($row)) { continue; } $strKey = fxStaticSyncRowBusinessKey($row, $tabKeys); $tabKeyed[$strKey] = array( 'hash' => fxStaticSyncRowContentHash($row, $tabIgnore), 'row' => $row, ); } return array('error' => null, 'rows' => $tabKeyed, 'count' => count($tabKeyed)); } /** * @return array status: ok|content_diff|missing_rows|extra_rows|missing_table|no_connection|error */ function fxStaticSyncCompareTable(array $tabSourceData, array $tabTargetData) { 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; } } $strStatus = 'ok'; if (count($tabExtra) > 0) { $strStatus = 'extra_rows'; } elseif (count($tabMissing) > 0) { $strStatus = 'missing_rows'; } elseif (count($tabDiff) > 0) { $strStatus = 'content_diff'; } return array( 'status' => $strStatus, 'source_count' => $tabSourceData['count'], 'target_count' => $tabTargetData['count'], 'missing_keys' => $tabMissing, 'extra_keys' => $tabExtra, 'diff_keys' => $tabDiff, ); } function fxStaticSyncCopyTable($objSource, $objTarget, $strTable) { 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.'); } $tabRows = $objSource->fxGetResults('SELECT * FROM `' . $strTable . '`'); $intCopied = 0; $objTarget->fxQuery('SET FOREIGN_KEY_CHECKS=0'); $objTarget->fxQuery('TRUNCATE TABLE `' . $strTable . '`'); if (is_array($tabRows) && count($tabRows) > 0) { $tabColumns = array_keys($tabRows[1]); $strColList = '`' . implode('`,`', $tabColumns) . '`'; $intBatchSize = 50; $tabBatch = array(); foreach ($tabRows as $row) { if (!is_array($row)) { continue; } $tabVals = array(); foreach ($tabColumns 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) { $sql = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch); $objTarget->fxQuery($sql); $intCopied += count($tabBatch); $tabBatch = array(); } } if (count($tabBatch) > 0) { $sql = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch); $objTarget->fxQuery($sql); $intCopied += count($tabBatch); } } $objTarget->fxQuery('SET FOREIGN_KEY_CHECKS=1'); return array( 'ok' => true, 'message' => $intCopied . ' ligne(s) copiée(s).', 'rows' => $intCopied, ); } function fxStaticSyncStatusLabel($strStatus) { switch ($strStatus) { case 'ok': return 'Aligné'; case 'content_diff': return 'Contenu différent'; case 'missing_rows': return 'Lignes manquantes'; case 'extra_rows': return 'Lignes en trop'; case 'missing_table': return 'Table absente'; case 'no_connection': return 'Connexion impossible'; 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'; 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 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(), ); continue; } $tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef); $tabRow['envs'][$strEnvId] = fxStaticSyncCompareTable($tabSourceData, $tabTargetData); } $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; } $tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef); $tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef); $tabCompare = fxStaticSyncCompareTable($tabSourceData, $tabTargetData); $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; }