This commit updates the connection timeout for MySQL operations and introduces new functions for managing connection errors and flash messages. The `fxStaticSyncConnectError` and `fxStaticSyncFormatConnectionError` functions provide clearer error reporting during synchronization. Additionally, the user interface is improved to display relevant flash messages, enhancing the overall user experience and robustness of the synchronization workflow.
924 lines
30 KiB
PHP
924 lines
30 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/inc_fx_static_sync_config.php';
|
|
|
|
/**
|
|
* Environnements comparables — configs depuis inc_settings.php uniquement.
|
|
* Source : $arrConDatabasePreProd (dev préprod sur serveur dev).
|
|
*/
|
|
function fxStaticSyncEnvironmentDefinitions() {
|
|
global $arrConDatabasePreProd, $arrConDatabaseProd;
|
|
global $arrConDatabaseClientPreProd, $arrConDatabaseClientProd;
|
|
|
|
$tabEnvs = array(
|
|
'dev_preprod' => 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, 10);
|
|
|
|
if (!@mysqli_real_connect(
|
|
$mysqli,
|
|
$arrDatabase['host'],
|
|
$arrDatabase['user'],
|
|
$arrDatabase['pass'],
|
|
$arrDatabase['db'],
|
|
3306
|
|
)) {
|
|
if (!isset($GLOBALS['ms1_static_sync_connect_errors'])) {
|
|
$GLOBALS['ms1_static_sync_connect_errors'] = array();
|
|
}
|
|
$GLOBALS['ms1_static_sync_connect_errors'][$arrDatabase['db']] = mysqli_connect_error();
|
|
return;
|
|
}
|
|
|
|
// Pas de mysqli_set_charset ici — charset serveur / table legacy (latin1, utf8, etc.).
|
|
$this->con = $mysqli;
|
|
}
|
|
}
|
|
|
|
function fxStaticSyncConnectError($strDb) {
|
|
if (empty($GLOBALS['ms1_static_sync_connect_errors'][$strDb])) {
|
|
return '';
|
|
}
|
|
|
|
return $GLOBALS['ms1_static_sync_connect_errors'][$strDb];
|
|
}
|
|
|
|
function fxStaticSyncFormatConnectionError($tabEnv) {
|
|
$strDb = isset($tabEnv['config']['db']) ? $tabEnv['config']['db'] : '';
|
|
$strMsg = $tabEnv['label'] . ' (' . $strDb . ')';
|
|
$strErr = fxStaticSyncConnectError($strDb);
|
|
if ($strErr !== '') {
|
|
$strMsg .= ' — ' . $strErr;
|
|
}
|
|
|
|
return $strMsg;
|
|
}
|
|
|
|
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 fxStaticSyncNormalizeCellValue($val) {
|
|
if ($val === null) {
|
|
return null;
|
|
}
|
|
|
|
return (string)$val;
|
|
}
|
|
|
|
function fxStaticSyncSqlOrderByKeys(array $tabKeys) {
|
|
$tabParts = array();
|
|
foreach ($tabKeys as $strCol) {
|
|
$tabParts[] = '`' . $strCol . '`';
|
|
}
|
|
|
|
return implode(',', $tabParts);
|
|
}
|
|
|
|
function fxStaticSyncGetTableRowCount($objDb, $strTable) {
|
|
if (!$objDb || !fxStaticSyncTableExists($objDb, $strTable)) {
|
|
return 0;
|
|
}
|
|
|
|
return (int)$objDb->fxGetVar('SELECT COUNT(*) FROM `' . $strTable . '`');
|
|
}
|
|
|
|
function fxStaticSyncKeyColumnsValid($objDb, $strTable, array $tabKeys) {
|
|
if (count($tabKeys) === 0) {
|
|
return false;
|
|
}
|
|
|
|
$tabCols = fxStaticSyncGetTableColumnNames($objDb, $strTable);
|
|
foreach ($tabKeys as $strCol) {
|
|
if (!in_array($strCol, $tabCols, true)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Charge les lignes — une entrée par clé métier (dernière gagne, ordre stable si ORDER BY possible).
|
|
*/
|
|
function fxStaticSyncFetchRowsByKey($objDb, array $tabDef) {
|
|
$strTable = $tabDef['table'];
|
|
$tabKeys = $tabDef['keys'];
|
|
|
|
if (!fxStaticSyncTableExists($objDb, $strTable)) {
|
|
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => null);
|
|
}
|
|
|
|
if (!fxStaticSyncKeyColumnsValid($objDb, $strTable, $tabKeys)) {
|
|
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
|
|
}
|
|
|
|
$sql = 'SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys);
|
|
$tabResults = $objDb->fxGetResults($sql);
|
|
$tabKeyed = array();
|
|
$intDupes = 0;
|
|
$intPhysical = 0;
|
|
|
|
if (!is_array($tabResults)) {
|
|
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'query_failed');
|
|
}
|
|
|
|
foreach ($tabResults as $row) {
|
|
if (!is_array($row)) {
|
|
continue;
|
|
}
|
|
$intPhysical++;
|
|
$strKey = fxStaticSyncRowBusinessKey($row, $tabKeys);
|
|
if (isset($tabKeyed[$strKey])) {
|
|
$intDupes++;
|
|
}
|
|
$tabKeyed[$strKey] = $row;
|
|
}
|
|
|
|
return array(
|
|
'rows' => $tabKeyed,
|
|
'duplicate_count' => $intDupes,
|
|
'physical_count' => $intPhysical,
|
|
'error' => null,
|
|
);
|
|
}
|
|
|
|
function fxStaticSyncRowContentHash(array $row, array $tabCompareCols) {
|
|
$tabData = array();
|
|
foreach ($tabCompareCols as $strCol) {
|
|
$tabData[$strCol] = array_key_exists($strCol, $row)
|
|
? fxStaticSyncNormalizeCellValue($row[$strCol])
|
|
: null;
|
|
}
|
|
ksort($tabData);
|
|
|
|
return md5(json_encode($tabData, JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
function fxStaticSyncLoadKeyedRows($objDb, array $tabDef, array $tabCompareCols = null) {
|
|
$tabKeys = $tabDef['keys'];
|
|
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
|
|
|
|
if (!fxStaticSyncTableExists($objDb, $tabDef['table'])) {
|
|
return array('error' => 'missing_table', 'rows' => array(), 'count' => 0, 'physical_count' => 0);
|
|
}
|
|
|
|
$tabFetch = fxStaticSyncFetchRowsByKey($objDb, $tabDef);
|
|
if ($tabFetch['error'] === 'bad_keys') {
|
|
return array('error' => 'bad_keys', 'rows' => array(), 'count' => 0, 'physical_count' => 0);
|
|
}
|
|
if ($tabFetch['error'] === 'query_failed') {
|
|
return array('error' => 'query_failed', 'rows' => array(), 'count' => 0, 'physical_count' => 0);
|
|
}
|
|
$tabKeyed = array();
|
|
|
|
if ($tabCompareCols === null) {
|
|
$tabCompareCols = array();
|
|
if (count($tabFetch['rows']) > 0) {
|
|
$row = reset($tabFetch['rows']);
|
|
foreach (array_keys($row) as $strCol) {
|
|
if (!in_array($strCol, $tabIgnore, true)) {
|
|
$tabCompareCols[] = $strCol;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($tabFetch['rows'] as $strKey => $row) {
|
|
$tabKeyed[$strKey] = array(
|
|
'hash' => fxStaticSyncRowContentHash($row, $tabCompareCols),
|
|
'row' => $row,
|
|
);
|
|
}
|
|
|
|
return array(
|
|
'error' => null,
|
|
'rows' => $tabKeyed,
|
|
'count' => count($tabKeyed),
|
|
'physical_count' => $tabFetch['physical_count'],
|
|
'duplicate_count' => $tabFetch['duplicate_count'],
|
|
);
|
|
}
|
|
|
|
/** Tous les environnements cibles sont syncables (dev prod, client préprod, client prod). */
|
|
function fxStaticSyncWriteAllowed($strEnvId) {
|
|
return true;
|
|
}
|
|
|
|
function fxStaticSyncSetFlash($strMessage, $strType) {
|
|
$_SESSION['ms1_static_sync_flash'] = $strMessage;
|
|
$_SESSION['ms1_static_sync_flash_type'] = $strType;
|
|
}
|
|
|
|
function fxStaticSyncPopFlash() {
|
|
if (empty($_SESSION['ms1_static_sync_flash'])) {
|
|
return null;
|
|
}
|
|
|
|
$tabFlash = array(
|
|
'message' => $_SESSION['ms1_static_sync_flash'],
|
|
'type' => !empty($_SESSION['ms1_static_sync_flash_type']) ? $_SESSION['ms1_static_sync_flash_type'] : 'info',
|
|
);
|
|
unset($_SESSION['ms1_static_sync_flash'], $_SESSION['ms1_static_sync_flash_type']);
|
|
|
|
return $tabFlash;
|
|
}
|
|
|
|
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'],
|
|
'source_physical' => isset($tabSourceData['physical_count']) ? $tabSourceData['physical_count'] : $tabSourceData['count'],
|
|
'target_physical' => isset($tabTargetData['physical_count']) ? $tabTargetData['physical_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 d'écriture — erreur remontée dans le message flash (fxQuery n'echo pas).
|
|
*/
|
|
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.');
|
|
}
|
|
|
|
if (!$tabDef) {
|
|
return array('ok' => false, 'message' => 'Table non configurée.');
|
|
}
|
|
|
|
$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.',
|
|
);
|
|
}
|
|
|
|
$tabCopyCols = $tabSchema['copy_cols'];
|
|
|
|
if (count($tabCopyCols) === 0) {
|
|
$tabFetchCheck = fxStaticSyncFetchRowsByKey($objSource, $tabDef);
|
|
if (count($tabFetchCheck['rows']) > 0) {
|
|
return array(
|
|
'ok' => false,
|
|
'message' => 'Aucune colonne commune entre source et cible.',
|
|
);
|
|
}
|
|
}
|
|
|
|
$tabFetch = fxStaticSyncFetchRowsByKey($objSource, $tabDef);
|
|
$tabRowsToCopy = $tabFetch['rows'];
|
|
$intDupes = $tabFetch['duplicate_count'];
|
|
|
|
// Préparer les INSERT avant toute écriture destructive (évite une table vide si échec).
|
|
$tabInsertBatches = array();
|
|
$intCopied = count($tabRowsToCopy);
|
|
|
|
if (count($tabCopyCols) > 0 && count($tabRowsToCopy) > 0) {
|
|
$strColList = '`' . implode('`,`', $tabCopyCols) . '`';
|
|
$intBatchSize = 50;
|
|
$tabBatch = array();
|
|
|
|
foreach ($tabRowsToCopy 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);
|
|
$tabBatch = array();
|
|
}
|
|
}
|
|
|
|
if (count($tabBatch) > 0) {
|
|
$tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $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).';
|
|
if ($intDupes > 0) {
|
|
$strMessage .= ' (' . $intDupes . ' doublon(s) ignoré(s) en source — même clé métier.)';
|
|
}
|
|
|
|
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';
|
|
case 'error':
|
|
return 'Erreur lecture';
|
|
}
|
|
|
|
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';
|
|
case 'error':
|
|
return 'ss-bad';
|
|
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']);
|
|
|
|
if (!empty($tabSourceData['error']) || !empty($tabTargetData['error'])) {
|
|
return array(
|
|
'status' => 'error',
|
|
'source_count' => isset($tabSourceData['count']) ? $tabSourceData['count'] : 0,
|
|
'target_count' => isset($tabTargetData['count']) ? $tabTargetData['count'] : 0,
|
|
'missing_keys' => array(),
|
|
'extra_keys' => array(),
|
|
'diff_keys' => array(),
|
|
'schema_missing_cols' => array(),
|
|
'schema_extra_cols' => array(),
|
|
);
|
|
}
|
|
|
|
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'],
|
|
'source_physical' => $tabSourceData['physical_count'],
|
|
'source_dupes' => $tabSourceData['duplicate_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;
|
|
}
|