Files
Inventory/MIGRATION_PLAN.md

1420 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<VirtualHost *:80>
ServerName localhost
DocumentRoot /var/www/html/public
<Directory /var/www/html/public>
AllowOverride All
Require all granted
FallbackResource /index.php
</Directory>
# Logs
ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined
</VirtualHost>
```
#### 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.