passed_at, libelle=>encrypted_name, email=>encrypted_email, phone=>encrypted_phone) */ require_once dirname(__DIR__) . '/config.php'; require_once __DIR__ . '/MigrationConfig.php'; require_once dirname(dirname(__DIR__)) . '/src/Services/ApiService.php'; try { // Vérifier si un processus utilise déjà le port du tunnel SSH echo "Vérification du port SSH..." . PHP_EOL; // Création du tunnel SSH avec une meilleure gestion des erreurs try { // Tuer tout processus existant qui utilise le port 13306 // Sous Linux/Mac @exec('kill $(lsof -t -i:13306) 2>/dev/null'); // Attendre un moment pour s'assurer que le port est libéré sleep(1); createSshTunnel(); echo "Tunnel SSH créé avec succès." . PHP_EOL; } catch (Exception $e) { echo "ERREUR lors de la création du tunnel SSH: " . $e->getMessage() . PHP_EOL; exit(1); } // Connexion aux bases de données avec paramètres pour éviter le timeout try { echo "Connexion à la base source..." . PHP_EOL; $sourceDb = getSourceConnection(); echo "Connexion à la base source établie." . PHP_EOL; } catch (Exception $e) { echo "ERREUR de connexion à la base source: " . $e->getMessage() . PHP_EOL; closeSshTunnel(); exit(1); } // Configuration spéciale pour éviter les timeouts sur les grosses opérations try { echo "Connexion à la base cible..." . PHP_EOL; $targetDb = getTargetConnection(); echo "Connexion à la base cible établie." . PHP_EOL; // Configuration des timeouts adaptés à MariaDB 10.11 $targetDb->setAttribute(PDO::ATTR_TIMEOUT, 600); // 10 minutes pour PDO echo " - Configuration des timeouts pour MariaDB 10.11" . PHP_EOL; // Configurations spécifiques à MariaDB 10.11 $timeoutVars = [ "wait_timeout" => 3600, // 1 heure "net_read_timeout" => 3600, // 1 heure "net_write_timeout" => 3600, // 1 heure "innodb_lock_wait_timeout" => 3600 // 1 heure ]; // Configurer les variables de session foreach ($timeoutVars as $var => $value) { try { $sql = "SET SESSION $var=$value"; $targetDb->exec($sql); echo " - Config MariaDB: $var = $value" . PHP_EOL; } catch (PDOException $e) { echo " - Impossible de configurer $var: " . $e->getMessage() . PHP_EOL; } } echo "Paramètres de timeout configurés." . PHP_EOL; } catch (Exception $e) { echo "ERREUR de connexion à la base cible: " . $e->getMessage() . PHP_EOL; closeSshTunnel(); exit(1); } // Début de la migration // Vérifions la version de la base de données cible (MariaDB) $versionTarget = $targetDb->query('SELECT VERSION() as version')->fetch(); echo "Version de la base cible (MariaDB): " . $versionTarget['version'] . PHP_EOL; // Vérifions la version de la base de données source (MySQL) $versionSource = $sourceDb->query('SELECT VERSION() as version')->fetch(); echo "Version de la base source (MySQL): " . $versionSource['version'] . PHP_EOL; // Note sur les privilèges de contraintes echo "NOTE: La suppression et recréation des contraintes nécessitent des privilèges SUPER ou ALTER TABLE." . PHP_EOL; echo " Ces opérations peuvent être ignorées si l'utilisateur n'a pas les privilèges suffisants." . PHP_EOL; echo " Il est recommandé d'exécuter ces opérations manuellement avec un utilisateur admin." . PHP_EOL; // Suppression des contraintes relationnelles (tentatif) echo "Tentative de suppression des contraintes relationnelles... " . PHP_EOL; $dropConstraintsQueries = [ "ALTER TABLE ope_pass DROP FOREIGN KEY ope_pass_ibfk_1", "ALTER TABLE ope_pass DROP FOREIGN KEY ope_pass_ibfk_2", "ALTER TABLE ope_pass DROP FOREIGN KEY ope_pass_ibfk_3", "ALTER TABLE ope_pass DROP FOREIGN KEY ope_pass_ibfk_4" ]; $constraintDropFailed = false; foreach ($dropConstraintsQueries as $query) { try { $targetDb->exec($query); echo " - Contrainte supprimée avec succès : " . substr($query, 0, 60) . "..." . PHP_EOL; } catch (PDOException $e) { echo " - Erreur lors de la suppression de la contrainte : " . $e->getMessage() . PHP_EOL; $constraintDropFailed = true; } } if ($constraintDropFailed) { echo "ATTENTION: Les contraintes n'ont pas pu être supprimées. La migration continue sans cette étape." . PHP_EOL; echo " Vous devrez peut-être désactiver les contraintes manuellement si la suppression échoue." . PHP_EOL; } // Suppression de toutes les données existantes dans la table ope_pass par lots pour éviter les timeouts echo "Suppression des données existantes dans la table ope_pass (par lots)... " . PHP_EOL; try { // Désactiver temporairement les vérifications de clés étrangères // Cela fonctionne à la fois dans MySQL et MariaDB try { $targetDb->exec("SET FOREIGN_KEY_CHECKS=0"); echo " - Vérification des clés étrangères temporairement désactivée." . PHP_EOL; } catch (PDOException $e) { echo " - Erreur lors de la désactivation des clés étrangères: " . $e->getMessage() . PHP_EOL; } // Suppression par lots $batchSize = 100000; $totalDeleted = 0; $continue = true; echo " - Suppression par lots de $batchSize enregistrements:" . PHP_EOL; while ($continue) { $deleteQuery = "DELETE FROM ope_pass LIMIT $batchSize"; $rowCount = $targetDb->exec($deleteQuery); $totalDeleted += $rowCount; echo " * Lot supprimé: $rowCount enregistrements (Total: $totalDeleted)" . PHP_EOL; // Vérifier si nous avons terminé if ($rowCount < $batchSize) { $continue = false; } // Petit délai pour permettre des traitements serveur if ($continue) { usleep(100000); // 0.1 seconde } } echo " - Total: $totalDeleted enregistrements supprimés." . PHP_EOL; // Réactiver les vérifications de clés étrangères try { $targetDb->exec("SET FOREIGN_KEY_CHECKS=1"); echo " - Vérification des clés étrangères réactivée." . PHP_EOL; } catch (PDOException $e) { echo " - Erreur lors de la réactivation des clés étrangères: " . $e->getMessage() . PHP_EOL; } } catch (PDOException $e) { echo " - Erreur lors de la suppression des données : " . $e->getMessage() . PHP_EOL; // Réactiver les vérifications de clés étrangères en cas d'erreur try { $targetDb->exec("SET FOREIGN_KEY_CHECKS=1"); } catch (Exception $e2) { echo " - Erreur lors de la réactivation des clés étrangères : " . $e2->getMessage() . PHP_EOL; } closeSshTunnel(); exit(1); } // Récupération des IDs des opérations qui ont été migrées $stmt = $targetDb->query("SELECT id FROM operations"); $migratedOperations = $stmt->fetchAll(PDO::FETCH_COLUMN); if (empty($migratedOperations)) { echo "ERREUR: Aucune opération n'a été migrée. Veuillez d'abord migrer la table operations." . PHP_EOL; closeSshTunnel(); exit(1); } // Récupération des IDs des utilisateurs qui ont été migrés $stmt = $targetDb->query("SELECT id FROM users"); $migratedUsers = $stmt->fetchAll(PDO::FETCH_COLUMN); if (empty($migratedUsers)) { echo "ERREUR: Aucun utilisateur n'a été migré. Veuillez d'abord migrer la table users." . PHP_EOL; closeSshTunnel(); exit(1); } // Récupération de la correspondance entre les anciens secteurs et les nouveaux $query = "SELECT id, fk_operation, fk_old_sector FROM ope_sectors WHERE fk_old_sector IS NOT NULL"; $stmt = $targetDb->query($query); $sectorMapping = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($sectorMapping)) { echo "ERREUR: Aucun secteur n'a été migré. Veuillez d'abord migrer la table ope_sectors." . PHP_EOL; closeSshTunnel(); exit(1); } // Création d'un tableau associatif pour faciliter la recherche des correspondances $sectorMap = []; foreach ($sectorMapping as $mapping) { $key = $mapping['fk_operation'] . '_' . $mapping['fk_old_sector']; $sectorMap[$key] = $mapping['id']; } // Pas d'affichage en mode silencieux // Création de la liste des IDs d'opérations pour la requête IN $operationIds = implode(',', $migratedOperations); // Compter le nombre total de passages à migrer pour estimer le volume $countQuery = " SELECT COUNT(*) as total FROM ope_pass p WHERE p.fk_operation IN ($operationIds) AND p.active = 1 "; $countStmt = $sourceDb->query($countQuery); $totalCount = $countStmt->fetch(PDO::FETCH_ASSOC)['total']; echo "Nombre total de passages à migrer: $totalCount" . PHP_EOL; // Définir la taille des lots pour éviter les problèmes de mémoire $batchSize = 5000; $totalBatches = ceil($totalCount / $batchSize); echo "Traitement par lots de $batchSize passages ($totalBatches lots au total)" . PHP_EOL; // Pas d'affichage du nombre de passages à migrer en mode silencieux // Préparation de la requête d'insertion $insertQuery = "INSERT INTO ope_pass ( fk_operation, fk_sector, fk_user, fk_adresse, passed_at, fk_type, numero, rue, rue_bis, ville, fk_habitat, appt, niveau, gps_lat, gps_lng, encrypted_name, montant, fk_type_reglement, remarque, encrypted_email, nom_recu, email_erreur, chk_email_sent, encrypted_phone, docremis, date_repasser, nb_passages, chk_gps_maj, chk_map_create, chk_mobile, chk_synchro, chk_api_adresse, chk_maj_adresse, anomalie, created_at, fk_user_creat, updated_at, fk_user_modif, chk_active ) VALUES ( :fk_operation, :fk_sector, :fk_user, :fk_adresse, :passed_at, :fk_type, :numero, :rue, :rue_bis, :ville, :fk_habitat, :appt, :niveau, :gps_lat, :gps_lng, :encrypted_name, :montant, :fk_type_reglement, :remarque, :encrypted_email, :nom_recu, :email_erreur, :chk_email_sent, :encrypted_phone, :docremis, :date_repasser, :nb_passages, :chk_gps_maj, :chk_map_create, :chk_mobile, :chk_synchro, :chk_api_adresse, :chk_maj_adresse, :anomalie, :created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active ) ON DUPLICATE KEY UPDATE fk_sector = VALUES(fk_sector), passed_at = VALUES(passed_at), numero = VALUES(numero), rue = VALUES(rue), rue_bis = VALUES(rue_bis), ville = VALUES(ville), fk_habitat = VALUES(fk_habitat), appt = VALUES(appt), niveau = VALUES(niveau), gps_lat = VALUES(gps_lat), gps_lng = VALUES(gps_lng), encrypted_name = VALUES(encrypted_name), montant = VALUES(montant), fk_type_reglement = VALUES(fk_type_reglement), remarque = VALUES(remarque), encrypted_email = VALUES(encrypted_email), nom_recu = VALUES(nom_recu), email_erreur = VALUES(email_erreur), chk_email_sent = VALUES(chk_email_sent), encrypted_phone = VALUES(encrypted_phone), docremis = VALUES(docremis), date_repasser = VALUES(date_repasser), nb_passages = VALUES(nb_passages), chk_gps_maj = VALUES(chk_gps_maj), chk_map_create = VALUES(chk_map_create), chk_mobile = VALUES(chk_mobile), chk_synchro = VALUES(chk_synchro), chk_api_adresse = VALUES(chk_api_adresse), chk_maj_adresse = VALUES(chk_maj_adresse), anomalie = VALUES(anomalie), updated_at = VALUES(updated_at), fk_user_modif = VALUES(fk_user_modif), chk_active = VALUES(chk_active)"; $insertStmt = $targetDb->prepare($insertQuery); // Compteurs $inserted = 0; $skipped = 0; $errors = 0; // Traitement par lots pour éviter les problèmes de mémoire for ($batch = 0; $batch < $totalBatches; $batch++) { $offset = $batch * $batchSize; echo "Traitement du lot " . ($batch + 1) . "/$totalBatches (offset: $offset)" . PHP_EOL; // Récupération d'un lot de passages $query = " SELECT p.* FROM ope_pass p WHERE p.fk_operation IN ($operationIds) AND p.active = 1 LIMIT $batchSize OFFSET $offset "; $stmt = $sourceDb->query($query); $passages = $stmt->fetchAll(PDO::FETCH_ASSOC); echo " - " . count($passages) . " passages récupérés dans ce lot" . PHP_EOL; // Libérer la mémoire après avoir récupéré les données $stmt->closeCursor(); unset($stmt); // Traitement des passages de ce lot $batchInserted = 0; $batchSkipped = 0; $batchErrors = 0; // Commencer une transaction pour les insertions de ce lot $targetDb->beginTransaction(); foreach ($passages as $passage) { $fkOperation = $passage['fk_operation']; $fkOldSector = $passage['fk_sector']; // Vérifier si le secteur existe dans la table ope_sectors de la cible // On utilise la requête pour trouver le secteur correspondant dans la table ope_sectors $sectorQuery = "SELECT id FROM ope_sectors WHERE fk_operation = :fk_operation AND fk_old_sector = :fk_old_sector"; $sectorStmt = $targetDb->prepare($sectorQuery); $sectorStmt->execute([ ':fk_operation' => $fkOperation, ':fk_old_sector' => $fkOldSector ]); $newSector = $sectorStmt->fetch(PDO::FETCH_ASSOC); // Si le secteur n'a pas été migré, on ignore ce passage silencieusement if (!$newSector) { $skipped++; continue; } $fkNewSector = $newSector['id']; // Vérifier si l'utilisateur existe dans la table users de la cible $fkUser = $passage['fk_user'] ?? 0; if ($fkUser > 0 && !in_array($fkUser, $migratedUsers)) { // L'utilisateur n'a pas été migré, on ignore ce passage silencieusement $skipped++; continue; } // Conversion des dates $passedAt = !empty($passage['date_eve']) ? date('Y-m-d H:i:s', strtotime($passage['date_eve'])) : null; $dateRepasser = !empty($passage['date_repasser']) ? date('Y-m-d H:i:s', strtotime($passage['date_repasser'])) : null; $createdAt = !empty($passage['date_creat']) ? date('Y-m-d H:i:s', strtotime($passage['date_creat'])) : date('Y-m-d H:i:s'); $updatedAt = !empty($passage['date_modif']) ? date('Y-m-d H:i:s', strtotime($passage['date_modif'])) : null; // Chiffrement des données sensibles // Validation et chiffrement du nom $encryptedName = ''; if (!empty($passage['libelle'])) { $encryptedName = ApiService::encryptData($passage['libelle']); } // Validation et chiffrement de l'email $encryptedEmail = ''; if (!empty($passage['email'])) { // Vérifier si l'email est valide if (filter_var($passage['email'], FILTER_VALIDATE_EMAIL)) { $encryptedEmail = ApiService::encryptSearchableData($passage['email']); } } $encryptedPhone = !empty($passage['phone']) ? ApiService::encryptData($passage['phone']) : ''; // Vérification et correction du type de règlement $fkTypeReglement = $passage['fk_type_reglement'] ?? 1; if (!in_array($fkTypeReglement, [1, 2, 3])) { $fkTypeReglement = 4; // Forcer à 4 si différent de 1, 2 ou 3 } // Préparation des données pour l'insertion $passageData = [ 'fk_operation' => $fkOperation, 'fk_sector' => $fkNewSector, 'fk_user' => $passage['fk_user'] ?? 0, 'fk_adresse' => $passage['fk_adresse'] ?? '', 'passed_at' => $passedAt, // Mapping date_eve => passed_at 'fk_type' => (isset($passage['fk_type']) && $passage['fk_type'] == '9') ? 6 : ((isset($passage['fk_type']) && $passage['fk_type'] == '8') ? 5 : (isset($passage['fk_type']) ? (int)$passage['fk_type'] : 0)), 'numero' => $passage['numero'] ?? '', 'rue' => $passage['rue'] ?? '', 'rue_bis' => $passage['rue_bis'] ?? '', 'ville' => $passage['ville'] ?? '', 'fk_habitat' => $passage['fk_habitat'] ?? 1, 'appt' => $passage['appt'] ?? '', 'niveau' => $passage['niveau'] ?? '', 'gps_lat' => $passage['gps_lat'] ?? '', 'gps_lng' => $passage['gps_lng'] ?? '', 'encrypted_name' => $encryptedName, // Mapping libelle => encrypted_name avec chiffrement 'montant' => $passage['montant'] ?? 0, 'fk_type_reglement' => $fkTypeReglement, // Valeur corrigée 'remarque' => $passage['remarque'] ?? '', 'encrypted_email' => $encryptedEmail, // Mapping email => encrypted_email avec chiffrement 'nom_recu' => $passage['recu'] ?? null, // Mapping recu => nom_recu 'email_erreur' => $passage['email_erreur'] ?? '', 'chk_email_sent' => $passage['chk_email_sent'] ?? 0, 'encrypted_phone' => $encryptedPhone, // Mapping phone => encrypted_phone avec chiffrement 'docremis' => $passage['docremis'] ?? 0, 'date_repasser' => $dateRepasser, 'nb_passages' => $passage['nb_passages'] ?? 1, 'chk_gps_maj' => $passage['chk_gps_maj'] ?? 0, 'chk_map_create' => $passage['chk_map_create'] ?? 0, 'chk_mobile' => $passage['chk_mobile'] ?? 0, 'chk_synchro' => $passage['chk_synchro'] ?? 1, 'chk_api_adresse' => $passage['chk_api_adresse'] ?? 0, 'chk_maj_adresse' => $passage['chk_maj_adresse'] ?? 0, 'anomalie' => $passage['anomalie'] ?? 0, 'created_at' => $createdAt, 'fk_user_creat' => $passage['fk_user_creat'] ?? null, 'updated_at' => $updatedAt, 'fk_user_modif' => $passage['fk_user_modif'] ?? null, 'chk_active' => $passage['active'] ?? 1 ]; try { // Insertion dans la table cible $insertStmt->execute($passageData); $inserted++; $batchInserted++; // Libérer un peu de mémoire entre chaque insertion if ($batchInserted % 100 == 0) { unset($passageData); gc_collect_cycles(); // Forcer le garbage collector } } catch (PDOException $e) { echo "ERREUR: Migration du passage (rowid " . $passage['rowid'] . ", opération $fkOperation) : " . $e->getMessage() . "\n"; $errors++; $batchErrors++; } // Libérer la mémoire du passage traité unset($passage); } // Valider la transaction pour ce lot try { $targetDb->commit(); echo " - Lot $batch commité avec succès: $batchInserted insérés, $batchSkipped ignorés, $batchErrors erreurs" . PHP_EOL; } catch (PDOException $e) { $targetDb->rollBack(); echo "ERREUR lors du commit du lot $batch: " . $e->getMessage() . PHP_EOL; } // Libérer la mémoire après chaque lot unset($passages); gc_collect_cycles(); // Forcer le garbage collector // Petite pause entre les lots pour éviter de surcharger le serveur sleep(1); } echo "Migration de la table ope_pass terminée. $inserted passages insérés, $skipped passages ignorés, $errors erreurs." . PHP_EOL; // Recréation des contraintes relationnelles echo "Tentative de recréation des contraintes relationnelles... " . PHP_EOL; $addConstraintsQueries = [ "ALTER TABLE ope_pass ADD CONSTRAINT ope_pass_ibfk_1 FOREIGN KEY (fk_operation) REFERENCES operations (id) ON DELETE RESTRICT ON UPDATE CASCADE", "ALTER TABLE ope_pass ADD CONSTRAINT ope_pass_ibfk_2 FOREIGN KEY (fk_sector) REFERENCES ope_sectors (id) ON DELETE RESTRICT ON UPDATE CASCADE", "ALTER TABLE ope_pass ADD CONSTRAINT ope_pass_ibfk_3 FOREIGN KEY (fk_user) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE", "ALTER TABLE ope_pass ADD CONSTRAINT ope_pass_ibfk_4 FOREIGN KEY (fk_type_reglement) REFERENCES x_types_reglements (id) ON DELETE RESTRICT ON UPDATE CASCADE" ]; $constraintAddFailed = false; foreach ($addConstraintsQueries as $query) { try { $targetDb->exec($query); echo " - Contrainte recréée avec succès : " . substr($query, 0, 60) . "..." . PHP_EOL; } catch (PDOException $e) { echo " - Erreur lors de la recréation de la contrainte : " . $e->getMessage() . PHP_EOL; $constraintAddFailed = true; } } if ($constraintAddFailed) { echo "ATTENTION: Certaines contraintes n'ont pas pu être recréées." . PHP_EOL; echo " Script SQL pour recréer manuellement les contraintes:" . PHP_EOL; echo "--------------------------------------------------------------" . PHP_EOL; foreach ($addConstraintsQueries as $query) { echo "$query;" . PHP_EOL; } echo "--------------------------------------------------------------" . PHP_EOL; } // Fermer le tunnel SSH closeSshTunnel(); } catch (Exception $e) { echo "ERREUR CRITIQUE: " . $e->getMessage() . PHP_EOL; // Fermer le tunnel SSH en cas d'erreur closeSshTunnel(); exit(1); }