feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles

- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD)
  * DEV: Clés TEST Pierre (mode test)
  * REC: Clés TEST Client (mode test)
  * PROD: Clés LIVE Client (mode live)
- Ajout de la gestion des bases de données immeubles/bâtiments
  * Configuration buildings_database pour DEV/REC/PROD
  * Service BuildingService pour enrichissement des adresses
- Optimisations pages et améliorations ergonomie
- Mises à jour des dépendances Composer
- Nettoyage des fichiers obsolètes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-11-09 18:26:27 +01:00
parent 21657a3820
commit 2f5946a184
812 changed files with 142105 additions and 25992 deletions

View File

@@ -0,0 +1,410 @@
# Migration v2 - Architecture modulaire
## Vue d'ensemble
Cette nouvelle architecture simplifie la migration en utilisant :
- **Source fixe** : `geosector` (synchronisée 2x/jour par PM7 depuis nx4)
- **Multi-environnement** : `--env=dva` (développement), `--env=rca` (recette) ou `--env=pra` (production)
- **Auto-détection** : L'environnement est détecté automatiquement selon le serveur
- **Classes réutilisables** : Configuration, Logger, Connexion
## Structure modulaire
```
migration2/
├── README.md # Ce fichier
├── logs/ # Logs de migration (auto-créé)
│ └── .gitignore
├── php/
│ ├── migrate_from_backup.php # Script principal orchestrateur
│ └── lib/
│ ├── DatabaseConfig.php # Configuration multi-env
│ ├── MigrationLogger.php # Gestion des logs
│ ├── DatabaseConnection.php # Connexions PDO
│ ├── OperationMigrator.php # Migration des opérations
│ ├── UserMigrator.php # Migration des ope_users
│ ├── SectorMigrator.php # Migration des secteurs
│ └── PassageMigrator.php # Migration des passages
```
**Architecture modulaire** : Chaque type de données a son propre migrator spécialisé, orchestré par le script principal.
## ⚠️ AVERTISSEMENT IMPORTANT
**Par défaut, le script SUPPRIME toutes les données de l'entité dans la base cible avant la migration.**
Cela inclut :
- ✅ Toutes les opérations de l'entité
- ✅ Tous les utilisateurs de l'entité
- ✅ Tous les secteurs et passages
- ✅ Tous les médias associés
- L'entité elle-même est conservée (seules les données liées sont supprimées)
Pour **désactiver** la suppression et conserver les données existantes :
```bash
php php/migrate_from_backup.php --mode=entity --entity-id=45 --delete-before=false
```
⚠️ **Attention** : Sans suppression préalable, risque de doublons si les données existent déjà.
---
## Utilisation
### Migration d'une entité spécifique
#### Sur dva-geo (IN3)
```bash
# Auto-détection de l'environnement (recommandé)
php php/migrate_from_backup.php --mode=entity --entity-id=45
# Ou avec environnement explicite
php php/migrate_from_backup.php --env=dva --mode=entity --entity-id=45
```
#### Sur rca-geo (IN3)
```bash
# Auto-détection de l'environnement (recommandé)
php php/migrate_from_backup.php --mode=entity --entity-id=45
# Ou avec environnement explicite
php php/migrate_from_backup.php --env=rca --mode=entity --entity-id=45
```
#### Sur pra-geo (IN4)
```bash
# Auto-détection de l'environnement (recommandé)
php php/migrate_from_backup.php --mode=entity --entity-id=45
# Ou avec environnement explicite
php php/migrate_from_backup.php --env=pra --mode=entity --entity-id=45
```
### Migration globale (toutes les entités)
```bash
# Sur dva-geo, rca-geo ou pra-geo
php php/migrate_from_backup.php --mode=global
```
### Options disponibles
```bash
--env=ENV # 'dva' (développement), 'rca' (recette) ou 'pra' (production)
# Par défaut : auto-détection selon le hostname
--mode=MODE # 'global' ou 'entity' (défaut: global)
--entity-id=ID # ID de l'entité à migrer (requis si mode=entity)
--log=PATH # Fichier de log personnalisé
# Par défaut : logs/migration_YYYYMMDD_HHMMSS.log
--delete-before # Supprimer les données existantes avant migration (défaut: true)
--help # Afficher l'aide complète
```
### Exemples d'utilisation
```bash
# Migration STANDARD (avec suppression des données existantes - recommandé)
php php/migrate_from_backup.php --mode=entity --entity-id=45
# Migration SANS suppression (pour ajout/mise à jour uniquement - risque de doublons)
php php/migrate_from_backup.php --mode=entity --entity-id=45 --delete-before=false
# Migration avec log personnalisé
php php/migrate_from_backup.php --mode=entity --entity-id=45 --log=/custom/path/entity_45.log
# Afficher l'aide complète
php php/migrate_from_backup.php --help
```
## Différences avec l'ancienne version
| Aspect | Ancien | Nouveau |
|--------|--------|---------|
| **Source** | `--source-db=geosector_YYYYMMDD_HH` | Toujours `geosector` (fixe) |
| **Cible** | `--target-db=pra_geo` | Déduite de `--env` ou auto-détectée (dva_geo, rca_geo, pra_geo) |
| **Config** | Constantes hardcodées | Classes configurables |
| **Environnement** | Manuel | Auto-détection par hostname (dva-geo, rca-geo, pra-geo) |
| **Arguments** | 2 arguments DB requis | 1 seul `--env` (optionnel) |
## Avantages
**Plus simple** : Plus besoin de spécifier les noms de bases
**Plus sûr** : Moins de risques d'erreurs de saisie
**Plus flexible** : Fonctionne sur dva-geo, rca-geo et pra-geo sans modification
**Plus maintenable** : Configuration centralisée dans DatabaseConfig
**Meilleurs logs** : Séparateurs, niveaux (info/warning/error/success)
## Déploiement
### Copier vers dva-geo (IN3)
```bash
scp -r migration2 root@195.154.80.116:/tmp/
ssh root@195.154.80.116 "incus file push -r /tmp/migration2 dva-geo/var/www/geosector/api/scripts/"
```
### Copier vers rca-geo (IN3)
```bash
scp -r migration2 root@195.154.80.116:/tmp/
ssh root@195.154.80.116 "incus file push -r /tmp/migration2 rca-geo/var/www/geosector/api/scripts/"
```
### Copier vers pra-geo (IN4)
```bash
scp -r migration2 root@51.159.7.190:/tmp/
ssh root@51.159.7.190 "incus file push -r /tmp/migration2 pra-geo/var/www/geosector/api/scripts/"
```
### Test après déploiement
```bash
# Se connecter au container
incus exec dva-geo -- bash # ou rca-geo, ou pra-geo
# Tester avec une entité
cd /var/www/geosector/api/scripts/migration2
php php/migrate_from_backup.php --mode=entity --entity-id=45
```
## Logs
Les logs sont enregistrés par défaut dans :
```
scripts/migration2/logs/migration_[MODE]_YYYYMMDD_HHMMSS.log
```
**Nommage automatique selon le mode :**
- Migration globale : `migration_global_20251021_143045.log`
- Migration d'une entité : `migration_entite_45_20251021_143045.log`
Format des logs :
- `[INFO]` : Informations générales
- `[SUCCESS]` : Opérations réussies
- `[WARNING]` : Avertissements
- `[ERROR]` : Erreurs
Le dossier `logs/` est créé automatiquement si nécessaire.
**Note :** Vous pouvez toujours spécifier un fichier de log personnalisé avec l'option `--log=PATH`.
## Récapitulatif de migration
À la fin de chaque migration, un **récapitulatif détaillé** est automatiquement affiché et enregistré dans le fichier de log.
### Format du récapitulatif
```
========================================
📊 RÉCAPITULATIF DE LA MIGRATION
========================================
Entité: Nom de l'entité (ID: XX)
Date: YYYY-MM-DD HH:MM:SS
Opérations migrées: 3
Opération #1: "Adhésions 2024" (ID: 850)
├─ Utilisateurs: 12
├─ Secteurs: 5
├─ Passages totaux: 245
└─ Détail par secteur:
├─ Centre-ville (ID: 5400)
│ ├─ Utilisateurs affectés: 3
│ └─ Passages: 67
├─ Quartier Est (ID: 5401)
│ ├─ Utilisateurs affectés: 5
│ └─ Passages: 98
└─ Nord (ID: 5402)
├─ Utilisateurs affectés: 4
└─ Passages: 80
Opération #2: "Collecte Printemps" (ID: 851)
├─ Utilisateurs: 8
├─ Secteurs: 3
├─ Passages totaux: 156
└─ Détail par secteur:
[...]
========================================
```
### Informations fournies
Le récapitulatif inclut pour chaque migration :
**Au niveau de l'entité :**
- Nom et ID de l'entité
- Date et heure de la migration
- Nombre total d'opérations migrées
**Pour chaque opération :**
- Nom et nouvel ID
- Nombre d'utilisateurs migrés
- Nombre de secteurs migrés
- Nombre total de passages migrés
**Pour chaque secteur :**
- Nom et nouvel ID
- Nombre d'utilisateurs affectés au secteur
- Nombre de passages effectués dans le secteur
### Utilisation du récapitulatif
Ce récapitulatif permet de :
- ✅ Vérifier rapidement que toutes les données ont été migrées
- ✅ Comparer avec les données source pour validation
- ✅ Identifier d'éventuelles anomalies (secteurs vides, passages manquants)
- ✅ Documenter précisément ce qui a été migré
- ✅ Tracer les migrations pour audit
Le récapitulatif est présent à la fois :
- **À l'écran** (stdout) en temps réel
- **Dans le fichier de log** pour conservation
## Dépannage
### Erreur "env doit être 'dva', 'rca' ou 'pra'"
L'auto-détection a échoué. Spécifiez manuellement :
```bash
php php/migrate_from_backup.php --env=dva --mode=entity --entity-id=45
```
### Erreur de connexion
Vérifiez que vous êtes bien dans le bon container (dva-geo, rca-geo ou pra-geo).
### Données dupliquées après migration
Si vous avez des doublons, c'est que vous avez utilisé `--delete-before=false` sur des données existantes.
**Solution** : Refaire la migration avec suppression (défaut) :
```bash
php php/migrate_from_backup.php --mode=entity --entity-id=45
```
### Vérifier ce qui sera supprimé avant migration
Consultez la section "Ordre de suppression" ci-dessous pour voir exactement quelles tables seront affectées.
### Logs non créés
Vérifiez les permissions du dossier `logs/` :
```bash
ls -la scripts/migration2/logs/
```
## Détails techniques
### Architecture hiérarchique de migration
La migration fonctionne par **opération** avec une hiérarchie complète :
```
Pour chaque opération de l'entité:
migrateOperation($oldOperationId)
├── Créer operation
├── Migrer ope_users (DISTINCT depuis ope_users_sectors)
│ └── Mapper oldUserId → newOpeUserId
├── Pour chaque secteur DISTINCT de l'opération:
│ └── migrateSector($oldOperationId, $newOperationId, $oldSectorId)
│ ├── Créer ope_sectors
│ ├── Mapper "opId_sectId" → newOpeSectorId
│ ├── Migrer sectors_adresses (fk_sector = newOpeSectorId)
│ ├── Migrer ope_users_sectors (avec mappings users + sector)
│ ├── Migrer ope_pass (avec mappings users + sector)
│ │ └── Pour chaque passage:
│ │ └── migratePassageHisto($oldPassId, $newPassId)
│ └── Migrer médias des passages
└── Migrer médias de l'opération
```
### Changement d'organisation des données : Exemple concret
#### Contexte : Opération de collecte des adhésions 2024
**Ancienne organisation** (base geosector - partagée) :
- 1 opération "Adhésions 2024" avec ID 450
- 3 utilisateurs affectés : Jean (ID 100), Marie (ID 101), Paul (ID 102)
- 2 secteurs utilisés : Centre-ville (ID 1004) et Quartier Est (ID 1005)
- Jean travaille sur Centre-ville, Marie et Paul sur Quartier Est
Dans l'ancienne base :
- Les 3 users existent UNE SEULE FOIS dans la table centrale `users`
- Les 2 secteurs existent UNE SEULE FOIS dans la table centrale `sectors`
- Les liens entre users et secteurs sont dans `ope_users_sectors`
- Les passages font référence directement aux users (ID 100, 101, 102)
**Nouvelle organisation** (base rca_geo/pra_geo - isolée par opération) :
Après migration, **CHAQUE opération devient autonome** :
- L'opération "Adhésions 2024" reçoit un nouvel ID (exemple : 850)
- Les 3 utilisateurs sont **dupliqués** dans `ope_users` avec de nouveaux IDs :
- Jean → ope_users.id = 2500 (avec fk_user = 100 et fk_operation = 850)
- Marie → ope_users.id = 2501 (avec fk_user = 101 et fk_operation = 850)
- Paul → ope_users.id = 2502 (avec fk_user = 102 et fk_operation = 850)
- Les 2 secteurs sont **dupliqués** dans `ope_sectors` :
- Centre-ville → ope_sectors.id = 5400 (avec fk_operation = 850)
- Quartier Est → ope_sectors.id = 5401 (avec fk_operation = 850)
- Tous les passages sont mis à jour pour référencer les NOUVEAUX IDs (2500, 2501, 2502)
**Pourquoi cette duplication ?**
**Isolation complète** : Si l'opération est supprimée, tout part avec (secteurs, users, passages)
**Performance** : Pas de jointures complexes entre opérations
**Historique** : Les données de l'opération restent figées dans le temps
**Simplicité** : Chaque opération est indépendante
**Impact pour un utilisateur qui travaille sur 3 opérations différentes** :
- Il existera 1 seule fois dans la table centrale `users` (ID 100)
- Il existera 3 fois dans `ope_users` (1 enregistrement par opération)
- Chaque enregistrement `ope_users` garde la référence vers `users.id = 100`
Cette architecture permet de **fermer** une opération complètement sans impacter les autres.
### Sélection des opérations à migrer
Pour chaque entité, **maximum 3 opérations** sont migrées :
1. **1 opération active** (`active = 1`)
2. **2 dernières opérations inactives** (`active = 0`) ayant au moins **10 passages effectués** (`fk_type = 1`)
### Ordre de suppression (si --delete-before=true)
Les données sont supprimées dans cet ordre pour respecter les contraintes de clés étrangères :
1. `medias` - Médias associés à l'entité ou aux opérations
2. `ope_pass_histo` - Historique des passages
3. `ope_pass` - Passages
4. `ope_users_sectors` - Associations utilisateurs/secteurs
5. `ope_users` - Utilisateurs d'opérations
6. `sectors_adresses` - Adresses de secteurs
7. `ope_sectors` - Secteurs d'opérations
8. `operations` - Opérations
9. `users` - Utilisateurs de l'entité
⚠️ **L'entité elle-même** (`entites`) **n'est jamais supprimée**.
### Tables de référence non migrées
Les tables suivantes ne sont **pas** migrées car déjà remplies dans la cible :
- `x_*` - Tables de référence (secteurs, adresses, etc.)
## Notes importantes
1. **Configuration centralisée** : Les paramètres de connexion DB sont récupérés depuis `AppConfig.php` - pas de duplication
2. **Chiffrement** : ApiService est toujours utilisé pour les mots de passe
3. **Logique métier** : Inchangée (migrateEntites, migrateUsers, etc.)
4. **Mappings** : Secteurs et adresses sont toujours mappés automatiquement
5. **Backup** : Un backup de l'ancien script est disponible dans `migrate_from_backup.php.backup`
6. **Suppression par défaut** : Activée pour éviter les doublons et garantir une migration propre
## Statut
**Architecture modulaire v2** :
- ✅ DatabaseConfig.php - Configuration multi-environnement
- ✅ MigrationLogger.php - Gestion des logs
- ✅ DatabaseConnection.php - Connexions PDO
- ✅ OperationMigrator.php - Migration hiérarchique des opérations
- ✅ UserMigrator.php - Migration des utilisateurs par opération
- ✅ SectorMigrator.php - Migration des secteurs par opération
- ✅ PassageMigrator.php - Migration des passages et historiques
- ✅ migrate_from_backup.php - Script principal orchestrateur
- ⏳ Tests sur rca-geo
- ⏳ Tests sur pra-geo
## Support
En cas de problème, consulter les logs détaillés ou contacter l'équipe technique.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
*.log

