docs : update project documentation and frontend submodule pointer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 13:47:46 +01:00
parent efc6ec5691
commit 33e3f25850
7 changed files with 2264 additions and 287 deletions

View File

@@ -49,13 +49,14 @@ Inventory/ # Backend Symfony (repo principal)
# Docker
make start # Démarrer les containers
make stop # Arrêter
make shell # Shell dans le container PHP
make shell # Shell interactif (nécessite un TTY)
make install # Install complet (composer + npm + build)
# Backend
make test # PHPUnit
docker compose exec php vendor/bin/php-cs-fixer fix # Linter PHP
docker compose exec php php bin/console doctrine:migrations:migrate
make test # PHPUnit (tous les tests)
make test FILES=tests/Api/Entity/MachineTest.php # Un test spécifique
make php-cs-fixer-allow-risky # Linter PHP (cs-fixer)
docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate
# Frontend (dans Inventory_frontend/)
npm run dev # Dev server (port 3001)
@@ -138,13 +139,18 @@ ROLE_ADMIN → ROLE_GESTIONNAIRE → ROLE_VIEWER → ROLE_USER
## Règles Importantes
### CLAUDE.md — Maintenance obligatoire
- **Toujours consulter** ce fichier en début de conversation pour respecter les conventions
- **Mettre à jour** ce fichier quand une nouvelle convention, pattern ou décision architecturale est établie
- **Utiliser comme source de vérité** pour les commandes, patterns et règles du projet
### Toujours faire AVANT de modifier du code
1. **Lire le fichier** avant de l'éditer — ne jamais proposer de changements sur du code non lu
2. **Comprendre le pattern existant** — reproduire le style du fichier (noms, indentation, structure)
3. **Vérifier les deux repos** — un changement peut impacter backend ET frontend
### Après chaque modification
1. Backend PHP : `docker compose exec php vendor/bin/php-cs-fixer fix`
1. Backend PHP : `make php-cs-fixer-allow-risky`
2. Frontend : `npm run lint:fix` puis `npx nuxi typecheck` si fichiers TS modifiés
### Ne jamais faire
@@ -161,6 +167,26 @@ Quand les branches `master` et `develop` divergent sur l'un des deux repos, **to
- Main repo : `git checkout master && git merge develop && git push`
- Frontend : `git checkout develop && git merge master && git push` (ou l'inverse selon le cas)
## Tests
### Stack de test
- **PHPUnit 12** + **API Platform Test** (`ApiTestCase`)
- **DAMA DoctrineTestBundle** — wrappe chaque test dans une transaction avec rollback automatique (pas de TRUNCATE)
- Base de test : même PG, env `test`
### Commandes
```bash
make test # Tous les tests
make test FILES=tests/Api/Entity/MachineTest.php # Un fichier spécifique
make test-setup # Créer/mettre à jour le schéma test
```
### Pattern de test
- Hériter de `AbstractApiTestCase` (helpers auth + factories)
- Ne PAS faire de TRUNCATE/cleanup dans tearDown — DAMA s'en occupe par rollback
- Factories : `createProfile()`, `createMachine()`, `createSite()`, etc.
- Auth : `createViewerClient()`, `createGestionnaireClient()`, `createAdminClient()`
## URLs Locales
- API Symfony : `http://localhost:8081/api`
- Nuxt dev : `http://localhost:3001`

214
DEPLOY.md
View File

@@ -1,17 +1,29 @@
# Inventory - Guide de Déploiement & Release
# Inventory Guide de Déploiement
## Architecture
Guide pour déployer l'application sur un serveur de production.
## Architecture de production
```
inventory.malio-dev.fr/ → Frontend Nuxt (statique)
inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM)
inventory.malio-dev.fr/ → Frontend Nuxt (fichiers statiques servis par Nginx)
inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM derrière Nginx)
```
| Composant | Technologie | Emplacement serveur |
|-----------|-------------|---------------------|
| Backend | Symfony 8 + API Platform | `/var/www/Inventory/` |
| Frontend | Nuxt 4 (statique) | `/var/www/Inventory/Inventory_frontend/.output/public/` |
| Base de données | PostgreSQL 16 | `inventory` |
| Frontend | Nuxt 4 (site statique) | `/var/www/Inventory/Inventory_frontend/.output/public/` |
| Base de données | PostgreSQL 16 | Base `inventory` |
### Schéma simplifié
```
Navigateur
↓ HTTPS
Nginx (reverse proxy)
├── /api/* → PHP-FPM (Symfony) → PostgreSQL
└── /* → Fichiers statiques (Nuxt build)
```
---
@@ -24,7 +36,8 @@ inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM)
- **PostgreSQL** : 16
- **Composer**
Vérifier :
### Vérification des prérequis
```bash
php -v # PHP 8.4+
php -m | grep -E 'pgsql|intl|zip|gd|mbstring'
@@ -73,10 +86,10 @@ psql -U ferme_user -h 127.0.0.1 -d inventory -f /tmp/backup_v1.0.0_clean.sql
```bash
cd /var/www/Inventory
# Installer les dépendances
# Installer les dépendances (sans les outils de dev)
composer install --no-dev --optimize-autoloader
# Créer .env.local
# Créer le fichier de configuration locale
cat > .env.local << 'EOF'
APP_ENV=prod
APP_DEBUG=0
@@ -85,28 +98,20 @@ APP_SECRET=CHANGE_ME
DATABASE_URL="postgresql://ferme_user:fermerecette@127.0.0.1:5432/inventory?serverVersion=16"
CORS_ALLOW_ORIGIN='^https?://inventory\.malio-dev\.fr$'
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=inventoryjwt
EOF
# Générer APP_SECRET
# Générer un secret aléatoire
sed -i "s/CHANGE_ME/$(openssl rand -hex 32)/" .env.local
# Générer les clés JWT
mkdir -p config/jwt
openssl genrsa -out config/jwt/private.pem -aes256 4096
# Passphrase : inventoryjwt
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem
chmod 600 config/jwt/private.pem
# Permissions
# Permissions pour le dossier var/ (cache, logs)
sudo chown -R www-data:www-data var/
sudo chmod -R 775 var/
# Vider le cache
php bin/console cache:clear --env=prod
# Appliquer les migrations (si première installation ou mise à jour)
php bin/console doctrine:migrations:migrate --no-interaction
```
### 4. Configurer le frontend Nuxt
@@ -120,7 +125,7 @@ sudo chown -R malio:malio .
# Installer les dépendances
npm install
# Créer .env
# Créer le fichier d'environnement
cat > .env << 'EOF'
NUXT_PUBLIC_API_BASE_URL=http://inventory.malio-dev.fr/api
EOF
@@ -141,7 +146,7 @@ server {
listen 80;
server_name inventory.malio-dev.fr;
# Gros fichiers (100MB max)
# Gros fichiers (100MB max pour les uploads de documents)
client_max_body_size 100M;
client_body_timeout 300s;
send_timeout 300s;
@@ -149,12 +154,13 @@ server {
access_log /var/log/nginx/inventory-access.log;
error_log /var/log/nginx/inventory-error.log;
# Backend Symfony - /api
# Backend Symfony — toutes les requêtes /api
location /api {
root /var/www/Inventory/public;
try_files $uri /index.php$is_args$args;
}
# PHP-FPM (exécute le code PHP)
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
@@ -165,27 +171,27 @@ server {
internal;
}
# Frontend statique
# Frontend statique — tout le reste
location / {
root /var/www/Inventory/Inventory_frontend/.output/public;
index index.html;
try_files $uri $uri/ /index.html;
try_files $uri $uri/ /index.html; # SPA fallback
}
}
```
Activer :
Activer le site :
```bash
sudo ln -s /etc/nginx/sites-available/inventory /etc/nginx/sites-enabled/
sudo nginx -t
sudo nginx -t # Vérifier la syntaxe
sudo systemctl reload nginx
```
### 6. Vérifier
```bash
curl http://inventory.malio-dev.fr
curl http://inventory.malio-dev.fr/api
curl http://inventory.malio-dev.fr # Frontend
curl http://inventory.malio-dev.fr/api # API (doc Swagger)
```
---
@@ -197,12 +203,13 @@ curl http://inventory.malio-dev.fr/api
```bash
cd /var/www/Inventory
# Pull les changements
# Récupérer les changements
git pull
git submodule update --init --recursive
# Backend
composer install --no-dev --optimize-autoloader
php bin/console doctrine:migrations:migrate --no-interaction
php bin/console cache:clear --env=prod
sudo chown -R www-data:www-data var/
@@ -214,56 +221,76 @@ npx nuxi generate
---
## Versioning & Releases
## Backup base de données
### Source de vérité
Le fichier `VERSION` à la racine contient le numéro de version (ex: `1.0.0`).
Cette version est synchronisée avec :
- Le footer de l'application
- `config/packages/api_platform.yaml`
### Créer une release
### Export (faire un backup)
```bash
# Depuis le PC de dev
./scripts/release.sh patch # 1.0.0 → 1.0.1
./scripts/release.sh minor # 1.0.0 → 1.1.0
./scripts/release.sh major # 1.0.0 → 2.0.0
./scripts/release.sh 2.0.0 # Version exacte
pg_dump -U ferme_user -h 127.0.0.1 -d inventory \
--no-owner --no-acl --inserts --column-inserts \
--clean --if-exists > backup_inventory_$(date +%Y%m%d).sql
```
Le script :
1. Vérifie/commit le submodule frontend
2. Met à jour `VERSION` et `api_platform.yaml`
3. Commit et tag les deux repos
4. Affiche les commandes pour push
### Pousser la release
### Import (restaurer un backup)
```bash
# Frontend (submodule)
cd Inventory_frontend && git push && git push --tags && cd ..
# Backend
git push && git push --tags
psql -U ferme_user -h 127.0.0.1 -d inventory -f backup_inventory_YYYYMMDD.sql
```
### Créer la release sur Gitea
1. Aller sur le dépôt Gitea
2. **Releases** > **New Release**
3. Sélectionner le tag `vX.Y.Z`
4. Ajouter les notes de release
---
## Commandes utiles
## Troubleshooting
### Erreur 502 Bad Gateway
PHP-FPM ne tourne pas ou est crashé :
```bash
systemctl status php8.4-fpm
sudo systemctl restart php8.4-fpm
```
### Erreur 403 Forbidden
Problème de permissions sur les fichiers :
```bash
sudo chown -R www-data:www-data /var/www/Inventory/var/
sudo chmod -R 775 /var/www/Inventory/var/
```
### Erreur API "No route found"
Le cache Symfony est probablement périmé :
```bash
php /var/www/Inventory/bin/console cache:clear --env=prod
```
### Frontend ne se met pas à jour
Les fichiers statiques sont en cache. Rebuilder :
```bash
cd /var/www/Inventory/Inventory_frontend
rm -rf .output
npx nuxi generate
```
### L'API retourne 401 sur toutes les requêtes
La session PHP ne se crée pas correctement. Vérifier :
```bash
# Vérifier que le dossier de sessions existe et est accessible
ls -la /var/lib/php/sessions/
# Ou vérifier les logs Symfony
tail -f /var/www/Inventory/var/log/prod.log
```
---
## Commandes utiles en production
```bash
# Logs Nginx
tail -f /var/log/nginx/inventory-error.log
tail -f /var/log/nginx/inventory-access.log
# Logs Symfony
tail -f /var/www/Inventory/var/log/prod.log
@@ -274,55 +301,12 @@ php /var/www/Inventory/bin/console cache:clear --env=prod
# Rebuild frontend
cd /var/www/Inventory/Inventory_frontend && npx nuxi generate
# Status PHP-FPM
# Status des services
systemctl status php8.4-fpm
systemctl status nginx
systemctl status postgresql
# Reload Nginx
# Redémarrer les services
sudo systemctl restart php8.4-fpm
sudo systemctl reload nginx
```
---
## Backup base de données
### Export
```bash
pg_dump -U ferme_user -h 127.0.0.1 -d inventory --no-owner --no-acl --inserts --column-inserts --clean --if-exists > backup_inventory_$(date +%Y%m%d).sql
```
### Import
```bash
psql -U ferme_user -h 127.0.0.1 -d inventory -f backup_inventory_YYYYMMDD.sql
```
---
## Troubleshooting
### Erreur 502 Bad Gateway
```bash
# Vérifier PHP-FPM
systemctl status php8.4-fpm
sudo systemctl restart php8.4-fpm
```
### Erreur 403 Forbidden
```bash
# Vérifier les permissions
sudo chown -R www-data:www-data /var/www/Inventory/var/
sudo chmod -R 775 /var/www/Inventory/var/
```
### Erreur API "No route found"
```bash
# Vider le cache
php /var/www/Inventory/bin/console cache:clear --env=prod
```
### Frontend ne se met pas à jour
```bash
# Rebuild
cd /var/www/Inventory/Inventory_frontend
rm -rf .output
npx nuxi generate
```

269
README.md
View File

@@ -2,52 +2,87 @@
Application de gestion d'inventaire industriel pour **Malio**. Gestion complète du parc machines, des pièces, composants, produits, fournisseurs et documents associés, avec traçabilité et contrôle d'accès par rôles.
## C'est quoi ce projet ?
Inventory est une application web qui permet de gérer un parc de machines industrielles. Concrètement, elle permet de :
- **Cataloguer** les machines d'une usine, site par site
- **Décomposer** chaque machine en composants, pièces et produits (structure arborescente)
- **Suivre** les fournisseurs/constructeurs de chaque élément
- **Stocker** les documents techniques (PDF, images, fiches techniques)
- **Tracer** toutes les modifications (qui a changé quoi, quand) via un journal d'audit
- **Commenter** les fiches pour collaborer entre équipes
- **Gérer les accès** avec un système de rôles (admin, gestionnaire, lecteur)
L'application se compose de deux parties :
- Un **backend** (API REST) qui gère les données, la sécurité et la logique métier
- Un **frontend** (interface web) qui affiche les données et permet l'interaction utilisateur
## Stack technique
| Couche | Technologie | Version |
|--------|-------------|---------|
| Backend | Symfony + API Platform | 8.0 / 4.2 |
| PHP | PHP | >= 8.4 |
| Base de données | PostgreSQL | 16 |
| Frontend | Nuxt (SPA, SSR off) | 4 |
| UI | Vue 3 Composition API + TypeScript | 3.5 / 5.7 |
| CSS | TailwindCSS + DaisyUI | 4 / 5 |
| Conteneurs | Docker Compose | |
| Couche | Technologie | Version | Rôle |
|--------|-------------|---------|------|
| Backend | Symfony + API Platform | 8.0 / 4.2 | API REST, logique métier, sécurité |
| PHP | PHP | >= 8.4 | Langage backend |
| Base de données | PostgreSQL | 16 | Stockage des données |
| Frontend | Nuxt (SPA, SSR off) | 4 | Framework web (rendu côté client) |
| UI | Vue 3 Composition API + TypeScript | 3.5 / 5.7 | Composants d'interface |
| CSS | TailwindCSS + DaisyUI | 4 / 5 | Mise en page et composants visuels |
| Conteneurs | Docker Compose | | Environnement de développement |
## Prérequis
- **Docker** et **Docker Compose**
- **Node.js** >= 20 (via nvm)
- **make**
- **Docker** et **Docker Compose** (pour lancer le projet sans rien installer)
- **Node.js** >= 20 (via [nvm](https://github.com/nvm-sh/nvm))
- **make** (normalement déjà installé sur Linux/macOS)
### Installation de l'environnement
### Guides d'installation de l'environnement
| OS | Documentation |
|----|---------------|
| Windows | [WSL2 + Ubuntu + Docker](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/windows) |
| Linux | [Docker + nvm](https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux) |
## Installation
## Installation rapide
```bash
# 1. Cloner le projet avec le frontend (submodule)
git clone --recurse-submodules <url-du-repo>
cd Inventory
# 2. Démarrer les conteneurs Docker (PHP, PostgreSQL, Adminer)
make start
# 3. Installer les dépendances et builder le projet
make install
```
> Si `make start` échoue sur le port de la BDD, modifier `POSTGRES_PORT` dans `docker/.env.docker.local`.
### Que fait `make install` ?
1. Installe les dépendances PHP (via Composer)
2. Installe les dépendances Node.js (via npm)
3. Build le frontend Nuxt
### Premier lancement
Une fois l'installation terminée, tu peux :
1. Charger des données de test : `make fixtures-load`
2. Lancer le frontend en mode dev : `make dev-nuxt`
3. Ouvrir l'application : http://localhost:3001
## URLs locales
| Service | URL |
|---------|-----|
| API Symfony | http://localhost:8081/api |
| Frontend Nuxt | http://localhost:3001 |
| Adminer (BDD) | http://localhost:5050 |
| PostgreSQL | `localhost:5433` (user: root, pass: root, db: inventory) |
| Service | URL | Description |
|---------|-----|-------------|
| API Symfony | http://localhost:8081/api | Documentation interactive de l'API (Swagger) |
| Frontend Nuxt | http://localhost:3001 | L'application web |
| Adminer (BDD) | http://localhost:5050 | Interface web pour explorer la base de données |
| PostgreSQL | `localhost:5433` | Connexion directe (user: root, pass: root, db: inventory) |
## Commandes
## Commandes utiles
### Docker
@@ -56,26 +91,28 @@ make install
| `make start` | Démarrer les conteneurs |
| `make stop` | Arrêter les conteneurs |
| `make restart` | Redémarrer les conteneurs |
| `make shell` | Shell bash dans le conteneur PHP |
| `make reset` | Reset complet (supprime volumes, réinstalle) |
| `make shell` | Ouvrir un terminal dans le conteneur PHP (pour lancer des commandes Symfony) |
| `make reset` | Reset complet (supprime les volumes, réinstalle tout) |
### Backend
| Commande | Description |
|----------|-------------|
| `make test` | Lancer les tests PHPUnit |
| `make php-cs-fixer-allow-risky` | Formatter le code PHP |
| `make cache-clear` | Vider le cache Symfony |
| `make db-reset` | Reset de la BDD (supprime les données) |
| `make fixtures-load` | Charger les fixtures SQL |
| `make fixtures-dump` | Dumper la BDD dans fixtures/data.sql |
| `make test FILES=tests/Api/Entity/MachineTest.php` | Lancer un test spécifique |
| `make test-setup` | Créer/mettre à jour la base de test |
| `make php-cs-fixer-allow-risky` | Formatter le code PHP (indentation, espaces, etc.) |
| `make cache-clear` | Vider le cache Symfony (à faire si tu as des erreurs bizarres) |
| `make db-reset` | Reset de la BDD (supprime toutes les données) |
| `make fixtures-load` | Charger les données de test |
| `make fixtures-dump` | Sauvegarder la BDD actuelle dans fixtures/data.sql |
### Frontend
| Commande | Description |
|----------|-------------|
| `make dev-nuxt` | Serveur de dev Nuxt |
| `make build-nuxtJS` | Build de production |
| `make dev-nuxt` | Lancer le serveur de dev Nuxt (avec rechargement automatique) |
| `make build-nuxtJS` | Builder le frontend pour la production |
### Release
@@ -85,79 +122,115 @@ make install
Synchronise automatiquement la version dans `VERSION`, `api_platform.yaml` et `nuxt.config.ts`, crée le tag git et pousse les deux repos.
## Architecture
## Architecture globale
### Comment ça marche ?
```
┌──────────────────┐ HTTP (JSON) ┌──────────────────┐ SQL ┌────────────┐
│ Frontend │ ◄─────────────────► │ Backend │ ◄──────────► │ PostgreSQL │
│ (Nuxt/Vue) │ cookies session │ (Symfony/API) │ │ (BDD) │
│ localhost:3001 │ │ localhost:8081 │ │ port 5433 │
└──────────────────┘ └──────────────────┘ └────────────┘
│ │
Interface web API REST + logique
pour l'utilisateur métier + sécurité
```
1. L'utilisateur ouvre le navigateur sur `localhost:3001`
2. Le frontend (Vue/Nuxt) affiche l'interface
3. Quand l'utilisateur fait une action (créer une machine, etc.), le frontend envoie une requête HTTP à l'API backend
4. Le backend valide la requête, vérifie les permissions, exécute la logique métier
5. Le backend lit/écrit dans PostgreSQL et renvoie une réponse JSON
6. Le frontend met à jour l'interface avec les nouvelles données
### Structure du projet
```
Inventory/ # Backend Symfony (repo principal)
├── src/
│ ├── Entity/ # 20 entités Doctrine (attributs PHP 8)
│ ├── Controller/ # 16 contrôleurs custom
│ ├── EventSubscriber/ # 9 subscribers (audit onFlush)
│ ├── EventListener/ # Listeners documents (cleanup, compression)
│ ├── Command/ # 3 commandes CLI
│ ├── Service/ # 3 services (stockage, conversion, PDF)
│ ├── State/ # 3 processeurs API Platform
│ ├── Repository/ # 19 repositories Doctrine
── Security/ # Authenticateur session
│ └── Serializer/ # Normalizer custom (Document)
├── config/ # Configuration Symfony
├── migrations/ # 4 migrations Doctrine (SQL PostgreSQL)
│ ├── Entity/ # Les "modèles" de données (Machine, Piece, etc.)
│ ├── Controller/ # Les endpoints API personnalisés
│ ├── EventSubscriber/ # Logique déclenchée automatiquement (audit, etc.)
│ ├── Command/ # Commandes CLI (lancer via php bin/console)
│ ├── Service/ # Services métier (stockage fichiers, PDF, etc.)
│ ├── State/ # Processeurs API Platform (hashage mot de passe, upload)
│ ├── Repository/ # Requêtes BDD personnalisées
│ ├── Security/ # Authentification par session
── Serializer/ # Conversion entité ↔ JSON personnalisée
├── config/ # Configuration Symfony (routes, sécurité, etc.)
├── migrations/ # Scripts de modification de la BDD
├── fixtures/ # Données de test (SQL)
├── tests/ # Tests automatisés (PHPUnit)
├── scripts/ # Utilitaires (release, migration, normalisation)
├── docker/ # Dockerfile + config Docker
├── makefile # Commandes de dev
├── VERSION # Version courante (semver)
└── Inventory_frontend/ # Submodule git (repo séparé)
├── app/pages/ # 36 pages Nuxt (file-based routing)
├── app/components/ # 57 composants Vue
├── app/composables/ # 45 composables
── app/shared/ # Types, utils, validation
├── makefile # Commandes de dev raccourcies
├── VERSION # Version courante (ex: 1.8.1)
└── Inventory_frontend/ # Submodule git (frontend, repo séparé)
├── app/pages/ # Les pages de l'app (1 fichier = 1 route URL)
├── app/components/ # Composants Vue réutilisables
├── app/composables/ # Logique métier partagée (appels API, états)
── app/shared/ # Types TypeScript, utilitaires, validation
├── app/middleware/ # Vérification de session automatique
└── app/services/ # Couche service (wrappers API)
```
### Entités principales
### Entités principales (les "tables" de la BDD)
| Entité | Description |
|--------|-------------|
| `Machine` | Machines du parc industriel |
| `Composant` | Composants rattachés aux machines |
| `Piece` | Pièces détachées |
| `Product` | Produits (consommables, outillage) |
| `Site` | Sites physiques / usines |
| `Constructeur` | Fournisseurs / fabricants |
| `TypeMachine` | Types de machines avec squelettes de structure |
| `ModelType` | Catégories (pièce, composant, produit) avec champs personnalisés |
| `CustomField` / `CustomFieldValue` | Champs personnalisés extensibles |
| `Document` | Documents uploadés (stockage fichier + compression PDF) |
| `AuditLog` | Journal d'audit (diff + snapshot) |
| `Comment` | Commentaires / tickets sur les fiches |
| `Profile` | Utilisateurs avec rôles |
| Entité | Description | Exemple |
|--------|-------------|---------|
| `Machine` | Machines du parc industriel | "CNC Mazak 01" |
| `Composant` | Composants fonctionnels d'une machine | "Broche principale" |
| `Piece` | Pièces détachées/de rechange | "Roulement SKF 6205" |
| `Product` | Produits fournisseur (consommables, outillage) | "Huile de coupe X" |
| `Site` | Sites physiques / usines | "Usine de Strasbourg" |
| `Constructeur` | Fournisseurs / fabricants | "SKF", "Mazak" |
| `ModelType` | Catégories avec squelettes de structure | "Type: Moteur électrique" |
| `CustomField` / `CustomFieldValue` | Champs personnalisés (dynamiques) | "Tension : 220V" |
| `Document` | Documents uploadés (PDF, images, etc.) | "Fiche technique CNC.pdf" |
| `AuditLog` | Journal d'audit (historique des modifications) | "Machine X modifiée par Jean" |
| `Comment` | Commentaires / tickets sur les fiches | "Vérifier le roulement" |
| `Profile` | Comptes utilisateurs avec rôles | "admin@malio.fr (ADMIN)" |
### Commandes Symfony
### Structure hiérarchique d'une machine
| Commande | Description |
|----------|-------------|
| `app:compress-pdf` | Compresser les PDFs existants (supporte `--dry-run`) |
| `app:migrate-documents-to-filesystem` | Migrer les documents Base64 vers le système de fichiers |
| `app:init-profile-passwords` | Initialiser mots de passe et rôles en masse |
Une machine peut contenir une arborescence de composants, pièces et produits :
```
Machine "CNC Mazak 01"
├── Composant "Broche principale"
│ ├── Pièce "Roulement avant"
│ │ └── Produit "Graisse SKF LGMT2"
│ └── Pièce "Joint d'étanchéité"
├── Composant "Système hydraulique"
│ ├── Pièce "Pompe HP"
│ └── Produit "Huile hydraulique ISO 46"
└── Produit "Filtre à air cabine"
```
### Rôles et permissions
```
ROLE_ADMIN → ROLE_GESTIONNAIRE → ROLE_VIEWER → ROLE_USER
ROLE_ADMIN → Tout faire + gérer les utilisateurs
↓ hérite de
ROLE_GESTIONNAIRE → Créer, modifier, supprimer les données
↓ hérite de
ROLE_VIEWER → Lecture seule sur toutes les données
↓ hérite de
ROLE_USER → Accès de base (rôle minimum)
```
- **ADMIN** : accès complet, gestion des profils
- **GESTIONNAIRE** : CRUD sur toutes les entités, résolution des commentaires
- **VIEWER** : lecture seule sur toutes les entités
- **USER** : accès de base
### Authentification
Authentification par **session (cookies)**, pas de JWT. Le profil actif est stocké en session côté serveur.
Authentification par **session (cookies)**, pas de JWT. Le profil actif est stocké en session côté serveur. Concrètement :
### Base de données
1. L'utilisateur choisit son profil sur la page de login
2. Il entre son mot de passe
3. Le backend crée une session et envoie un cookie au navigateur
4. À chaque requête suivante, le navigateur envoie automatiquement ce cookie
5. Le backend vérifie le cookie et identifie l'utilisateur
### Base de données — Points importants
PostgreSQL 16 avec les particularités suivantes :
- **IDs** : chaînes CUID (`'cl' + bin2hex(random_bytes(12))`), pas d'auto-increment
@@ -171,7 +244,7 @@ PostgreSQL 16 avec les particularités suivantes :
|---------|-------|------|------|
| `web` | PHP 8.4 + Apache + Node | 8081, 3001 | API Symfony + Nuxt dev |
| `db` | PostgreSQL 16 Alpine | 5433 | Base de données |
| `adminer` | Adminer | 5050 | Interface web BDD |
| `adminer` | Adminer | 5050 | Interface web pour explorer la BDD |
## Xdebug
@@ -191,30 +264,42 @@ Configuration PhpStorm / VSCode :
- `develop` : branche principale de dev (cible des PR)
- `feat/xxx`, `fix/xxx`, `refactor/xxx` : branches de travail
### Convention de commit
### Convention de commit (enforced par un hook)
```
<type>(<scope>) : <message>
```
**Espace obligatoire autour du `:`**. Types : `feat`, `fix`, `perf`, `refactor`, `chore`, `docs`, `test`, `style`, `build`, `ci`, `revert`, `wip`.
**Espace obligatoire autour du `:`**. Types autorisés (minuscules) :
`feat`, `fix`, `perf`, `refactor`, `chore`, `docs`, `test`, `style`, `build`, `ci`, `revert`, `wip`
Exemples :
```
feat(machines) : add clone functionality
fix(documents) : prevent duplicate upload
refactor(audit) : merge history controllers
chore(deps) : update composer packages
```
### Pre-commit hook
1. php-cs-fixer sur les fichiers PHP stagés
2. PHPUnit — bloque le commit si les tests échouent
Le hook `pre-commit` s'exécute automatiquement avant chaque commit :
1. **php-cs-fixer** — Formate automatiquement les fichiers PHP modifiés
2. **PHPUnit** — Lance les tests. Si un test échoue, le commit est bloqué
### Submodule frontend
Le frontend est un **submodule git** dans `Inventory_frontend/`. Workflow :
Le frontend est un **submodule git** dans `Inventory_frontend/` (c'est un repo git séparé, inclus dans le repo principal). Workflow de commit :
1. Commiter dans `Inventory_frontend/` d'abord
2. Commiter dans le repo principal pour mettre à jour le pointeur
2. Commiter dans le repo principal pour mettre à jour le pointeur du submodule
3. Pousser les deux repos
## Documentation complémentaire
## Documentation détaillée
- [DEPLOY.md](DEPLOY.md) : guide de déploiement serveur (Nginx, PHP-FPM, PostgreSQL)
- [RELEASE.md](RELEASE.md) : processus de release et versioning
- [CHANGELOG.md](CHANGELOG.md) : historique des versions
- [Frontend README](Inventory_frontend/README.md) : documentation du frontend Nuxt
- **[docs/BACKEND.md](docs/BACKEND.md)** : guide complet du backend (entités, controllers, API, audit, tests)
- **[docs/FRONTEND.md](docs/FRONTEND.md)** : guide complet du frontend (pages, composables, composants, patterns)
- **[DEPLOY.md](DEPLOY.md)** : guide de déploiement serveur (Nginx, PHP-FPM, PostgreSQL)
- **[RELEASE.md](RELEASE.md)** : processus de release et versioning
- **[CHANGELOG.md](CHANGELOG.md)** : historique des versions
- **[Frontend README](Inventory_frontend/README.md)** : documentation du frontend Nuxt

View File

@@ -1,12 +1,18 @@
# Guide de Release
## Versioning
## C'est quoi une release ?
Le projet utilise le [Semantic Versioning](https://semver.org/) (SemVer) : `MAJOR.MINOR.PATCH`
Une release c'est une **version officielle** de l'application. Chaque release a un numéro de version (ex: `1.8.1`) et est marquée par un **tag git**.
- **MAJOR** : Changements incompatibles avec les versions précédentes
- **MINOR** : Nouvelles fonctionnalités rétrocompatibles
- **PATCH** : Corrections de bugs rétrocompatibles
## Versioning (Semantic Versioning)
Le projet utilise le [Semantic Versioning](https://semver.org/) : `MAJOR.MINOR.PATCH`
| Type | Quand l'utiliser | Exemple |
|------|------------------|---------|
| **PATCH** | Correction de bug, pas de nouvelle fonctionnalité | `1.8.0``1.8.1` |
| **MINOR** | Nouvelle fonctionnalité, rétrocompatible | `1.8.1``1.9.0` |
| **MAJOR** | Changement majeur, potentiellement incompatible | `1.9.0``2.0.0` |
La version est centralisée dans le fichier `VERSION` à la racine du projet.
@@ -14,9 +20,9 @@ La version est centralisée dans le fichier `VERSION` à la racine du projet.
### Prérequis
- Tous les changements doivent être commités
- Les tests doivent passer
- Être sur la branche à releaser (ex: `main`, `develop`)
- Tous les changements doivent être commités (pas de fichiers modifiés non commités)
- Les tests doivent passer (`make test`)
- Être sur la branche à releaser (généralement `develop` ou `master`)
### Utilisation du script
@@ -24,28 +30,38 @@ La version est centralisée dans le fichier `VERSION` à la racine du projet.
# Afficher l'aide et la version actuelle
./scripts/release.sh
# Bump patch : 1.0.0 → 1.0.1
# Bump patch : 1.8.1 → 1.8.2
./scripts/release.sh patch
# Bump minor : 1.0.0 → 1.1.0
# Bump minor : 1.8.1 → 1.9.0
./scripts/release.sh minor
# Bump major : 1.0.0 → 2.0.0
# Bump major : 1.8.1 → 2.0.0
./scripts/release.sh major
# Version spécifique
./scripts/release.sh 2.0.0
```
Le script :
1. Met à jour le fichier `VERSION`
2. Met à jour `config/packages/api_platform.yaml`
3. Crée un commit `chore(release): vX.Y.Z`
4. Crée le tag `vX.Y.Z`
### Que fait le script ?
1. Vérifie qu'il n'y a pas de changements non commités
2. Vérifie/commit le submodule frontend si nécessaire
3. Met à jour le fichier `VERSION` avec le nouveau numéro
4. Met à jour `config/packages/api_platform.yaml` (version affichée dans l'API)
5. Crée un commit `chore(release) : vX.Y.Z`
6. Crée le tag git `vX.Y.Z`
7. Affiche les commandes pour pousser
### Pousser la release
Après avoir exécuté le script :
```bash
# Pousser le frontend d'abord (si modifié)
cd Inventory_frontend && git push && git push --tags && cd ..
# Pousser le backend
git push && git push --tags
```
@@ -54,12 +70,21 @@ git push && git push --tags
1. Aller sur le dépôt Gitea
2. **Releases** > **New Release**
3. Sélectionner le tag `vX.Y.Z`
4. Titre : `v1.0.0` (ou avec un nom descriptif)
5. Description : résumé des changements (voir section Notes de release)
4. Titre : `vX.Y.Z` (ou avec un nom descriptif)
5. Description : résumé des changements (copier depuis CHANGELOG.md)
## Fichiers impactés par le versioning
| Fichier | Rôle |
|---------|------|
| `VERSION` | Source unique de vérité |
| `config/packages/api_platform.yaml` | Version affichée dans la doc API (Swagger) |
| `Inventory_frontend/nuxt.config.ts` | Lit `VERSION` au build pour l'afficher dans le footer |
| Footer de l'app | Affiche `v{{ appVersion }}` |
## Notes de release
Template pour les notes de release :
Template pour les notes de release (à copier dans Gitea) :
```markdown
## Nouveautés
@@ -73,66 +98,25 @@ Template pour les notes de release :
## Changements
- Refactoring de Z
- Mise à jour des dépendances
## Migration requise
\`\`\`bash
docker compose exec web php bin/console doctrine:migrations:migrate
\`\`\`
```
## Fichiers impactés par le versioning
## Déploiement après une release
| Fichier | Usage |
|---------|-------|
| `VERSION` | Source unique de vérité |
| `config/packages/api_platform.yaml` | Version affichée dans l'API |
| `Inventory_frontend/nuxt.config.ts` | Lit VERSION au build |
| Footer de l'app | Affiche `v{{ appVersion }}` |
Voir [DEPLOY.md](DEPLOY.md) pour les instructions de mise à jour en production.
## Déploiement en production
### 1. Base de données
Dump de la base locale :
```bash
pg_dump -h localhost -p 5433 -U root -d inventory > backup_v1.0.0.sql
```
Import en production :
```bash
psql -h <PROD_HOST> -U <PROD_USER> -d inventory < backup_v1.0.0.sql
```
### 2. Variables d'environnement production
Créer un fichier `.env.local` en production avec :
```env
APP_ENV=prod
APP_SECRET=<générer avec: openssl rand -hex 32>
DATABASE_URL="postgresql://user:password@host:5432/inventory?serverVersion=16"
CORS_ALLOW_ORIGIN='^https://votre-domaine\.com$'
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=<votre-passphrase>
```
### 3. Build production
Backend :
En résumé :
```bash
# Sur le serveur de production
cd /var/www/Inventory
git pull
git submodule update --init --recursive
composer install --no-dev --optimize-autoloader
php bin/console cache:clear --env=prod
php bin/console doctrine:migrations:migrate --no-interaction
php bin/console cache:clear --env=prod
cd Inventory_frontend && npm install && npx nuxi generate
```
Frontend :
```bash
cd Inventory_frontend
NUXT_PUBLIC_API_BASE_URL=https://api.votre-domaine.com yarn build
```
### 4. Checklist avant mise en prod
- [ ] Tests passent
- [ ] Migrations DB testées
- [ ] Variables d'environnement configurées
- [ ] Clés JWT générées
- [ ] CORS configuré
- [ ] SSL/HTTPS actif
- [ ] Backup de la DB prod existante (si upgrade)

846
docs/BACKEND.md Normal file
View File

@@ -0,0 +1,846 @@
# Guide Backend — Inventory
Guide complet du backend Symfony pour comprendre comment tout fonctionne, même si tu débutes.
## Table des matières
1. [Vue d'ensemble](#vue-densemble)
2. [Comment fonctionne une API REST](#comment-fonctionne-une-api-rest)
3. [Symfony + API Platform — les bases](#symfony--api-platform--les-bases)
4. [Les Entités (les modèles de données)](#les-entités)
5. [Les Controllers (les endpoints personnalisés)](#les-controllers)
6. [Le système d'audit](#le-système-daudit)
7. [L'authentification par session](#lauthentification-par-session)
8. [Les services](#les-services)
9. [Les migrations de base de données](#les-migrations)
10. [Les tests](#les-tests)
11. [Flux complet d'une requête](#flux-complet-dune-requête)
12. [Commandes Symfony utiles](#commandes-symfony-utiles)
---
## Vue d'ensemble
Le backend est une **API REST** construite avec :
- **Symfony 8** : le framework PHP (gère le routing, la sécurité, la config, etc.)
- **API Platform 4.2** : une surcouche qui génère automatiquement les endpoints CRUD à partir des entités
- **Doctrine ORM** : fait le lien entre les objets PHP et les tables PostgreSQL
- **PostgreSQL 16** : la base de données relationnelle
### Le principe
Au lieu d'écrire manuellement chaque endpoint (GET /machines, POST /machines, etc.), **API Platform** les génère automatiquement à partir des entités PHP. Tu déclares tes champs, tes relations, tes règles de sécurité directement sur la classe PHP, et API Platform fait le reste.
---
## Comment fonctionne une API REST
### C'est quoi une API REST ?
Une API REST c'est un serveur qui répond à des requêtes HTTP (comme un site web, mais au lieu de renvoyer du HTML, il renvoie du JSON).
### Les verbes HTTP
| Verbe | Action | Exemple |
|-------|--------|---------|
| `GET` | Lire des données | `GET /api/machines` → liste toutes les machines |
| `POST` | Créer une donnée | `POST /api/machines` + body JSON → crée une machine |
| `PUT` | Remplacer une donnée | `PUT /api/machines/123` + body JSON → remplace la machine 123 |
| `PATCH` | Modifier partiellement | `PATCH /api/machines/123` + body JSON → modifie certains champs |
| `DELETE` | Supprimer | `DELETE /api/machines/123` → supprime la machine 123 |
### Les codes de réponse HTTP
| Code | Signification | Quand |
|------|---------------|-------|
| `200` | OK | Requête réussie |
| `201` | Created | Ressource créée avec succès (POST) |
| `204` | No Content | Suppression réussie (DELETE) |
| `400` | Bad Request | Données invalides envoyées |
| `401` | Unauthorized | Pas connecté / session expirée |
| `403` | Forbidden | Connecté mais pas les permissions |
| `404` | Not Found | La ressource n'existe pas |
| `409` | Conflict | Doublon (ex: nom déjà pris) |
| `500` | Server Error | Bug côté serveur |
### Le format JSON-LD
L'API utilise **JSON-LD** (JSON Linked Data), une extension de JSON qui ajoute des métadonnées :
```json
{
"@context": "/api/contexts/Machine",
"@id": "/api/machines/cl1a2b3c4d5e6f7g8h9i0j1k",
"@type": "Machine",
"id": "cl1a2b3c4d5e6f7g8h9i0j1k",
"name": "CNC Mazak 01",
"reference": "CNM-001",
"prix": "50000.00",
"site": "/api/sites/cl9z8y7x6w5v4u3t2s1r0q",
"createdAt": "2026-01-15T10:30:00+00:00"
}
```
Points importants :
- `@id` est l'**IRI** (Internationalized Resource Identifier) : c'est l'identifiant unique de la ressource dans l'API
- Les relations utilisent des IRIs : `"site": "/api/sites/cl9z8..."` au lieu d'un simple ID
- Les collections retournent un format hydra avec pagination :
```json
{
"@context": "/api/contexts/Machine",
"@id": "/api/machines",
"@type": "hydra:Collection",
"hydra:totalItems": 42,
"hydra:member": [
{ "@id": "/api/machines/cl...", "name": "CNC 01", ... },
{ "@id": "/api/machines/cl...", "name": "Tour 02", ... }
]
}
```
---
## Symfony + API Platform — les bases
### La structure des fichiers
```
src/
├── Entity/ # Les classes PHP qui représentent les tables de la BDD
├── Controller/ # Les endpoints HTTP personnalisés (quand API Platform ne suffit pas)
├── EventSubscriber/ # Du code qui s'exécute automatiquement quand quelque chose se passe
├── Repository/ # Les requêtes SQL personnalisées
├── Service/ # La logique métier réutilisable
├── State/ # Les processeurs API Platform (interceptent le flux CRUD)
├── Security/ # L'authentification
├── Serializer/ # Personnalisation de la conversion entité ↔ JSON
├── Command/ # Commandes CLI (php bin/console app:xxx)
├── Enum/ # Les énumérations PHP (ex: catégories)
└── OpenApi/ # Personnalisation de la doc Swagger
```
### Comment Symfony traite une requête
```
Requête HTTP
Symfony Router (quel code doit répondre ?)
Sécurité (l'utilisateur a-t-il le droit ?)
Controller ou API Platform (traitement)
Doctrine ORM (lecture/écriture en BDD)
Serializer (conversion entité → JSON)
Réponse HTTP (JSON envoyé au frontend)
```
### Les attributs PHP 8
Le projet utilise les **attributs PHP 8** (les `#[...]`) au lieu des annotations (les `@...`). C'est la syntaxe moderne de PHP :
```php
// Attribut PHP 8 (ce qu'on utilise) ✅
#[ORM\Column(type: 'string', length: 255)]
private string $name;
// Annotation (ancien style, on ne l'utilise pas) ❌
/** @ORM\Column(type="string", length=255) */
```
---
## Les Entités
Les entités sont les classes PHP qui représentent les tables de la base de données. Chaque propriété de la classe correspond à une colonne.
### Anatomie d'une entité
Prenons un exemple simplifié :
```php
<?php
// src/Entity/Machine.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: MachineRepository::class)] // ← Lié à une table en BDD
#[ORM\HasLifecycleCallbacks] // ← Active les hooks PrePersist/PreUpdate
#[ApiResource( // ← Génère les endpoints API
operations: [
new GetCollection(security: "is_granted('ROLE_VIEWER')"), // GET /api/machines
new Get(security: "is_granted('ROLE_VIEWER')"), // GET /api/machines/{id}
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"), // POST /api/machines
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"), // PATCH /api/machines/{id}
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"), // DELETE /api/machines/{id}
],
normalizationContext: ['groups' => ['machine:read']], // ← Quels champs exposer en lecture
denormalizationContext: ['groups' => ['machine:write']], // ← Quels champs accepter en écriture
paginationItemsPerPage: 30, // ← 30 résultats par page
)]
class Machine
{
#[ORM\Id]
#[ORM\Column(type: 'string', length: 36)]
#[Groups(['machine:read'])] // ← Exposé en lecture uniquement
private string $id;
#[ORM\Column(type: 'string', length: 255, unique: true)]
#[Groups(['machine:read', 'machine:write'])] // ← Exposé en lecture ET écriture
private string $name;
#[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)]
#[Groups(['machine:read', 'machine:write'])]
private ?string $prix = null;
#[ORM\ManyToOne(targetEntity: Site::class)] // ← Relation : chaque machine appartient à un site
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] // ← Obligatoire, supprimé en cascade
#[Groups(['machine:read', 'machine:write'])]
private Site $site;
#[ORM\Column(type: 'datetime_immutable')]
#[Groups(['machine:read'])] // ← Lecture seule (pas dans machine:write)
private \DateTimeImmutable $createdAt;
// ... getters et setters
}
```
### Décryptage des attributs importants
| Attribut | Signification |
|----------|--------------|
| `#[ORM\Entity]` | Cette classe est stockée en BDD |
| `#[ORM\Column]` | Cette propriété est une colonne |
| `#[ORM\Id]` | C'est la clé primaire |
| `#[ORM\ManyToOne]` | Relation N→1 (plusieurs machines → un site) |
| `#[ORM\OneToMany]` | Relation 1→N (un site → plusieurs machines) |
| `#[ORM\ManyToMany]` | Relation N→N (machines ↔ constructeurs) |
| `#[ApiResource]` | API Platform génère les endpoints CRUD |
| `#[Groups]` | Contrôle quels champs sont visibles/modifiables |
| `security: "is_granted('ROLE_X')"` | Qui a le droit d'utiliser cet endpoint |
### Le trait CuidEntityTrait
Toutes les entités utilisent un trait partagé qui génère les IDs et gère les timestamps :
```php
// src/Entity/Trait/CuidEntityTrait.php
trait CuidEntityTrait
{
#[ORM\PrePersist] // ← S'exécute automatiquement AVANT l'insertion en BDD
public function generateId(): void
{
if (!isset($this->id)) {
$this->id = 'cl' . bin2hex(random_bytes(12)); // ← Génère un ID unique de 26 chars
}
$this->createdAt = new \DateTimeImmutable();
$this->updatedAt = new \DateTimeImmutable();
}
#[ORM\PreUpdate] // ← S'exécute automatiquement AVANT une mise à jour
public function updateTimestamp(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
}
```
### Les entités du projet
#### Entités "catalogue" (les éléments qu'on gère)
| Entité | Table | Champs clés | Relations |
|--------|-------|-------------|-----------|
| **Machine** | `machine` | name, reference, prix | → Site, ↔ Constructeur, → Documents |
| **Composant** | `composant` | name, reference, description, prix, structure (JSON) | → ModelType, → Product, ↔ Constructeur |
| **Piece** | `piece` | name, reference, description, prix, productIds (JSON) | → ModelType, → Product, ↔ Constructeur |
| **Product** | `product` | name, reference, supplierPrice | → ModelType, ↔ Constructeur |
#### Entités de classification
| Entité | Table | Champs clés | Rôle |
|--------|-------|-------------|------|
| **Site** | `site` | name, contactName, contactPhone, contactAddress | Regrouper les machines par lieu |
| **Constructeur** | `constructeur` | name, email, phone | Fournisseurs/fabricants partagés |
| **ModelType** | `model_type` | name, code, category (enum), skeletons (JSON) | Catégoriser composants/pièces/produits |
#### Entités de liaison hiérarchique (structure machine)
| Entité | Rôle | Relations |
|--------|------|-----------|
| **MachineComponentLink** | Lie un composant à une machine | → Machine, → Composant, → parent (self) |
| **MachinePieceLink** | Lie une pièce à une machine | → Machine, → Piece, → parent composant |
| **MachineProductLink** | Lie un produit à une machine | → Machine, → Product, → parent (flexible) |
Ces entités permettent la **structure arborescente** : un composant peut contenir des pièces, qui contiennent des produits.
#### Entités de métadonnées
| Entité | Rôle |
|--------|------|
| **CustomField** | Définition d'un champ personnalisé (nom, type, options) |
| **CustomFieldValue** | Valeur d'un champ personnalisé pour une entité donnée |
| **Document** | Fichier uploadé (PDF, image) rattaché à une entité |
| **AuditLog** | Entrée du journal d'audit (diff + snapshot) |
| **Comment** | Commentaire/ticket sur une fiche |
| **Profile** | Compte utilisateur (email, rôle, mot de passe hashé) |
### Les relations entre entités (schéma simplifié)
```
Site ──1:N──► Machine ──1:N──► MachineComponentLink ──► Composant
│ │
│ └──1:N──► MachinePieceLink ──► Piece
│ │
│ └──1:N──► MachineProductLink ──► Product
└──N:N──► Constructeur (via table de jointure)
ModelType ──1:N──► Composant / Piece / Product
└──► CustomField ──1:N──► CustomFieldValue
Machine / Composant / Piece / Product ──1:N──► Document
──1:N──► CustomFieldValue
```
---
## Les Controllers
API Platform génère automatiquement les endpoints CRUD standard. Les controllers personnalisés gèrent les cas plus complexes.
### Liste des controllers
#### Authentification (3 controllers)
**SessionProfileController** (`/api/session/profile`) — Login/Logout
```
POST /api/session/profile → Se connecter (payload: { profileId, password })
GET /api/session/profile → Récupérer le profil connecté
DELETE /api/session/profile → Se déconnecter
```
**SessionProfilesController** (`/api/session/profiles`) — Liste des profils
```
GET /api/session/profiles → Liste tous les profils actifs (page de login)
```
**AdminProfileController** (`/api/admin/profiles`) — Administration des utilisateurs
```
GET /api/admin/profiles → Liste tous les profils (ADMIN only)
POST /api/admin/profiles → Créer un profil
PUT /api/admin/profiles/{id}/role → Changer le rôle d'un profil
PUT /api/admin/profiles/{id}/password → Réinitialiser un mot de passe
PUT /api/admin/profiles/{id}/deactivate → Désactiver un profil
```
#### Données et logique métier
**MachineStructureController** — Structure hiérarchique des machines
```
GET /api/machines/{id}/structure → Récupérer l'arborescence complète
PATCH /api/machines/{id}/structure → Modifier l'arborescence
POST /api/machines/{id}/clone → Cloner une machine avec toute sa structure
```
**MachineCustomFieldsController** — Champs personnalisés machines
```
POST /api/machines/{id}/custom-fields/init → Initialiser les champs personnalisés manquants
```
**EntityHistoryController** — Historique d'audit par entité
```
GET /api/{entityType}/{id}/history → 200 derniers événements d'audit
```
**ActivityLogController** — Journal d'activité global
```
GET /api/activity-log → Liste paginée avec filtres (entityType, action)
```
**CommentController** — Commentaires/tickets
```
POST /api/comments → Créer un commentaire
PATCH /api/comments/{id}/resolve → Résoudre un commentaire
GET /api/comments/unresolved-count → Nombre de commentaires non résolus
```
**CustomFieldValueController** — Valeurs de champs personnalisés
```
POST /api/custom-field-values → Créer/mettre à jour une valeur (upsert)
DELETE /api/custom-field-values/{id} → Supprimer une valeur
```
#### Fichiers
**DocumentQueryController** — Requêter les documents par entité
```
GET /api/documents/by-site/{id} → Documents d'un site
GET /api/documents/by-machine/{id} → Documents d'une machine
GET /api/documents/by-composant/{id} → Documents d'un composant
GET /api/documents/by-piece/{id} → Documents d'une pièce
GET /api/documents/by-product/{id} → Documents d'un produit
```
**DocumentServeController** — Servir les fichiers
```
GET /api/documents/{id}/file → Afficher le fichier (inline)
GET /api/documents/{id}/download → Télécharger le fichier (attachment)
```
#### Monitoring
**HealthCheckController** — Vérification de santé
```
GET /api/health → Version, latence BDD, mémoire, version PHP
```
### Exemple de controller commenté
```php
<?php
// src/Controller/CommentController.php
namespace App\Controller;
use App\Entity\Comment;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
class CommentController extends AbstractController
{
#[Route('/api/comments', methods: ['POST'])] // ← Définit l'URL et le verbe HTTP
public function create(
Request $request, // ← La requête HTTP entrante
EntityManagerInterface $em, // ← Pour écrire en BDD (injecté automatiquement)
): JsonResponse {
$this->denyAccessUnlessGranted('ROLE_VIEWER'); // ← Vérifie que l'utilisateur est connecté
$data = json_decode($request->getContent(), true); // ← Parse le body JSON
$comment = new Comment();
$comment->setContent($data['content']);
$comment->setEntityType($data['entityType']);
$comment->setEntityId($data['entityId']);
// ... autres champs
$em->persist($comment); // ← Dit à Doctrine "je veux sauvegarder ça"
$em->flush(); // ← Exécute réellement le INSERT SQL
return $this->json($comment, 201); // ← Renvoie le commentaire créé avec le code 201
}
}
```
---
## Le système d'audit
Chaque modification sur les entités principales est automatiquement enregistrée dans un journal d'audit. C'est un des points forts de l'application.
### Comment ça marche ?
Les **Event Subscribers** de Doctrine interceptent les opérations de base de données **avant** qu'elles soient exécutées (événement `onFlush`).
```
L'utilisateur modifie une machine
Doctrine détecte le changement
onFlush se déclenche
Le subscriber calcule le diff (ancien → nouveau)
Le subscriber crée un AuditLog avec :
- entityType : "machine"
- entityId : "cl1a2b3c..."
- action : "update"
- diff : { "name": { "from": "CNC 01", "to": "CNC 02" } }
- snapshot : { état complet de la machine }
- actorProfileId : "cl9z8y7x..." (qui a fait la modif)
Les deux (machine + audit log) sont sauvegardés en même temps
```
### Le diff
Le diff capture exactement ce qui a changé :
```json
{
"name": { "from": "CNC Mazak 01", "to": "CNC Mazak 02" },
"prix": { "from": "45000.00", "to": "50000.00" },
"constructeurIds": {
"from": ["cl111...", "cl222..."],
"to": ["cl111...", "cl333..."]
}
}
```
### Le snapshot
Le snapshot capture l'état complet de l'entité au moment de la modification :
```json
{
"id": "cl1a2b3c...",
"name": "CNC Mazak 02",
"reference": "CNM-001",
"prix": "50000.00",
"siteId": "cl9z8y7x...",
"constructeurIds": ["cl111...", "cl333..."]
}
```
### Les subscribers d'audit
| Subscriber | Entité | Type |
|------------|--------|------|
| MachineAuditSubscriber | Machine | Complex (avec constructeurs + custom fields) |
| ComposantAuditSubscriber | Composant | Complex |
| PieceAuditSubscriber | Piece | Complex |
| ProductAuditSubscriber | Product | Complex |
| ConstructeurAuditSubscriber | Constructeur | Simple |
| DocumentAuditSubscriber | Document | Simple |
| ModelTypeAuditSubscriber | ModelType | Simple |
**Simple** = suit seulement les champs de l'entité
**Complex** = suit aussi les relations ManyToMany (constructeurs) et les champs personnalisés
### AbstractAuditSubscriber
La classe de base qui contient toute la logique partagée :
```php
abstract class AbstractAuditSubscriber implements EventSubscriber
{
// Méthode à implémenter par chaque subscriber
abstract protected function getEntityClass(): string; // Ex: Machine::class
abstract protected function getEntityType(): string; // Ex: 'machine'
abstract protected function buildSnapshot($entity): array; // Construit le snapshot
// Deux chemins d'exécution :
// 1. onFlushSimple() : pour les entités sans collections ManyToMany
// 2. onFlushComplex() : pour les entités avec constructeurs (détecte les ajouts/suppressions)
}
```
### Autres subscribers
| Subscriber | Rôle |
|------------|------|
| **PieceProductSyncSubscriber** | Synchronise le champ `productIds` sur Piece quand un Product est lié/délié |
| **UniqueConstraintSubscriber** | Capture les erreurs de doublon PostgreSQL et renvoie un message clair |
---
## L'authentification par session
### Le flux complet
```
1. GET /api/session/profiles
→ Retourne la liste des profils actifs (nom, prénom, email, hasPassword)
→ Le frontend affiche la page de login avec les profils disponibles
2. POST /api/session/profile
Body: { "profileId": "cl...", "password": "secret" }
→ Le backend vérifie le mot de passe
→ Si OK : stocke profileId dans la session PHP, retourne le profil
→ Si KO : retourne 401
3. GET /api/session/profile (à chaque chargement de page)
→ Le navigateur envoie le cookie de session automatiquement
→ Le backend retrouve le profil via la session
→ Retourne le profil connecté ou 401
4. DELETE /api/session/profile
→ Supprime le profileId de la session
→ L'utilisateur est déconnecté
```
### La sécurité sur les endpoints
Chaque endpoint API Platform a une règle de sécurité :
```php
new GetCollection(security: "is_granted('ROLE_VIEWER')") // Lecture → minimum ROLE_VIEWER
new Post(security: "is_granted('ROLE_GESTIONNAIRE')") // Création → minimum ROLE_GESTIONNAIRE
```
Les controllers personnalisés utilisent :
```php
$this->denyAccessUnlessGranted('ROLE_VIEWER');
```
### La hiérarchie des rôles
Grâce à la hiérarchie, un ADMIN a automatiquement tous les rôles inférieurs :
```
ROLE_ADMIN ─── a aussi ──► ROLE_GESTIONNAIRE ──► ROLE_VIEWER ──► ROLE_USER
```
Donc `is_granted('ROLE_VIEWER')` accepte aussi les GESTIONNAIRES et les ADMINS.
---
## Les services
### DocumentStorageService
Gère le stockage des fichiers sur le système de fichiers :
```php
// Stocker un fichier uploadé
$path = $storageService->store($uploadedFile, $entityType, $entityId);
// Supprimer un fichier
$storageService->delete($path);
```
Les fichiers sont stockés dans `var/documents/{entityType}/{entityId}/{filename}`.
### PdfCompressorService
Compresse les fichiers PDF via Ghostscript pour réduire leur taille :
```php
$compressorService->compress($filePath);
```
### ModelTypeCategoryConversionService
Permet de convertir la catégorie d'un ModelType (ex: transformer un type "composant" en type "pièce").
---
## Les migrations
Les migrations sont des scripts SQL qui modifient la structure de la base de données. Elles sont dans le dossier `migrations/`.
### Principe
Quand tu ajoutes un champ à une entité, il faut créer une migration pour mettre à jour la BDD :
```bash
# Générer une migration à partir des changements détectés
make shell
php bin/console doctrine:migrations:diff
# Appliquer les migrations
php bin/console doctrine:migrations:migrate
```
### Particularités PostgreSQL
Les migrations utilisent du **SQL brut** avec des gardes pour l'idempotence :
```sql
-- On peut relancer la migration sans erreur
ALTER TABLE machine ADD COLUMN IF NOT EXISTS description TEXT;
DROP INDEX IF EXISTS idx_machine_name;
CREATE UNIQUE INDEX IF NOT EXISTS idx_machine_name ON machine (name);
```
**Attention aux noms de colonnes** : PostgreSQL stocke tout en **minuscules**. Donc `typePieceId` en PHP devient `typepieceid` en SQL. Toujours utiliser des noms lowercase dans le SQL brut.
---
## Les tests
### Stack de test
- **PHPUnit 12** : framework de test PHP
- **API Platform Test** : utilitaires pour tester des endpoints API
- **DAMA DoctrineTestBundle** : wrappe chaque test dans une transaction avec rollback automatique (pas besoin de nettoyer la BDD entre les tests)
### Structure
```
tests/
├── AbstractApiTestCase.php # Classe de base avec helpers
└── Api/
└── Entity/
├── MachineTest.php # Tests des endpoints machine
├── SiteTest.php # Tests des endpoints site
└── ...
```
### Exemple de test
```php
class MachineTest extends AbstractApiTestCase
{
public function testCreateMachine(): void
{
// Créer un client HTTP connecté en tant que gestionnaire
$client = $this->createGestionnaireClient();
// Créer un site (prérequis)
$site = $this->createSite();
// Envoyer une requête POST pour créer une machine
$client->request('POST', '/api/machines', [
'json' => [
'name' => 'Machine Test',
'reference' => 'MT-001',
'site' => '/api/sites/' . $site->getId(),
],
]);
// Vérifier que la réponse est 201 Created
$this->assertResponseStatusCodeSame(201);
// Vérifier le contenu de la réponse
$this->assertJsonContains([
'name' => 'Machine Test',
'reference' => 'MT-001',
]);
}
}
```
### Helpers disponibles dans AbstractApiTestCase
| Méthode | Description |
|---------|-------------|
| `createViewerClient()` | Client HTTP connecté avec ROLE_VIEWER |
| `createGestionnaireClient()` | Client HTTP connecté avec ROLE_GESTIONNAIRE |
| `createAdminClient()` | Client HTTP connecté avec ROLE_ADMIN |
| `createProfile()` | Crée un profil utilisateur en BDD |
| `createSite()` | Crée un site en BDD |
| `createMachine()` | Crée une machine en BDD |
### Lancer les tests
```bash
make test # Tous les tests
make test FILES=tests/Api/Entity/MachineTest.php # Un fichier
make test-setup # (Re)créer la BDD de test
```
---
## Flux complet d'une requête
### Exemple : créer une machine
```
1. Le frontend envoie :
POST /api/machines
Content-Type: application/ld+json
Cookie: PHPSESSID=abc123
{
"name": "CNC Mazak 01",
"reference": "CNM-001",
"prix": "50000.00",
"site": "/api/sites/cl9z8y7x..."
}
2. Symfony reçoit la requête
→ Le routeur identifie : c'est un endpoint API Platform (POST sur Machine)
3. Sécurité
→ Vérifie le cookie de session → retrouve le profil connecté
→ Vérifie is_granted('ROLE_GESTIONNAIRE') → OK
4. Désérialisation (JSON → objet PHP)
→ API Platform convertit le JSON en objet Machine
→ Le champ "site" (IRI) est résolu en objet Site
→ Seuls les champs du groupe 'machine:write' sont acceptés
5. Validation
→ Vérifie les contraintes (name non vide, site existe, etc.)
6. Persistence (objet PHP → BDD)
→ Doctrine déclenche PrePersist (CuidEntityTrait)
→ Génère l'ID : "cl" + 24 hex chars aléatoires
→ Set createdAt et updatedAt
→ Doctrine détecte l'INSERT à faire
7. Audit (onFlush)
→ MachineAuditSubscriber détecte la nouvelle machine
→ Crée un AuditLog avec action='create', diff, snapshot
→ L'AuditLog est aussi ajouté à la transaction
8. Flush
→ Doctrine exécute les requêtes SQL :
INSERT INTO machine (id, name, reference, ...) VALUES (...)
INSERT INTO audit_log (id, entity_type, entity_id, action, diff, snapshot, ...) VALUES (...)
9. Sérialisation (objet PHP → JSON)
→ API Platform convertit la Machine en JSON-LD
→ Seuls les champs du groupe 'machine:read' sont inclus
10. Réponse
HTTP/1.1 201 Created
{
"@context": "/api/contexts/Machine",
"@id": "/api/machines/cl1a2b3c...",
"@type": "Machine",
"id": "cl1a2b3c...",
"name": "CNC Mazak 01",
...
}
```
---
## Commandes Symfony utiles
Lancer ces commandes dans le conteneur Docker (`make shell` pour y entrer) :
| Commande | Description |
|----------|-------------|
| `php bin/console debug:router` | Voir toutes les routes disponibles |
| `php bin/console debug:config api_platform` | Voir la config API Platform |
| `php bin/console doctrine:schema:validate` | Vérifier que les entités sont synchronisées avec la BDD |
| `php bin/console doctrine:migrations:diff` | Générer une migration à partir des changements |
| `php bin/console doctrine:migrations:migrate` | Appliquer les migrations |
| `php bin/console cache:clear` | Vider le cache (résout beaucoup de problèmes) |
| `php bin/console app:compress-pdf` | Compresser les PDFs existants |
| `php bin/console app:create-profile` | Créer un profil utilisateur |
---
## Résumé des points clés pour un débutant
1. **API Platform génère les endpoints CRUD automatiquement** à partir des entités — tu n'as pas besoin d'écrire de controllers pour les opérations standard
2. **Les attributs PHP 8** (`#[...]`) remplacent les annotations et configurent tout : BDD, API, sérialisation, sécurité
3. **Les groupes de sérialisation** (`machine:read`, `machine:write`) contrôlent quels champs sont visibles/modifiables
4. **L'audit est automatique** : chaque modification est tracée sans rien avoir à faire manuellement
5. **L'authentification est par session (cookies)**, pas par tokens JWT
6. **Les IDs sont des CUID** (chaînes aléatoires), pas des auto-increment
7. **PostgreSQL stocke les noms en minuscules** : attention dans le SQL brut
8. **Les tests utilisent des transactions** : chaque test est isolé et la BDD est nettoyée automatiquement

1052
docs/FRONTEND.md Normal file

File diff suppressed because it is too large Load Diff