1764 lines
63 KiB
PHP
1764 lines
63 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).
|
||
* 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);
|
||
}
|
||
|
||
$sql = !empty($tabDef['join_role_code'])
|
||
? ('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')
|
||
: ('SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys));
|
||
|
||
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');
|
||
}
|
||
$tabResults = $objDb->fxGetResults($sql);
|
||
} else {
|
||
if (!fxStaticSyncKeyColumnsValid($objDb, $strTable, $tabKeys)) {
|
||
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
|
||
}
|
||
$tabResults = $objDb->fxGetResults($sql);
|
||
}
|
||
|
||
// CON-333 — clsMysql::fxGetResults renvoie null si 0 ligne (pas une erreur SQL).
|
||
if (!is_array($tabResults)) {
|
||
$tabResults = array();
|
||
}
|
||
|
||
$tabKeyed = array();
|
||
$intDupes = 0;
|
||
$intPhysical = 0;
|
||
|
||
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. N’insè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 — info seulement : retirer les « en trop » qui sont des poubelles (texte = clé).
|
||
* Les vrais libellés présents seulement en cible sont CONSERVÉS (pas de DELETE).
|
||
*/
|
||
function fxStaticSyncRemoveInfoExtraPoubelle($objSource, $objTarget, array $tabDef = null) {
|
||
if (!$tabDef) {
|
||
$tabDef = fxStaticSyncFindTableDef('info');
|
||
}
|
||
if (!$tabDef || $tabDef['table'] !== 'info') {
|
||
return array('ok' => false, 'message' => 'Réservé à info.');
|
||
}
|
||
|
||
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, 'info', $tabDef);
|
||
if (empty($tabPrep['ok'])) {
|
||
return $tabPrep;
|
||
}
|
||
|
||
$tabToDelete = array();
|
||
$intKeptReal = 0;
|
||
foreach ($tabPrep['target_rows'] as $strKey => $tabEntry) {
|
||
if (isset($tabPrep['source_rows'][$strKey])) {
|
||
continue;
|
||
}
|
||
$row = $tabEntry['row'];
|
||
if (fxStaticSyncInfoRowIsPoubelle($row)) {
|
||
$tabToDelete[] = $row;
|
||
} else {
|
||
$intKeptReal++;
|
||
}
|
||
}
|
||
|
||
$intDel = count($tabToDelete);
|
||
if ($intDel === 0) {
|
||
return array(
|
||
'ok' => true,
|
||
'message' => 'Aucune poubelle en trop à retirer. '
|
||
. $intKeptReal . ' vrai(s) libellé(s) seulement en cible — conservé(s) (pas de DELETE).',
|
||
'deleted' => 0,
|
||
'kept_real' => $intKeptReal,
|
||
);
|
||
}
|
||
|
||
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, 'info') === 'INNODB');
|
||
if ($blnTransactional) {
|
||
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
|
||
if (!$tabTx['ok']) {
|
||
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
|
||
}
|
||
}
|
||
|
||
$intDeleted = 0;
|
||
foreach ($tabToDelete as $row) {
|
||
$strSql = 'DELETE FROM `info` WHERE '
|
||
. fxStaticSyncSqlWhereBusinessKeys($objTarget, $row, $tabDef['keys'])
|
||
. ' AND `info_texte`=`info_clef` LIMIT 1';
|
||
$tabDel = fxStaticSyncExecQuery($objTarget, $strSql);
|
||
if (!$tabDel['ok']) {
|
||
if ($blnTransactional) {
|
||
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
|
||
}
|
||
return array('ok' => false, 'message' => 'DELETE poubelle : ' . $tabDel['error']);
|
||
}
|
||
$intDeleted++;
|
||
}
|
||
|
||
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' => $intDeleted . ' poubelle(s) en trop retirée(s). '
|
||
. $intKeptReal . ' vrai(s) libellé(s) seulement en cible — conservé(s) volontairement.',
|
||
'deleted' => $intDeleted,
|
||
'kept_real' => $intKeptReal,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* CON-333 — Retirer les lignes « en trop » (catalogue accès v2).
|
||
* info : jamais. Rôles : refuse si encore utilisés dans inscriptions_eve_acces.
|
||
*/
|
||
function fxStaticSyncRemoveExtras($objSource, $objTarget, $strTable, array $tabDef = null) {
|
||
if (!$tabDef || empty($tabDef['allow_remove_extras'])) {
|
||
return array('ok' => false, 'message' => 'Retrait des en trop non autorisé pour cette table.');
|
||
}
|
||
if ($strTable === 'info') {
|
||
return array('ok' => false, 'message' => 'Interdit : jamais de DELETE sur info.');
|
||
}
|
||
|
||
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, $tabDef);
|
||
if (empty($tabPrep['ok'])) {
|
||
return $tabPrep;
|
||
}
|
||
|
||
$tabExtras = array();
|
||
foreach ($tabPrep['target_rows'] as $strKey => $tabEntry) {
|
||
if (!isset($tabPrep['source_rows'][$strKey])) {
|
||
$tabExtras[$strKey] = $tabEntry['row'];
|
||
}
|
||
}
|
||
|
||
$intExtra = count($tabExtras);
|
||
if ($intExtra === 0) {
|
||
return array('ok' => true, 'message' => 'Aucun en trop à retirer.', 'deleted' => 0, 'skipped' => 0);
|
||
}
|
||
|
||
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB');
|
||
if ($blnTransactional) {
|
||
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
|
||
if (!$tabTx['ok']) {
|
||
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
|
||
}
|
||
}
|
||
|
||
$intDeleted = 0;
|
||
$intSkipped = 0;
|
||
$tabSkipReasons = array();
|
||
|
||
foreach ($tabExtras as $strKey => $row) {
|
||
if ($strTable === 'inscriptions_eve_roles') {
|
||
$intRoleId = isset($row['role_id']) ? (int)$row['role_id'] : 0;
|
||
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?';
|
||
if ($intRoleId <= 0) {
|
||
$intSkipped++;
|
||
$tabSkipReasons[] = $strCode . ' (role_id invalide)';
|
||
continue;
|
||
}
|
||
$intUsed = 0;
|
||
if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_acces')) {
|
||
$intUsed = (int)$objTarget->fxGetVar(
|
||
'SELECT COUNT(*) FROM inscriptions_eve_acces WHERE role_id=' . $intRoleId
|
||
);
|
||
}
|
||
if ($intUsed > 0) {
|
||
$intSkipped++;
|
||
$tabSkipReasons[] = $strCode . ' (encore ' . $intUsed . ' accès promoteur — non effacé)';
|
||
continue;
|
||
}
|
||
// Nettoyer d’abord les liens matrice orphelins de ce rôle.
|
||
if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_role_permissions')) {
|
||
$tabDelMat = fxStaticSyncExecQuery(
|
||
$objTarget,
|
||
'DELETE FROM `inscriptions_eve_role_permissions` WHERE role_id=' . $intRoleId
|
||
);
|
||
if (!$tabDelMat['ok']) {
|
||
if ($blnTransactional) {
|
||
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
|
||
}
|
||
return array('ok' => false, 'message' => 'DELETE matrice orpheline : ' . $tabDelMat['error']);
|
||
}
|
||
}
|
||
$strSql = 'DELETE FROM `inscriptions_eve_roles` WHERE role_id=' . $intRoleId
|
||
. ' AND role_code=\'' . $objTarget->fxEscape($strCode) . '\' LIMIT 1';
|
||
} elseif ($strTable === 'inscriptions_eve_role_permissions') {
|
||
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '';
|
||
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '';
|
||
$intRoleId = isset($row['role_id']) ? (int)$row['role_id'] : 0;
|
||
if ($strPerm === '' || ($intRoleId <= 0 && $strCode === '')) {
|
||
$intSkipped++;
|
||
continue;
|
||
}
|
||
if ($intRoleId <= 0) {
|
||
$intRoleId = (int)$objTarget->fxGetVar(
|
||
"SELECT role_id FROM inscriptions_eve_roles WHERE role_code='"
|
||
. $objTarget->fxEscape($strCode) . "' LIMIT 1"
|
||
);
|
||
}
|
||
if ($intRoleId <= 0) {
|
||
$intSkipped++;
|
||
$tabSkipReasons[] = $strCode . '+' . $strPerm . ' (rôle introuvable)';
|
||
continue;
|
||
}
|
||
$strSql = 'DELETE FROM `inscriptions_eve_role_permissions` WHERE role_id=' . $intRoleId
|
||
. ' AND perm_key=\'' . $objTarget->fxEscape($strPerm) . '\' LIMIT 1';
|
||
} elseif ($strTable === 'inscriptions_eve_permissions') {
|
||
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '';
|
||
if ($strPerm === '') {
|
||
$intSkipped++;
|
||
continue;
|
||
}
|
||
// Si encore référencée dans la matrice cible, ne pas casser.
|
||
$intMat = 0;
|
||
if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_role_permissions')) {
|
||
$intMat = (int)$objTarget->fxGetVar(
|
||
"SELECT COUNT(*) FROM inscriptions_eve_role_permissions WHERE perm_key='"
|
||
. $objTarget->fxEscape($strPerm) . "'"
|
||
);
|
||
}
|
||
if ($intMat > 0) {
|
||
$intSkipped++;
|
||
$tabSkipReasons[] = $strPerm . ' (encore ' . $intMat . ' lien(s) matrice)';
|
||
continue;
|
||
}
|
||
$strSql = 'DELETE FROM `inscriptions_eve_permissions` WHERE perm_key=\''
|
||
. $objTarget->fxEscape($strPerm) . '\' LIMIT 1';
|
||
} else {
|
||
if ($blnTransactional) {
|
||
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
|
||
}
|
||
return array('ok' => false, 'message' => 'Table non gérée pour retrait en trop.');
|
||
}
|
||
|
||
$tabDel = fxStaticSyncExecQuery($objTarget, $strSql);
|
||
if (!$tabDel['ok']) {
|
||
if ($blnTransactional) {
|
||
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
|
||
}
|
||
return array('ok' => false, 'message' => 'DELETE en trop : ' . $tabDel['error']);
|
||
}
|
||
$intDeleted++;
|
||
}
|
||
|
||
if ($blnTransactional) {
|
||
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
|
||
if (!$tabCommit['ok']) {
|
||
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
|
||
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
|
||
}
|
||
}
|
||
|
||
$strMessage = $intDeleted . ' en trop retiré(s) (alignement catalogue sur préprod).';
|
||
if ($intSkipped > 0) {
|
||
$strMessage .= ' ' . $intSkipped . ' conservé(s) (encore utilisés) : '
|
||
. implode(' ; ', array_slice($tabSkipReasons, 0, 8));
|
||
}
|
||
|
||
return array(
|
||
'ok' => true,
|
||
'message' => $strMessage,
|
||
'deleted' => $intDeleted,
|
||
'skipped' => $intSkipped,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* CON-333 — Ancien Sync destructif (DELETE + INSERT) : désactivé.
|
||
* Conservé pour ne pas casser d’appels 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';
|
||
}
|
||
|
||
// CON-333 — catalogues accès v2 : retirer les en trop dans le même passage (pas les laisser).
|
||
if (!empty($tabDef['allow_remove_extras'])) {
|
||
$tabCmp3 = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
|
||
$intExtra3 = count($tabCmp3['extra_keys']);
|
||
if ($intExtra3 > 0) {
|
||
$tabRem = fxStaticSyncRemoveExtras($objSource, $objTarget, $strTable, $tabDef);
|
||
if (empty($tabRem['ok'])) {
|
||
return array(
|
||
'ok' => false,
|
||
'message' => 'ÉCHEC sur ' . $tabDef['label'] . ' (retirer en trop) : ' . $tabRem['message'],
|
||
'lines' => $tabLines,
|
||
);
|
||
}
|
||
$tabLines[] = $tabDef['label'] . ' en trop : ' . $tabRem['message'];
|
||
} else {
|
||
$tabLines[] = $tabDef['label'] . ' en trop : 0';
|
||
}
|
||
}
|
||
}
|
||
|
||
return array(
|
||
'ok' => true,
|
||
'message' => 'Tout appliquer terminé sur ' . $strEnvId
|
||
. ' (info : jamais DELETE ; catalogues accès v2 : en trop retirés si possible). Voir détail.',
|
||
'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;
|
||
}
|
||
|
||
/**
|
||
* CON-333 — Expliquer une ligne « en trop » (présente en cible, absente en préprod).
|
||
*/
|
||
function fxStaticSyncExplainExtraRow(array $tabDef, array $row = null) {
|
||
$strTable = $tabDef['table'];
|
||
|
||
if (!is_array($row)) {
|
||
return 'Présente seulement en cible — absente de la préprod (source).';
|
||
}
|
||
|
||
if ($strTable === 'info') {
|
||
$strClef = isset($row['info_clef']) ? (string)$row['info_clef'] : '';
|
||
$strLang = isset($row['info_langue']) ? (string)$row['info_langue'] : '';
|
||
$strPrg = isset($row['info_prg']) ? (string)$row['info_prg'] : '';
|
||
$strTexte = isset($row['info_texte']) ? (string)$row['info_texte'] : '';
|
||
$blnPoub = ($strTexte !== '' && $strTexte === $strClef);
|
||
if ($blnPoub) {
|
||
return 'Auto-créée en cible (texte = clé) — prg « ' . $strPrg . ' » / ' . $strLang
|
||
. '. N’existe pas en préprod ; souvent résidu après trou info.';
|
||
}
|
||
$strPreview = function_exists('mb_substr') ? mb_substr($strTexte, 0, 80) : substr($strTexte, 0, 80);
|
||
return 'Libellé seulement en cible (pas en préprod) — prg « ' . $strPrg . ' » / ' . $strLang
|
||
. ' : « ' . $strPreview . ' ». Soit ajouté seulement ici, soit jamais repris en préprod.';
|
||
}
|
||
|
||
if ($strTable === 'inscriptions_eve_roles') {
|
||
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?';
|
||
$strLab = isset($row['role_label_fr']) ? (string)$row['role_label_fr'] : '';
|
||
return 'Rôle seulement en cible : « ' . $strCode . ' »'
|
||
. ($strLab !== '' ? ' (' . $strLab . ')' : '')
|
||
. '. Pas dans la préprod — ancien rôle ou créé hors sync.';
|
||
}
|
||
|
||
if ($strTable === 'inscriptions_eve_role_permissions') {
|
||
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?';
|
||
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '?';
|
||
return 'Droit accordé seulement en cible : rôle « ' . $strCode . ' » → « ' . $strPerm . ' ». '
|
||
. 'Absent de la matrice préprod — soit préprod incomplète, soit droit en trop à retirer plus tard.';
|
||
}
|
||
|
||
if ($strTable === 'inscriptions_pages') {
|
||
$strCode = isset($row['pag_code']) ? (string)$row['pag_code'] : '?';
|
||
return 'Page promoteur seulement en cible : « ' . $strCode . ' ».';
|
||
}
|
||
|
||
if ($strTable === 'inscriptions_eve_permissions') {
|
||
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '?';
|
||
return 'Permission catalogue seulement en cible : « ' . $strPerm . ' ».';
|
||
}
|
||
|
||
return 'Ligne seulement en cible (clé métier absente en préprod).';
|
||
}
|
||
|
||
/** CON-333 — Échantillon d’écart enrichi pour l’UI. */
|
||
function fxStaticSyncBuildSample(array $tabDef, $strKey, $strType, $tabSrcEntry, $tabTgtEntry) {
|
||
$tabSrc = is_array($tabSrcEntry) && isset($tabSrcEntry['row']) ? $tabSrcEntry['row'] : null;
|
||
$tabTgt = is_array($tabTgtEntry) && isset($tabTgtEntry['row']) ? $tabTgtEntry['row'] : null;
|
||
|
||
$tabSample = array(
|
||
'key' => $strKey,
|
||
'type' => $strType,
|
||
'source' => $tabSrc,
|
||
'target' => $tabTgt,
|
||
'explain' => '',
|
||
);
|
||
|
||
if ($strType === 'extra') {
|
||
$tabSample['explain'] = fxStaticSyncExplainExtraRow($tabDef, is_array($tabTgt) ? $tabTgt : null);
|
||
} elseif ($strType === 'missing') {
|
||
$tabSample['explain'] = 'Absente en cible — présente en préprod (Appliquer manquants).';
|
||
} elseif ($strType === 'poubelle') {
|
||
$tabSample['explain'] = 'Poubelle en cible (texte = clé) — préprod a un vrai libellé (Appliquer corrections).';
|
||
} else {
|
||
$tabSample['explain'] = 'Même clé, contenu différent (Appliquer corrections = préprod gagne).';
|
||
}
|
||
|
||
if ($tabDef['table'] === 'info' && ($strType === 'diff' || $strType === 'poubelle' || $strType === 'extra')) {
|
||
$tabSample['source_texte'] = is_array($tabSrc) && isset($tabSrc['info_texte']) ? $tabSrc['info_texte'] : '';
|
||
$tabSample['target_texte'] = is_array($tabTgt) && isset($tabTgt['info_texte']) ? $tabTgt['info_texte'] : '';
|
||
}
|
||
|
||
return $tabSample;
|
||
}
|
||
|
||
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;
|
||
|
||
// CON-333 — en trop info qui sont des poubelles (retirables) vs vrais libellés (à garder).
|
||
$tabPoubExtra = array();
|
||
$tabRealExtra = array();
|
||
if ($tabDef['table'] === 'info') {
|
||
foreach ($tabTargetData['rows'] as $strKey => $tabTgtEntry) {
|
||
if (isset($tabSourceData['rows'][$strKey])) {
|
||
continue;
|
||
}
|
||
$tabTgt = $tabTgtEntry['row'];
|
||
if (fxStaticSyncInfoRowIsPoubelle($tabTgt)) {
|
||
$tabPoubExtra[] = $strKey;
|
||
} else {
|
||
$tabRealExtra[] = $strKey;
|
||
}
|
||
}
|
||
}
|
||
$tabCmp['poubelle_extra_keys'] = $tabPoubExtra;
|
||
$tabCmp['real_extra_keys'] = $tabRealExtra;
|
||
|
||
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);
|
||
if (!empty($tabDef['join_role_code'])) {
|
||
$tabSchema['compare_cols'] = $tabDef['keys'];
|
||
}
|
||
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']);
|
||
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']);
|
||
|
||
$tabPoubelle = isset($tabCompare['poubelle_keys']) ? $tabCompare['poubelle_keys'] : array();
|
||
$intExtra = count($tabCompare['extra_keys']);
|
||
|
||
$strExtraContext = '';
|
||
if ($strTable === 'inscriptions_eve_role_permissions' && $intExtra > 0) {
|
||
$strExtraContext = 'La préprod n\'a que ' . (int)$tabCompare['source_count']
|
||
. ' lien(s) matrice. Les « en trop » sont des droits présents en cible seulement — '
|
||
. 'à revoir un par un (préprod incomplète vs anciens droits).';
|
||
} elseif ($strTable === 'info' && $intExtra > 0) {
|
||
$strExtraContext = 'Clés info seulement en cible : souvent créées à chaud (afficheTexte) '
|
||
. 'ou jamais reprises en préprod. Liste ci-dessous : poubelle vs vrai libellé.';
|
||
} elseif ($intExtra > 0) {
|
||
$strExtraContext = 'Lignes présentes seulement en cible. Soft sync ne les efface pas — '
|
||
. 'décider : garder, reporter en préprod, ou retirer plus tard (mode catalogue).';
|
||
}
|
||
|
||
$tabDetail = array(
|
||
'compare' => $tabCompare,
|
||
'samples_missing' => array(),
|
||
'samples_diff' => array(),
|
||
'samples_extra' => array(),
|
||
'note' => 'CON-333 — Les « en trop » sont listés et expliqués ; ils ne sont pas oubliés.',
|
||
'extra_context' => $strExtraContext,
|
||
);
|
||
|
||
foreach (array_slice($tabCompare['missing_keys'], 0, 80) as $strKey) {
|
||
$tabDetail['samples_missing'][] = fxStaticSyncBuildSample(
|
||
$tabDef,
|
||
$strKey,
|
||
'missing',
|
||
isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null,
|
||
null
|
||
);
|
||
}
|
||
|
||
foreach (array_slice(array_diff($tabCompare['diff_keys'], $tabPoubelle), 0, 80) as $strKey) {
|
||
$tabDetail['samples_diff'][] = fxStaticSyncBuildSample(
|
||
$tabDef,
|
||
$strKey,
|
||
'diff',
|
||
isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null,
|
||
isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null
|
||
);
|
||
}
|
||
foreach (array_slice($tabPoubelle, 0, 40) as $strKey) {
|
||
$tabDetail['samples_diff'][] = fxStaticSyncBuildSample(
|
||
$tabDef,
|
||
$strKey,
|
||
'poubelle',
|
||
isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null,
|
||
isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null
|
||
);
|
||
}
|
||
|
||
foreach (array_slice($tabCompare['extra_keys'], 0, 300) as $strKey) {
|
||
$tabDetail['samples_extra'][] = fxStaticSyncBuildSample(
|
||
$tabDef,
|
||
$strKey,
|
||
'extra',
|
||
null,
|
||
isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null
|
||
);
|
||
}
|
||
|
||
$tabDetail['samples'] = array_merge(
|
||
$tabDetail['samples_missing'],
|
||
$tabDetail['samples_diff'],
|
||
$tabDetail['samples_extra']
|
||
);
|
||
|
||
return $tabDetail;
|
||
}
|