View File

@@ -0,0 +1,467 @@
#!/bin/bash
###############################################################################
# Script de migration en batch des entités depuis geosector_20251008
#
# Usage: ./migrate_batch.sh [options]
#
# Options:
# --start N Commencer à partir de l'entité N (défaut: 1)
# --limit N Migrer seulement N entités (défaut: toutes)
# --dry-run Simuler sans exécuter
# --continue Continuer après une erreur (défaut: s'arrêter)
# --interactive Mode interactif (défaut si aucune option)
#
# Exemple:
# ./migrate_batch.sh --start 10 --limit 5
# ./migrate_batch.sh --continue
# ./migrate_batch.sh --interactive
###############################################################################
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
JSON_FILE="${SCRIPT_DIR}/migrations_entites.json"
LOG_DIR="/var/www/geosector/api/logs/migrations"
MIGRATION_SCRIPT="${SCRIPT_DIR}/php/migrate_from_backup.php"
SOURCE_DB="geosector_20251013_13"
TARGET_DB="pra_geo"
# Paramètres par défaut
START_INDEX=1
LIMIT=0
DRY_RUN=0
CONTINUE_ON_ERROR=0
INTERACTIVE_MODE=0
SPECIFIC_ENTITY_ID=""
SPECIFIC_CP=""
# Couleurs
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Sauvegarder le nombre d'arguments avant le parsing
INITIAL_ARGS=$#
# Parse des arguments
while [[ $# -gt 0 ]]; do
case $1 in
--start)
START_INDEX="$2"
shift 2
;;
--limit)
LIMIT="$2"
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--continue)
CONTINUE_ON_ERROR=1
shift
;;
--interactive|-i)
INTERACTIVE_MODE=1
shift
;;
--help)
grep "^#" "$0" | grep -v "^#!/bin/bash" | sed 's/^# //'
exit 0
;;
*)
echo "Option inconnue: $1"
echo "Utilisez --help pour l'aide"
exit 1
;;
esac
done
# Activer le mode interactif si aucun argument n'a été fourni
if [ $INITIAL_ARGS -eq 0 ]; then
INTERACTIVE_MODE=1
fi
# Vérifications préalables
if [ ! -f "$JSON_FILE" ]; then
echo -e "${RED}❌ Fichier JSON introuvable: $JSON_FILE${NC}"
exit 1
fi
if [ ! -f "$MIGRATION_SCRIPT" ]; then
echo -e "${RED}❌ Script de migration introuvable: $MIGRATION_SCRIPT${NC}"
exit 1
fi
# Créer le répertoire de logs
mkdir -p "$LOG_DIR"
# Fichiers de log
BATCH_LOG="${LOG_DIR}/batch_$(date +%Y%m%d_%H%M%S).log"
SUCCESS_LOG="${LOG_DIR}/success.log"
ERROR_LOG="${LOG_DIR}/errors.log"
# MODE INTERACTIF
if [ $INTERACTIVE_MODE -eq 1 ]; then
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} 🔧 Mode interactif - Migration d'entités GeoSector${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
echo ""
# Question 1: Migration globale ou ciblée ?
echo -e "${YELLOW}1⃣ Type de migration :${NC}"
echo -e " ${CYAN}a)${NC} Migration globale (toutes les entités)"
echo -e " ${CYAN}b)${NC} Migration par lot (plage d'entités)"
echo -e " ${CYAN}c)${NC} Migration par code postal"
echo -e " ${CYAN}d)${NC} Migration d'une entité spécifique (ID)"
echo ""
echo -ne "${YELLOW}Votre choix (a/b/c/d) : ${NC}"
read -r MIGRATION_TYPE
echo ""
case $MIGRATION_TYPE in
a|A)
# Migration globale - garder les valeurs par défaut
START_INDEX=1
LIMIT=0
echo -e "${GREEN}${NC} Migration globale sélectionnée"
;;
b|B)
# Migration par lot
echo -e "${YELLOW}2⃣ Configuration du lot :${NC}"
echo -ne " Première entité (index, défaut=1) : "
read -r USER_START
if [ -n "$USER_START" ]; then
START_INDEX=$USER_START
fi
echo -ne " Limite (nombre d'entités, défaut=toutes) : "
read -r USER_LIMIT
if [ -n "$USER_LIMIT" ]; then
LIMIT=$USER_LIMIT
fi
echo ""
echo -e "${GREEN}${NC} Migration par lot : de l'index $START_INDEX, limite de $LIMIT entités"
;;
c|C)
# Migration par code postal
echo -ne "${YELLOW}2⃣ Code postal à migrer : ${NC}"
read -r SPECIFIC_CP
echo ""
if [ -z "$SPECIFIC_CP" ]; then
echo -e "${RED}❌ Code postal requis${NC}"
exit 1
fi
echo -e "${GREEN}${NC} Migration pour le code postal : $SPECIFIC_CP"
;;
d|D)
# Migration d'une entité spécifique - bypass complet du JSON
echo -ne "${YELLOW}2⃣ ID de l'entité à migrer : ${NC}"
read -r SPECIFIC_ENTITY_ID
echo ""
if [ -z "$SPECIFIC_ENTITY_ID" ]; then
echo -e "${RED}❌ ID d'entité requis${NC}"
exit 1
fi
echo -e "${GREEN}${NC} Migration de l'entité ID : $SPECIFIC_ENTITY_ID"
echo ""
# Demander si suppression des données de l'entité avant migration
echo -ne "${YELLOW}3⃣ Supprimer les données existantes de cette entité dans la TARGET avant migration ? (y/N): ${NC}"
read -r DELETE_BEFORE
DELETE_FLAG=""
if [[ $DELETE_BEFORE =~ ^[Yy]$ ]]; then
echo -e "${GREEN}${NC} Les données seront supprimées avant migration"
DELETE_FLAG="--delete-before"
else
echo -e "${BLUE}${NC} Les données seront conservées (ON DUPLICATE KEY UPDATE)"
fi
echo ""
# Confirmer la migration
echo -ne "${YELLOW}⚠️ Confirmer la migration de l'entité #${SPECIFIC_ENTITY_ID} ? (y/N): ${NC}"
read -r CONFIRM
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
echo -e "${RED}❌ Migration annulée${NC}"
exit 0
fi
# Exécuter directement la migration sans passer par le JSON
ENTITY_LOG="${LOG_DIR}/entity_${SPECIFIC_ENTITY_ID}_$(date +%Y%m%d_%H%M%S).log"
echo ""
echo -e "${BLUE}⏳ Migration de l'entité #${SPECIFIC_ENTITY_ID} en cours...${NC}"
php "$MIGRATION_SCRIPT" \
--source-db="$SOURCE_DB" \
--target-db="$TARGET_DB" \
--mode=entity \
--entity-id="$SPECIFIC_ENTITY_ID" \
--log="$ENTITY_LOG" \
$DELETE_FLAG
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}✅ Entité #${SPECIFIC_ENTITY_ID} migrée avec succès${NC}"
echo -e "${BLUE}📋 Log détaillé : $ENTITY_LOG${NC}"
else
echo -e "${RED}❌ Erreur lors de la migration de l'entité #${SPECIFIC_ENTITY_ID}${NC}"
echo -e "${RED}📋 Voir le log : $ENTITY_LOG${NC}"
exit 1
fi
exit 0
;;
*)
echo -e "${RED}❌ Choix invalide${NC}"
exit 1
;;
esac
echo ""
fi
# Fonctions utilitaires
log() {
echo -e "$1" | tee -a "$BATCH_LOG"
}
log_success() {
echo "$1" >> "$SUCCESS_LOG"
log "${GREEN}${NC} $1"
}
log_error() {
echo "$1" >> "$ERROR_LOG"
log "${RED}${NC} $1"
}
# Extraire les entity_id du JSON (compatible sans jq)
get_entity_ids() {
if [ -n "$SPECIFIC_ENTITY_ID" ]; then
# Entité spécifique par ID - chercher exactement "entity_id" : ID,
grep "\"entity_id\" : ${SPECIFIC_ENTITY_ID}," "$JSON_FILE" | sed 's/.*: \([0-9]*\).*/\1/'
elif [ -n "$SPECIFIC_CP" ]; then
# Entités par code postal
grep -B 2 "\"code_postal\" : \"$SPECIFIC_CP\"" "$JSON_FILE" | grep '"entity_id"' | sed 's/.*: \([0-9]*\).*/\1/'
else
# Toutes les entités
grep '"entity_id"' "$JSON_FILE" | sed 's/.*: \([0-9]*\).*/\1/'
fi
}
# Compter le nombre total d'entités
TOTAL_ENTITIES=$(get_entity_ids | wc -l)
# Vérifier si des entités ont été trouvées
if [ $TOTAL_ENTITIES -eq 0 ]; then
if [ -n "$SPECIFIC_ENTITY_ID" ]; then
echo -e "${RED}❌ Entité #${SPECIFIC_ENTITY_ID} introuvable dans le fichier JSON${NC}"
elif [ -n "$SPECIFIC_CP" ]; then
echo -e "${RED}❌ Aucune entité trouvée pour le code postal ${SPECIFIC_CP}${NC}"
else
echo -e "${RED}❌ Aucune entité trouvée${NC}"
fi
exit 1
fi
# Calculer le nombre d'entités à migrer
if [ $LIMIT -gt 0 ]; then
END_INDEX=$((START_INDEX + LIMIT - 1))
if [ $END_INDEX -gt $TOTAL_ENTITIES ]; then
END_INDEX=$TOTAL_ENTITIES
fi
else
END_INDEX=$TOTAL_ENTITIES
fi
# Bannière de démarrage
echo ""
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
log "${BLUE} Migration en batch des entités GeoSector${NC}"
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
log "📅 Date: $(date '+%Y-%m-%d %H:%M:%S')"
log "📁 Source: $SOURCE_DB"
log "📁 Cible: $TARGET_DB"
# Afficher les informations selon le mode
if [ -n "$SPECIFIC_ENTITY_ID" ]; then
log "🎯 Mode: Migration d'une entité spécifique"
log "📊 Entité ID: $SPECIFIC_ENTITY_ID"
elif [ -n "$SPECIFIC_CP" ]; then
log "🎯 Mode: Migration par code postal"
log "📮 Code postal: $SPECIFIC_CP"
log "📊 Entités trouvées: $TOTAL_ENTITIES"
else
TOTAL_AVAILABLE=$(grep '"entity_id"' "$JSON_FILE" | wc -l)
log "📊 Total entités disponibles: $TOTAL_AVAILABLE"
log "🎯 Entités à migrer: $START_INDEX à $END_INDEX"
fi
if [ $DRY_RUN -eq 1 ]; then
log "${YELLOW}🔍 Mode DRY-RUN (simulation)${NC}"
fi
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
echo ""
# Confirmation utilisateur
if [ $DRY_RUN -eq 0 ]; then
echo -ne "${YELLOW}⚠️ Confirmer la migration ? (y/N): ${NC}"
read -r CONFIRM
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
log "❌ Migration annulée par l'utilisateur"
exit 0
fi
echo ""
fi
# Compteurs
SUCCESS_COUNT=0
ERROR_COUNT=0
SKIPPED_COUNT=0
CURRENT_INDEX=0
# Début de la migration
START_TIME=$(date +%s)
# Lire les entity_id et migrer
get_entity_ids | while read -r ENTITY_ID; do
CURRENT_INDEX=$((CURRENT_INDEX + 1))
# Filtrer par index
if [ $CURRENT_INDEX -lt $START_INDEX ]; then
continue
fi
if [ $CURRENT_INDEX -gt $END_INDEX ]; then
break
fi
# Récupérer les détails de l'entité depuis le JSON (match exact avec la virgule)
ENTITY_INFO=$(grep -A 8 "\"entity_id\" : ${ENTITY_ID}," "$JSON_FILE")
ENTITY_NAME=$(echo "$ENTITY_INFO" | grep '"nom"' | sed 's/.*: "\(.*\)".*/\1/')
ENTITY_CP=$(echo "$ENTITY_INFO" | grep '"code_postal"' | sed 's/.*: "\(.*\)".*/\1/')
NB_USERS=$(echo "$ENTITY_INFO" | grep '"nb_users"' | sed 's/.*: \([0-9]*\).*/\1/')
NB_PASSAGES=$(echo "$ENTITY_INFO" | grep '"nb_passages"' | sed 's/.*: \([0-9]*\).*/\1/')
# Afficher la progression
PROGRESS=$((CURRENT_INDEX - START_INDEX + 1))
TOTAL=$((END_INDEX - START_INDEX + 1))
PERCENT=$((PROGRESS * 100 / TOTAL))
log ""
log "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
log "${BLUE}[$PROGRESS/$TOTAL - ${PERCENT}%]${NC} Entité #${ENTITY_ID}: ${ENTITY_NAME} (${ENTITY_CP})"
log " 👥 Users: ${NB_USERS} | 📍 Passages: ${NB_PASSAGES}"
# Mode dry-run
if [ $DRY_RUN -eq 1 ]; then
log "${YELLOW} 🔍 [DRY-RUN] Simulation de la migration${NC}"
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
continue
fi
# Exécuter la migration
ENTITY_LOG="${LOG_DIR}/entity_${ENTITY_ID}_$(date +%Y%m%d_%H%M%S).log"
log " ⏳ Migration en cours..."
php "$MIGRATION_SCRIPT" \
--source-db="$SOURCE_DB" \
--target-db="$TARGET_DB" \
--mode=entity \
--entity-id="$ENTITY_ID" \
--log="$ENTITY_LOG" > /tmp/migration_output_$$.txt 2>&1
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
# Succès
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
log_success "Entité #${ENTITY_ID} (${ENTITY_NAME}) migrée avec succès"
# Afficher un résumé du log avec détails
if [ -f "$ENTITY_LOG" ]; then
# Chercher la ligne avec les marqueurs #STATS#
STATS_LINE=$(grep "#STATS#" "$ENTITY_LOG" 2>/dev/null)
if [ -n "$STATS_LINE" ]; then
# Extraire chaque compteur
OPE=$(echo "$STATS_LINE" | grep -oE 'OPE:[0-9]+' | cut -d: -f2)
USERS=$(echo "$STATS_LINE" | grep -oE 'USER:[0-9]+' | cut -d: -f2)
SECTORS=$(echo "$STATS_LINE" | grep -oE 'SECTOR:[0-9]+' | cut -d: -f2)
PASSAGES=$(echo "$STATS_LINE" | grep -oE 'PASS:[0-9]+' | cut -d: -f2)
# Valeurs par défaut si extraction échoue
OPE=${OPE:-0}
USERS=${USERS:-0}
SECTORS=${SECTORS:-0}
PASSAGES=${PASSAGES:-0}
log " 📊 ope: ${OPE} | users: ${USERS} | sectors: ${SECTORS} | passages: ${PASSAGES}"
else
log " 📊 Statistiques non disponibles"
fi
fi
else
# Erreur
ERROR_COUNT=$((ERROR_COUNT + 1))
log_error "Entité #${ENTITY_ID} (${ENTITY_NAME}) - Erreur code $EXIT_CODE"
# Afficher les dernières lignes du log d'erreur
if [ -f "/tmp/migration_output_$$.txt" ]; then
log "${RED} 📋 Dernières erreurs:${NC}"
tail -5 "/tmp/migration_output_$$.txt" | sed 's/^/ /' | tee -a "$BATCH_LOG"
fi
# Arrêter ou continuer ?
if [ $CONTINUE_ON_ERROR -eq 0 ]; then
log ""
log "${RED}❌ Migration interrompue suite à une erreur${NC}"
log " Utilisez --continue pour continuer malgré les erreurs"
exit 1
fi
fi
# Nettoyage
rm -f "/tmp/migration_output_$$.txt"
# Pause entre les migrations (pour éviter de surcharger)
sleep 1
done
# Fin de la migration
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
HOURS=$((DURATION / 3600))
MINUTES=$(((DURATION % 3600) / 60))
SECONDS=$((DURATION % 60))
# Résumé final
log ""
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
log "${BLUE} Résumé de la migration${NC}"
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
log "✅ Succès: ${GREEN}${SUCCESS_COUNT}${NC}"
log "❌ Erreurs: ${RED}${ERROR_COUNT}${NC}"
log "⏭️ Ignorées: ${YELLOW}${SKIPPED_COUNT}${NC}"
log "⏱️ Durée: ${HOURS}h ${MINUTES}m ${SECONDS}s"
log ""
log "📋 Logs détaillés:"
log " - Batch: $BATCH_LOG"
log " - Succès: $SUCCESS_LOG"
log " - Erreurs: $ERROR_LOG"
log " - Individuels: $LOG_DIR/entity_*.log"
log "${BLUE}═══════════════════════════════════════════════════════════${NC}"
# Code de sortie
if [ $ERROR_COUNT -gt 0 ]; then
exit 1
else
exit 0
fi

