# Plan de Migration : NestJS/Prisma → Symfony/API Platform **Date de création**: 2026-01-10 **Dernière mise à jour**: 2026-01-11 **Objectif**: Migrer le backend Inventory (NestJS + Prisma) vers Symfony 8 + API Platform avec JWT auth, tout en conservant les données existantes et en minimisant les disruptions. --- ## 📊 État des Lieux - 2026-01-11 ### ✅ Phase 1 : Préparation (TERMINÉE, ajustée) - ✅ Bundles Symfony installés (JWT, VichUploader, Uid) - ✅ Entité Profile créée (UserInterface + session ready) - ✅ pgAdmin configuré et connecté à PostgreSQL - ✅ Docker fonctionnel (web:8081, db:5433, pgadmin:5050) - ✅ API Platform configurée avec préfixe `/api` - ⚠️ JWT désactivé temporairement (session cookie pour compatibilité front) ### ✅ Phase 2 : Tests & Validation (TERMINÉE, version session) - ✅ API Platform retourne JSON-LD correctement - ✅ Routes session `/api/session/profile` + `/api/session/profiles` opérationnelles - ✅ CORS prévu via Nelmio (mais variable d'env à injecter dans container) ### 📦 Données à Migrer (Base: `inventory-data`) **Total: 673 lignes réparties sur 22 tables (migrées)** | Table | Lignes | Priorité | Commentaire | |-------|--------|----------|-------------| | `sites` | 3 | ⚠️ Critique | Point d'entrée, relations cascades | | `machines` | 3 | ⚠️ Critique | Entité centrale | | `type_machines` | 3 | ⚠️ Critique | Configuration machines | | `ModelType` | 71 | 🔴 Haute | Templates composants/pièces/produits | | `composants` | 23 | 🔴 Haute | - | | `pieces` | 82 | 🔴 Haute | Plus grande table métier | | `products` | 2 | 🟡 Moyenne | - | | `constructeurs` | 20 | 🟡 Moyenne | - | | `custom_fields` | 95 | 🔴 Haute | Configuration champs custom | | `custom_field_values` | 203 | 🔴 Haute | Plus grande table (valeurs) | | `documents` | 14 | 🟡 Moyenne | Fichiers uploadés | | `machine_component_links` | 25 | 🔴 Haute | Relations machines ↔ composants | | `machine_piece_links` | 99 | 🔴 Haute | Relations machines ↔ pièces | | `machine_product_links` | 0 | 🟢 Basse | Table vide | | `type_machine_*_requirements` (×3) | 22 | 🟡 Moyenne | Config requirements types | | `_*Constructeurs` (×4) | 14 | 🟡 Moyenne | Tables de liaison ManyToMany | | `profiles` | 2 | 🟢 Basse | Utilisateurs session (migrés) | **Complexités identifiées (résolues ou contournées)** : - ✅ IDs CUID Prisma → conservés en `string(36)` côté Doctrine (compatibles) - ✅ Champs JSON complexes importés (components, criticalParts, machinePieces, specifications) - ✅ Relations polymorphiques Document + CustomFieldValue migrées - ✅ Tables de liaison ManyToMany (`_*Constructeurs`) migrées - ✅ Custom fields dynamiques migrés (95 définitions, 203 valeurs) ### 🏗️ Structure Actuelle des Projets ``` /home/r-dev/Inventory/ ├── Inventory_backend/ # NestJS + Prisma (11k lignes) │ ├── src/ # 18 modules NestJS │ ├── prisma/schema.prisma # Schéma DB source │ └── package.json ├── Inventory_frontend/ # Nuxt 3 (front principal) │ ├── app/ │ │ ├── composables/ │ │ └── pages/ │ └── package.json ├── src/ # ⚡ Symfony backend │ ├── Entity/ # Toutes entités créées │ ├── Controller/ # Session + custom fields + documents + skeleton │ └── Repository/ ├── config/ # Configuration Symfony ├── migrations/ # Migrations Doctrine └── docker-compose.yml # Web + DB + pgAdmin ``` ### 🎯 Prochaines Étapes (Phase 3 - Mise en production fonctionnelle) #### Étape 3.1 : Réorganisation du Projet (optionnel) - [ ] Déplacer `Inventory_frontend/` vers `frontend/` (si besoin) - [ ] Archiver `Inventory_backend/` vers `_archives/backend_nestjs/` - [ ] Mettre à jour .gitignore #### Étape 3.2 : Entités & API Platform (TERMINÉE) - [x] Site, TypeMachine, Machine - [x] ModelType, Composant, Piece, Product - [x] Constructeur, Document - [x] CustomField, CustomFieldValue - [x] MachineComponentLink, MachinePieceLink, MachineProductLink - [x] TypeMachine*Requirement (×3) #### Étape 3.3 : Migration des Données (TERMINÉE) - [x] Backup `inventory-data` - [x] Schéma Doctrine aligné - [x] Import `inventory-data` → `inventory` - [x] Validation des comptes (script `scripts/validate-migration.php`) #### Étape 3.4 : Front Nuxt (EN COURS) - [x] Base URL API -> `http://localhost:8081/api` - [x] Parsing JSON-LD dans composables - [x] Normalisation des relations vers IRI - [x] Endpoints session profile compatibles - [ ] Corriger CORS (injecter `CORS_ALLOW_ORIGIN` dans container + clear cache) - [ ] Valider le squelette machine (endpoint `/api/machines/{id}/skeleton`) - [ ] Valider documents catalogue (composant/piece/produit) #### Étape 3.5 : Sécurité (À FAIRE) - [ ] Réactiver JWT (après stabilisation) - [ ] Ajouter refresh token si nécessaire --- ## Table des Matières 1. [Vue d'ensemble](#vue-densemble) 2. [Architecture Cible](#architecture-cible) 3. [Mapping Prisma → Doctrine](#mapping-prisma--doctrine) 4. [Stratégie de Migration des Données](#stratégie-de-migration-des-données) 5. [Configuration Docker](#configuration-docker) 6. [Plan d'Exécution Phase par Phase](#plan-dexécution-phase-par-phase) 7. [Gestion des Risques](#gestion-des-risques) 8. [Checklist de Migration](#checklist-de-migration) --- ## 1. Vue d'ensemble ### Situation Actuelle - **Backend NestJS**: Port 3000, Prisma ORM, PostgreSQL, Sessions cookie-based - **Frontend Nuxt 3**: Port 3001, API calls vers localhost:3000 - **Base de données**: PostgreSQL 16 sur port 5433, 28 modèles Prisma, données en production - **Docker**: Container unique avec PHP 8.4 + Node 24.12, Apache, XDebug ### Objectif Final - **Backend Symfony 8**: Port 8081, Doctrine ORM, API Platform 4.2, JWT auth - **Frontend Nuxt 3**: Port 3001, API calls vers localhost:8081 - **Base de données**: Même PostgreSQL, migration Prisma → Doctrine - **Docker**: 2 backends en parallèle pendant la transition (NestJS:3000 + Symfony:8081) ### Pourquoi cette migration ? - Code généré par IA (ChatGPT) à nettoyer et reprendre en main - Symfony + API Platform = stack PHP standard, mature, bien documentée - JWT auth plus adapté pour API REST moderne - Meilleure intégration avec l'écosystème PHP existant --- ## 2. Architecture Cible ### Stack Technique #### Backend Symfony ``` Symfony 8.0 ├── API Platform 4.2 # REST API auto-documentée (OpenAPI) ├── Doctrine ORM 3.6 # ORM pour PostgreSQL ├── Lexik JWT Bundle # JWT authentication ├── Nelmio CORS Bundle # CORS pour frontend ├── VichUploader Bundle # Upload de documents ├── Symfony Validator # Validation des DTOs └── Doctrine Migrations # Gestion des migrations DB ``` #### Structure de Projet ``` src/ ├── Entity/ # 16 entités Doctrine │ ├── Site.php │ ├── Machine.php │ ├── Composant.php │ ├── Piece.php │ ├── Product.php │ ├── TypeMachine.php │ ├── ModelType.php │ ├── Constructeur.php │ ├── Profile.php │ ├── Document.php │ ├── CustomField.php │ ├── CustomFieldValue.php │ ├── MachineComponentLink.php │ ├── MachinePieceLink.php │ ├── MachineProductLink.php │ └── TypeMachine*Requirement.php (×3) ├── Repository/ # Repositories Doctrine ├── Controller/ # Controllers custom si besoin ├── State/ # State providers/processors API Platform ├── Validator/ # Contraintes de validation custom └── Service/ # Services métier (équivalents NestJS services) ``` #### Configuration JWT **Flow d'authentification**: ``` 1. POST /api/login_check Body: {"username": "...", "password": "..."} Response: {"token": "eyJ0eXAiOiJKV1QiLCJhbGc..."} 2. Requêtes authentifiées Header: Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc... 3. Token refresh (optionnel) POST /api/token/refresh Body: {"refresh_token": "..."} ``` **Configuration sécurité** (`config/packages/security.yaml`): ```yaml security: password_hashers: App\Entity\Profile: algorithm: auto providers: app_user_provider: entity: class: App\Entity\Profile property: email # À ajouter à Profile firewalls: login: pattern: ^/api/login stateless: true json_login: check_path: /api/login_check success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure api: pattern: ^/api stateless: true jwt: ~ access_control: - { path: ^/api/login, roles: PUBLIC_ACCESS } - { path: ^/api/docs, roles: PUBLIC_ACCESS } - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } ``` --- ## 3. Mapping Prisma → Doctrine ### 3.1 Types de Données | Prisma | Doctrine | Notes | |--------|----------|-------| | `String` | `string` | `@ORM\Column(type="string")` | | `String @id @default(cuid())` | `Ulid` | `@ORM\Id` + `@ORM\GeneratedValue(strategy="CUSTOM")` | | `Int` | `integer` | `@ORM\Column(type="integer")` | | `Decimal @db.Decimal(10,2)` | `string` | `@ORM\Column(type="decimal", precision=10, scale=2)` | | `Boolean` | `bool` | `@ORM\Column(type="boolean")` | | `DateTime` | `\DateTimeImmutable` | `@ORM\Column(type="datetime_immutable")` | | `Json` | `array` | `@ORM\Column(type="json")` | | `String[]` | `array` | `@ORM\Column(type="json")` | | `String?` | `?string` | `@ORM\Column(nullable=true)` | ### 3.2 Relations | Prisma | Doctrine | Exemple | |--------|----------|---------| | `@relation(onDelete: Cascade)` | `@ORM\JoinColumn(onDelete="CASCADE")` | Machine → Site | | `@relation(onDelete: SetNull)` | `@ORM\JoinColumn(nullable=true, onDelete="SET NULL")` | Composant → Product | | `Model[]` | `@ORM\OneToMany` + `Collection` | Site → machines | | `Model` | `@ORM\ManyToOne` | Machine → site | | Pas de `@relation` | `@ORM\ManyToMany` | Machine ↔ Constructeurs | ### 3.3 Contraintes et Index | Prisma | Doctrine | |--------|----------| | `@unique` | `@ORM\Column(unique=true)` | | `@@unique([field1, field2])` | `@ORM\UniqueConstraint(columns=["field1", "field2"])` | | `@@map("table_name")` | `@ORM\Table(name="table_name")` | | `@db.VarChar(120)` | `@ORM\Column(type="string", length=120)` | | `@default("")` | `#[ORM\Column(options: ['default' => ''])]` | ### 3.4 Mapping Détaillé des 16 Entités #### 1. Site ```php #[ORM\Entity(repositoryClass: SiteRepository::class)] #[ORM\Table(name: 'sites')] #[ApiResource( operations: [ new Get(), new GetCollection(), new Post(), new Put(), new Delete() ] )] class Site { #[ORM\Id] #[ORM\Column(type: 'ulid', unique: true)] #[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\CustomIdGenerator(class: UlidGenerator::class)] private ?Ulid $id = null; #[ORM\Column(type: 'string', length: 255)] #[Assert\NotBlank] private string $name; #[ORM\Column(type: 'string', length: 255, options: ['default' => ''])] private string $contactName = ''; #[ORM\Column(type: 'string', length: 20, options: ['default' => ''])] private string $contactPhone = ''; #[ORM\Column(type: 'string', length: 500, options: ['default' => ''])] private string $contactAddress = ''; #[ORM\Column(type: 'string', length: 10, options: ['default' => ''])] private string $contactPostalCode = ''; #[ORM\Column(type: 'string', length: 100, options: ['default' => ''])] private string $contactCity = ''; #[ORM\Column(type: 'datetime_immutable')] private \DateTimeImmutable $createdAt; #[ORM\Column(type: 'datetime_immutable')] private \DateTimeImmutable $updatedAt; #[ORM\OneToMany(mappedBy: 'site', targetEntity: Machine::class, cascade: ['persist', 'remove'], orphanRemoval: true)] private Collection $machines; #[ORM\OneToMany(mappedBy: 'site', targetEntity: Document::class, cascade: ['remove'], orphanRemoval: true)] private Collection $documents; #[ORM\PrePersist] public function setCreatedAtValue(): void { $this->createdAt = new \DateTimeImmutable(); $this->updatedAt = new \DateTimeImmutable(); } #[ORM\PreUpdate] public function setUpdatedAtValue(): void { $this->updatedAt = new \DateTimeImmutable(); } } ``` #### 2. Machine ```php #[ORM\Entity] #[ORM\Table(name: 'machines')] #[ApiResource] class Machine { #[ORM\Id] #[ORM\Column(type: 'ulid')] private ?Ulid $id = null; #[ORM\Column(type: 'string', length: 255, unique: true)] private string $name; #[ORM\Column(type: 'string', length: 255, nullable: true)] private ?string $reference = null; #[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)] private ?string $prix = null; #[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'machines')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] private Site $site; #[ORM\ManyToOne(targetEntity: TypeMachine::class, inversedBy: 'machines')] #[ORM\JoinColumn(nullable: true)] private ?TypeMachine $typeMachine = null; #[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'machines')] #[ORM\JoinTable(name: 'machine_constructeurs')] private Collection $constructeurs; #[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachineComponentLink::class, cascade: ['persist', 'remove'])] private Collection $componentLinks; #[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachinePieceLink::class, cascade: ['persist', 'remove'])] private Collection $pieceLinks; #[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachineProductLink::class, cascade: ['persist', 'remove'])] private Collection $productLinks; #[ORM\OneToMany(mappedBy: 'machine', targetEntity: Document::class, cascade: ['remove'])] private Collection $documents; #[ORM\OneToMany(mappedBy: 'machine', targetEntity: CustomFieldValue::class, cascade: ['persist', 'remove'])] private Collection $customFieldValues; #[ORM\Column(type: 'datetime_immutable')] private \DateTimeImmutable $createdAt; #[ORM\Column(type: 'datetime_immutable')] private \DateTimeImmutable $updatedAt; } ``` #### 3. TypeMachine (avec champs JSON) ```php #[ORM\Entity] #[ORM\Table(name: 'type_machines')] #[ApiResource] class TypeMachine { #[ORM\Id] #[ORM\Column(type: 'ulid')] private ?Ulid $id = null; #[ORM\Column(type: 'string', length: 255, unique: true)] private string $name; #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; #[ORM\Column(type: 'string', length: 100, nullable: true)] private ?string $category = null; #[ORM\Column(type: 'string', length: 100, nullable: true)] private ?string $maintenanceFrequency = null; // Champs JSON (structure hiérarchique, specs techniques) #[ORM\Column(type: 'json', nullable: true)] private ?array $components = null; #[ORM\Column(type: 'json', nullable: true)] private ?array $criticalParts = null; #[ORM\Column(type: 'json', nullable: true)] private ?array $machinePieces = null; #[ORM\Column(type: 'json', nullable: true)] private ?array $specifications = null; #[ORM\OneToMany(mappedBy: 'typeMachine', targetEntity: Machine::class)] private Collection $machines; #[ORM\OneToMany(mappedBy: 'typeMachine', targetEntity: CustomField::class, cascade: ['remove'])] private Collection $customFields; #[ORM\OneToMany(mappedBy: 'typeMachine', targetEntity: TypeMachineComponentRequirement::class, cascade: ['persist', 'remove'])] private Collection $componentRequirements; #[ORM\OneToMany(mappedBy: 'typeMachine', targetEntity: TypeMachinePieceRequirement::class, cascade: ['persist', 'remove'])] private Collection $pieceRequirements; #[ORM\OneToMany(mappedBy: 'typeMachine', targetEntity: TypeMachineProductRequirement::class, cascade: ['persist', 'remove'])] private Collection $productRequirements; } ``` #### 4. ModelType (avec Enum) ```php enum ModelCategory: string { case COMPONENT = 'COMPONENT'; case PIECE = 'PIECE'; case PRODUCT = 'PRODUCT'; } #[ORM\Entity] #[ORM\Table(name: 'model_types')] #[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])] #[ApiResource] class ModelType { #[ORM\Id] #[ORM\Column(type: 'ulid')] private ?Ulid $id = null; #[ORM\Column(type: 'string', length: 120)] private string $name; #[ORM\Column(type: 'string', length: 60, unique: true)] private string $code; #[ORM\Column(type: 'string', enumType: ModelCategory::class)] private ModelCategory $category; #[ORM\Column(type: 'text', nullable: true)] private ?string $notes = null; #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; #[ORM\Column(type: 'json', nullable: true)] private ?array $componentSkeleton = null; #[ORM\Column(type: 'json', nullable: true)] private ?array $pieceSkeleton = null; #[ORM\Column(type: 'json', nullable: true)] private ?array $productSkeleton = null; #[ORM\OneToMany(mappedBy: 'typeComposant', targetEntity: Composant::class)] private Collection $composants; #[ORM\OneToMany(mappedBy: 'typePiece', targetEntity: Piece::class)] private Collection $pieces; #[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: Product::class)] private Collection $products; // Relations avec les requirements (×3) // Relations avec les custom fields (×3) } ``` #### 5. Document (Relations Polymorphiques) ```php #[ORM\Entity] #[ORM\Table(name: 'documents')] #[ApiResource( normalizationContext: ['groups' => ['document:read']], denormalizationContext: ['groups' => ['document:write']] )] class Document { #[ORM\Id] #[ORM\Column(type: 'ulid')] private ?Ulid $id = null; #[ORM\Column(type: 'string', length: 255)] private string $name; #[ORM\Column(type: 'string', length: 255)] private string $filename; #[ORM\Column(type: 'string', length: 500)] private string $path; #[ORM\Column(type: 'string', length: 100)] private string $mimeType; #[ORM\Column(type: 'integer')] private int $size; // Relations polymorphiques (nullable) #[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'documents')] #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')] private ?Machine $machine = null; #[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'documents')] #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')] private ?Composant $composant = null; #[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'documents')] #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')] private ?Piece $piece = null; #[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'documents')] #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')] private ?Product $product = null; #[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'documents')] #[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')] private ?Site $site = null; } ``` #### 6-16. Autres Entités Les autres entités suivent le même pattern : - **Composant, Piece, Product** : Similaires à Machine avec relations vers ModelType - **Constructeur** : ManyToMany avec Machine/Composant/Piece/Product - **Profile** : Entité utilisateur (à enrichir avec email/password pour JWT) - **CustomField, CustomFieldValue** : Système de champs dynamiques - **MachineComponentLink, MachinePieceLink, MachineProductLink** : Tables de liaison avec overrides - **TypeMachine*Requirement (×3)** : Définition des requirements (minCount, maxCount, label, etc.) --- ## 4. Stratégie de Migration des Données ### 4.1 Problème : CUID vs ULID **Prisma utilise CUID** (`@default(cuid())`) : - Format: `clabcd123xyz...` (25 caractères) - Exemple: `cldu8kzj30000vh8q9k0y8h7i` **Doctrine recommande ULID** : - Format: `01ARZ3NDEKTSV4RRFFQ69G5FAV` (26 caractères) - Type Symfony : `Symfony\Component\Uid\Ulid` **Solutions possibles** : #### Option A : Garder les CUID comme chaînes (RECOMMANDÉE) ```php #[ORM\Id] #[ORM\Column(type: 'string', length: 30)] private string $id; ``` **Avantages** : - ✅ Aucune modification des IDs existants - ✅ Migration transparente, pas de mapping d'IDs - ✅ Relations FK préservées - ✅ Frontend continue à utiliser les mêmes IDs **Inconvénients** : - ❌ Pas de type Ulid natif Symfony - ❌ Moins "moderne" #### Option B : Migrer CUID → ULID avec mapping ```php #[ORM\Id] #[ORM\Column(type: 'ulid')] private Ulid $id; ``` **Nécessite** : 1. Créer une table de mapping `cuid_to_ulid` 2. Générer de nouveaux ULIDs pour chaque ligne 3. Mettre à jour toutes les FK 4. Adapter le frontend **Risque** : Complexité élevée, risque de perte de données. ### 4.2 Approche Recommandée : Migration en 3 Étapes #### Étape 1 : Créer le schéma Doctrine en parallèle (SAFE) 1. Générer les migrations Doctrine sans exécution : ```bash php bin/console doctrine:migrations:diff ``` 2. **NE PAS exécuter** la migration immédiatement 3. Analyser le SQL généré pour détecter les différences : - Noms de colonnes (camelCase → snake_case) - Types de données (Decimal en Prisma → string en Doctrine) - Contraintes (vérifier les FK, indexes) #### Étape 2 : Script de Validation (Dry-run) Créer un script PHP qui : 1. Lit toutes les tables Prisma actuelles 2. Compare avec le schéma Doctrine généré 3. Génère un rapport de différences 4. Valide que toutes les données peuvent être mappées ```php // scripts/validate-migration.php use Doctrine\ORM\EntityManagerInterface; class MigrationValidator { public function validate(): array { $issues = []; // Vérifier chaque table foreach ($this->getTables() as $table) { // Comparer schéma Prisma vs Doctrine $schemaIssues = $this->compareSchemas($table); // Vérifier les données peuvent être migrées $dataIssues = $this->validateData($table); $issues = array_merge($issues, $schemaIssues, $dataIssues); } return $issues; } } ``` #### Étape 3 : Migration réelle (avec Backup) ```bash # 1. BACKUP de la DB pg_dump -U postgres -h localhost -p 5433 inventory_db > backup_$(date +%Y%m%d_%H%M%S).sql # 2. Exécuter les migrations Doctrine php bin/console doctrine:migrations:migrate --no-interaction # 3. Vérifier l'intégrité php bin/console doctrine:schema:validate # 4. Si erreur : ROLLBACK psql -U postgres -h localhost -p 5433 inventory_db < backup_YYYYMMDD_HHMMSS.sql ``` ### 4.3 Mapping des Noms de Colonnes Prisma → Doctrine naming strategy : | Prisma | Doctrine | Action | |--------|----------|--------| | `siteId` | `site_id` | Annotation `#[ORM\JoinColumn(name="siteId")]` ou laisser Doctrine snake_case | | `createdAt` | `created_at` | Idem | | `contactName` | `contact_name` | Idem | **Recommandation** : Utiliser `#[ORM\Column(name="xxx")]` pour garder les noms Prisma existants et éviter les ALTER TABLE. ### 4.4 Checklist de Migration DB - [ ] Créer backup complet de la DB - [ ] Générer migration Doctrine (`doctrine:migrations:diff`) - [ ] Analyser SQL généré manuellement - [ ] Tester migration sur copie locale de la DB - [ ] Valider que les données sont intactes après migration - [ ] Créer script de rollback - [ ] Documenter les différences schéma Prisma vs Doctrine - [ ] Tester toutes les requêtes via Doctrine - [ ] Vérifier les performances (indexes, FK) --- ## 5. Configuration Docker ### 5.1 Setup : 2 Backends en Parallèle **Objectif** : Faire tourner NestJS (port 3000) ET Symfony (port 8081) simultanément pendant la transition. #### Modification `docker-compose.yml` ```yaml services: web: container_name: php-${DOCKER_APP_NAME}-apache build: context: ./docker/php dockerfile: Dockerfile args: DOCKER_PHP_VERSION: ${DOCKER_PHP_VERSION} DOCKER_NODE_VERSION: ${DOCKER_NODE_VERSION} CURRENT_UID: ${CURRENT_UID} CURRENT_GID: ${CURRENT_GID} environment: PHP_IDE_CONFIG: serverName=${DOCKER_APP_NAME}-docker XDEBUG_CLIENT_HOST: ${XDEBUG_CLIENT_HOST:-host.docker.internal} XDEBUG_CONFIG: client_host=${XDEBUG_CLIENT_HOST:-host.docker.internal} client_port=9003 DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=16&charset=utf8" # Variables pour NestJS NESTJS_DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?schema=public" NESTJS_PORT: 3000 NESTJS_CORS_ORIGIN: "http://localhost:3001" # Variables pour Symfony APP_ENV: dev APP_SECRET: ${APP_SECRET} JWT_SECRET_KEY: ${JWT_SECRET_KEY} JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY} JWT_PASSPHRASE: ${JWT_PASSPHRASE} volumes: - ./:/var/www/html - ~/.cache:/var/www/.cache - ~/.config:/var/www/.config - ~/.composer:/var/www/.composer - ./docker/php/config/php.ini:/usr/local/etc/php/php.ini - ./docker/php/config/vhost.conf:/etc/apache2/sites-available/000-default.conf - ./docker/php/config/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini - ./LOG:/var/www/html/LOG - ./LOG/logs_apache:/var/log/apache2/ extra_hosts: - "host.docker.internal:host-gateway" depends_on: - db ports: - "8081:80" # Apache/Symfony - "3000:3000" # NestJS backend (nouveau mapping) - "3001:3001" # Nuxt frontend restart: unless-stopped db: image: postgres:16-alpine environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - pg_data:/var/lib/postgresql/data ports: - "${POSTGRES_PORT:-5433}:5432" restart: unless-stopped volumes: pg_data: ``` #### Configuration Apache VirtualHost ```apache # /docker/php/config/vhost.conf ServerName localhost DocumentRoot /var/www/html/public AllowOverride All Require all granted FallbackResource /index.php # Logs ErrorLog /var/log/apache2/error.log CustomLog /var/log/apache2/access.log combined ``` #### Script de Démarrage des 2 Backends Créer `/docker/start-services.sh` : ```bash #!/bin/bash # Démarrer Apache (Symfony) apache2-foreground & # Installer dépendances NestJS si besoin cd /var/www/html/Inventory_backend if [ ! -d "node_modules" ]; then npm install fi # Démarrer NestJS npm run start:dev & # Installer dépendances Nuxt si besoin cd /var/www/html/Inventory_frontend if [ ! -d "node_modules" ]; then npm install fi # Démarrer Nuxt npm run dev & # Garder le container actif wait -n ``` Modifier `Dockerfile` pour utiliser ce script : ```dockerfile # À la fin du Dockerfile COPY start-services.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/start-services.sh CMD ["start-services.sh"] ``` ### 5.2 Variables d'Environnement Créer `/docker/.env.docker.local` (déjà existant, à compléter) : ```env # Existant DOCKER_APP_NAME=inventory DOCKER_PHP_VERSION=8.4.6 DOCKER_NODE_VERSION=24.12.0 CURRENT_UID=1000 CURRENT_GID=1000 POSTGRES_DB=inventory_db POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_PORT=5433 # Nouveau : Symfony APP_ENV=dev APP_SECRET=changeme_super_secret_key_123456789 JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem JWT_PASSPHRASE=your_jwt_passphrase # Nouveau : NestJS NESTJS_PORT=3000 SESSION_SECRET=changeme_session_secret CORS_ORIGIN=http://localhost:3001 ``` ### 5.3 Génération des Clés JWT Ajouter au script de démarrage : ```bash # Générer clés JWT si inexistantes mkdir -p /var/www/html/config/jwt if [ ! -f /var/www/html/config/jwt/private.pem ]; then openssl genpkey -out /var/www/html/config/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keybits:4096 -pass pass:${JWT_PASSPHRASE} openssl pkey -in /var/www/html/config/jwt/private.pem -passin pass:${JWT_PASSPHRASE} -out /var/www/html/config/jwt/public.pem -pubout chmod 600 /var/www/html/config/jwt/*.pem fi ``` --- ## 6. Plan d'Exécution Phase par Phase ### Phase 1 : Préparation (Semaine 1) #### Tâches 1. **Installer bundles Symfony** : ```bash composer require lexik/jwt-authentication-bundle composer require vich/uploader-bundle composer require symfony/uid ``` 2. **Configurer JWT** : - Générer clés RSA - Configurer `lexik_jwt_authentication.yaml` - Configurer `security.yaml` 3. **Créer entité Profile enrichie** : ```php class Profile implements UserInterface { // Ajouter email, password, roles } ``` 4. **Analyser schéma Prisma** : - Documenter toutes les relations - Identifier les champs JSON complexes - Lister les contraintes de validation 5. **Setup Docker** : - Modifier `docker-compose.yml` - Créer script de démarrage multi-services - Tester les 2 backends en parallèle #### Livrables - [ ] Symfony configuré avec tous les bundles - [ ] JWT fonctionnel avec endpoint `/api/login_check` - [ ] Docker lance NestJS (3000) + Symfony (8081) + Nuxt (3001) - [ ] Documentation schéma Prisma complète --- ### Phase 2 : Création des Entités (Semaine 2-3) #### Tâches 1. **Créer toutes les entités Doctrine** (16 entités) : - Utiliser les attributs PHP 8 (`#[ORM\Entity]`) - Mapper exactement le schéma Prisma - Ajouter annotations API Platform - Implémenter les lifecycle callbacks (`#[ORM\PrePersist]`, etc.) 2. **Générer repositories** : ```bash php bin/console make:entity --regenerate ``` 3. **Créer migration Doctrine** : ```bash php bin/console doctrine:migrations:diff ``` 4. **Analyser SQL généré** : - Comparer avec schéma Prisma actuel - Identifier les différences - Préparer script d'ajustement si besoin 5. **Tester sur DB vide** : ```bash # Créer DB test createdb inventory_test # Appliquer migrations php bin/console doctrine:migrations:migrate --env=test # Valider schéma php bin/console doctrine:schema:validate ``` #### Livrables - [ ] 16 entités Doctrine complètes avec relations - [ ] Migration Doctrine générée et validée - [ ] Tests sur DB vide réussis - [ ] Documentation des différences Prisma/Doctrine --- ### Phase 3 : Migration de la Base de Données (Semaine 4) #### Tâches 1. **Backup complet** : ```bash pg_dump -U postgres -h localhost -p 5433 inventory_db > backup_avant_migration.sql ``` 2. **Créer script de validation** : ```php // scripts/validate-schema.php // Comparer schéma Prisma vs Doctrine ``` 3. **Exécuter migration Doctrine** : ```bash php bin/console doctrine:migrations:migrate --no-interaction ``` 4. **Valider données migrées** : - Compter les lignes par table (Prisma count vs Doctrine count) - Vérifier intégrité référentielle - Tester quelques requêtes 5. **Tests de régression** : - Lire toutes les entités via Doctrine - Vérifier les relations - Tester les champs JSON #### Livrables - [ ] Base de données migrée avec succès - [ ] Backup de sécurité créé - [ ] Script de validation OK (0 différences) - [ ] Tests de lecture/écriture via Doctrine OK --- ### Phase 4 : Logique Métier (Semaine 5-6) #### Tâches 1. **Migrer services NestJS → Symfony** : **Mapping des services** : | NestJS Service | Symfony Service | Responsabilités | |----------------|-----------------|-----------------| | `SitesService` | `SiteManager` | CRUD sites | | `MachinesService` | `MachineManager` | CRUD machines, reconfiguration | | `ComposantsService` | `ComposantManager` | CRUD composants | | `PiecesService` | `PieceManager` | CRUD pieces | | `ProductsService` | `ProductManager` | CRUD products | | `TypesService` | `TypeMachineManager` | Gestion types machines | | `ModelTypeService` | `ModelTypeManager` | Gestion model types | | `DocumentsService` | `DocumentManager` | Upload/download documents | | `CustomFieldsService` | `CustomFieldManager` | Gestion custom fields | | `ConstructeursService` | `ConstructeurManager` | Gestion constructeurs | | `ProfilesService` | `ProfileManager` | Gestion profiles | | `SessionService` | Supprimé (JWT) | - | 2. **Implémenter upload de documents** : - Configurer VichUploaderBundle - Créer endpoint `POST /api/documents` - Créer endpoint `GET /api/documents/{id}/download` 3. **Implémenter custom fields** : - Service de gestion des définitions - Service de gestion des valeurs - Validation dynamique 4. **Créer State Processors API Platform** : - Pour logique custom (ex: reconfiguration machine) - Pour validation complexe 5. **Tests unitaires** : - Tester chaque service - Mocker les repositories - Vérifier logique métier #### Livrables - [ ] Tous les services métier migrés - [ ] Upload/download documents fonctionnel - [ ] Custom fields opérationnels - [ ] Tests unitaires couvrant 80%+ du code --- ### Phase 5 : API Platform (Semaine 7) #### Tâches 1. **Configurer les endpoints API Platform** : - Vérifier routes auto-générées - Customiser opérations si besoin - Ajouter filtres de recherche - Configurer pagination 2. **Sérialisation** : - Créer groupes de normalisation - Gérer relations circulaires - Optimiser avec `@Groups` 3. **Validation** : - Ajouter contraintes de validation - Créer validators custom - Gérer messages d'erreur 4. **Documentation API** : - Générer OpenAPI spec - Tester via Swagger UI - Documenter endpoints custom 5. **Tests d'intégration** : - Tester tous les endpoints CRUD - Vérifier codes de réponse HTTP - Valider structure JSON #### Livrables - [ ] Tous les endpoints API fonctionnels - [ ] Documentation OpenAPI complète - [ ] Tests d'intégration OK - [ ] Swagger UI accessible sur `/api/docs` --- ### Phase 6 : Adaptation Frontend (Semaine 8) #### Tâches 1. **Modifier `useApi.js`** : ```js // Ancien const baseURL = 'http://localhost:3000'; // Nouveau const baseURL = import.meta.env.VITE_API_URL || 'http://localhost:8081/api'; ``` 2. **Implémenter gestion JWT** : ```js // composables/useAuth.js export const useAuth = () => { const token = ref(localStorage.getItem('jwt_token')); const login = async (username, password) => { const response = await fetch('/api/login_check', { method: 'POST', body: JSON.stringify({ username, password }) }); const { token } = await response.json(); localStorage.setItem('jwt_token', token); token.value = token; }; const logout = () => { localStorage.removeItem('jwt_token'); token.value = null; }; return { token, login, logout }; }; ``` 3. **Adapter les composables** : - `useSites.js` → vérifier format réponse - `useMachines.js` → idem - Tous les autres composables 4. **Gestion des changements d'API** : - Ajuster parsing des réponses JSON:API vs JSON simple - Gérer les nouveaux formats d'erreur - Adapter les IDs si changement 5. **Tests fonctionnels** : - Tester chaque page du frontend - Vérifier tous les flows utilisateur - Tester upload/download documents #### Livrables - [ ] Frontend connecté au backend Symfony - [ ] Authentification JWT fonctionnelle - [ ] Tous les composables adaptés - [ ] Tests fonctionnels réussis --- ### Phase 7 : Tests & Décommissionnement NestJS (Semaine 9) #### Tâches 1. **Tests de charge** : - Comparer performances NestJS vs Symfony - Vérifier temps de réponse - Tester avec données réelles 2. **Tests de régression complète** : - Rejouer tous les scénarios utilisateur - Comparer résultats NestJS vs Symfony - Fixer les bugs 3. **Documentation** : - Documenter nouvelle architecture - Créer guide migration pour équipe - Documenter différences API 4. **Décommissionner NestJS** : - Arrêter le service NestJS - Supprimer du docker-compose - Archiver le code (ne pas supprimer) 5. **Cleanup** : - Supprimer dépendances NestJS inutilisées - Nettoyer fichiers de config - Optimiser Docker #### Livrables - [ ] Tests de charge validés - [ ] 0 bugs de régression - [ ] Documentation complète - [ ] NestJS arrêté, Symfony en production - [ ] Code NestJS archivé --- ## 7. Gestion des Risques ### 7.1 Risques Techniques | Risque | Probabilité | Impact | Mitigation | |--------|-------------|--------|------------| | **Perte de données pendant migration DB** | Faible | Critique | Backup complet avant migration, script de rollback, validation post-migration | | **Incompatibilité schéma Prisma/Doctrine** | Moyenne | Élevé | Validation schema dry-run, tests sur DB copie, garder noms de colonnes Prisma | | **Performances Symfony < NestJS** | Faible | Moyen | Benchmarks, optimisation requêtes Doctrine, cache API Platform | | **Bugs lors migration logique métier** | Moyenne | Élevé | Tests unitaires + intégration, comparison NestJS/Symfony side-by-side | | **JWT incompatible avec frontend** | Faible | Moyen | Tests d'auth dès Phase 1, documentation claire | | **Upload documents ne fonctionne pas** | Faible | Moyen | Tester VichUploader tôt, fallback manuel si besoin | | **Relations Doctrine mal configurées** | Moyenne | Élevé | Tests avec données réelles, validation via `doctrine:schema:validate` | ### 7.2 Risques Projet | Risque | Probabilité | Impact | Mitigation | |--------|-------------|--------|------------| | **Timeline trop optimiste** | Élevée | Moyen | Buffer de 2 semaines, découpage en phases indépendantes | | **Manque de tests** | Moyenne | Élevé | Écrire tests dès Phase 2, coverage > 80% | | **Documentation insuffisante** | Moyenne | Moyen | Documenter au fur et à mesure, pas à la fin | | **Réversion nécessaire** | Faible | Critique | Garder NestJS actif en parallèle, backup DB, script de rollback | ### 7.3 Plan de Rollback Si problème critique : 1. **Arrêter Symfony** : ```bash docker-compose stop web ``` 2. **Restaurer DB depuis backup** : ```bash psql -U postgres -h localhost -p 5433 inventory_db < backup_avant_migration.sql ``` 3. **Redémarrer NestJS** : ```bash cd Inventory_backend && npm run start:dev ``` 4. **Reconfigurer frontend** : ```js // Revert API URL const baseURL = 'http://localhost:3000'; ``` 5. **Analyser l'erreur** : - Lire logs Symfony (`var/log/dev.log`) - Lire logs PostgreSQL - Identifier root cause 6. **Fix et retry** : - Corriger le problème - Retester en local - Relancer migration --- ## 8. Checklist de Migration ### Avant de Commencer - [ ] Lire entièrement ce document - [ ] Comprendre schéma Prisma actuel - [ ] Valider que Docker fonctionne - [ ] Créer backup DB initial - [ ] Setup environnement de dev local ### Phase 1 : Préparation - [ ] Installer bundles Symfony (JWT, VichUploader, Uid) - [ ] Configurer JWT avec clés RSA - [ ] Enrichir entité Profile (email, password, roles) - [ ] Modifier docker-compose.yml pour 2 backends - [ ] Tester démarrage des 2 backends en parallèle - [ ] Documenter schéma Prisma ### Phase 2 : Entités - [ ] Créer 16 entités Doctrine - [ ] Ajouter annotations API Platform - [ ] Générer migration Doctrine - [ ] Analyser SQL généré - [ ] Tester migration sur DB vide - [ ] Valider schéma Doctrine ### Phase 3 : Migration DB - [ ] Créer backup DB production - [ ] Créer script de validation - [ ] Exécuter migration Doctrine - [ ] Appliquer migrations post-migration (ajouts colonnes, types JSON) - [ ] Valider count de lignes par table - [ ] Tester requêtes Doctrine - [ ] Vérifier intégrité référentielle ### Phase 4 : Services - [ ] Migrer les 11 services métier - [ ] Implémenter upload documents - [ ] Implémenter custom fields - [ ] Écrire tests unitaires - [ ] Valider logique métier ### Phase 5 : API - [ ] Configurer endpoints API Platform - [ ] Implémenter sérialisation - [ ] Ajouter validation - [ ] Générer doc OpenAPI - [ ] Tests d'intégration ### Phase 6 : Frontend - [ ] Modifier useApi.js (nouveau baseURL) - [ ] Implémenter gestion JWT - [ ] Adapter tous les composables - [ ] Tester toutes les pages - [ ] Valider flows utilisateur ### Phase 7 : Finalisation - [ ] Tests de charge - [ ] Tests de régression complète - [ ] Documenter nouvelle architecture - [ ] Arrêter NestJS - [ ] Archiver code NestJS - [ ] Cleanup Docker --- ## Annexes ### A. Commandes Utiles ```bash # Symfony php bin/console doctrine:schema:validate php bin/console doctrine:migrations:diff php bin/console doctrine:migrations:migrate php bin/console doctrine:query:sql "SELECT COUNT(*) FROM sites" php bin/console debug:router php bin/console cache:clear # Docker docker-compose up -d --build docker-compose logs -f web docker-compose exec web bash docker-compose down # Base de données pg_dump -U postgres -h localhost -p 5433 inventory_db > backup.sql psql -U postgres -h localhost -p 5433 inventory_db < backup.sql psql -U postgres -h localhost -p 5433 inventory_db -c "SELECT COUNT(*) FROM sites" # Tests php bin/phpunit php vendor/bin/phpstan analyse src ``` ### B. Structure Finale du Projet ``` /home/r-dev/Inventory/ ├── Inventory_backend/ # À archiver après migration ├── Inventory_frontend/ # Frontend Nuxt (à garder) │ ├── app/ │ │ ├── composables/ │ │ │ ├── useApi.js # Modifier baseURL │ │ │ ├── useAuth.js # Nouveau : JWT │ │ │ └── ... │ │ └── pages/ ├── src/ # Backend Symfony (nouveau) │ ├── Entity/ │ │ ├── Site.php │ │ ├── Machine.php │ │ └── ... (16 entités) │ ├── Repository/ │ ├── Service/ │ │ ├── SiteManager.php │ │ ├── MachineManager.php │ │ └── ... (11 services) │ ├── Controller/ # Si besoin de custom controllers │ └── State/ # State providers API Platform ├── config/ │ ├── packages/ │ │ ├── doctrine.yaml │ │ ├── api_platform.yaml │ │ ├── lexik_jwt_authentication.yaml │ │ ├── security.yaml │ │ └── nelmio_cors.yaml │ └── jwt/ │ ├── private.pem │ └── public.pem ├── migrations/ # Migrations Doctrine ├── docker/ │ ├── php/ │ │ ├── Dockerfile │ │ └── config/ │ ├── start-services.sh # Nouveau : lance Symfony + Nuxt │ ├── .env.docker │ └── .env.docker.local ├── docker-compose.yml # Modifié : 2 backends ├── composer.json # Symfony dependencies └── MIGRATION_PLAN.md # Ce document ``` ### C. Ressources - [API Platform Docs](https://api-platform.com/docs/) - [Doctrine ORM Docs](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/) - [Lexik JWT Bundle](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst) - [Symfony Best Practices](https://symfony.com/doc/current/best_practices.html) - [Prisma to Doctrine Migration Guide](https://symfony.com/doc/current/doctrine.html#creating-an-entity-class) --- ## Conclusion Ce plan de migration détaille la stratégie pour passer de NestJS/Prisma à Symfony/API Platform de manière progressive et sécurisée. Les points clés : 1. **Migration incrémentale** : Garder NestJS actif en parallèle 2. **Sécurité des données** : Backups, validation, rollback 3. **Modernisation** : JWT auth, API Platform, documentation OpenAPI 4. **Minimiser les risques** : Tests à chaque phase, validation continue **Durée estimée** : 9 semaines (avec buffer) **Effort requis** : 1 développeur full-time **Prêt à démarrer ?** Validation de ce plan avant de commencer la Phase 1.