Files
ms1inscription-v5/php/inc_fx_static_sync.php

1371 lines
46 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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).
* CON-333 — matrice rôles : jointure role_code (role_id non portable entre BD).
*/
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 (!empty($tabDef['join_role_code'])) {
if (!fxStaticSyncTableExists($objDb, 'inscriptions_eve_roles')) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
}
$sql = 'SELECT erp.role_id, erp.perm_key, r.role_code'
. ' FROM `inscriptions_eve_role_permissions` erp'
. ' INNER JOIN `inscriptions_eve_roles` r ON r.role_id = erp.role_id'
. ' ORDER BY r.role_code, erp.perm_key';
$tabResults = $objDb->fxGetResults($sql);
} else {
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']) : '';
}
/**
* CON-333 — Colonnes à écrire en INSERT soft (hors PK / timestamps ignore_cols).
* Les clés métier restent incluses (elles ne sont pas dans ignore_cols).
*/
function fxStaticSyncInsertColumns(array $tabSchema, array $tabDef) {
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
$tabCols = array();
foreach ($tabSchema['copy_cols'] as $strCol) {
if (!in_array($strCol, $tabIgnore, true)) {
$tabCols[] = $strCol;
}
}
return $tabCols;
}
/** CON-333 — Ligne info « poubelle » (texte = clé, créé à chaud par afficheTexte). */
function fxStaticSyncInfoRowIsPoubelle(array $row) {
if (!isset($row['info_clef']) || !isset($row['info_texte'])) {
return false;
}
return (string)$row['info_texte'] === (string)$row['info_clef'];
}
/** CON-333 — Source a un vrai libellé (pas poubelle / pas vide). */
function fxStaticSyncInfoRowHasRealTexte(array $row) {
if (!isset($row['info_clef']) || !isset($row['info_texte'])) {
return false;
}
$strTexte = trim((string)$row['info_texte']);
if ($strTexte === '') {
return false;
}
return $strTexte !== (string)$row['info_clef'];
}
/**
* CON-333 — Prépare le schéma + jeux de lignes source/cible pour écriture soft.
* Aucune écriture. Retourne ok=false si structure / connexion invalide.
*/
function fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, array $tabDef = null) {
if (!$objSource || !$objTarget) {
return array('ok' => false, 'message' => 'Connexion BD invalide.');
}
if (!$tabDef) {
return array('ok' => false, 'message' => 'Table non configurée.');
}
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 = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $strTable, $tabIgnore);
// CON-333 — matrice : comparer sur role_code+perm_key seulement.
if (!empty($tabDef['join_role_code'])) {
$tabSchema['compare_cols'] = $tabDef['keys'];
}
if (!fxStaticSyncSchemasAligned($tabSchema)) {
return array(
'ok' => false,
'message' => 'Structures non alignées — '
. fxStaticSyncFormatSchemaDiff($tabSchema)
. '. Aligner les structures (Navicat) avant. Aucune donnée modifiée.',
);
}
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']);
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']);
if (!empty($tabSourceData['error']) && $tabSourceData['error'] !== null) {
return array('ok' => false, 'message' => 'Lecture source : ' . $tabSourceData['error']);
}
if (!empty($tabTargetData['error']) && $tabTargetData['error'] !== null
&& $tabTargetData['error'] !== 'missing_table') {
return array('ok' => false, 'message' => 'Lecture cible : ' . $tabTargetData['error']);
}
return array(
'ok' => true,
'schema' => $tabSchema,
'source_rows' => $tabSourceData['rows'],
'target_rows' => $tabTargetData['rows'],
'source_dupes' => isset($tabSourceData['duplicate_count']) ? (int)$tabSourceData['duplicate_count'] : 0,
);
}
/**
* CON-333 — INSERT uniquement les clés absentes en cible. Jamais de DELETE.
* Ne touche pas aux lignes déjà présentes (même poubelles — voir repair).
*/
function fxStaticSyncApplyMissingRows($objSource, $objTarget, $strTable, array $tabDef = null) {
if ($tabDef && !empty($tabDef['join_role_code'])) {
return fxStaticSyncApplyMissingRolePermissions($objSource, $objTarget, $tabDef);
}
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
}
$tabInsertCols = fxStaticSyncInsertColumns($tabPrep['schema'], $tabDef);
if (count($tabInsertCols) === 0) {
return array('ok' => false, 'message' => 'Aucune colonne insérable (hors ignore_cols).');
}
$tabMissingRows = array();
foreach ($tabPrep['source_rows'] as $strKey => $tabEntry) {
if (!isset($tabPrep['target_rows'][$strKey])) {
$tabMissingRows[$strKey] = $tabEntry['row'];
}
}
$intMissing = count($tabMissingRows);
if ($intMissing === 0) {
return array(
'ok' => true,
'message' => 'Aucun manquant à ajouter.',
'inserted' => 0,
'skipped_extra' => count($tabPrep['target_rows']) - count(array_intersect_key($tabPrep['target_rows'], $tabPrep['source_rows'])),
);
}
$strColList = '`' . implode('`,`', $tabInsertCols) . '`';
$intBatchSize = 50;
$tabBatch = array();
$tabInsertBatches = array();
foreach ($tabMissingRows as $row) {
if (!is_array($row)) {
continue;
}
$tabVals = array();
foreach ($tabInsertCols as $strCol) {
if ($strTable === 'info' && $strCol === 'info_creation') {
$tabVals[] = 'NOW()';
continue;
}
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');
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
foreach ($tabInsertBatches as $strSql) {
$tabInsert = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabInsert['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array(
'ok' => false,
'message' => 'Insertion manquants : ' . $tabInsert['error']
. ($blnTransactional ? ' (rollback — aucune ligne ajoutée).' : ''),
);
}
}
if ($blnTransactional) {
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
$strMessage = $intMissing . ' ligne(s) ajoutée(s) (INSERT manquants uniquement, aucun DELETE).';
if (!empty($tabPrep['source_dupes'])) {
$strMessage .= ' (' . (int)$tabPrep['source_dupes'] . ' doublon(s) clé en source ignoré(s).)';
}
return array(
'ok' => true,
'message' => $strMessage,
'inserted' => $intMissing,
);
}
/**
* CON-333 — WHERE métier (toutes les colonnes keys).
*/
function fxStaticSyncSqlWhereBusinessKeys($objDb, array $row, array $tabKeys) {
$tabParts = array();
foreach ($tabKeys as $strCol) {
if (!array_key_exists($strCol, $row) || $row[$strCol] === null) {
$tabParts[] = '`' . $strCol . '` IS NULL';
} else {
$tabParts[] = '`' . $strCol . "`='" . $objDb->fxEscape($row[$strCol]) . "'";
}
}
return implode(' AND ', $tabParts);
}
/**
* CON-333 — Matrice rôles : INSERT (role_id cible, perm_key) via role_code.
* Les role_id source ne sont jamais copiés tels quels.
*/
function fxStaticSyncApplyMissingRolePermissions($objSource, $objTarget, array $tabDef) {
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, 'inscriptions_eve_role_permissions', $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
}
$tabMissing = array();
foreach ($tabPrep['source_rows'] as $strKey => $tabEntry) {
if (!isset($tabPrep['target_rows'][$strKey])) {
$tabMissing[] = $tabEntry['row'];
}
}
if (count($tabMissing) === 0) {
return array('ok' => true, 'message' => 'Aucun manquant à ajouter (matrice rôles).', 'inserted' => 0);
}
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, 'inscriptions_eve_role_permissions') === 'INNODB');
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
$intInserted = 0;
foreach ($tabMissing as $row) {
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '';
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '';
if ($strCode === '' || $strPerm === '') {
continue;
}
$sqlRole = "SELECT role_id FROM inscriptions_eve_roles WHERE role_code='"
. $objTarget->fxEscape($strCode) . "' LIMIT 1";
$strRoleId = $objTarget->fxGetVar($sqlRole);
if ($strRoleId === null || $strRoleId === '' || (int)$strRoleId <= 0) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array(
'ok' => false,
'message' => 'Rôle absent en cible (role_code=' . $strCode
. '). Appliquer d\'abord inscriptions_eve_roles.',
);
}
$strSql = 'INSERT INTO `inscriptions_eve_role_permissions` (`role_id`,`perm_key`) VALUES ('
. (int)$strRoleId . ",'" . $objTarget->fxEscape($strPerm) . "')";
$tabIns = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabIns['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'INSERT matrice : ' . $tabIns['error']);
}
$intInserted++;
}
if ($blnTransactional) {
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
return array(
'ok' => true,
'message' => $intInserted . ' lien(s) matrice ajouté(s) via role_code (aucun DELETE).',
'inserted' => $intInserted,
);
}
/**
* CON-333 — UPDATE des clés déjà présentes mais contenu différent (préprod = vérité textes).
* Inclut les corrections de libellés et les poubelles. Ne DELETE rien. Ninsère pas les manquants.
*/
function fxStaticSyncApplyContentUpdates($objSource, $objTarget, $strTable, array $tabDef = null) {
// Matrice rôles = présence de liens seulement ; pas de colonnes de contenu à écraser.
if ($tabDef && !empty($tabDef['join_role_code'])) {
return array(
'ok' => true,
'message' => 'Matrice rôles : pas de corrections de contenu (seulement Appliquer manquants).',
'updated' => 0,
);
}
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
}
$tabUpdateCols = fxStaticSyncInsertColumns($tabPrep['schema'], $tabDef);
$tabKeys = $tabDef['keys'];
$tabUpdateCols = array_values(array_filter($tabUpdateCols, function ($strCol) use ($tabKeys) {
return !in_array($strCol, $tabKeys, true);
}));
if (count($tabUpdateCols) === 0) {
return array('ok' => false, 'message' => 'Aucune colonne de contenu à mettre à jour.');
}
$tabSchemaCols = $tabPrep['schema']['copy_cols'];
$intUpdated = 0;
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB');
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
foreach ($tabPrep['source_rows'] as $strKey => $tabSrcEntry) {
if (!isset($tabPrep['target_rows'][$strKey])) {
continue;
}
if ($tabPrep['target_rows'][$strKey]['hash'] === $tabSrcEntry['hash']) {
continue;
}
$tabSrc = $tabSrcEntry['row'];
$tabTgt = $tabPrep['target_rows'][$strKey]['row'];
$tabSets = array();
foreach ($tabUpdateCols as $strCol) {
if (!array_key_exists($strCol, $tabSrc) || $tabSrc[$strCol] === null) {
$tabSets[] = '`' . $strCol . '`=NULL';
} else {
$tabSets[] = '`' . $strCol . "`='" . $objTarget->fxEscape($tabSrc[$strCol]) . "'";
}
}
if ($strTable === 'info' && in_array('info_maj', $tabSchemaCols, true)) {
$tabSets[] = '`info_maj`=NOW()';
}
$strSql = 'UPDATE `' . $strTable . '` SET ' . implode(',', $tabSets)
. ' WHERE ' . fxStaticSyncSqlWhereBusinessKeys($objTarget, $tabTgt, $tabKeys);
$tabUpd = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabUpd['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'UPDATE corrections : ' . $tabUpd['error']);
}
$intUpdated++;
}
if ($blnTransactional) {
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
return array(
'ok' => true,
'message' => $intUpdated . ' ligne(s) mise(s) à jour depuis la source (corrections / diffs, aucun DELETE).',
'updated' => $intUpdated,
);
}
/**
* CON-333 — info seulement : UPDATE des poubelles cible quand la source a un vrai texte.
* Nécrase jamais un libellé déjà OK. Ne DELETE rien.
*/
function fxStaticSyncRepairInfoPoubelle($objSource, $objTarget, array $tabDef = null) {
if (!$tabDef) {
$tabDef = fxStaticSyncFindTableDef('info');
}
if (!$tabDef || $tabDef['table'] !== 'info') {
return array('ok' => false, 'message' => 'Réparation poubelle réservée à la table info.');
}
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, 'info', $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
}
$tabUpdateCols = array(
'info_texte', 'info_aide', 'info_description', 'info_trie', 'info_actif',
'info_option1', 'info_option2', 'info_option3',
);
$tabSchemaCols = $tabPrep['schema']['copy_cols'];
$tabUpdateCols = array_values(array_intersect($tabUpdateCols, $tabSchemaCols));
$intRepaired = 0;
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, 'info') === 'INNODB');
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
foreach ($tabPrep['source_rows'] as $strKey => $tabSrcEntry) {
if (!isset($tabPrep['target_rows'][$strKey])) {
continue;
}
$tabSrc = $tabSrcEntry['row'];
$tabTgt = $tabPrep['target_rows'][$strKey]['row'];
if (!fxStaticSyncInfoRowIsPoubelle($tabTgt) || !fxStaticSyncInfoRowHasRealTexte($tabSrc)) {
continue;
}
$tabSets = array();
foreach ($tabUpdateCols as $strCol) {
if (!array_key_exists($strCol, $tabSrc) || $tabSrc[$strCol] === null) {
$tabSets[] = '`' . $strCol . '`=NULL';
} else {
$tabSets[] = '`' . $strCol . "`='" . $objTarget->fxEscape($tabSrc[$strCol]) . "'";
}
}
if (in_array('info_maj', $tabSchemaCols, true)) {
$tabSets[] = '`info_maj`=NOW()';
}
$strWhere = '`info_clef`=\'' . $objTarget->fxEscape($tabTgt['info_clef']) . '\''
. ' AND `info_langue`=\'' . $objTarget->fxEscape($tabTgt['info_langue']) . '\''
. ' AND `info_prg`=\'' . $objTarget->fxEscape($tabTgt['info_prg']) . '\''
. ' AND `info_texte`=`info_clef`';
$strSql = 'UPDATE `info` SET ' . implode(',', $tabSets) . ' WHERE ' . $strWhere;
$tabUpd = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabUpd['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'UPDATE poubelle : ' . $tabUpd['error']);
}
$intRepaired++;
}
if ($blnTransactional) {
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
return array(
'ok' => true,
'message' => $intRepaired . ' poubelle(s) réparée(s) depuis la source (aucun DELETE, libellés OK inchangés).',
'repaired' => $intRepaired,
);
}
/**
* CON-333 — Ancien Sync destructif (DELETE + INSERT) : désactivé.
* Conservé pour ne pas casser dappels résiduels — renvoie toujours une erreur.
*/
function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef = null) {
return array(
'ok' => false,
'message' => 'CON-333 — Sync destructif désactivé (DELETE interdit). '
. 'Utiliser « Appliquer manquants » et/ou « Appliquer corrections ».',
);
}
/**
* CON-333 — Dry-run complet : toutes les tables, manquants puis corrections, un environnement.
* Aucun DELETE. Arrête à la première erreur décriture.
*
* @return array{ok:bool,message:string,lines:array}
*/
function fxStaticSyncApplyAllSoft($objSource, $objTarget, $strEnvId) {
$tabLines = array();
$tabDefs = fxStaticSyncTableDefinitions();
foreach ($tabDefs as $tabDef) {
$strTable = $tabDef['table'];
$tabCmp = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
if (in_array($tabCmp['status'], array('no_connection', 'missing_table', 'schema_mismatch', 'error'), true)) {
$tabLines[] = $tabDef['label'] . ' : SAUTÉ — ' . fxStaticSyncStatusLabel($tabCmp['status'], $tabCmp);
continue;
}
$intMiss = count($tabCmp['missing_keys']);
if ($intMiss > 0) {
$tabMiss = fxStaticSyncApplyMissingRows($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabMiss['ok'])) {
return array(
'ok' => false,
'message' => 'ÉCHEC sur ' . $tabDef['label'] . ' (manquants) : ' . $tabMiss['message'],
'lines' => $tabLines,
);
}
$tabLines[] = $tabDef['label'] . ' manquants : ' . $tabMiss['message'];
} else {
$tabLines[] = $tabDef['label'] . ' manquants : 0';
}
$tabCmp2 = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$intDiff2 = count($tabCmp2['diff_keys']);
if ($intDiff2 > 0 && empty($tabDef['join_role_code'])) {
$tabUpd = fxStaticSyncApplyContentUpdates($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabUpd['ok'])) {
return array(
'ok' => false,
'message' => 'ÉCHEC sur ' . $tabDef['label'] . ' (corrections) : ' . $tabUpd['message'],
'lines' => $tabLines,
);
}
$tabLines[] = $tabDef['label'] . ' corrections : ' . $tabUpd['message'];
} else {
$tabLines[] = $tabDef['label'] . ' corrections : 0';
}
}
return array(
'ok' => true,
'message' => 'Tout appliquer terminé sur ' . $strEnvId . ' (aucun DELETE). Voir détail ci-dessous.',
'lines' => $tabLines,
);
}
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);
// CON-333 — matrice : comparer sur role_code+perm_key seulement.
if (!empty($tabDef['join_role_code'])) {
$tabSchema['compare_cols'] = $tabDef['keys'];
}
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(),
'poubelle_keys' => array(),
'schema_missing_cols' => array(),
'schema_extra_cols' => array(),
);
}
$tabCmp = fxStaticSyncCompareTable($tabSourceData, $tabTargetData, $tabSchema);
// CON-333 — poubelles info réparables (cible brute, source OK) — jamais auto-supprimées.
$tabPoubelle = array();
if ($tabDef['table'] === 'info') {
foreach ($tabSourceData['rows'] as $strKey => $tabSrcEntry) {
if (!isset($tabTargetData['rows'][$strKey])) {
continue;
}
$tabSrc = $tabSrcEntry['row'];
$tabTgt = $tabTargetData['rows'][$strKey]['row'];
if (fxStaticSyncInfoRowIsPoubelle($tabTgt) && fxStaticSyncInfoRowHasRealTexte($tabSrc)) {
$tabPoubelle[] = $strKey;
}
}
}
$tabCmp['poubelle_keys'] = $tabPoubelle;
return $tabCmp;
}
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']);
$tabPoubelle = isset($tabCompare['poubelle_keys']) ? $tabCompare['poubelle_keys'] : array();
$tabDetail = array(
'compare' => $tabCompare,
'samples' => array(),
'note' => 'CON-333 — Les clés « en trop » en cible ne sont jamais effacées automatiquement.',
);
// Priorité daffichage : manquants, poubelles, autres diffs, extras (revue manuelle).
$tabKeysToShow = array_slice(
array_merge(
$tabCompare['missing_keys'],
$tabPoubelle,
array_diff($tabCompare['diff_keys'], $tabPoubelle),
$tabCompare['extra_keys']
),
0,
40
);
foreach ($tabKeysToShow as $strKey) {
$strType = 'diff';
if (in_array($strKey, $tabCompare['extra_keys'], true)) {
$strType = 'extra';
} elseif (in_array($strKey, $tabCompare['missing_keys'], true)) {
$strType = 'missing';
} elseif (in_array($strKey, $tabPoubelle, true)) {
$strType = 'poubelle';
}
$tabSample = array(
'key' => $strKey,
'type' => $strType,
'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' && ($strType === 'diff' || $strType === 'poubelle')) {
$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;
}