View File

@@ -0,0 +1,176 @@
<?php
/**
* Classe abstraite de base pour tous les migrators
*
* Fournit les méthodes communes pour migrer des données d'une table
*/
abstract class DataMigrator
{
protected $connection;
protected $logger;
protected $sourceDb;
protected $targetDb;
/**
* Constructeur
*
* @param DatabaseConnection $connection Connexion aux bases
* @param MigrationLogger $logger Logger
*/
public function __construct(DatabaseConnection $connection, MigrationLogger $logger)
{
$this->connection = $connection;
$this->logger = $logger;
$this->sourceDb = $connection->getSourceDb();
$this->targetDb = $connection->getTargetDb();
}
/**
* Méthode principale de migration (à implémenter dans chaque migrator)
*
* @param int|null $entityId ID de l'entité à migrer (null = toutes)
* @param bool $deleteBefore Supprimer les données existantes avant migration
* @return array ['success' => int, 'errors' => int]
*/
abstract public function migrate(?int $entityId = null, bool $deleteBefore = false): array;
/**
* Retourne le nom de la table gérée par ce migrator
*/
abstract public function getTableName(): string;
/**
* Supprime les données d'une entité dans la cible
* À surcharger si la logique de suppression est spécifique
*
* @param int $entityId ID de l'entité
* @return int Nombre de lignes supprimées
*/
protected function deleteEntityData(int $entityId): int
{
$table = $this->getTableName();
try {
// Par défaut: suppression simple avec fk_entite
$stmt = $this->targetDb->prepare("DELETE FROM $table WHERE fk_entite = ?");
$stmt->execute([$entityId]);
$deleted = $stmt->rowCount();
if ($deleted > 0) {
$this->logger->debug(" Supprimé $deleted ligne(s) de $table pour entité #$entityId");
}
return $deleted;
} catch (PDOException $e) {
$this->logger->warning(" Erreur suppression $table: " . $e->getMessage());
return 0;
}
}
/**
* Compte les lignes dans la source
*
* @param int|null $entityId ID de l'entité (null = toutes)
* @return int Nombre de lignes
*/
protected function countSourceRows(?int $entityId = null): int
{
return $this->connection->countSourceRows($this->getTableName(), $entityId);
}
/**
* Compte les lignes dans la cible
*
* @param int|null $entityId ID de l'entité (null = toutes)
* @return int Nombre de lignes
*/
protected function countTargetRows(?int $entityId = null): int
{
return $this->connection->countTargetRows($this->getTableName(), $entityId);
}
/**
* Log le début de la migration d'une table
*/
protected function logStart(?int $entityId = null): void
{
$table = $this->getTableName();
$entityStr = $entityId ? " pour entité #$entityId" : " (toutes les entités)";
$this->logger->info("🔄 Migration de $table{$entityStr}...");
}
/**
* Log la fin de la migration avec statistiques
*
* @param int $success Nombre de succès
* @param int $errors Nombre d'erreurs
* @param int|null $entityId ID de l'entité
*/
protected function logEnd(int $success, int $errors, ?int $entityId = null): void
{
$table = $this->getTableName();
$sourceCount = $this->countSourceRows($entityId);
$targetCount = $this->countTargetRows($entityId);
$diff = $targetCount - $sourceCount;
$diffStr = $diff >= 0 ? "+$diff" : "$diff";
if ($errors > 0) {
$this->logger->warning(" ⚠️ $table: $success succès, $errors erreurs");
} else {
$this->logger->success("$table: $success enregistrement(s) migré(s)");
}
$this->logger->info(" 📊 SOURCE: $sourceCount → CIBLE: $targetCount (différence: $diffStr)");
}
/**
* Exécute une requête INSERT avec ON DUPLICATE KEY UPDATE
*
* @param string $insertSql SQL d'insertion
* @param array $data Données à insérer
* @return bool True si succès
*/
protected function insertOrUpdate(string $insertSql, array $data): bool
{
try {
$stmt = $this->targetDb->prepare($insertSql);
$stmt->execute($data);
return true;
} catch (PDOException $e) {
$this->logger->debug(" Erreur INSERT: " . $e->getMessage());
return false;
}
}
/**
* Démarre une transaction sur la cible
*/
protected function beginTransaction(): void
{
if (!$this->targetDb->inTransaction()) {
$this->targetDb->beginTransaction();
}
}
/**
* Commit la transaction
*/
protected function commit(): void
{
if ($this->targetDb->inTransaction()) {
$this->targetDb->commit();
}
}
/**
* Rollback la transaction
*/
protected function rollback(): void
{
if ($this->targetDb->inTransaction()) {
$this->targetDb->rollBack();
}
}
}

