Files
ms1inscription-v5/php/inc_fx_static_sync.php
stephan 8eda3d40c3 Refactor static database synchronization configuration and connection handling
This commit updates the static database synchronization logic by removing hardcoded connection details from the `fxStaticSyncClientDatabaseConfigs` function. It now relies on configurations defined in `inc_settings.php` for client connections. The `fxStaticSyncEnvironmentDefinitions` and `fxStaticSyncOpenConnections` functions are modified to utilize these new configurations, enhancing flexibility and maintainability in managing database connections for different environments.
2026-06-23 09:37:49 -04:00

459 lines
14 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, 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;
}