Files
ms1inscription-v5/php/inc_fx_static_sync.php

1764 lines
63 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);
}
$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. 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 — 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 dabord 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 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';
}
// 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
. '. Nexiste 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 lUI. */
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;
}