View File

@@ -0,0 +1,192 @@
<?php
/**
* Configuration des environnements de migration
*
* Utilise AppConfig pour récupérer la configuration DB
* Source: geosector (synchronisée par PM7)
* Cibles: dva_geo (IN3/maria3), rca_geo (IN3/maria3) ou pra_geo (IN4/maria4)
*/
class DatabaseConfig
{
private const ENV_MAPPING = [
'dva' => [
'name' => 'DÉVELOPPEMENT',
'hostname' => 'dapp.geosector.fr',
'source_db' => 'geosector',
'target_db' => 'dva_geo'
],
'rca' => [
'name' => 'RECETTE',
'hostname' => 'rapp.geosector.fr',
'source_db' => 'geosector',
'target_db' => 'rca_geo'
],
'pra' => [
'name' => 'PRODUCTION',
'hostname' => 'app3.geosector.fr',
'source_db' => 'geosector',
'target_db' => 'pra_geo'
]
];
private $env;
private $config;
private $appConfig;
/**
* Constructeur
*
* @param string $env Environnement: 'dva', 'rca' ou 'pra'
* @throws Exception Si l'environnement est invalide
*/
public function __construct(string $env)
{
if (!isset(self::ENV_MAPPING[$env])) {
throw new Exception("Invalid environment: $env. Use 'dva', 'rca' or 'pra'");
}
$this->env = $env;
// Charger AppConfig (remonter de 4 niveaux: lib -> php -> migration2 -> scripts -> api)
$appConfigPath = dirname(__DIR__, 4) . '/src/Config/AppConfig.php';
if (!file_exists($appConfigPath)) {
throw new Exception("AppConfig not found at: $appConfigPath");
}
require_once $appConfigPath;
// Simuler le host pour AppConfig en CLI
$hostname = self::ENV_MAPPING[$env]['hostname'];
$_SERVER['SERVER_NAME'] = $hostname;
$_SERVER['HTTP_HOST'] = $hostname;
$this->appConfig = AppConfig::getInstance();
// Récupérer la config DB depuis AppConfig
$dbConfig = $this->appConfig->getDatabaseConfig();
if (!$dbConfig || !isset($dbConfig['host'])) {
throw new Exception("Database configuration not found for hostname: $hostname");
}
// Construire la config pour la migration
$this->config = [
'name' => self::ENV_MAPPING[$env]['name'],
'host' => $dbConfig['host'],
'port' => $dbConfig['port'] ?? 3306,
'user' => $dbConfig['username'],
'pass' => $dbConfig['password'],
'source_db' => self::ENV_MAPPING[$env]['source_db'],
'target_db' => self::ENV_MAPPING[$env]['target_db']
];
}
/**
* Retourne l'environnement actuel
*/
public function getEnv(): string
{
return $this->env;
}
/**
* Retourne le nom complet de l'environnement
*/
public function getEnvName(): string
{
return $this->config['name'];
}
/**
* Retourne l'hôte de la base de données
*/
public function getHost(): string
{
return $this->config['host'];
}
/**
* Retourne le port de la base de données
*/
public function getPort(): int
{
return $this->config['port'];
}
/**
* Retourne l'utilisateur de la base de données
*/
public function getUser(): string
{
return $this->config['user'];
}
/**
* Retourne le mot de passe de la base de données
*/
public function getPassword(): string
{
return $this->config['pass'];
}
/**
* Retourne le nom de la base source
*/
public function getSourceDb(): string
{
return $this->config['source_db'];
}
/**
* Retourne le nom de la base cible
*/
public function getTargetDb(): string
{
return $this->config['target_db'];
}
/**
* Retourne toute la configuration
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Détecte automatiquement l'environnement depuis le hostname
*
* @return string 'dva', 'rca' ou 'pra' (défaut: 'dva')
*/
public static function autoDetect(): string
{
$hostname = gethostname();
switch ($hostname) {
case 'dva-geo':
return 'dva';
case 'rca-geo':
return 'rca';
case 'pra-geo':
return 'pra';
default:
return 'dva'; // Défaut
}
}
/**
* Vérifie si un environnement existe
*/
public static function exists(string $env): bool
{
return isset(self::ENV_MAPPING[$env]);
}
/**
* Retourne la liste des environnements disponibles
*/
public static function getAvailableEnvironments(): array
{
return array_keys(self::ENV_MAPPING);
}
}

View File

@@ -0,0 +1,201 @@
<?php
/**
* Gestion des connexions PDO
*
* Crée et maintient les connexions aux bases source et cible
*/
class DatabaseConnection
{
private $config;
private $logger;
private $sourceDb;
private $targetDb;
/**
* Constructeur
*
* @param DatabaseConfig $config Configuration de l'environnement
* @param MigrationLogger $logger Logger pour les messages
*/
public function __construct(DatabaseConfig $config, MigrationLogger $logger)
{
$this->config = $config;
$this->logger = $logger;
}
/**
* Établit les connexions aux bases source et cible
*
* @return bool True si succès
*/
public function connect(): bool
{
try {
// Connexion à la base source
$this->connectSource();
// Connexion à la base cible
$this->connectTarget();
// Vérifier les versions MariaDB
$this->checkVersions();
return true;
} catch (PDOException $e) {
$this->logger->error("Erreur de connexion: " . $e->getMessage());
return false;
}
}
/**
* Connexion à la base source
*/
private function connectSource(): void
{
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
$this->config->getHost(),
$this->config->getPort(),
$this->config->getSourceDb()
);
$this->sourceDb = new PDO($dsn, $this->config->getUser(), $this->config->getPassword(), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_TIMEOUT => 600,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
]);
$this->logger->success("✓ Connexion SOURCE: {$this->config->getSourceDb()} sur {$this->config->getHost()}");
}
/**
* Connexion à la base cible
*/
private function connectTarget(): void
{
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
$this->config->getHost(),
$this->config->getPort(),
$this->config->getTargetDb()
);
$this->targetDb = new PDO($dsn, $this->config->getUser(), $this->config->getPassword(), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_TIMEOUT => 600,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
]);
$this->logger->success("✓ Connexion CIBLE: {$this->config->getTargetDb()} sur {$this->config->getHost()}");
}
/**
* Vérifie et affiche les versions MariaDB
*/
private function checkVersions(): void
{
$sourceVersion = $this->sourceDb->query("SELECT VERSION()")->fetchColumn();
$targetVersion = $this->targetDb->query("SELECT VERSION()")->fetchColumn();
$this->logger->info(" Version SOURCE: $sourceVersion");
$this->logger->info(" Version CIBLE: $targetVersion");
}
/**
* Retourne la connexion à la base source
*/
public function getSourceDb(): PDO
{
if (!$this->sourceDb) {
throw new Exception("Source database not connected. Call connect() first.");
}
return $this->sourceDb;
}
/**
* Retourne la connexion à la base cible
*/
public function getTargetDb(): PDO
{
if (!$this->targetDb) {
throw new Exception("Target database not connected. Call connect() first.");
}
return $this->targetDb;
}
/**
* Compte le nombre de lignes dans une table de la source
*
* @param string $table Nom de la table
* @param int|null $entityId Filtrer par fk_entite (optionnel)
* @return int Nombre de lignes
*/
public function countSourceRows(string $table, ?int $entityId = null): int
{
$sql = "SELECT COUNT(*) FROM $table";
if ($entityId !== null) {
// Tables avec fk_entite direct
if (in_array($table, ['users', 'operations', 'entites'])) {
$sql .= " WHERE fk_entite = :entity_id";
}
// Tables liées via operations
elseif (in_array($table, ['ope_sectors', 'ope_users', 'ope_pass'])) {
$sql .= " WHERE fk_operation IN (SELECT id FROM operations WHERE fk_entite = :entity_id)";
}
}
$stmt = $this->sourceDb->prepare($sql);
if ($entityId !== null) {
$stmt->execute(['entity_id' => $entityId]);
} else {
$stmt->execute();
}
return (int) $stmt->fetchColumn();
}
/**
* Compte le nombre de lignes dans une table de la cible
*
* @param string $table Nom de la table
* @param int|null $entityId Filtrer par fk_entite (optionnel)
* @return int Nombre de lignes
*/
public function countTargetRows(string $table, ?int $entityId = null): int
{
$sql = "SELECT COUNT(*) FROM $table";
if ($entityId !== null) {
if (in_array($table, ['users', 'operations', 'entites'])) {
$sql .= " WHERE fk_entite = :entity_id";
}
elseif (in_array($table, ['ope_sectors', 'ope_users', 'ope_pass'])) {
$sql .= " WHERE fk_operation IN (SELECT id FROM operations WHERE fk_entite = :entity_id)";
}
}
$stmt = $this->targetDb->prepare($sql);
if ($entityId !== null) {
$stmt->execute(['entity_id' => $entityId]);
} else {
$stmt->execute();
}
return (int) $stmt->fetchColumn();
}
/**
* Ferme les connexions
*/
public function close(): void
{
$this->sourceDb = null;
$this->targetDb = null;
$this->logger->info("Connexions fermées");
}
}

View File

