1419 lines
44 KiB
Markdown
1419 lines
44 KiB
Markdown
# 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
|
||
- [ ] 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.
|