Files
ms1inscription-v5/php/inc_fx_static_sync.php
stephan d74d06a3b0 Enhance static database connection handling and synchronization logic
This commit updates the `fxStaticSyncClientDatabaseConfigs` function to return detailed database connection configurations for both preprod and prod clients. It introduces a new class, `clsMysqlStaticSync`, to manage MySQL connections with a short timeout, improving connection handling. The `fxStaticSyncConnect` function is refined to utilize this new class, and the synchronization UI is updated to reflect the new client configurations. These changes streamline database interactions and enhance the overall synchronization process.
2026-06-23 09:20:28 -04:00

453 lines
13 KiB
PHP

<?php
require_once __DIR__ . '/inc_fx_static_sync_config.php';
/**
* Environnements comparables (source = dev_preprod).
*/
function fxStaticSyncEnvironmentDefinitions() {
global $arrConDatabasePreProd, $arrConDatabaseProd;
$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,
),
);
foreach (fxStaticSyncClientDatabaseConfigs() as $strEnvId => $tabClient) {
$tabEnvs[$strEnvId] = array(
'label' => $tabClient['label'],
'config' => array(
'host' => $tabClient['host'],
'user' => $tabClient['user'],
'pass' => $tabClient['pass'],
'db' => $tabClient['db'],
),
'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 inc_fonctions.php. Client : connexions localhost séparées (ms1incription_*).
*/
function fxStaticSyncOpenConnections() {
global $objDatabasePreProd, $objDatabaseProd;
$tabConnections = array(
'dev_preprod' => !empty($objDatabasePreProd) ? $objDatabasePreProd : null,
'dev_prod' => !empty($objDatabaseProd) ? $objDatabaseProd : null,
);
foreach (fxStaticSyncClientDatabaseConfigs() as $strEnvId => $tabClient) {
$tabConnections[$strEnvId] = fxStaticSyncConnect(array(
'host' => $tabClient['host'],
'user' => $tabClient['user'],
'pass' => $tabClient['pass'],
'db' => $tabClient['db'],
));
}
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;
}