@@ -0,0 +1,219 @@
<?php
/**
* Gestion des logs de migration
*
* Écrit dans un fichier et affiche à l'écran avec timestamps
*/
class MigrationLogger
{
private $logFile;
private $verbose;
/**
* Constructeur
*
* @param string|null $logFile Chemin du fichier de log (null = auto-généré)
* @param bool $verbose Afficher les logs à l'écran
*/
public function __construct(?string $logFile = null, bool $verbose = true)
{
// Définir le répertoire de logs par défaut (migration2/logs/)
$defaultLogDir = dirname(__DIR__, 2) . '/logs';
$this->logFile = $logFile ?? $defaultLogDir . '/migration_' . date('Ymd_His') . '.log';
$this->verbose = $verbose;
// Créer le dossier parent si nécessaire
$dir = dirname($this->logFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
// Vérifier que le fichier est accessible en écriture
if (!is_writable(dirname($this->logFile))) {
throw new Exception("Log directory is not writable: " . dirname($this->logFile));
}
}
/**
* Log un message avec niveau INFO
*/
public function info(string $message): void
{
$this->log($message, 'INFO');
}
/**
* Log un message avec niveau SUCCESS
*/
public function success(string $message): void
{
$this->log($message, 'SUCCESS');
}
/**
* Log un message avec niveau WARNING
*/
public function warning(string $message): void
{
$this->log($message, 'WARNING');
}
/**
* Log un message avec niveau ERROR
*/
public function error(string $message): void
{
$this->log($message, 'ERROR');
}
/**
* Log un message avec niveau DEBUG
*/
public function debug(string $message): void
{
$this->log($message, 'DEBUG');
}
/**
* Log une ligne de séparation
*/
public function separator(): void
{
$this->log(str_repeat('=', 80), 'INFO');
}
/**
* Log générique
*
* @param string $message Message à logger
* @param string $level Niveau: INFO, SUCCESS, WARNING, ERROR, DEBUG
*/
private function log(string $message, string $level = 'INFO'): void
{
$timestamp = date('Y-m-d H:i:s');
$logLine = "[{$timestamp}] [{$level}] {$message}\n";
// Écriture dans le fichier
file_put_contents($this->logFile, $logLine, FILE_APPEND);
// Affichage à l'écran si verbose
if ($this->verbose) {
$this->printColored($message, $level);
}
}
/**
* Affiche un message coloré selon le niveau
*/
private function printColored(string $message, string $level): void
{
$colors = [
'INFO' => "\033[0;37m", // Blanc
'SUCCESS' => "\033[0;32m", // Vert
'WARNING' => "\033[0;33m", // Jaune
'ERROR' => "\033[0;31m", // Rouge
'DEBUG' => "\033[0;36m" // Cyan
];
$reset = "\033[0m";
$color = $colors[$level] ?? $colors['INFO'];
echo $color . $message . $reset . "\n";
}
/**
* Retourne le chemin du fichier de log
*/
public function getLogFile(): string
{
return $this->logFile;
}
/**
* Log des statistiques de migration
*
* @param array $stats Tableau associatif [table => count]
*/
public function logStats(array $stats): void
{
$this->separator();
$this->info("📊 Statistiques de migration:");
foreach ($stats as $table => $count) {
$this->info(" - {$table}: {$count} enregistrement(s)");
}
$this->separator();
}
/**
* Log une ligne spéciale pour parsing automatique
* Format: #STATS# KEY1:VAL1 KEY2:VAL2 ...
*/
public function logParsableStats(array $stats): void
{
$pairs = [];
foreach ($stats as $key => $value) {
$pairs[] = strtoupper($key) . ':' . $value;
}
$line = '#STATS# ' . implode(' ', $pairs);
$this->log($line, 'INFO');
}
/**
* Affiche et log un récapitulatif complet de migration
*
* @param array $summary Tableau de statistiques hiérarchique
*/
public function logMigrationSummary(array $summary): void
{
$this->separator();
$this->separator();
$this->info("📊 RÉCAPITULATIF DE LA MIGRATION");
$this->separator();
// Entité
if (isset($summary['entity'])) {
$this->info("Entité: {$summary['entity']['name']} (ID: {$summary['entity']['id']})");
}
$this->info("Date: " . date('Y-m-d H:i:s'));
$this->info("");
// Nombre total d'opérations
$totalOperations = count($summary['operations'] ?? []);
$this->success("Opérations migrées: {$totalOperations}");
$this->info("");
// Détail par opération
$operationNum = 1;
foreach ($summary['operations'] ?? [] as $operation) {
$this->info("Opération #{$operationNum}: \"{$operation['name']}\" (ID: {$operation['id']})");
$this->info(" ├─ Utilisateurs: {$operation['users']}");
$this->info(" ├─ Secteurs: {$operation['sectors']}");
$this->info(" ├─ Passages totaux: {$operation['total_passages']}");
if (!empty($operation['sectors_detail'])) {
$this->info(" └─ Détail par secteur:");
$sectorCount = count($operation['sectors_detail']);
$sectorNum = 0;
foreach ($operation['sectors_detail'] as $sector) {
$sectorNum++;
$isLast = ($sectorNum === $sectorCount);
$prefix = $isLast ? " └─" : " ├─";
$this->info("{$prefix} {$sector['name']} (ID: {$sector['id']})");
$this->info(" " . ($isLast ? " " : "") . " ├─ Utilisateurs affectés: {$sector['users']}");
$this->info(" " . ($isLast ? " " : "") . " └─ Passages: {$sector['passages']}");
}
}
$this->info("");
$operationNum++;
}
$this->separator();
}
}

View File

@@ -0,0 +1,312 @@
<?php
/**
* Migration des opérations complètes
*
* Orchestre la migration d'une opération avec tous ses utilisateurs,
* secteurs, passages et médias. Utilise UserMigrator et SectorMigrator.
*/
class OperationMigrator
{
private PDO $sourceDb;
private PDO $targetDb;
private MigrationLogger $logger;
private UserMigrator $userMigrator;
private SectorMigrator $sectorMigrator;
/**
* Constructeur
*
* @param PDO $sourceDb Connexion source
* @param PDO $targetDb Connexion cible
* @param MigrationLogger $logger Logger
* @param UserMigrator $userMigrator Migrator d'utilisateurs
* @param SectorMigrator $sectorMigrator Migrator de secteurs
*/
public function __construct(
PDO $sourceDb,
PDO $targetDb,
MigrationLogger $logger,
UserMigrator $userMigrator,
SectorMigrator $sectorMigrator
) {
$this->sourceDb = $sourceDb;
$this->targetDb = $targetDb;
$this->logger = $logger;
$this->userMigrator = $userMigrator;
$this->sectorMigrator = $sectorMigrator;
}
/**
* Récupère les opérations à migrer pour une entité
* - 1 opération active
* - 2 dernières opérations inactives avec au moins 10 passages effectués
*
* @param int $entityId ID de l'entité
* @return array Liste des IDs d'opérations à migrer
*/
public function getOperationsToMigrate(int $entityId): array
{
$operationIds = [];
// 1. Récupérer l'opération active (pour vérification)
$stmt = $this->sourceDb->prepare("
SELECT rowid
FROM operations
WHERE fk_entite = :entity_id AND active = 1
LIMIT 1
");
$stmt->execute([':entity_id' => $entityId]);
$activeOp = $stmt->fetch(PDO::FETCH_COLUMN);
// 2. Récupérer les 2 dernières opérations inactives avec >= 10 passages effectués
// ORDER BY DESC pour avoir les plus récentes, puis on inverse
$stmt = $this->sourceDb->prepare("
SELECT o.rowid, COUNT(p.rowid) as nb_passages
FROM operations o
LEFT JOIN ope_pass p ON p.fk_operation = o.rowid AND p.fk_type = 1
WHERE o.fk_entite = :entity_id
AND o.active = 0
" . ($activeOp ? "AND o.rowid != :active_id" : "") . "
GROUP BY o.rowid
HAVING nb_passages >= 10
ORDER BY o.rowid DESC
LIMIT 2
");
$params = [':entity_id' => $entityId];
if ($activeOp) {
$params[':active_id'] = $activeOp;
}
$stmt->execute($params);
$inactiveOps = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Inverser pour avoir l'ordre chronologique (plus ancienne → plus récente)
$inactiveOps = array_reverse($inactiveOps);
foreach ($inactiveOps as $op) {
$operationIds[] = $op['rowid'];
$this->logger->info("✓ Opération inactive trouvée: {$op['rowid']} ({$op['nb_passages']} passages)");
}
// 3. Ajouter l'opération active EN DERNIER
if ($activeOp) {
$operationIds[] = $activeOp;
$this->logger->info("✓ Opération active trouvée: {$activeOp}");
}
$this->logger->info("📊 Total: " . count($operationIds) . " opération(s) à migrer");
return $operationIds;
}
/**
* Migre une opération complète avec tous ses utilisateurs et secteurs
*
* @param int $oldOperationId ID de l'opération dans l'ancienne base
* @return array|null Tableau de statistiques ou null en cas d'erreur
*/
public function migrateOperation(int $oldOperationId): ?array
{
$this->logger->separator();
$this->logger->info("🔄 Migration de l'opération ID: {$oldOperationId}");
try {
// 1. Récupérer l'opération source
$stmt = $this->sourceDb->prepare("
SELECT * FROM operations
WHERE rowid = :id
");
$stmt->execute([':id' => $oldOperationId]);
$operation = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$operation) {
$this->logger->warning("Opération {$oldOperationId} non trouvée");
return null;
}
// 2. Créer l'opération dans la nouvelle base
$newOperationId = $this->createOperation($operation);
if (!$newOperationId) {
return null;
}
$this->logger->success("✓ Opération créée avec ID: {$newOperationId}");
// 3. Migrer les utilisateurs de l'opération
// Pour opération active : tous les users actifs de l'entité
// Pour opération inactive : uniquement ceux dans ope_users_sectors
$entityId = (int)$operation['fk_entite'];
$isActiveOperation = (int)$operation['active'] === 1;
$userResult = $this->userMigrator->migrateOperationUsers(
$oldOperationId,
$newOperationId,
$entityId,
$isActiveOperation
);
$userMapping = $userResult['mapping'];
$usersCount = $userResult['count'];
if (empty($userMapping)) {
$this->logger->warning("Aucun utilisateur migré, abandon de l'opération {$oldOperationId}");
return null;
}
// 4. Récupérer les secteurs DISTINCTS de l'opération
$stmt = $this->sourceDb->prepare("
SELECT DISTINCT fk_sector
FROM ope_users_sectors
WHERE fk_operation = :operation_id AND active = 1
");
$stmt->execute([':operation_id' => $oldOperationId]);
$sectors = $stmt->fetchAll(PDO::FETCH_COLUMN);
$this->logger->info("📍 " . count($sectors) . " secteur(s) distinct(s) à migrer");
// 5. Migrer chaque secteur et collecter les stats
$sectorsDetail = [];
$totalPassages = 0;
foreach ($sectors as $oldSectorId) {
$sectorStats = $this->sectorMigrator->migrateSector(
$oldOperationId,
$newOperationId,
$oldSectorId,
$userMapping
);
if ($sectorStats) {
$sectorsDetail[] = $sectorStats;
$totalPassages += $sectorStats['passages'];
}
}
// 6. Migrer les médias de l'opération (support='operations')
$this->migrateOperationMedias($oldOperationId, $newOperationId);
$this->logger->success("✅ Migration de l'opération {$oldOperationId} terminée");
// 7. Retourner les statistiques
return [
'id' => $newOperationId,
'name' => $operation['libelle'],
'users' => $usersCount,
'sectors' => count($sectorsDetail),
'total_passages' => $totalPassages,
'sectors_detail' => $sectorsDetail
];
} catch (Exception $e) {
$this->logger->error("❌ Erreur migration opération {$oldOperationId}: " . $e->getMessage());
return null;
}
}
/**
* Crée une opération dans la nouvelle base
*
* @param array $operation Données de l'opération
* @return int|null ID de la nouvelle opération ou null en cas d'erreur
*/
private function createOperation(array $operation): ?int
{
try {
$stmt = $this->targetDb->prepare("
INSERT INTO operations (
fk_entite, libelle, date_deb, date_fin,
chk_distinct_sectors,
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
) VALUES (
:fk_entite, :libelle, :date_deb, :date_fin,
:chk_distinct_sectors,
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
)
");
$stmt->execute([
':fk_entite' => $operation['fk_entite'],
':libelle' => $operation['libelle'],
':date_deb' => $operation['date_deb'],
':date_fin' => $operation['date_fin'],
':chk_distinct_sectors' => $operation['chk_distinct_sectors'],
':created_at' => $operation['date_creat'],
':fk_user_creat' => $operation['fk_user_creat'],
':updated_at' => $operation['date_modif'],
':fk_user_modif' => $operation['fk_user_modif'] ?? 0,
':chk_active' => $operation['active']
]);
return (int)$this->targetDb->lastInsertId();
} catch (Exception $e) {
$this->logger->error("❌ Erreur création opération: " . $e->getMessage());
return null;
}
}
/**
* Migre les médias d'une opération
*
* @param int $oldOperationId ID ancienne opération
* @param int $newOperationId ID nouvelle opération
* @return int Nombre de médias migrés
*/
private function migrateOperationMedias(int $oldOperationId, int $newOperationId): int
{
$stmt = $this->sourceDb->prepare("
SELECT * FROM medias
WHERE support = 'operations' AND support_rowid = :operation_id
");
$stmt->execute([':operation_id' => $oldOperationId]);
$medias = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($medias)) {
return 0;
}
$count = 0;
foreach ($medias as $media) {
$stmt = $this->targetDb->prepare("
INSERT INTO medias (
dir0, dir1, dir2, support, support_rowid,
fichier, type_fichier, description, position,
hauteur, largeur, niveaugris,
created_at, fk_user_creat, updated_at, fk_user_modif
) VALUES (
:dir0, :dir1, :dir2, :support, :support_rowid,
:fichier, :type_fichier, :description, :position,
:hauteur, :largeur, :niveaugris,
:created_at, :fk_user_creat, :updated_at, :fk_user_modif
)
");
$stmt->execute([
':dir0' => $media['dir0'],
':dir1' => $media['dir1'],
':dir2' => $media['dir2'],
':support' => $media['support'],
':support_rowid' => $newOperationId,
':fichier' => $media['fichier'],
':type_fichier' => $media['type_fichier'],
':description' => $media['description'],
':position' => $media['position'],
':hauteur' => $media['hauteur'],
':largeur' => $media['largeur'],
':niveaugris' => $media['niveaugris'],
':created_at' => $media['date_creat'],
':fk_user_creat' => $media['fk_user_creat'],
':updated_at' => $media['date_modif'],
':fk_user_modif' => $media['fk_user_modif']
]);
$count++;
}
$this->logger->success("{$count} média(s) migré(s)");
return $count;
}
}

View File

@@ -0,0 +1,256 @@
<?php
/**
* Migration des passages (ope_pass) et historiques (ope_pass_histo)
*
* Gère la migration des passages avec vérification du trio
* (operation, user, sector) et migration des historiques associés
*/
class PassageMigrator
{
private PDO $sourceDb;
private PDO $targetDb;
private MigrationLogger $logger;
/**
* Constructeur
*
* @param PDO $sourceDb Connexion source
* @param PDO $targetDb Connexion cible
* @param MigrationLogger $logger Logger
*/
public function __construct(PDO $sourceDb, PDO $targetDb, MigrationLogger $logger)
{
$this->sourceDb = $sourceDb;
$this->targetDb = $targetDb;
$this->logger = $logger;
}
/**
* Migre les passages d'un secteur dans une opération
*
* @param int $oldOperationId ID ancienne opération
* @param int $newOperationId ID nouvelle opération
* @param int $oldSectorId ID ancien secteur
* @param int $newOpeSectorId ID nouveau ope_sectors
* @param array $userMapping Mapping oldUserId => newOpeUserId
* @return int Nombre de passages migrés
*/
public function migratePassages(
int $oldOperationId,
int $newOperationId,
int $oldSectorId,
int $newOpeSectorId,
array $userMapping
): int {
$stmt = $this->sourceDb->prepare("
SELECT * FROM ope_pass
WHERE fk_operation = :operation_id
AND fk_sector = :sector_id
");
$stmt->execute([
':operation_id' => $oldOperationId,
':sector_id' => $oldSectorId
]);
$passages = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($passages)) {
return 0;
}
$count = 0;
foreach ($passages as $passage) {
// Vérifier que l'utilisateur a été migré
if (!isset($userMapping[$passage['fk_user']])) {
$this->logger->warning(" ⚠ Passage {$passage['rowid']}: User {$passage['fk_user']} non trouvé dans mapping");
continue;
}
// Récupérer l'ID de ope_users depuis le mapping
$newOpeUserId = $userMapping[$passage['fk_user']];
// Vérifier que le trio (operation, user, sector) existe dans ope_users_sectors
if (!$this->verifyUserSectorAssociation($newOperationId, $newOpeUserId, $newOpeSectorId)) {
$this->logger->warning(" ⚠ Passage {$passage['rowid']}: Trio (op={$newOperationId}, user={$newOpeUserId}, sector={$newOpeSectorId}) inexistant");
continue;
}
// Insérer le passage avec l'ID de ope_users
$newPassId = $this->insertPassage($passage, $newOperationId, $newOpeSectorId, $newOpeUserId);
if ($newPassId) {
// Migrer l'historique du passage
$this->migratePassageHisto($passage['rowid'], $newPassId, $userMapping);
$count++;
}
}
if ($count > 0) {
$this->logger->success("{$count} passage(s) migré(s)");
}
return $count;
}
/**
* Vérifie qu'une association user-sector existe dans ope_users_sectors
*
* @param int $operationId ID opération
* @param int $userId ID ope_users (mapping)
* @param int $sectorId ID ope_sectors
* @return bool True si l'association existe
*/
private function verifyUserSectorAssociation(int $operationId, int $userId, int $sectorId): bool
{
$stmt = $this->targetDb->prepare("
SELECT COUNT(*) FROM ope_users_sectors
WHERE fk_operation = :operation_id
AND fk_user = :user_id
AND fk_sector = :sector_id
");
$stmt->execute([
':operation_id' => $operationId,
':user_id' => $userId,
':sector_id' => $sectorId
]);
return $stmt->fetchColumn() > 0;
}
/**
* Insère un passage dans la nouvelle base
*
* @param array $passage Données du passage
* @param int $newOperationId ID nouvelle opération
* @param int $newOpeSectorId ID nouveau secteur
* @param int $userId ID de ope_users (mapping)
* @return int|null ID du nouveau passage ou null en cas d'erreur
*/
private function insertPassage(
array $passage,
int $newOperationId,
int $newOpeSectorId,
int $userId
): ?int {
try {
$stmt = $this->targetDb->prepare("
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, residence,
gps_lat, gps_lng, encrypted_name, montant, fk_type_reglement,
remarque, nom_recu, encrypted_email, 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, :residence,
:gps_lat, :gps_lng, :encrypted_name, :montant, :fk_type_reglement,
:remarque, :nom_recu, :encrypted_email, :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
)
");
// Chiffrer les données sensibles
require_once dirname(__DIR__, 4) . '/src/Services/ApiService.php';
$stmt->execute([
':fk_operation' => $newOperationId,
':fk_sector' => $newOpeSectorId,
':fk_user' => $userId, // ID de ope_users (mapping)
':fk_adresse' => $passage['fk_adresse'],
':passed_at' => $passage['date_eve'],
':fk_type' => $passage['fk_type'],
':numero' => $passage['numero'],
':rue' => $passage['rue'],
':rue_bis' => $passage['rue_bis'],
':ville' => $passage['ville'],
':fk_habitat' => $passage['fk_habitat'],
':appt' => $passage['appt'],
':niveau' => $passage['niveau'],
':residence' => $passage['lieudit'] ?? null,
':gps_lat' => $passage['gps_lat'],
':gps_lng' => $passage['gps_lng'],
':encrypted_name' => $passage['libelle'] ? ApiService::encryptData($passage['libelle']) : '', // Chiffrer avec IV aléatoire
':montant' => $passage['montant'],
':fk_type_reglement' => (!empty($passage['fk_type_reglement']) && $passage['fk_type_reglement'] > 0) ? $passage['fk_type_reglement'] : 4,
':remarque' => $passage['remarque'],
':nom_recu' => $passage['recu'] ?? null,
':encrypted_email' => $passage['email'] ? ApiService::encryptSearchableData($passage['email']) : null,
':email_erreur' => $passage['email_erreur'],
':chk_email_sent' => $passage['chk_email_sent'],
':encrypted_phone' => $passage['phone'] ? ApiService::encryptData($passage['phone']) : '',
':docremis' => $passage['docremis'],
':date_repasser' => $passage['date_repasser'],
':nb_passages' => ($passage['fk_type'] == 2) ? 0 : $passage['nb_passages'],
':chk_gps_maj' => $passage['chk_gps_maj'],
':chk_map_create' => $passage['chk_map_create'],
':chk_mobile' => $passage['chk_mobile'],
':chk_synchro' => $passage['chk_synchro'],
':chk_api_adresse' => $passage['chk_api_adresse'],
':chk_maj_adresse' => $passage['chk_maj_adresse'],
':anomalie' => $passage['anomalie'],
':created_at' => $passage['date_creat'],
':fk_user_creat' => $passage['fk_user_creat'] ?? 0,
':updated_at' => $passage['date_modif'],
':fk_user_modif' => $passage['fk_user_modif'] ?? 0,
':chk_active' => $passage['active']
]);
return (int)$this->targetDb->lastInsertId();
} catch (Exception $e) {
$this->logger->error(" ❌ Erreur insertion passage {$passage['rowid']}: " . $e->getMessage());
return null;
}
}
/**
* Migre l'historique d'un passage
*
* @param int $oldPassId ID ancien passage
* @param int $newPassId ID nouveau passage
* @param array $userMapping Non utilisé (conservé pour compatibilité)
* @return int Nombre d'entrées d'historique migrées
*/
public function migratePassageHisto(int $oldPassId, int $newPassId, array $userMapping): int
{
$stmt = $this->sourceDb->prepare("
SELECT * FROM ope_pass_histo WHERE fk_pass = :pass_id
");
$stmt->execute([':pass_id' => $oldPassId]);
$histos = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($histos)) {
return 0;
}
$count = 0;
foreach ($histos as $histo) {
$stmt = $this->targetDb->prepare("
INSERT INTO ope_pass_histo (
fk_pass, date_histo, sujet, remarque
) VALUES (
:fk_pass, :date_histo, :sujet, :remarque
)
");
$stmt->execute([
':fk_pass' => $newPassId,
':date_histo' => $histo['date_histo'],
':sujet' => $histo['sujet'],
':remarque' => $histo['remarque']
]);
$count++;
}
return $count;
}
}

View File

@@ -0,0 +1,289 @@
<?php
/**
* Migration des secteurs (ope_sectors) et données associées
*
* Gère la migration des secteurs avec leurs adresses, associations
* utilisateurs-secteurs, et passages. Utilise PassageMigrator pour les passages.
*/
class SectorMigrator
{
private PDO $sourceDb;
private PDO $targetDb;
private MigrationLogger $logger;
private PassageMigrator $passageMigrator;
private array $sectorMapping = [];
/**
* Constructeur
*
* @param PDO $sourceDb Connexion source
* @param PDO $targetDb Connexion cible
* @param MigrationLogger $logger Logger
* @param PassageMigrator $passageMigrator Migrator de passages
*/
public function __construct(
PDO $sourceDb,
PDO $targetDb,
MigrationLogger $logger,
PassageMigrator $passageMigrator
) {
$this->sourceDb = $sourceDb;
$this->targetDb = $targetDb;
$this->logger = $logger;
$this->passageMigrator = $passageMigrator;
}
/**
* Migre un secteur dans le contexte d'une opération
*
* @param int $oldOperationId ID ancienne opération
* @param int $newOperationId ID nouvelle opération
* @param int $oldSectorId ID ancien secteur
* @param array $userMapping Mapping oldUserId => newOpeUserId
* @return array|null ['id' => int, 'name' => string, 'users' => int, 'passages' => int] ou null en cas d'erreur
*/
public function migrateSector(
int $oldOperationId,
int $newOperationId,
int $oldSectorId,
array $userMapping
): ?array {
$this->logger->info(" 📍 Migration secteur ID: {$oldSectorId}");
try {
// 1. Récupérer le secteur source
$stmt = $this->sourceDb->prepare("
SELECT * FROM sectors WHERE rowid = :id
");
$stmt->execute([':id' => $oldSectorId]);
$sector = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$sector) {
$this->logger->warning(" Secteur {$oldSectorId} non trouvé");
return null;
}
// 2. Créer dans ope_sectors
$newOpeSectorId = $this->createOpeSector($sector, $newOperationId);
if (!$newOpeSectorId) {
return null;
}
// 3. Mapper "operationId_sectorId" → newOpeSectorId
$mappingKey = "{$oldOperationId}_{$oldSectorId}";
$this->sectorMapping[$mappingKey] = $newOpeSectorId;
$this->logger->success(" ✓ Secteur créé avec ID: {$newOpeSectorId}");
// 4. Migrer sectors_adresses
$this->migrateSectorAddresses($oldSectorId, $newOpeSectorId);
// 5. Migrer ope_users_sectors
$usersCount = $this->migrateUsersSectors($oldOperationId, $newOperationId, $oldSectorId, $newOpeSectorId, $userMapping);
// 6. Migrer ope_pass
$passagesCount = $this->passageMigrator->migratePassages(
$oldOperationId,
$newOperationId,
$oldSectorId,
$newOpeSectorId,
$userMapping
);
return [
'id' => $newOpeSectorId,
'name' => $sector['libelle'],
'users' => $usersCount,
'passages' => $passagesCount
];
} catch (Exception $e) {
$this->logger->error(" ❌ Erreur migration secteur {$oldSectorId}: " . $e->getMessage());
return null;
}
}
/**
* Crée un secteur dans ope_sectors
*
* @param array $sector Données du secteur
* @param int $newOperationId ID nouvelle opération
* @return int|null ID du nouveau secteur ou null en cas d'erreur
*/
private function createOpeSector(array $sector, int $newOperationId): ?int
{
try {
$stmt = $this->targetDb->prepare("
INSERT INTO ope_sectors (
fk_operation, libelle, sector, color,
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
) VALUES (
:fk_operation, :libelle, :sector, :color,
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
)
");
$stmt->execute([
':fk_operation' => $newOperationId,
':libelle' => $sector['libelle'],
':sector' => $sector['sector'],
':color' => $sector['color'],
':created_at' => $sector['date_creat'],
':fk_user_creat' => $sector['fk_user_creat'] ?? 0,
':updated_at' => $sector['date_modif'],
':fk_user_modif' => $sector['fk_user_modif'] ?? 0,
':chk_active' => $sector['active']
]);
return (int)$this->targetDb->lastInsertId();
} catch (Exception $e) {
$this->logger->error(" ❌ Erreur création secteur: " . $e->getMessage());
return null;
}
}
/**
* Migre les adresses d'un secteur
*
* @param int $oldSectorId ID ancien secteur
* @param int $newOpeSectorId ID nouveau ope_sectors
* @return int Nombre d'adresses migrées
*/
private function migrateSectorAddresses(int $oldSectorId, int $newOpeSectorId): int
{
$stmt = $this->sourceDb->prepare("
SELECT * FROM sectors_adresses WHERE fk_sector = :sector_id
");
$stmt->execute([':sector_id' => $oldSectorId]);
$addresses = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($addresses)) {
return 0;
}
$count = 0;
foreach ($addresses as $address) {
$stmt = $this->targetDb->prepare("
INSERT INTO sectors_adresses (
fk_adresse, fk_sector, numero, rue_bis, rue, cp, ville,
gps_lat, gps_lng
) VALUES (
:fk_adresse, :fk_sector, :numero, :rue_bis, :rue, :cp, :ville,
:gps_lat, :gps_lng
)
");
$stmt->execute([
':fk_adresse' => $address['fk_adresse'], // Garde la valeur telle quelle
':fk_sector' => $newOpeSectorId,
':numero' => $address['numero'],
':rue_bis' => $address['rue_bis'],
':rue' => $address['rue'],
':cp' => $address['cp'],
':ville' => $address['ville'],
':gps_lat' => $address['gps_lat'],
':gps_lng' => $address['gps_lng']
]);
$count++;
}
$this->logger->success("{$count} adresse(s) migrée(s)");
return $count;
}
/**
* Migre les associations utilisateurs-secteurs
*
* @param int $oldOperationId ID ancienne opération
* @param int $newOperationId ID nouvelle opération
* @param int $oldSectorId ID ancien secteur
* @param int $newOpeSectorId ID nouveau ope_sectors
* @param array $userMapping Mapping oldUserId => newOpeUserId
* @return int Nombre d'associations migrées
*/
private function migrateUsersSectors(
int $oldOperationId,
int $newOperationId,
int $oldSectorId,
int $newOpeSectorId,
array $userMapping
): int {
$stmt = $this->sourceDb->prepare("
SELECT * FROM ope_users_sectors
WHERE fk_operation = :operation_id
AND fk_sector = :sector_id
AND active = 1
");
$stmt->execute([
':operation_id' => $oldOperationId,
':sector_id' => $oldSectorId
]);
$usersSectors = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($usersSectors)) {
return 0;
}
$count = 0;
foreach ($usersSectors as $us) {
// Vérifier que l'utilisateur existe dans le mapping
// (le mapping sert juste à vérifier que l'user a été migré)
if (!isset($userMapping[$us['fk_user']])) {
$this->logger->warning(" ⚠ User {$us['fk_user']} non trouvé dans mapping");
continue;
}
$stmt = $this->targetDb->prepare("
INSERT INTO ope_users_sectors (
fk_operation, fk_user, fk_sector,
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
) VALUES (
:fk_operation, :fk_user, :fk_sector,
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
)
");
$stmt->execute([
':fk_operation' => $newOperationId,
':fk_user' => $userMapping[$us['fk_user']], // ID de ope_users (mapping)
':fk_sector' => $newOpeSectorId,
':created_at' => date('Y-m-d H:i:s'),
':fk_user_creat' => 0,
':updated_at' => null,
':fk_user_modif' => null,
':chk_active' => $us['active']
]);
$count++;
}
$this->logger->success("{$count} association(s) user-secteur migrée(s)");
return $count;
}
/**
* Retourne le mapping des secteurs
*
* @return array "operationId_sectorId" => newOpeSectorId
*/
public function getSectorMapping(): array
{
return $this->sectorMapping;
}
/**
* Définit le mapping des secteurs (utile pour réutilisation)
*
* @param array $mapping "operationId_sectorId" => newOpeSectorId
*/
public function setSectorMapping(array $mapping): void
{
$this->sectorMapping = $mapping;
}
}

View File

@@ -0,0 +1,163 @@
<?php
/**
* Migration des utilisateurs d'opérations (ope_users)
*
* Gère la création des utilisateurs par opération et le mapping
* oldUserId (users.rowid) → newOpeUserId (ope_users.id)
*/
class UserMigrator
{
private PDO $sourceDb;
private PDO $targetDb;
private MigrationLogger $logger;
private array $userMapping = [];
/**
* Constructeur
*
* @param PDO $sourceDb Connexion source
* @param PDO $targetDb Connexion cible
* @param MigrationLogger $logger Logger
*/
public function __construct(PDO $sourceDb, PDO $targetDb, MigrationLogger $logger)
{
$this->sourceDb = $sourceDb;
$this->targetDb = $targetDb;
$this->logger = $logger;
}
/**
* Migre les utilisateurs d'une opération
* - Si opération active : TOUS les users actifs de l'entité
* - Si opération inactive : Uniquement ceux dans ope_users_sectors
*
* @param int $oldOperationId ID ancienne opération
* @param int $newOperationId ID nouvelle opération
* @param int $entityId ID de l'entité
* @param bool $isActiveOperation True si opération active
* @return array ['mapping' => array, 'count' => int]
*/
public function migrateOperationUsers(
int $oldOperationId,
int $newOperationId,
int $entityId,
bool $isActiveOperation
): array {
$this->logger->info("👥 Migration des utilisateurs de l'opération...");
// Réinitialiser le mapping pour cette opération
$this->userMapping = [];
// Récupérer les utilisateurs selon le type d'opération
if ($isActiveOperation) {
// Pour l'opération active : TOUS les users actifs de l'entité
$this->logger->info(" Opération ACTIVE : migration de tous les users actifs de l'entité");
$stmt = $this->sourceDb->prepare("
SELECT rowid
FROM users
WHERE fk_entite = :entity_id AND active = 1
");
$stmt->execute([':entity_id' => $entityId]);
$userIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
} else {
// Pour les opérations inactives : uniquement ceux dans ope_users_sectors
$this->logger->info(" Opération INACTIVE : migration des users affectés aux secteurs");
$stmt = $this->sourceDb->prepare("
SELECT DISTINCT fk_user
FROM ope_users_sectors
WHERE fk_operation = :operation_id AND active = 1
");
$stmt->execute([':operation_id' => $oldOperationId]);
$userIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
}
if (empty($userIds)) {
$this->logger->warning("Aucun utilisateur trouvé pour l'opération {$oldOperationId}");
return ['mapping' => [], 'count' => 0];
}
$count = 0;
foreach ($userIds as $oldUserId) {
// Récupérer les infos utilisateur depuis la table users
$stmt = $this->sourceDb->prepare("
SELECT * FROM users WHERE rowid = :id AND active = 1
");
$stmt->execute([':id' => $oldUserId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
$this->logger->warning(" ⚠ Utilisateur {$oldUserId} non trouvé ou inactif");
continue;
}
// Créer dans ope_users de la nouvelle base
$stmt = $this->targetDb->prepare("
INSERT INTO ope_users (
fk_operation, fk_user, fk_role,
first_name, encrypted_name, sect_name,
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
) VALUES (
:fk_operation, :fk_user, :fk_role,
:first_name, :encrypted_name, :sect_name,
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
)
");
$stmt->execute([
':fk_operation' => $newOperationId,
':fk_user' => $oldUserId, // Référence vers users.id
':fk_role' => $user['fk_role'],
':first_name' => $user['prenom'],
':encrypted_name' => ApiService::encryptData($user['libelle']), // Chiffrer le nom avec IV aléatoire
':sect_name' => $user['nom_tournee'],
':created_at' => $user['date_creat'],
':fk_user_creat' => $user['fk_user_creat'],
':updated_at' => $user['date_modif'],
':fk_user_modif' => $user['fk_user_modif'],
':chk_active' => $user['active']
]);
$newOpeUserId = (int)$this->targetDb->lastInsertId();
// Mapper oldUserId → newOpeUserId
$this->userMapping[$oldUserId] = $newOpeUserId;
$count++;
}
$this->logger->success("{$count} utilisateur(s) migré(s)");
return ['mapping' => $this->userMapping, 'count' => $count];
}
/**
* Retourne le mapping des utilisateurs
*
* @return array oldUserId => newOpeUserId
*/
public function getUserMapping(): array
{
return $this->userMapping;
}
/**
* Définit le mapping des utilisateurs (utile pour réutilisation)
*
* @param array $mapping oldUserId => newOpeUserId
*/
public function setUserMapping(array $mapping): void
{
$this->userMapping = $mapping;
}
/**
* Récupère le nouvel ID ope_users depuis le mapping
*
* @param int $oldUserId ID ancien utilisateur
* @return int|null Nouvel ID ope_users ou null si non trouvé
*/
public function getMappedUserId(int $oldUserId): ?int
{
return $this->userMapping[$oldUserId] ?? null;
}
}

View File

@@ -0,0 +1,471 @@
#!/usr/bin/env php
<?php
/**
* Script de migration v2 - Architecture modulaire
*
* Utilise les migrators spécialisés pour une migration hiérarchique par opération.
* Source fixe: geosector (synchronisée 2x/jour par PM7 depuis nx4)
* Cible: dva_geo (développement), rca_geo (recette) ou pra_geo (production)
*
* Usage:
* Migration d'une entité:
* php migrate_from_backup.php --mode=entity --entity-id=2
*
* Migration globale (toutes les entités):
* php migrate_from_backup.php --mode=global
*
* Avec environnement explicite:
* php migrate_from_backup.php --env=dva --mode=entity --entity-id=2
*/
// Inclure ApiService pour le chiffrement
require_once dirname(__DIR__, 3) . '/src/Services/ApiService.php';
// Inclure les classes v2
require_once __DIR__ . '/lib/DatabaseConfig.php';
require_once __DIR__ . '/lib/MigrationLogger.php';
require_once __DIR__ . '/lib/DatabaseConnection.php';
require_once __DIR__ . '/lib/UserMigrator.php';
require_once __DIR__ . '/lib/PassageMigrator.php';
require_once __DIR__ . '/lib/SectorMigrator.php';
require_once __DIR__ . '/lib/OperationMigrator.php';
// Configuration PHP pour les grosses migrations
ini_set('memory_limit', '512M');
ini_set('max_execution_time', '3600'); // 1 heure max
class DataMigration
{
private PDO $sourceDb;
private PDO $targetDb;
private MigrationLogger $logger;
private DatabaseConfig $config;
private OperationMigrator $operationMigrator;
// Options
private string $mode;
private ?int $entityId;
private bool $deleteBefore;
// Statistiques
private array $migrationStats = [];
public function __construct(string $env, string $mode = 'global', ?int $entityId = null, ?string $logFile = null, bool $deleteBefore = true)
{
// Initialisation config et logger
$this->config = new DatabaseConfig($env);
$this->mode = $mode;
$this->entityId = $entityId;
$this->deleteBefore = $deleteBefore;
// Générer le nom du fichier log selon le mode si non spécifié
if (!$logFile) {
$logDir = dirname(__DIR__, 2) . '/logs';
$timestamp = date('Ymd_His');
if ($mode === 'entity' && $entityId) {
$logFile = "{$logDir}/migration_entite_{$entityId}_{$timestamp}.log";
} else {
$logFile = "{$logDir}/migration_global_{$timestamp}.log";
}
}
$this->logger = new MigrationLogger($logFile);
// Log header
$this->logHeader();
// Connexions
$dbConnection = new DatabaseConnection($this->config, $this->logger);
$dbConnection->connect();
$this->sourceDb = $dbConnection->getSourceDb();
$this->targetDb = $dbConnection->getTargetDb();
// Initialiser les migrators
$this->initializeMigrators();
}
private function initializeMigrators(): void
{
// Créer les migrators dans l'ordre de dépendance
$passageMigrator = new PassageMigrator($this->sourceDb, $this->targetDb, $this->logger);
$sectorMigrator = new SectorMigrator($this->sourceDb, $this->targetDb, $this->logger, $passageMigrator);
$userMigrator = new UserMigrator($this->sourceDb, $this->targetDb, $this->logger);
$this->operationMigrator = new OperationMigrator(
$this->sourceDb,
$this->targetDb,
$this->logger,
$userMigrator,
$sectorMigrator
);
}
public function run(): void
{
if ($this->mode === 'entity') {
if (!$this->entityId) {
throw new Exception("entity-id requis en mode entity");
}
$this->migrateEntity($this->entityId);
} else {
$this->migrateAllEntities();
}
// Afficher le récapitulatif
if (!empty($this->migrationStats)) {
$this->logger->logMigrationSummary($this->migrationStats);
}
$this->logger->separator();
$this->logger->success("🎉 Migration terminée !");
$this->logger->info("📄 Log: " . $this->logger->getLogFile());
}
private function migrateEntity(int $entityId): void
{
$this->logger->separator();
$this->logger->info("🏢 Migration de l'entité ID: {$entityId}");
// Supprimer les données existantes si demandé
if ($this->deleteBefore) {
$this->deleteEntityData($entityId);
}
// Migrer l'entité elle-même
$this->migrateEntityRecord($entityId);
// Migrer les users de l'entité (table centrale users)
$this->migrateEntityUsers($entityId);
// Récupérer le nom de l'entité pour les stats
$stmt = $this->sourceDb->prepare("SELECT libelle FROM users_entites WHERE rowid = :id");
$stmt->execute([':id' => $entityId]);
$entityName = $stmt->fetchColumn();
// Récupérer et migrer les opérations
$operationIds = $this->operationMigrator->getOperationsToMigrate($entityId);
$operations = [];
foreach ($operationIds as $oldOperationId) {
$operationStats = $this->operationMigrator->migrateOperation($oldOperationId);
if ($operationStats) {
$operations[] = $operationStats;
}
}
// Stocker les stats pour cette entité
$this->migrationStats = [
'entity' => [
'id' => $entityId,
'name' => $entityName ?: "Entité #{$entityId}"
],
'operations' => $operations
];
}
private function migrateAllEntities(): void
{
// Récupérer toutes les entités actives
$stmt = $this->sourceDb->query("SELECT rowid FROM users_entites WHERE active = 1 ORDER BY rowid");
$entities = $stmt->fetchAll(PDO::FETCH_COLUMN);
$this->logger->info("📊 " . count($entities) . " entité(s) à migrer");
$allOperations = [];
foreach ($entities as $entityId) {
// Sauvegarder les stats actuelles avant de migrer
$previousStats = $this->migrationStats;
$this->migrateEntity($entityId);
// Agréger les opérations de toutes les entités
if (!empty($this->migrationStats['operations'])) {
$allOperations = array_merge($allOperations, $this->migrationStats['operations']);
}
}
// Stocker les stats globales
$this->migrationStats = [
'operations' => $allOperations
];
}
private function deleteEntityData(int $entityId): void
{
$this->logger->separator();
$this->logger->warning("🗑️ Suppression des données de l'entité {$entityId}...");
// Ordre inverse des contraintes FK
$tables = [
'medias' => "fk_entite = {$entityId} OR fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId})",
'ope_pass_histo' => "fk_pass IN (SELECT id FROM ope_pass WHERE fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId}))",
'ope_pass' => "fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId})",
'ope_users_sectors' => "fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId})",
'ope_users' => "fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId})",
'sectors_adresses' => "fk_sector IN (SELECT id FROM ope_sectors WHERE fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId}))",
'ope_sectors' => "fk_operation IN (SELECT id FROM operations WHERE fk_entite = {$entityId})",
'operations' => "fk_entite = {$entityId}",
'users' => "fk_entite = {$entityId}"
];
foreach ($tables as $table => $condition) {
$stmt = $this->targetDb->query("DELETE FROM {$table} WHERE {$condition}");
$count = $stmt->rowCount();
if ($count > 0) {
$this->logger->info("{$table}: {$count} ligne(s) supprimée(s)");
}
}
$this->logger->success("✓ Suppression terminée");
}
private function migrateEntityRecord(int $entityId): void
{
// Vérifier si existe déjà
$stmt = $this->targetDb->prepare("SELECT COUNT(*) FROM entites WHERE id = :id");
$stmt->execute([':id' => $entityId]);
if ($stmt->fetchColumn() > 0) {
$this->logger->info("Entité {$entityId} existe déjà, skip");
return;
}
// Récupérer depuis source
$stmt = $this->sourceDb->prepare("SELECT * FROM users_entites WHERE rowid = :id");
$stmt->execute([':id' => $entityId]);
$entity = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$entity) {
throw new Exception("Entité {$entityId} non trouvée");
}
// Insérer dans cible (schéma geo_app)
$stmt = $this->targetDb->prepare("
INSERT INTO entites (
id, encrypted_name, adresse1, adresse2, code_postal, ville,
fk_region, fk_type, encrypted_phone, encrypted_mobile, encrypted_email,
gps_lat, gps_lng, chk_stripe, encrypted_stripe_id, encrypted_iban, encrypted_bic,
chk_demo, chk_mdp_manuel, chk_username_manuel, chk_user_delete_pass,
chk_copie_mail_recu, chk_accept_sms, chk_lot_actif,
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
) VALUES (
:id, :encrypted_name, :adresse1, :adresse2, :code_postal, :ville,
:fk_region, :fk_type, :encrypted_phone, :encrypted_mobile, :encrypted_email,
:gps_lat, :gps_lng, :chk_stripe, :encrypted_stripe_id, :encrypted_iban, :encrypted_bic,
:chk_demo, :chk_mdp_manuel, :chk_username_manuel, :chk_user_delete_pass,
:chk_copie_mail_recu, :chk_accept_sms, :chk_lot_actif,
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
)
");
$stmt->execute([
':id' => $entityId,
':encrypted_name' => $entity['libelle'] ? ApiService::encryptData($entity['libelle']) : '',
':adresse1' => $entity['adresse1'] ?? '',
':adresse2' => $entity['adresse2'] ?? '',
':code_postal' => $entity['cp'] ?? '',
':ville' => $entity['ville'] ?? '',
':fk_region' => $entity['fk_region'],
':fk_type' => $entity['fk_type'] ?? 1,
':encrypted_phone' => $entity['tel1'] ? ApiService::encryptData($entity['tel1']) : '',
':encrypted_mobile' => $entity['tel2'] ? ApiService::encryptData($entity['tel2']) : '',
':encrypted_email' => $entity['email'] ? ApiService::encryptSearchableData($entity['email']) : '',
':gps_lat' => $entity['gps_lat'] ?? '',
':gps_lng' => $entity['gps_lng'] ?? '',
':chk_stripe' => 0,
':encrypted_stripe_id' => '',
':encrypted_iban' => $entity['iban'] ? ApiService::encryptData($entity['iban']) : '',
':encrypted_bic' => $entity['bic'] ? ApiService::encryptData($entity['bic']) : '',
':chk_demo' => $entity['demo'] ?? 1,
':chk_mdp_manuel' => $entity['chk_mdp_manuel'] ?? 0,
':chk_username_manuel' => 0,
':chk_user_delete_pass' => 0,
':chk_copie_mail_recu' => $entity['chk_copie_mail_recu'] ?? 0,
':chk_accept_sms' => $entity['chk_accept_sms'] ?? 0,
':chk_lot_actif' => 0,
':created_at' => date('Y-m-d H:i:s'),
':fk_user_creat' => 0,
':updated_at' => $entity['date_modif'],
':fk_user_modif' => $entity['fk_user_modif'] ?? 0,
':chk_active' => $entity['active'] ?? 1
]);
$this->logger->success("✓ Entité {$entityId} migrée");
}
private function migrateEntityUsers(int $entityId): void
{
$stmt = $this->sourceDb->prepare("SELECT * FROM users WHERE fk_entite = :entity_id AND active = 1");
$stmt->execute([':entity_id' => $entityId]);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
$count = 0;
foreach ($users as $user) {
// Vérifier si existe déjà
$stmt = $this->targetDb->prepare("SELECT COUNT(*) FROM users WHERE id = :id");
$stmt->execute([':id' => $user['rowid']]);
if ($stmt->fetchColumn() > 0) {
continue; // Skip si existe
}
// Insérer l'utilisateur
$stmt = $this->targetDb->prepare("
INSERT INTO users (
id, fk_entite, fk_role, first_name, encrypted_name,
encrypted_user_name, user_pass_hash, encrypted_email, encrypted_phone, encrypted_mobile,
created_at, fk_user_creat, updated_at, fk_user_modif, chk_active
) VALUES (
:id, :fk_entite, :fk_role, :first_name, :encrypted_name,
:encrypted_user_name, :user_pass_hash, :encrypted_email, :encrypted_phone, :encrypted_mobile,
:created_at, :fk_user_creat, :updated_at, :fk_user_modif, :chk_active
)
");
$stmt->execute([
':id' => $user['rowid'],
':fk_entite' => $user['fk_entite'],
':fk_role' => $user['fk_role'],
':first_name' => $user['prenom'],
':encrypted_name' => ApiService::encryptData($user['libelle']), // Chiffrer avec IV aléatoire
':encrypted_user_name' => ApiService::encryptSearchableData($user['username']),
':user_pass_hash' => $user['userpswd'], // Hash bcrypt du mot de passe
':encrypted_email' => $user['email'] ? ApiService::encryptSearchableData($user['email']) : null,
':encrypted_phone' => $user['telephone'] ? ApiService::encryptData($user['telephone']) : null,
':encrypted_mobile' => $user['mobile'] ? ApiService::encryptData($user['mobile']) : null,
':created_at' => $user['date_creat'],
':fk_user_creat' => $user['fk_user_creat'],
':updated_at' => $user['date_modif'],
':fk_user_modif' => $user['fk_user_modif'],
':chk_active' => $user['active']
]);
$count++;
}
$this->logger->success("{$count} utilisateur(s) de l'entité migré(s)");
}
private function logHeader(): void
{
$this->logger->separator();
$this->logger->info("🚀 Migration v2 - Architecture modulaire");
$this->logger->info("📅 Date: " . date('Y-m-d H:i:s'));
$this->logger->info("🌍 Environnement: " . $this->config->getEnvName());
$this->logger->info("🔧 Mode: " . $this->mode);
if ($this->entityId) {
$this->logger->info("🏢 Entité: " . $this->entityId);
}
$this->logger->info("🗑️ Suppression avant: " . ($this->deleteBefore ? 'OUI' : 'NON'));
$this->logger->separator();
}
}
// === GESTION DES ARGUMENTS CLI ===
function parseArguments(array $argv): array
{
$options = [
'env' => DatabaseConfig::autoDetect(),
'mode' => 'global',
'entity-id' => null,
'log' => null,
'delete-before' => true,
'help' => false
];
foreach ($argv as $arg) {
if ($arg === '--help') {
$options['help'] = true;
} elseif (preg_match('/^--env=(.+)$/', $arg, $matches)) {
$options['env'] = $matches[1];
} elseif (preg_match('/^--mode=(.+)$/', $arg, $matches)) {
$options['mode'] = $matches[1];
} elseif (preg_match('/^--entity-id=(\d+)$/', $arg, $matches)) {
$options['entity-id'] = (int)$matches[1];
} elseif (preg_match('/^--log=(.+)$/', $arg, $matches)) {
$options['log'] = $matches[1];
} elseif ($arg === '--delete-before=false') {
$options['delete-before'] = false;
}
}
return $options;
}
function showHelp(): void
{
echo <<<HELP
🚀 Migration v2 - Architecture modulaire
USAGE:
php migrate_from_backup.php [OPTIONS]
OPTIONS:
--env=ENV Environnement: 'dva' (développement), 'rca' (recette) ou 'pra' (production)
Par défaut: auto-détection selon hostname
--mode=MODE Mode de migration: 'global' ou 'entity'
Par défaut: global
--entity-id=ID ID de l'entité à migrer (requis si mode=entity)
--log=PATH Fichier de log personnalisé
Par défaut: logs/migration_YYYYMMDD_HHMMSS.log
--delete-before Supprimer les données existantes avant migration
Par défaut: true
Utiliser --delete-before=false pour désactiver
--help Afficher cette aide
EXEMPLES:
# Migration d'une entité avec suppression (recommandé)
php migrate_from_backup.php --mode=entity --entity-id=2
# Migration sans suppression (risque de doublons)
php migrate_from_backup.php --mode=entity --entity-id=2 --delete-before=false
# Migration globale de toutes les entités
php migrate_from_backup.php --mode=global
# Spécifier l'environnement manuellement (DVA, RCA ou PRA)
php migrate_from_backup.php --env=dva --mode=entity --entity-id=2
HELP;
}
// === POINT D'ENTRÉE ===
try {
$options = parseArguments($argv);
if ($options['help']) {
showHelp();
exit(0);
}
// Valider l'environnement
if (!DatabaseConfig::exists($options['env'])) {
throw new Exception("Invalid environment: {$options['env']}. Use 'dva', 'rca' or 'pra'");
}
// Créer et exécuter la migration
$migration = new DataMigration(
$options['env'],
$options['mode'],
$options['entity-id'],
$options['log'],
$options['delete-before']
);
$migration->run();
} catch (Exception $e) {
echo "❌ ERREUR: " . $e->getMessage() . "\n";
exit(1);
}

File diff suppressed because it is too large Load Diff